[llvm] [ORC] Add synchronous helper APIs to CallViaEPC.h (PR #170743)
Lang Hames via llvm-commits
llvm-commits at lists.llvm.org
Thu Dec 4 12:52:57 PST 2025
https://github.com/lhames created https://github.com/llvm/llvm-project/pull/170743
This commit adds synchronous variants of callViaEPC and associated APIs ( EPCCaller, EPCCall), that take a promise as their first argument and block on the resulting future, returning a result. The promise object is passed to the asynchronous variant of callViaEPC to make the call.
The type of the promise argument is a template parameter, so types other than std::promise may be used. The promise type must provide a `get_future` operation to get a future for the result, and the future type must provide a `get` operation to wait on the result (compatible with std::promise / std::future).
The value type of the promise must be Expected<T> or Error, and determines the return type of the synchronous call. E.g.
```
Expected<int32_t> Result =
callViaEPC(std::promise<Expected<int32_t>>(), EPC, Serializer, Args...);
```
>From 55979460365762017d7cc218f4193f3c98a00a2a Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Thu, 4 Dec 2025 15:23:01 +1100
Subject: [PATCH] [ORC] Add synchronous helper APIs to CallViaEPC.h
This commit adds synchronous variants of callViaEPC and associated APIs (
EPCCaller, EPCCall), that take a promise as their first argument and block on
the resulting future, returning a result. The promise object is passed to the
asynchronous variant of callViaEPC to make the call.
The type of the promise argument is a template parameter, so types other than
std::promise may be used. The promise type must provide a `get_future`
operation to get a future for the result, and the future type must provide a
`get` operation to wait on the result (compatible with std::promise /
std::future).
The value type of the promise must be Expected<T> or Error, and determines the
return type of the synchronous call. E.g.
```
Expected<int32_t> Result =
callViaEPC(std::promise<Expected<int32_t>>(), EPC, Serializer, Args...);
```
---
.../llvm/ExecutionEngine/Orc/CallViaEPC.h | 59 +++++++++++++-----
.../ExecutionEngine/Orc/CallSPSViaEPCTest.cpp | 61 ++++++++++++++++++-
2 files changed, 104 insertions(+), 16 deletions(-)
diff --git a/llvm/include/llvm/ExecutionEngine/Orc/CallViaEPC.h b/llvm/include/llvm/ExecutionEngine/Orc/CallViaEPC.h
index 36acf05cb48e9..7797250fa3016 100644
--- a/llvm/include/llvm/ExecutionEngine/Orc/CallViaEPC.h
+++ b/llvm/include/llvm/ExecutionEngine/Orc/CallViaEPC.h
@@ -17,6 +17,8 @@
#include "llvm/ExecutionEngine/Orc/CallableTraitsHelper.h"
#include "llvm/ExecutionEngine/Orc/ExecutorProcessControl.h"
+#include <type_traits>
+
namespace llvm::orc {
namespace detail {
@@ -34,16 +36,17 @@ template <typename HandlerT> struct HandlerTraits {
} // namespace detail
-/// Call a wrapper function via EPC.
-template <typename HandlerT, typename Serializer, typename... ArgTs>
-void callViaEPC(HandlerT &&H, ExecutorProcessControl &EPC, Serializer S,
- ExecutorSymbolDef Fn, ArgTs &&...Args) {
- using RetT = typename detail::HandlerTraits<HandlerT>::RetT;
+/// Call a wrapper function via EPC asynchronously.
+template <typename HandlerFn, typename Serializer, typename... ArgTs>
+std::enable_if_t<std::is_invocable_v<HandlerFn, Error>>
+callViaEPC(HandlerFn &&H, ExecutorProcessControl &EPC, Serializer S,
+ ExecutorSymbolDef Fn, ArgTs &&...Args) {
+ using RetT = typename detail::HandlerTraits<HandlerFn>::RetT;
if (auto ArgBytes = S.serialize(std::forward<ArgTs>(Args)...))
EPC.callWrapperAsync(
Fn.getAddress(),
- [S = std::move(S), H = std::forward<HandlerT>(H)](
+ [S = std::move(S), H = std::forward<HandlerFn>(H)](
shared::WrapperFunctionResult R) mutable {
if (const char *ErrMsg = R.getOutOfBandError())
H(make_error<StringError>(ErrMsg, inconvertibleErrorCode()));
@@ -55,6 +58,25 @@ void callViaEPC(HandlerT &&H, ExecutorProcessControl &EPC, Serializer S,
H(ArgBytes.takeError());
}
+/// Call a wrapper function via EPC synchronously using the given promise.
+///
+/// This performs a blocking call by making an asynchronous call to set the
+/// promise and waiting on a future.
+///
+/// Blocking calls should only be used for convenience by ORC clients, never
+/// internally.
+template <typename PromiseT, typename Serializer, typename... ArgTs>
+std::enable_if_t<!std::is_invocable_v<PromiseT, Error>,
+ decltype(std::declval<PromiseT>().get_future().get())>
+callViaEPC(PromiseT &&P, ExecutorProcessControl &EPC, Serializer S,
+ ExecutorSymbolDef Fn, ArgTs &&...Args) {
+ auto F = P.get_future();
+ using RetT = decltype(F.get());
+ callViaEPC([P = std::move(P)](RetT R) mutable { P.set_value(std::move(R)); },
+ EPC, std::move(S), std::move(Fn), std::forward<ArgTs>(Args)...);
+ return F.get();
+}
+
/// Encapsulates calls via EPC to any function that's compatible with the given
/// serialization scheme.
template <typename Serializer> class EPCCaller {
@@ -65,11 +87,15 @@ template <typename Serializer> class EPCCaller {
// TODO: Add an ExecutionSession constructor once ExecutionSession has been
// moved to its own header.
- // Async call version.
- template <typename HandlerT, typename... ArgTs>
- void operator()(HandlerT &&H, ExecutorSymbolDef Fn, ArgTs &&...Args) {
- callViaEPC(std::forward<HandlerT>(H), EPC, S, Fn,
- std::forward<ArgTs>(Args)...);
+ // Make a call to the given function using callViaEPC.
+ //
+ // The PromiseOrHandlerT value is forwarded. Its type will determine both the
+ // return value type and the dispatch method (asynchronous vs synchronous).
+ template <typename PromiseOrHandlerT, typename... ArgTs>
+ decltype(auto) operator()(PromiseOrHandlerT &&R, ExecutorSymbolDef Fn,
+ ArgTs &&...Args) {
+ return callViaEPC(std::forward<PromiseOrHandlerT>(R), EPC, S, Fn,
+ std::forward<ArgTs>(Args)...);
}
private:
@@ -87,9 +113,14 @@ template <typename Serializer> class EPCCall {
// TODO: Add an ExecutionSession constructor once ExecutionSession has been
// moved to its own header.
- template <typename HandlerT, typename... ArgTs>
- void operator()(HandlerT &&H, ArgTs &&...Args) {
- Caller(std::forward<HandlerT>(H), Fn, std::forward<ArgTs>(Args)...);
+ // Make a call using callViaEPC.
+ //
+ // The PromiseOrHandlerT value is forwarded. Its type will determine both the
+ // return value type and the dispatch method (asynchronous vs synchronous).
+ template <typename PromiseOrHandlerT, typename... ArgTs>
+ decltype(auto) operator()(PromiseOrHandlerT &&R, ArgTs &&...Args) {
+ return Caller(std::forward<PromiseOrHandlerT>(R), Fn,
+ std::forward<ArgTs>(Args)...);
}
private:
diff --git a/llvm/unittests/ExecutionEngine/Orc/CallSPSViaEPCTest.cpp b/llvm/unittests/ExecutionEngine/Orc/CallSPSViaEPCTest.cpp
index 6ad5835ed6e8f..148947153d973 100644
--- a/llvm/unittests/ExecutionEngine/Orc/CallSPSViaEPCTest.cpp
+++ b/llvm/unittests/ExecutionEngine/Orc/CallSPSViaEPCTest.cpp
@@ -11,6 +11,8 @@
#include "llvm/Testing/Support/Error.h"
+#include <future>
+
#include "gtest/gtest.h"
using namespace llvm;
@@ -26,7 +28,7 @@ static CWrapperFunctionResult mainWrapper(const char *ArgData, size_t ArgSize) {
.release();
}
-TEST(CallSPSViaEPCTest, CallMainViaCaller) {
+TEST(CallSPSViaEPCTest, CallMainViaCallerAsync) {
auto EPC = cantFail(SelfExecutorProcessControl::Create());
SPSEPCCaller<int32_t(SPSSequence<SPSString>)> C(*EPC);
std::vector<std::string> Args;
@@ -59,7 +61,7 @@ TEST(CallSPSViaEPCTest, CallMainViaCaller) {
EXPECT_EQ(**R4, 0);
}
-TEST(CallSPSViaEPCTest, CallMainViaGenericCall) {
+TEST(CallSPSViaEPCTest, CallMainViaGenericCallAsync) {
auto EPC = cantFail(SelfExecutorProcessControl::Create());
SPSEPCCall<int32_t(SPSSequence<SPSString>)> C(
*EPC, ExecutorSymbolDef::fromPtr(mainWrapper));
@@ -88,3 +90,58 @@ TEST(CallSPSViaEPCTest, CallMainViaGenericCall) {
ASSERT_THAT_EXPECTED(*R4, Succeeded());
EXPECT_EQ(**R4, 0);
}
+
+TEST(CallSPSViaEPCTest, CallMainViaCallerSync) {
+ auto EPC = cantFail(SelfExecutorProcessControl::Create());
+ SPSEPCCaller<int32_t(SPSSequence<SPSString>)> C(*EPC);
+ std::vector<std::string> Args;
+
+ Expected<int32_t> R1 = C(std::promise<Expected<int32_t>>(),
+ ExecutorSymbolDef::fromPtr(mainWrapper), Args);
+ ASSERT_THAT_EXPECTED(R1, Succeeded());
+ EXPECT_EQ(*R1, 0);
+
+ Args.push_back("foo");
+ Expected<int32_t> R2 = C(std::promise<Expected<int32_t>>(),
+ ExecutorSymbolDef::fromPtr(mainWrapper), Args);
+ ASSERT_THAT_EXPECTED(R2, Succeeded());
+ EXPECT_EQ(*R2, 1);
+
+ Args.push_back("foo");
+ Expected<int32_t> R3 = C(std::promise<Expected<int32_t>>(),
+ ExecutorSymbolDef::fromPtr(mainWrapper), Args);
+ ASSERT_THAT_EXPECTED(R3, Succeeded());
+ EXPECT_EQ(*R3, 2);
+
+ Args.clear();
+ Expected<int32_t> R4 = C(std::promise<Expected<int32_t>>(),
+ ExecutorSymbolDef::fromPtr(mainWrapper), Args);
+ ASSERT_THAT_EXPECTED(R4, Succeeded());
+ EXPECT_EQ(*R4, 0);
+}
+
+TEST(CallSPSViaEPCTest, CallMainViaGenericCallSync) {
+ auto EPC = cantFail(SelfExecutorProcessControl::Create());
+ SPSEPCCall<int32_t(SPSSequence<SPSString>)> C(
+ *EPC, ExecutorSymbolDef::fromPtr(mainWrapper));
+ std::vector<std::string> Args;
+
+ Expected<int32_t> R1 = C(std::promise<Expected<int32_t>>(), Args);
+ ASSERT_THAT_EXPECTED(R1, Succeeded());
+ EXPECT_EQ(*R1, 0);
+
+ Args.push_back("foo");
+ Expected<int32_t> R2 = C(std::promise<Expected<int32_t>>(), Args);
+ ASSERT_THAT_EXPECTED(R2, Succeeded());
+ EXPECT_EQ(*R2, 1);
+
+ Args.push_back("foo");
+ Expected<int32_t> R3 = C(std::promise<Expected<int32_t>>(), Args);
+ ASSERT_THAT_EXPECTED(R3, Succeeded());
+ EXPECT_EQ(*R3, 2);
+
+ Args.clear();
+ Expected<int32_t> R4 = C(std::promise<Expected<int32_t>>(), Args);
+ ASSERT_THAT_EXPECTED(R4, Succeeded());
+ EXPECT_EQ(*R4, 0);
+}
More information about the llvm-commits
mailing list