[llvm] [ORC] Add rt::sps::MainCaller for running main-like functions. (PR #212948)
Lang Hames via llvm-commits
llvm-commits at lists.llvm.org
Thu Jul 30 01:14:09 PDT 2026
https://github.com/lhames updated https://github.com/llvm/llvm-project/pull/212948
>From 88be2f7297a64c3160c782c15476a065bb687455 Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Wed, 29 Jul 2026 14:42:24 +1000
Subject: [PATCH 1/2] [ORC] Add rt::sps::MainCaller for running main-like
functions.
rt::MainCaller is a controller-side interface for running functions with
a main-like signature (int(int argc, char *argv[])) in the executor.
rt::sps::MainCaller is rt::MainCaller's first implementation, which
invokes main functions using on ORC runtime's orc_rt_ci_sps_call_main
entrypoint (using Simple Packed Serialization for argument/return
encoding / decoding).
These utilities live under the ExecutionEngine/Orc/RTBridge directory,
which is intended to hold controller-side utilities for invoking ORC
runtime systems. Subdirectories (e.g. SPS) hold implementations for
specific serialization schemes.
Adds SPSCallersTest with coverage for direct construction, the synchronous
and asynchronous call operators, use through the rt::MainCaller interface,
and the Create / bootstrap-JITDylib lookup path.
---
.../llvm/ExecutionEngine/Orc/RTBridge/Calls.h | 55 +++++++
.../ExecutionEngine/Orc/RTBridge/SPS/Calls.h | 72 +++++++++
.../Orc/Shared/OrcRTBridge.cpp | 4 +
.../ExecutionEngine/Orc/CMakeLists.txt | 1 +
.../ExecutionEngine/Orc/SPSCallersTest.cpp | 144 ++++++++++++++++++
5 files changed, 276 insertions(+)
create mode 100644 llvm/include/llvm/ExecutionEngine/Orc/RTBridge/Calls.h
create mode 100644 llvm/include/llvm/ExecutionEngine/Orc/RTBridge/SPS/Calls.h
create mode 100644 llvm/unittests/ExecutionEngine/Orc/SPSCallersTest.cpp
diff --git a/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/Calls.h b/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/Calls.h
new file mode 100644
index 0000000000000..6f9977ddc9d5e
--- /dev/null
+++ b/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/Calls.h
@@ -0,0 +1,55 @@
+//===------- Calls.h - Runtime-agnostic executor call APIs ------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Runtime-agnostic interfaces for invoking executor-side operations. These
+// abstract over how a call reaches the executor, so clients can be written
+// once and used whether the operation is provided by a full ORC runtime or by
+// LLVM's own ORC-runtime-lite. Concrete implementations live in subdirectories
+// (e.g. RTBridge/SPS).
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_EXECUTIONENGINE_ORC_RTBRIDGE_CALLS_H
+#define LLVM_EXECUTIONENGINE_ORC_RTBRIDGE_CALLS_H
+
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/FunctionExtras.h"
+#include "llvm/ExecutionEngine/Orc/Shared/ExecutorAddress.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/MSVCErrorWorkarounds.h"
+
+#include <cstdint>
+#include <future>
+#include <string>
+
+namespace llvm::orc::rt {
+
+class MainCaller {
+public:
+ virtual ~MainCaller();
+
+ virtual void operator()(unique_function<void(Expected<int64_t>)> OnComplete,
+ ExecutorAddr MainFnAddr,
+ ArrayRef<std::string> Args) = 0;
+
+ Expected<int64_t> operator()(ExecutorAddr MainFnAddr,
+ ArrayRef<std::string> Args) {
+ std::promise<MSVCPExpected<int64_t>> P;
+ auto F = P.get_future();
+ this->operator()(
+ [P = std::move(P)](Expected<int64_t> R) mutable {
+ P.set_value(std::move(R));
+ },
+ MainFnAddr, Args);
+ return F.get();
+ }
+};
+
+} // namespace llvm::orc::rt
+
+#endif // LLVM_EXECUTIONENGINE_ORC_RTBRIDGE_CALLS_H
diff --git a/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/SPS/Calls.h b/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/SPS/Calls.h
new file mode 100644
index 0000000000000..bba54bda8a3c8
--- /dev/null
+++ b/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/SPS/Calls.h
@@ -0,0 +1,72 @@
+//===------------- Calls.h - SPS-based Call Wrappers ------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// SPS-based wrappers for calling functions.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_EXECUTIONENGINE_ORC_RTBRIDGE_SPS_CALLS_H
+#define LLVM_EXECUTIONENGINE_ORC_RTBRIDGE_SPS_CALLS_H
+
+#include "llvm/ExecutionEngine/Orc/Core.h"
+#include "llvm/ExecutionEngine/Orc/RTBridge/Calls.h"
+#include "llvm/ExecutionEngine/Orc/Shared/WrapperFunctionUtils.h"
+
+namespace llvm::orc::rt::sps {
+
+/// Calls the executor-side "call-main" wrapper, which runs a program's main
+/// function with the given arguments and returns its result.
+class MainCaller : public rt::MainCaller {
+public:
+ /// Name of the controller interface wrapper function.
+ static constexpr const char *CIName = "orc_rt_ci_sps_call_main";
+
+ static void callAsync(unique_function<void(Expected<int64_t>)> OnComplete,
+ ExecutionSession &ES, ExecutorAddr CallMainFnAddr,
+ ExecutorAddr MainFnAddr, ArrayRef<std::string> Args) {
+ using namespace llvm::orc::shared;
+ ES.callSPSWrapperAsync<int64_t(SPSExecutorAddr, SPSSequence<SPSString>)>(
+ CallMainFnAddr,
+ [OnComplete = std::move(OnComplete)](Error SerErr,
+ int64_t Result) mutable {
+ if (SerErr)
+ return OnComplete(std::move(SerErr));
+ else
+ return OnComplete(Result);
+ },
+ MainFnAddr, Args);
+ }
+
+ MainCaller(ExecutionSession &ES, ExecutorAddr CallMainFnAddr)
+ : ES(ES), CallMainFnAddr(CallMainFnAddr) {}
+
+ /// Look up the call-main wrapper in the executor's bootstrap JITDylib and
+ /// build a MainCaller for it.
+ static Expected<MainCaller> Create(ExecutionSession &ES) {
+ if (auto CallMainSym = ES.lookup({&ES.getBootstrapJITDylib()}, CIName))
+ return MainCaller(ES, CallMainSym->getAddress());
+ else
+ return CallMainSym.takeError();
+ }
+
+ void operator()(unique_function<void(Expected<int64_t>)> OnComplete,
+ ExecutorAddr MainFnAddr,
+ ArrayRef<std::string> Args) override {
+ callAsync(std::move(OnComplete), ES, CallMainFnAddr, MainFnAddr, Args);
+ }
+
+ using rt::MainCaller::operator();
+
+private:
+ ExecutionSession &ES;
+ ExecutorAddr CallMainFnAddr;
+};
+
+} // namespace llvm::orc::rt::sps
+
+#endif // LLVM_EXECUTIONENGINE_ORC_RTBRIDGE_SPS_CALLS_H
diff --git a/llvm/lib/ExecutionEngine/Orc/Shared/OrcRTBridge.cpp b/llvm/lib/ExecutionEngine/Orc/Shared/OrcRTBridge.cpp
index 24c1ca6e4904c..7239582715f9e 100644
--- a/llvm/lib/ExecutionEngine/Orc/Shared/OrcRTBridge.cpp
+++ b/llvm/lib/ExecutionEngine/Orc/Shared/OrcRTBridge.cpp
@@ -8,10 +8,14 @@
#include "llvm/ExecutionEngine/Orc/Shared/OrcRTBridge.h"
+#include "llvm/ExecutionEngine/Orc/RTBridge/Calls.h"
+
namespace llvm {
namespace orc {
namespace rt {
+MainCaller::~MainCaller() = default;
+
const char *SimpleExecutorDylibManagerInstanceName =
"__llvm_orc_SimpleExecutorDylibManager_Instance";
const char *SimpleExecutorDylibManagerOpenWrapperName =
diff --git a/llvm/unittests/ExecutionEngine/Orc/CMakeLists.txt b/llvm/unittests/ExecutionEngine/Orc/CMakeLists.txt
index 82744eafd3e33..fa134ff212043 100644
--- a/llvm/unittests/ExecutionEngine/Orc/CMakeLists.txt
+++ b/llvm/unittests/ExecutionEngine/Orc/CMakeLists.txt
@@ -47,6 +47,7 @@ add_llvm_unittest(OrcJITTests
SharedMemoryMapperTest.cpp
SimpleExecutorMemoryManagerTest.cpp
SimplePackedSerializationTest.cpp
+ SPSCallersTest.cpp
SymbolStringPoolTest.cpp
TaskDispatchTest.cpp
ThreadSafeModuleTest.cpp
diff --git a/llvm/unittests/ExecutionEngine/Orc/SPSCallersTest.cpp b/llvm/unittests/ExecutionEngine/Orc/SPSCallersTest.cpp
new file mode 100644
index 0000000000000..067fa6dbeb574
--- /dev/null
+++ b/llvm/unittests/ExecutionEngine/Orc/SPSCallersTest.cpp
@@ -0,0 +1,144 @@
+//===- SPSCallersTest.cpp - Test SPS call wrappers ------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/ExecutionEngine/Orc/AbsoluteSymbols.h"
+#include "llvm/ExecutionEngine/Orc/RTBridge/SPS/Calls.h"
+#include "llvm/ExecutionEngine/Orc/SelfExecutorProcessControl.h"
+#include "llvm/ExecutionEngine/Orc/Shared/WrapperFunctionUtils.h"
+#include "llvm/Support/MSVCErrorWorkarounds.h"
+#include "llvm/Testing/Support/Error.h"
+
+#include <cstring>
+#include <future>
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+using namespace llvm;
+using namespace llvm::orc;
+using namespace llvm::orc::shared;
+using llvm::orc::rt::sps::MainCaller;
+// Test "main" function. Returns argc plus the length of the first element of
+// argv (if argv is non-empty). Does not inspect argv entries beyond the first.
+static int testMain(int argc, char *argv[]) {
+ int Result = argc;
+ if (argc > 0)
+ Result += static_cast<int>(std::strlen(argv[0]));
+ return Result;
+}
+
+// Executor-side "call-main" wrapper. Decodes the main-function address and the
+// argument vector, then invokes the main function with a C-style (argc, argv).
+static CWrapperFunctionBuffer callMainWrapper(const char *ArgData,
+ size_t ArgSize) {
+ return WrapperFunction<int64_t(SPSExecutorAddr, SPSSequence<SPSString>)>::
+ handle(ArgData, ArgSize,
+ [](ExecutorAddr MainFnAddr,
+ std::vector<std::string> Args) -> int64_t {
+ std::vector<char *> ArgV;
+ ArgV.reserve(Args.size() + 1);
+ for (auto &Arg : Args)
+ ArgV.push_back(Arg.data());
+ ArgV.push_back(nullptr);
+ auto *Main = MainFnAddr.toPtr<int(int, char **)>();
+ return Main(static_cast<int>(Args.size()), ArgV.data());
+ })
+ .release();
+}
+
+TEST(SPSCallersTest, CallMainSyncViaDirectConstruction) {
+ ExecutionSession ES(cantFail(SelfExecutorProcessControl::Create()));
+
+ MainCaller CallMain(ES, ExecutorAddr::fromPtr(callMainWrapper));
+ ExecutorAddr MainAddr = ExecutorAddr::fromPtr(testMain);
+
+ std::vector<std::string> Args;
+ Expected<int64_t> R0 = CallMain(MainAddr, Args);
+ ASSERT_THAT_EXPECTED(R0, Succeeded());
+ EXPECT_EQ(*R0, 0); // argc == 0, no argv[0].
+
+ Args = {"hello"};
+ Expected<int64_t> R1 = CallMain(MainAddr, Args);
+ ASSERT_THAT_EXPECTED(R1, Succeeded());
+ EXPECT_EQ(*R1, 1 + 5); // argc == 1, strlen("hello") == 5.
+
+ Args = {"a", "bb"};
+ Expected<int64_t> R2 = CallMain(MainAddr, Args);
+ ASSERT_THAT_EXPECTED(R2, Succeeded());
+ EXPECT_EQ(*R2, 2 + 1); // argc == 2, strlen("a") == 1.
+
+ cantFail(ES.endSession());
+}
+
+TEST(SPSCallersTest, CallMainAsyncViaCallOperator) {
+ ExecutionSession ES(cantFail(SelfExecutorProcessControl::Create()));
+
+ MainCaller CallMain(ES, ExecutorAddr::fromPtr(callMainWrapper));
+
+ std::vector<std::string> Args = {"foo", "bar"};
+ std::promise<MSVCPExpected<int64_t>> P;
+ auto F = P.get_future();
+ CallMain([&](Expected<int64_t> R) { P.set_value(std::move(R)); },
+ ExecutorAddr::fromPtr(testMain), Args);
+
+ Expected<int64_t> R = F.get();
+ ASSERT_THAT_EXPECTED(R, Succeeded());
+ EXPECT_EQ(*R, 2 + 3); // argc == 2, strlen("foo") == 3.
+
+ cantFail(ES.endSession());
+}
+
+TEST(SPSCallersTest, CallMainThroughRTInterface) {
+ ExecutionSession ES(cantFail(SelfExecutorProcessControl::Create()));
+
+ MainCaller CallMain(ES, ExecutorAddr::fromPtr(callMainWrapper));
+
+ // Drive the caller through the runtime-agnostic rt::MainCaller interface to
+ // exercise the virtual dispatch path (and to guard the interface's public
+ // accessibility).
+ rt::MainCaller &Base = CallMain;
+ ExecutorAddr MainAddr = ExecutorAddr::fromPtr(testMain);
+
+ // Synchronous call operator (inherited from rt::MainCaller).
+ std::vector<std::string> Args = {"hello"};
+ Expected<int64_t> RSync = Base(MainAddr, Args);
+ ASSERT_THAT_EXPECTED(RSync, Succeeded());
+ EXPECT_EQ(*RSync, 1 + 5); // argc == 1, strlen("hello") == 5.
+
+ // Asynchronous call operator (virtual).
+ std::promise<MSVCPExpected<int64_t>> P;
+ auto F = P.get_future();
+ Base([&](Expected<int64_t> R) { P.set_value(std::move(R)); }, MainAddr, Args);
+ Expected<int64_t> RAsync = F.get();
+ ASSERT_THAT_EXPECTED(RAsync, Succeeded());
+ EXPECT_EQ(*RAsync, 1 + 5);
+
+ cantFail(ES.endSession());
+}
+
+TEST(SPSCallersTest, CreateLooksUpCallMainInBootstrapJD) {
+ ExecutionSession ES(cantFail(SelfExecutorProcessControl::Create()));
+
+ // Register the call-main wrapper in the bootstrap JITDylib under the name
+ // MainCaller::Create looks for.
+ auto &BootstrapJD = ES.getBootstrapJITDylib();
+ cantFail(BootstrapJD.define(absoluteSymbols(
+ {{ES.intern(MainCaller::CIName),
+ {ExecutorAddr::fromPtr(callMainWrapper), JITSymbolFlags::Exported}}})));
+
+ Expected<MainCaller> CallMain = MainCaller::Create(ES);
+ ASSERT_THAT_EXPECTED(CallMain, Succeeded());
+
+ std::vector<std::string> Args = {"x", "y", "z"};
+ Expected<int64_t> R = (*CallMain)(ExecutorAddr::fromPtr(testMain), Args);
+ ASSERT_THAT_EXPECTED(R, Succeeded());
+ EXPECT_EQ(*R, 3 + 1); // argc == 3, strlen("x") == 1.
+
+ cantFail(ES.endSession());
+}
>From ff8c7f96f8cbd3f0e0f5f47b09f38c44c83458ac Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Thu, 30 Jul 2026 18:13:52 +1000
Subject: [PATCH 2/2] Add class docs for MainCaller
---
.../llvm/ExecutionEngine/Orc/RTBridge/Calls.h | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/Calls.h b/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/Calls.h
index 6f9977ddc9d5e..0122d2666e8b9 100644
--- a/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/Calls.h
+++ b/llvm/include/llvm/ExecutionEngine/Orc/RTBridge/Calls.h
@@ -29,14 +29,27 @@
namespace llvm::orc::rt {
+/// Interface for running a main-like function (int(int argc, char *argv[])) in
+/// the executor.
+///
+/// Given the address of the function to run and an argument vector, invokes the
+/// function in the executor and returns its integer result. Calls can be made
+/// asynchronously (delivering the result to an OnComplete continuation) or
+/// synchronously (blocking until the result is available). Concrete
+/// implementations (e.g. rt::sps::MainCaller) determine how the call reaches
+/// the executor.
class MainCaller {
public:
virtual ~MainCaller();
+ /// Asynchronously run the main-like function at MainFnAddr with the given
+ /// Args, delivering its result (or an error) to OnComplete.
virtual void operator()(unique_function<void(Expected<int64_t>)> OnComplete,
ExecutorAddr MainFnAddr,
ArrayRef<std::string> Args) = 0;
+ /// Run the main-like function at MainFnAddr with the given Args, blocking
+ /// until its result (or an error) is available.
Expected<int64_t> operator()(ExecutorAddr MainFnAddr,
ArrayRef<std::string> Args) {
std::promise<MSVCPExpected<int64_t>> P;
More information about the llvm-commits
mailing list