[llvm] [ORC] Add InProcessEPC: orc_rt::InProcessControllerAccess partner. (PR #206725)

Lang Hames via llvm-commits llvm-commits at lists.llvm.org
Tue Jun 30 06:16:41 PDT 2026


https://github.com/lhames created https://github.com/llvm/llvm-project/pull/206725

Adds an ExecutorProcessControl implementation for in-process JIT setups that use the new ORC runtime (orc-rt). This is the LLVM-side partner of orc_rt::InProcessControllerAccess (introduced in d1744cf76fbe) and is intended to be constructed from orc_rt::InProcessControllerAccess's OnConnect callback.

The two sides communicate through a shared refcounted C-ABI Connection struct exchanged at connect time: the fields intended for use by the controller side are populated by InProcessControllerAccess before OnConnect fires, and the fields intended for use by the executor side (IPEPC pointer, CallJITDispatch, ReturnWrapperResult) are populated by InProcessEPC::Create. A parallel BootstrapInfoAccess struct exposes page-size, target triple, and bootstrap key/value and symbol iterators across the same C-only boundary.

Create returns an Expected<std::unique_ptr<InProcessEPC>> and reports errors for missing page-size, missing triple, duplicate bootstrap keys, and corrupted iterators, so the caller's OnConnect can surface a clean failure back to InProcessControllerAccess (which then tears the half-built connection down via the shared lifecycle hooks).

Disconnect is symmetric and idempotent, and guaranteed to be called in all cases (including failed construction, and successful construction with or without subsequent attachment to an ExecutionSession). In all cases, pending wrapper-result handlers are also drained with an out-of-band "disconnected" error.

Unit tests cover construction success and failure modes. A MockIPCA mirrors the lifecycle and message-scope semantics of the real ConnectionImpl so the tests exercise the contract end-to-end without pulling in orc-rt.

>From 17b2f8ae170b2d4035f053c07e95add9447a6da3 Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Tue, 30 Jun 2026 18:34:21 +1000
Subject: [PATCH] [ORC] Add InProcessEPC: orc_rt::InProcessControllerAccess
 partner.

Adds an ExecutorProcessControl implementation for in-process JIT setups that
use the new ORC runtime (orc-rt). This is the LLVM-side partner of
orc_rt::InProcessControllerAccess (introduced in d1744cf76fbe) and is intended
to be constructed from orc_rt::InProcessControllerAccess's OnConnect callback.

The two sides communicate through a shared refcounted C-ABI Connection struct
exchanged at connect time: the fields intended for use by the controller side
are populated by InProcessControllerAccess before OnConnect fires, and the
fields intended for use by the executor side (IPEPC pointer, CallJITDispatch,
ReturnWrapperResult) are populated by InProcessEPC::Create. A parallel
BootstrapInfoAccess struct exposes page-size, target triple, and bootstrap
key/value and symbol iterators across the same C-only boundary.

Create returns an Expected<std::unique_ptr<InProcessEPC>> and reports errors for
missing page-size, missing triple, duplicate bootstrap keys, and corrupted
iterators, so the caller's OnConnect can surface a clean failure back to
InProcessControllerAccess (which then tears the half-built connection down via
the shared lifecycle hooks).

Disconnect is symmetric and idempotent, and guaranteed to be called in all
cases (including failed construction, and successful construction with or
without subsequent attachment to an ExecutionSession). In all cases, pending
wrapper-result handlers are also drained with an out-of-band "disconnected"
error.

Unit tests cover construction success and failure modes. A MockIPCA mirrors
the lifecycle and message-scope semantics of the real ConnectionImpl so the
tests exercise the contract end-to-end without pulling in orc-rt.
---
 .../llvm/ExecutionEngine/Orc/InProcessEPC.h   | 141 ++++
 llvm/lib/ExecutionEngine/Orc/CMakeLists.txt   |   1 +
 llvm/lib/ExecutionEngine/Orc/InProcessEPC.cpp | 291 ++++++++
 .../ExecutionEngine/Orc/CMakeLists.txt        |   1 +
 .../ExecutionEngine/Orc/InProcessEPCTest.cpp  | 649 ++++++++++++++++++
 5 files changed, 1083 insertions(+)
 create mode 100644 llvm/include/llvm/ExecutionEngine/Orc/InProcessEPC.h
 create mode 100644 llvm/lib/ExecutionEngine/Orc/InProcessEPC.cpp
 create mode 100644 llvm/unittests/ExecutionEngine/Orc/InProcessEPCTest.cpp

diff --git a/llvm/include/llvm/ExecutionEngine/Orc/InProcessEPC.h b/llvm/include/llvm/ExecutionEngine/Orc/InProcessEPC.h
new file mode 100644
index 0000000000000..28ff1986eedfc
--- /dev/null
+++ b/llvm/include/llvm/ExecutionEngine/Orc/InProcessEPC.h
@@ -0,0 +1,141 @@
+//===---- InProcessEPC.h - In-process EPC for new ORC runtime ---*- 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
+//
+//===----------------------------------------------------------------------===//
+//
+// ExecutorProcessControl implementation for in-process JITs that use the new
+// ORC runtime (orc-rt). Interfaces with orc_rt::InProcessControllerAccess via
+// direct function calls.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_EXECUTIONENGINE_ORC_INPROCESSEPC_H
+#define LLVM_EXECUTIONENGINE_ORC_INPROCESSEPC_H
+
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ExecutionEngine/Orc/ExecutorProcessControl.h"
+#include "llvm/ExecutionEngine/Orc/InProcessMemoryAccess.h"
+#include "llvm/ExecutionEngine/Orc/Shared/WrapperFunctionUtils.h"
+#include "llvm/Support/Compiler.h"
+
+#include <memory>
+#include <mutex>
+
+namespace llvm::orc {
+
+/// An ExecutorProcessControl implementation for in-process JITs that use the
+/// new ORC runtime (llvm-project/orc-rt).
+///
+/// This class communicates with the runtime's InProcessControllerAccess via
+/// direct function calls through a virtual connection object.
+class LLVM_ABI InProcessEPC : public ExecutorProcessControl {
+public:
+  /// Pseudo-connection C struct. Used to facilitate calls between InProcessEPC
+  /// and InProcessControllerAccess without relying on anything but C ABI.
+  /// Must be kept in-sync with the corresponding struct in
+  /// orc_rt::InProcessControllerAccess.
+  struct Connection {
+    void (*Retain)(Connection *C) = nullptr;
+    void (*Release)(Connection *C) = nullptr;
+    void (*Disconnect)(Connection *C) = nullptr;
+    int (*EnterMessageScope)(Connection *C) = nullptr;
+    void (*LeaveMessageScope)(Connection *C) = nullptr;
+
+    /// Accessors to be set by the InProcessEPC instance.
+    void *IPEPC = nullptr;
+    void (*CallJITDispatch)(void *IPEPC, uint64_t CallId, void *HandlerTag,
+                            shared::CWrapperFunctionBuffer ArgBytes) = nullptr;
+    void (*ReturnWrapperResult)(void *IPEPC, uint64_t CallId,
+                                shared::CWrapperFunctionBuffer ResultBytes) =
+        nullptr;
+
+    /// Accessors to be set by the InProcessControllerAccess instance.
+    void *IPCA = nullptr;
+    void (*CallWrapper)(void *IPCA, uint64_t CallId, void *Fn,
+                        shared::CWrapperFunctionBuffer ArgBytes) = nullptr;
+    void (*ReturnJITDispatchResult)(
+        void *IPCA, uint64_t CallId,
+        shared::CWrapperFunctionBuffer ResultBytes) = nullptr;
+  };
+
+  /// Provides access to bootstrap info.
+  /// Must be kept in-sync with the corresponding struct in
+  /// orc_rt::InProcessControllerAccess.
+  struct BootstrapInfoAccess {
+    uint64_t (*GetPageSize)(void *BIA) = nullptr;
+    const char *(*GetTargetTriple)(void *BIA) = nullptr;
+
+    int (*GetNextValue)(void *BIA, const char **Name, const char **ValueBytes,
+                        uint64_t *ValueSize) = nullptr;
+    int (*GetNextSymbol)(void *BIA, const char **Name,
+                         uint64_t *Addr) = nullptr;
+  };
+
+  /// Create a new InProcessEPC.
+  ///
+  /// If no symbol string pool is given then one will be created.
+  /// If no task dispatcher is given an InPlaceTaskDispatcher will be used.
+  static Expected<std::unique_ptr<InProcessEPC>>
+  Create(Connection *C, BootstrapInfoAccess *BIA,
+         std::shared_ptr<SymbolStringPool> SSP = nullptr,
+         std::unique_ptr<TaskDispatcher> D = nullptr);
+
+  ~InProcessEPC();
+
+  Expected<int32_t> runAsMain(ExecutorAddr MainFnAddr,
+                              ArrayRef<std::string> Args) override;
+
+  Expected<int32_t> runAsVoidFunction(ExecutorAddr VoidFnAddr) override;
+
+  Expected<int32_t> runAsIntFunction(ExecutorAddr IntFnAddr, int Arg) override;
+
+  void callWrapperAsync(ExecutorAddr WrapperFnAddr,
+                        IncomingWFRHandler OnComplete,
+                        ArrayRef<char> ArgBuffer) override;
+
+  Expected<std::unique_ptr<jitlink::JITLinkMemoryManager>>
+  createDefaultMemoryManager() override;
+
+  Expected<std::unique_ptr<DylibManager>> createDefaultDylibMgr() override;
+
+  Expected<std::unique_ptr<MemoryAccess>> createDefaultMemoryAccess() override;
+
+  Error disconnect() override;
+
+private:
+  InProcessEPC(Connection *C, std::shared_ptr<SymbolStringPool> SSP,
+               std::unique_ptr<TaskDispatcher> D)
+      : ExecutorProcessControl(std::move(SSP), std::move(D)), C(C) {
+    C->Retain(C);
+  }
+
+  uint64_t registerPendingCallWrapperResult(IncomingWFRHandler H);
+  void doDisconnect();
+
+  // Incoming JIT-dispatch call from the ORC runtime.
+  void callJITDispatch(uint64_t CallId, void *HandlerTag,
+                       shared::CWrapperFunctionBuffer ArgBytes);
+  static void callJITDispatchEntry(void *IPEPC, uint64_t CallId,
+                                   void *HandlerTag,
+                                   shared::CWrapperFunctionBuffer ArgBytes);
+
+  // Incoming wrapper function result from the ORC runtime.
+  void returnWrapperResult(uint64_t CallId,
+                           shared::CWrapperFunctionBuffer ResultBytes);
+  static void
+  returnWrapperResultEntry(void *IPEPC, uint64_t CallId,
+                           shared::CWrapperFunctionBuffer ResultBytes);
+
+  Connection *C;
+
+  std::mutex M;
+  uint64_t NextCallId = 0;
+  DenseMap<uint64_t, IncomingWFRHandler> PendingCallWrapperResults;
+};
+
+} // namespace llvm::orc
+
+#endif // LLVM_EXECUTIONENGINE_ORC_INPROCESSEPC_H
diff --git a/llvm/lib/ExecutionEngine/Orc/CMakeLists.txt b/llvm/lib/ExecutionEngine/Orc/CMakeLists.txt
index 8c78c0250cfb8..40505ef980a66 100644
--- a/llvm/lib/ExecutionEngine/Orc/CMakeLists.txt
+++ b/llvm/lib/ExecutionEngine/Orc/CMakeLists.txt
@@ -26,6 +26,7 @@ add_llvm_component_library(LLVMOrcJIT
   ExecutorResolutionGenerator.cpp
   ObjectFileInterface.cpp
   IndirectionUtils.cpp
+  InProcessEPC.cpp
   InProcessMemoryAccess.cpp
   IRCompileLayer.cpp
   IRTransformLayer.cpp
diff --git a/llvm/lib/ExecutionEngine/Orc/InProcessEPC.cpp b/llvm/lib/ExecutionEngine/Orc/InProcessEPC.cpp
new file mode 100644
index 0000000000000..1091215aae5b1
--- /dev/null
+++ b/llvm/lib/ExecutionEngine/Orc/InProcessEPC.cpp
@@ -0,0 +1,291 @@
+//===---------- InProcessEPC.cpp -- In-process EPC for new ORC runtime ----===//
+//
+// 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/InProcessEPC.h"
+
+#include "llvm/ExecutionEngine/Orc/Core.h"
+#include "llvm/ExecutionEngine/Orc/EPCGenericDylibManager.h"
+#include "llvm/ExecutionEngine/Orc/EPCGenericJITLinkMemoryManager.h"
+#include "llvm/ExecutionEngine/Orc/EPCGenericMemoryAccess.h"
+#include "llvm/ExecutionEngine/Orc/TargetProcess/TargetExecutionUtils.h"
+#include "llvm/Support/DynamicLibrary.h"
+#include "llvm/Support/Process.h"
+
+#define DEBUG_TYPE "orc"
+
+namespace llvm::orc {
+
+Expected<std::unique_ptr<InProcessEPC>>
+InProcessEPC::Create(Connection *C, BootstrapInfoAccess *BIA,
+                     std::shared_ptr<SymbolStringPool> SSP,
+                     std::unique_ptr<TaskDispatcher> D) {
+  assert(C && "C must not be null");
+  assert(BIA && "BIA must not be null");
+
+  // Lifecycle and IPCA-side fields must be populated by the controller side
+  // before OnConnect is invoked.
+  assert(C->Retain && "C->Retain not set by controller");
+  assert(C->Release && "C->Release not set by controller");
+  assert(C->Disconnect && "C->Disconnect not set by controller");
+  assert(C->EnterMessageScope && "C->EnterMessageScope not set by controller");
+  assert(C->LeaveMessageScope && "C->LeaveMessageScope not set by controller");
+  assert(C->IPCA && "C->IPCA not set by controller");
+  assert(C->CallWrapper && "C->CallWrapper not set by controller");
+  assert(C->ReturnJITDispatchResult &&
+         "C->ReturnJITDispatchResult not set by controller");
+
+  if (!SSP)
+    SSP = std::make_shared<SymbolStringPool>();
+
+  if (!D)
+    D = std::make_unique<InPlaceTaskDispatcher>();
+
+  std::unique_ptr<InProcessEPC> IPEPC(
+      new InProcessEPC(C, std::move(SSP), std::move(D)));
+
+  // First set values in C.
+  C->IPEPC = IPEPC.get();
+  C->CallJITDispatch = callJITDispatchEntry;
+  C->ReturnWrapperResult = returnWrapperResultEntry;
+
+  // Then grab bootstrap values.
+  if (auto PageSize = BIA->GetPageSize(BIA))
+    IPEPC->PageSize = PageSize;
+  else
+    return make_error<StringError>(
+        "Cannot create InProcessEPC with page-size = 0",
+        inconvertibleErrorCode());
+
+  if (auto TT = BIA->GetTargetTriple(BIA)) {
+    IPEPC->TargetTriple = llvm::Triple(TT);
+  } else
+    return make_error<StringError>(
+        "Cannot create InProcessEPC with target-triple = \"\"",
+        inconvertibleErrorCode());
+
+  {
+    const char *Name;
+    const char *ValBytes;
+    uint64_t ValSize;
+    int RC;
+    while ((RC = BIA->GetNextValue(BIA, &Name, &ValBytes, &ValSize)) == 1) {
+      if (!IPEPC->BootstrapMap
+               .try_emplace(Name,
+                            std::vector<char>(ValBytes, ValBytes + ValSize))
+               .second)
+        return make_error<StringError>(
+            ("Cannot create InProcessEPC: bootstrap-value map contains "
+             "duplicate key \"" +
+             StringRef(Name) + "\""),
+            inconvertibleErrorCode());
+    }
+    if (RC < 0)
+      return make_error<StringError>(
+          "Cannot create InProcessEPC: bootstrap-value map corrupted",
+          inconvertibleErrorCode());
+  }
+
+  {
+    const char *SymName;
+    uint64_t SymAddr;
+    int RC;
+    while ((RC = BIA->GetNextSymbol(BIA, &SymName, &SymAddr)) == 1) {
+      if (!IPEPC->BootstrapSymbols.try_emplace(SymName, ExecutorAddr(SymAddr))
+               .second)
+        return make_error<StringError>(
+            ("Cannot create InProcessEPC: bootstrap-symbol map contains "
+             "duplicate symbol \"" +
+             StringRef(SymName) + "\""),
+            inconvertibleErrorCode());
+    }
+    if (RC < 0)
+      return make_error<StringError>(
+          "Cannot create InProcessEPC: bootstrap-symbol map corrupted",
+          inconvertibleErrorCode());
+  }
+
+  return IPEPC;
+}
+
+InProcessEPC::~InProcessEPC() {
+  // Guarantee that a discarded InProcessEPC initiates disconnect, even if it
+  // was never attached to an ExecutionSession (e.g. Create failed partway
+  // through, or the caller dropped the returned object without handing it to
+  // a session). When the InProcessEPC *is* attached, the ExecutionSession is
+  // guaranteed to call disconnect() during shutdown, and this call becomes a
+  // no-op via the idempotency of C->Disconnect.
+  doDisconnect();
+
+  // Shut down the dispatcher.
+  D->shutdown();
+
+  // Release the connection object.
+  C->Release(C);
+}
+
+Expected<int32_t> InProcessEPC::runAsMain(ExecutorAddr MainFnAddr,
+                                          ArrayRef<std::string> Args) {
+  using MainTy = int (*)(int, char *[]);
+  return orc::runAsMain(MainFnAddr.toPtr<MainTy>(), Args);
+}
+
+Expected<int32_t> InProcessEPC::runAsVoidFunction(ExecutorAddr VoidFnAddr) {
+  using VoidTy = int (*)();
+  return orc::runAsVoidFunction(VoidFnAddr.toPtr<VoidTy>());
+}
+
+Expected<int32_t> InProcessEPC::runAsIntFunction(ExecutorAddr IntFnAddr,
+                                                 int Arg) {
+  using IntTy = int (*)(int);
+  return orc::runAsIntFunction(IntFnAddr.toPtr<IntTy>(), Arg);
+}
+
+void InProcessEPC::callWrapperAsync(ExecutorAddr WrapperFnAddr,
+                                    IncomingWFRHandler OnComplete,
+                                    ArrayRef<char> ArgBuffer) {
+  if (C->EnterMessageScope(C)) {
+    auto CallId = registerPendingCallWrapperResult(std::move(OnComplete));
+    auto ArgBytes = shared::WrapperFunctionBuffer::copyFrom(ArgBuffer.data(),
+                                                            ArgBuffer.size());
+
+    LLVM_DEBUG(dbgs() << "InProcessEPC: callWrapperAsync call id " << CallId
+                      << " to " << WrapperFnAddr << "\n");
+
+    C->CallWrapper(C->IPCA, CallId, WrapperFnAddr.toPtr<void *>(),
+                   ArgBytes.release());
+    C->LeaveMessageScope(C);
+  } else
+    OnComplete(shared::WrapperFunctionBuffer::createOutOfBandError(
+        "connection closed"));
+}
+
+Expected<std::unique_ptr<jitlink::JITLinkMemoryManager>>
+InProcessEPC::createDefaultMemoryManager() {
+  // FIXME: Should actually use InProcessMemoryManager for this.
+  return EPCGenericJITLinkMemoryManager::Create(getExecutionSession());
+}
+
+Expected<std::unique_ptr<DylibManager>> InProcessEPC::createDefaultDylibMgr() {
+  // FIXME: Should actually use in-process for this.
+  auto DM = EPCGenericDylibManager::Create(getExecutionSession());
+  if (!DM)
+    return DM.takeError();
+  return std::make_unique<EPCGenericDylibManager>(std::move(*DM));
+}
+
+Expected<std::unique_ptr<MemoryAccess>>
+InProcessEPC::createDefaultMemoryAccess() {
+  // FIXME: Should actually use in-process for this.
+  EPCGenericMemoryAccess::FuncAddrs FAs;
+  if (auto Err = getBootstrapSymbols(
+          {{FAs.WriteUInt8s, rt::MemoryWriteUInt8sWrapperName},
+           {FAs.WriteUInt16s, rt::MemoryWriteUInt16sWrapperName},
+           {FAs.WriteUInt32s, rt::MemoryWriteUInt32sWrapperName},
+           {FAs.WriteUInt64s, rt::MemoryWriteUInt64sWrapperName},
+           {FAs.WriteBuffers, rt::MemoryWriteBuffersWrapperName},
+           {FAs.WritePointers, rt::MemoryWritePointersWrapperName},
+           {FAs.ReadUInt8s, rt::MemoryReadUInt8sWrapperName},
+           {FAs.ReadUInt16s, rt::MemoryReadUInt16sWrapperName},
+           {FAs.ReadUInt32s, rt::MemoryReadUInt32sWrapperName},
+           {FAs.ReadUInt64s, rt::MemoryReadUInt64sWrapperName},
+           {FAs.ReadBuffers, rt::MemoryReadBuffersWrapperName},
+           {FAs.ReadStrings, rt::MemoryReadStringsWrapperName}}))
+    return std::move(Err);
+
+  return std::make_unique<EPCGenericMemoryAccess>(*this, FAs);
+}
+
+Error InProcessEPC::disconnect() {
+  doDisconnect();
+  return Error::success();
+}
+
+uint64_t InProcessEPC::registerPendingCallWrapperResult(IncomingWFRHandler H) {
+  std::scoped_lock<std::mutex> Lock(M);
+  assert(!PendingCallWrapperResults.count(NextCallId) &&
+         "CallId already in use");
+  PendingCallWrapperResults[NextCallId] = std::move(H);
+  return NextCallId++;
+}
+
+void InProcessEPC::doDisconnect() {
+  // Disconnect from InProcessControllerAccess. This should prevent any further
+  // incoming or outgoing calls.
+  C->Disconnect(C);
+
+  // Drain any pending handlers.
+  DenseMap<uint64_t, IncomingWFRHandler> HandlersToDrain;
+  {
+    std::scoped_lock<std::mutex> Lock(M);
+    HandlersToDrain = std::move(PendingCallWrapperResults);
+  }
+
+  for (auto &[_, H] : HandlersToDrain)
+    H(shared::WrapperFunctionBuffer::createOutOfBandError("disconnected"));
+}
+
+void InProcessEPC::callJITDispatch(uint64_t CallId, void *HandlerTag,
+                                   shared::CWrapperFunctionBuffer ArgBytes) {
+  assert(C->ReturnJITDispatchResult && "ReturnJITDispatchResult not set");
+
+  LLVM_DEBUG(dbgs() << "InProcessEPC: JIT-dispatch call id " << CallId << " to "
+                    << HandlerTag << "\n");
+
+  getExecutionSession().runJITDispatchHandler(
+      [this, CallId](shared::WrapperFunctionBuffer ResultBytes) {
+        LLVM_DEBUG(dbgs() << "InProcessEPC: Returning JIT-dispatch result for "
+                             "call id "
+                          << CallId << "\n");
+        if (C->EnterMessageScope(C)) {
+          C->ReturnJITDispatchResult(C->IPCA, CallId, ResultBytes.release());
+          C->LeaveMessageScope(C);
+        }
+      },
+      ExecutorAddr::fromPtr(HandlerTag),
+      shared::WrapperFunctionBuffer(ArgBytes));
+}
+
+void InProcessEPC::callJITDispatchEntry(
+    void *IPEPC, uint64_t CallId, void *HandlerTag,
+    shared::CWrapperFunctionBuffer ArgBytes) {
+  static_cast<InProcessEPC *>(IPEPC)->callJITDispatch(CallId, HandlerTag,
+                                                      ArgBytes);
+}
+
+void InProcessEPC::returnWrapperResult(
+    uint64_t CallId, shared::CWrapperFunctionBuffer ResultBytes) {
+
+  LLVM_DEBUG(dbgs() << "InProcessEPC: Wrapper result for call id " << CallId
+                    << "\n");
+
+  IncomingWFRHandler H;
+  {
+    std::scoped_lock<std::mutex> Lock(M);
+    auto I = PendingCallWrapperResults.find(CallId);
+    if (I != PendingCallWrapperResults.end()) {
+      H = std::move(I->second);
+      PendingCallWrapperResults.erase(I);
+    }
+  }
+
+  if (!H) {
+    getExecutionSession().reportError(make_error<StringError>(
+        "InProcessEPC received result for invalid call id " + Twine(CallId),
+        inconvertibleErrorCode()));
+    return;
+  }
+
+  H(shared::WrapperFunctionBuffer(ResultBytes));
+}
+
+void InProcessEPC::returnWrapperResultEntry(
+    void *IPEPC, uint64_t CallId, shared::CWrapperFunctionBuffer ResultBytes) {
+  static_cast<InProcessEPC *>(IPEPC)->returnWrapperResult(CallId, ResultBytes);
+}
+
+} // namespace llvm::orc
diff --git a/llvm/unittests/ExecutionEngine/Orc/CMakeLists.txt b/llvm/unittests/ExecutionEngine/Orc/CMakeLists.txt
index f048a8abc295c..82744eafd3e33 100644
--- a/llvm/unittests/ExecutionEngine/Orc/CMakeLists.txt
+++ b/llvm/unittests/ExecutionEngine/Orc/CMakeLists.txt
@@ -27,6 +27,7 @@ add_llvm_unittest(OrcJITTests
   EPCGenericMemoryAccessTest.cpp
   EPCGenericDylibManagerTest.cpp
   IndirectionUtilsTest.cpp
+  InProcessEPCTest.cpp
   JITTargetMachineBuilderTest.cpp
   LazyCallThroughAndReexportsTest.cpp
   LibraryResolverTest.cpp
diff --git a/llvm/unittests/ExecutionEngine/Orc/InProcessEPCTest.cpp b/llvm/unittests/ExecutionEngine/Orc/InProcessEPCTest.cpp
new file mode 100644
index 0000000000000..3e2fdf4be1427
--- /dev/null
+++ b/llvm/unittests/ExecutionEngine/Orc/InProcessEPCTest.cpp
@@ -0,0 +1,649 @@
+//===- InProcessEPCTest.cpp -- Tests for InProcessEPC ---------------------===//
+//
+// 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/InProcessEPC.h"
+
+#include "llvm/ADT/FunctionExtras.h"
+#include "llvm/ExecutionEngine/Orc/AbsoluteSymbols.h"
+#include "llvm/ExecutionEngine/Orc/Core.h"
+#include "llvm/ExecutionEngine/Orc/Shared/WrapperFunctionUtils.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+#include <condition_variable>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <vector>
+
+using namespace llvm;
+using namespace llvm::orc;
+
+namespace {
+
+// A minimal stand-in for orc_rt::InProcessControllerAccess. Owns a refcounted
+// ConnectionImpl that mirrors the lifecycle semantics of the real one (refcount
+// + Connected + InFlightCalls), exposes hooks for tests to react to incoming
+// controller-side calls, and exposes helpers to drive cross-calls in the
+// controller -> JIT direction.
+class MockIPCA {
+public:
+  using Connection = InProcessEPC::Connection;
+  using OnCallWrapperFn = unique_function<void(
+      uint64_t CallId, void *Fn, shared::WrapperFunctionBuffer ArgBytes)>;
+  using OnReturnJITDispatchResultFn = unique_function<void(
+      uint64_t CallId, shared::WrapperFunctionBuffer ResultBytes)>;
+
+  MockIPCA() : C(new ConnectionImpl(*this)) {}
+
+  MockIPCA(const MockIPCA &) = delete;
+  MockIPCA &operator=(const MockIPCA &) = delete;
+
+  ~MockIPCA() { C->Release(C); }
+
+  Connection *getConnection() { return C; }
+
+  bool isConnected() const { return C->isConnected(); }
+
+  void setOnCallWrapper(OnCallWrapperFn F) { OnCallWrapper = std::move(F); }
+  void setOnReturnJITDispatchResult(OnReturnJITDispatchResultFn F) {
+    OnReturnJITDispatchResult = std::move(F);
+  }
+
+  // Initiate a JIT-dispatch from the controller side. Returns the chosen
+  // CallId, or std::nullopt if the message scope is closed.
+  std::optional<uint64_t>
+  callJITDispatch(void *HandlerTag, shared::WrapperFunctionBuffer ArgBytes) {
+    if (C->EnterMessageScope(C)) {
+      uint64_t CallId = NextCallId++;
+      C->CallJITDispatch(C->IPEPC, CallId, HandlerTag, ArgBytes.release());
+      C->LeaveMessageScope(C);
+      return CallId;
+    }
+    return std::nullopt;
+  }
+
+  // Send a wrapper result back for a prior CallWrapper invocation, or with
+  // an arbitrary CallId for the "unknown id" path.
+  void returnWrapperResult(uint64_t CallId,
+                           shared::WrapperFunctionBuffer ResultBytes) {
+    if (C->EnterMessageScope(C)) {
+      C->ReturnWrapperResult(C->IPEPC, CallId, ResultBytes.release());
+      C->LeaveMessageScope(C);
+    }
+  }
+
+private:
+  struct ConnectionImpl : public Connection {
+    ConnectionImpl(MockIPCA &Owner) {
+      Retain = &retainEntry;
+      Release = &releaseEntry;
+      Disconnect = &disconnectEntry;
+      EnterMessageScope = &enterMessageScopeEntry;
+      LeaveMessageScope = &leaveMessageScopeEntry;
+      IPCA = &Owner;
+      CallWrapper = &callWrapperEntry;
+      ReturnJITDispatchResult = &returnJITDispatchResultEntry;
+    }
+
+    bool isConnected() const {
+      std::scoped_lock<std::mutex> Lock(M);
+      return Connected;
+    }
+
+  private:
+    void retain() {
+      std::scoped_lock<std::mutex> Lock(M);
+      ++RefCount;
+    }
+
+    static void retainEntry(Connection *C) {
+      static_cast<ConnectionImpl *>(C)->retain();
+    }
+
+    bool release() {
+      std::scoped_lock<std::mutex> Lock(M);
+      return --RefCount == 0;
+    }
+
+    static void releaseEntry(Connection *C) {
+      if (static_cast<ConnectionImpl *>(C)->release())
+        delete static_cast<ConnectionImpl *>(C);
+    }
+
+    void disconnect() {
+      std::unique_lock<std::mutex> Lock(M);
+      if (!Connected)
+        return;
+      Connected = false;
+      CV.wait(Lock, [this]() { return InFlightCalls == 0; });
+    }
+
+    static void disconnectEntry(Connection *C) {
+      static_cast<ConnectionImpl *>(C)->disconnect();
+    }
+
+    int enterMessageScope() {
+      std::scoped_lock<std::mutex> Lock(M);
+      if (!Connected)
+        return 0;
+      ++InFlightCalls;
+      return 1;
+    }
+
+    static int enterMessageScopeEntry(Connection *C) {
+      return static_cast<ConnectionImpl *>(C)->enterMessageScope();
+    }
+
+    void leaveMessageScope() {
+      bool Notify = false;
+      {
+        std::scoped_lock<std::mutex> Lock(M);
+        --InFlightCalls;
+        if (!Connected && InFlightCalls == 0)
+          Notify = true;
+      }
+      if (Notify)
+        CV.notify_one();
+    }
+
+    static void leaveMessageScopeEntry(Connection *C) {
+      static_cast<ConnectionImpl *>(C)->leaveMessageScope();
+    }
+
+    mutable std::mutex M;
+    std::condition_variable CV;
+    bool Connected = true;
+    size_t InFlightCalls = 0;
+    size_t RefCount = 1;
+  };
+
+  static void callWrapperEntry(void *IPCA, uint64_t CallId, void *Fn,
+                               shared::CWrapperFunctionBuffer ArgBytes) {
+    auto *Self = static_cast<MockIPCA *>(IPCA);
+    shared::WrapperFunctionBuffer Buf(ArgBytes);
+    if (Self->OnCallWrapper)
+      Self->OnCallWrapper(CallId, Fn, std::move(Buf));
+  }
+
+  static void
+  returnJITDispatchResultEntry(void *IPCA, uint64_t CallId,
+                               shared::CWrapperFunctionBuffer ResultBytes) {
+    auto *Self = static_cast<MockIPCA *>(IPCA);
+    shared::WrapperFunctionBuffer Buf(ResultBytes);
+    if (Self->OnReturnJITDispatchResult)
+      Self->OnReturnJITDispatchResult(CallId, std::move(Buf));
+  }
+
+  ConnectionImpl *C;
+  uint64_t NextCallId = 0;
+  OnCallWrapperFn OnCallWrapper;
+  OnReturnJITDispatchResultFn OnReturnJITDispatchResult;
+};
+
+// Provides a BootstrapInfoAccess backed by test-supplied page-size, triple,
+// value entries, and symbol entries, with knobs to simulate iteration errors.
+class MockBootstrapInfoAccess : public InProcessEPC::BootstrapInfoAccess {
+public:
+  MockBootstrapInfoAccess() {
+    GetPageSize = &getPageSizeEntry;
+    GetTargetTriple = &getTargetTripleEntry;
+    GetNextValue = &getNextValueEntry;
+    GetNextSymbol = &getNextSymbolEntry;
+  }
+
+  void setPageSize(uint64_t PS) { PageSize = PS; }
+  void setTargetTriple(std::string TT) { TargetTriple = std::move(TT); }
+  void addValue(std::string Name, std::vector<char> Bytes) {
+    Values.push_back({std::move(Name), std::move(Bytes)});
+  }
+  void addSymbol(std::string Name, uint64_t Addr) {
+    Symbols.push_back({std::move(Name), Addr});
+  }
+  void setValueIterCorrupt() { ValuesCorrupt = true; }
+  void setSymbolIterCorrupt() { SymbolsCorrupt = true; }
+
+private:
+  static uint64_t getPageSizeEntry(void *BIA) {
+    return static_cast<MockBootstrapInfoAccess *>(BIA)->PageSize;
+  }
+
+  static const char *getTargetTripleEntry(void *BIA) {
+    auto &Self = *static_cast<MockBootstrapInfoAccess *>(BIA);
+    return Self.TargetTriple.empty() ? nullptr : Self.TargetTriple.c_str();
+  }
+
+  static int getNextValueEntry(void *BIA, const char **Name,
+                               const char **ValueBytes, uint64_t *ValueSize) {
+    auto &Self = *static_cast<MockBootstrapInfoAccess *>(BIA);
+    if (Self.NextValue == Self.Values.size())
+      return Self.ValuesCorrupt ? -1 : 0;
+    auto &V = Self.Values[Self.NextValue++];
+    *Name = V.first.c_str();
+    *ValueBytes = V.second.data();
+    *ValueSize = V.second.size();
+    return 1;
+  }
+
+  static int getNextSymbolEntry(void *BIA, const char **Name, uint64_t *Addr) {
+    auto &Self = *static_cast<MockBootstrapInfoAccess *>(BIA);
+    if (Self.NextSymbol == Self.Symbols.size())
+      return Self.SymbolsCorrupt ? -1 : 0;
+    auto &S = Self.Symbols[Self.NextSymbol++];
+    *Name = S.first.c_str();
+    *Addr = S.second;
+    return 1;
+  }
+
+  uint64_t PageSize = 4096;
+  std::string TargetTriple = "x86_64-unknown-linux-gnu";
+  std::vector<std::pair<std::string, std::vector<char>>> Values;
+  std::vector<std::pair<std::string, uint64_t>> Symbols;
+  size_t NextValue = 0;
+  size_t NextSymbol = 0;
+  bool ValuesCorrupt = false;
+  bool SymbolsCorrupt = false;
+};
+
+Expected<std::unique_ptr<InProcessEPC>>
+createIPEPC(MockIPCA &IPCA, MockBootstrapInfoAccess &BIA) {
+  return InProcessEPC::Create(IPCA.getConnection(), &BIA);
+}
+
+} // namespace
+
+TEST(InProcessEPCTest, CreateSuccess) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  BIA.setPageSize(8192);
+  BIA.setTargetTriple("arm64-apple-darwin");
+  BIA.addValue("greeting", {'h', 'i'});
+  BIA.addSymbol("malloc", 0x1234);
+
+  auto EPC = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPC, Succeeded());
+
+  EXPECT_EQ((*EPC)->getPageSize(), 8192U);
+  EXPECT_EQ((*EPC)->getTargetTriple().str(), "arm64-apple-darwin");
+
+  const auto &VMap = (*EPC)->getBootstrapMap();
+  ASSERT_EQ(VMap.size(), 1U);
+  auto VI = VMap.find("greeting");
+  ASSERT_NE(VI, VMap.end());
+  EXPECT_EQ(std::string(VI->second.begin(), VI->second.end()), "hi");
+
+  const auto &SMap = (*EPC)->getBootstrapSymbolsMap();
+  ASSERT_EQ(SMap.size(), 1U);
+  auto SI = SMap.find("malloc");
+  ASSERT_NE(SI, SMap.end());
+  EXPECT_EQ(SI->second, ExecutorAddr(0x1234));
+
+  auto *C = IPCA.getConnection();
+  EXPECT_NE(C->IPEPC, nullptr);
+  EXPECT_NE(C->CallJITDispatch, nullptr);
+  EXPECT_NE(C->ReturnWrapperResult, nullptr);
+
+  cantFail((*EPC)->disconnect());
+}
+
+TEST(InProcessEPCTest, CreateFailsOnZeroPageSize) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  BIA.setPageSize(0);
+
+  auto EPC = createIPEPC(IPCA, BIA);
+  EXPECT_THAT_EXPECTED(std::move(EPC), Failed());
+  EXPECT_FALSE(IPCA.isConnected())
+      << "Failed Create should leave the connection torn down";
+}
+
+TEST(InProcessEPCTest, CreateFailsOnEmptyTriple) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  BIA.setTargetTriple("");
+
+  auto EPC = createIPEPC(IPCA, BIA);
+  EXPECT_THAT_EXPECTED(std::move(EPC), Failed());
+  EXPECT_FALSE(IPCA.isConnected());
+}
+
+TEST(InProcessEPCTest, CreateFailsOnDuplicateBootstrapValue) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  BIA.addValue("dup", {'a'});
+  BIA.addValue("dup", {'b'});
+
+  auto EPC = createIPEPC(IPCA, BIA);
+  EXPECT_THAT_EXPECTED(std::move(EPC), Failed());
+  EXPECT_FALSE(IPCA.isConnected());
+}
+
+TEST(InProcessEPCTest, CreateFailsOnDuplicateBootstrapSymbol) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  BIA.addSymbol("dup", 0x1);
+  BIA.addSymbol("dup", 0x2);
+
+  auto EPC = createIPEPC(IPCA, BIA);
+  EXPECT_THAT_EXPECTED(std::move(EPC), Failed());
+  EXPECT_FALSE(IPCA.isConnected());
+}
+
+TEST(InProcessEPCTest, CreateFailsOnCorruptedValueIteration) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  BIA.setValueIterCorrupt();
+
+  auto EPC = createIPEPC(IPCA, BIA);
+  EXPECT_THAT_EXPECTED(std::move(EPC), Failed());
+  EXPECT_FALSE(IPCA.isConnected());
+}
+
+TEST(InProcessEPCTest, CreateFailsOnCorruptedSymbolIteration) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  BIA.setSymbolIterCorrupt();
+
+  auto EPC = createIPEPC(IPCA, BIA);
+  EXPECT_THAT_EXPECTED(std::move(EPC), Failed());
+  EXPECT_FALSE(IPCA.isConnected());
+}
+
+#ifndef NDEBUG
+TEST(InProcessEPCDeathTest, CreateAssertsOnNullIPCA) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  IPCA.getConnection()->IPCA = nullptr;
+
+  EXPECT_DEATH(
+      { (void)createIPEPC(IPCA, BIA); }, "C->IPCA not set by controller");
+}
+#endif
+
+TEST(InProcessEPCTest, CallWrapperAsyncSuccess) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+  auto EPC = std::move(*EPCExp);
+
+  // Echo back whatever the JIT side sent us.
+  IPCA.setOnCallWrapper(
+      [&](uint64_t CallId, void *, shared::WrapperFunctionBuffer ArgBytes) {
+        IPCA.returnWrapperResult(CallId, std::move(ArgBytes));
+      });
+
+  std::optional<std::string> Result;
+  std::string Payload = "hello";
+  EPC->callWrapperAsync(ExecutorAddr(0x42),
+                        ExecutorProcessControl::RunInPlace()(
+                            [&](shared::WrapperFunctionBuffer R) {
+                              ASSERT_FALSE(R.getOutOfBandError())
+                                  << "Unexpected OOB error: "
+                                  << R.getOutOfBandError();
+                              Result = std::string(R.data(), R.size());
+                            }),
+                        ArrayRef<char>(Payload.data(), Payload.size()));
+
+  ASSERT_TRUE(Result);
+  EXPECT_EQ(*Result, Payload);
+
+  cantFail(EPC->disconnect());
+}
+
+TEST(InProcessEPCTest, CallWrapperAsyncOutOfBandError) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+  auto EPC = std::move(*EPCExp);
+
+  IPCA.setOnCallWrapper(
+      [&](uint64_t CallId, void *, shared::WrapperFunctionBuffer) {
+        IPCA.returnWrapperResult(
+            CallId,
+            shared::WrapperFunctionBuffer::createOutOfBandError("simulated"));
+      });
+
+  std::optional<std::string> ErrMsg;
+  std::string Payload = "x";
+  EPC->callWrapperAsync(ExecutorAddr(0x42),
+                        ExecutorProcessControl::RunInPlace()(
+                            [&](shared::WrapperFunctionBuffer R) {
+                              if (const char *Msg = R.getOutOfBandError())
+                                ErrMsg = Msg;
+                            }),
+                        ArrayRef<char>(Payload.data(), Payload.size()));
+
+  ASSERT_TRUE(ErrMsg);
+  EXPECT_EQ(*ErrMsg, "simulated");
+
+  cantFail(EPC->disconnect());
+}
+
+TEST(InProcessEPCTest, CallWrapperAsyncFailsAfterDisconnect) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+  auto EPC = std::move(*EPCExp);
+
+  cantFail(EPC->disconnect());
+
+  std::optional<std::string> ErrMsg;
+  std::string Payload = "x";
+  EPC->callWrapperAsync(ExecutorAddr(0x42),
+                        ExecutorProcessControl::RunInPlace()(
+                            [&](shared::WrapperFunctionBuffer R) {
+                              if (const char *Msg = R.getOutOfBandError())
+                                ErrMsg = Msg;
+                            }),
+                        ArrayRef<char>(Payload.data(), Payload.size()));
+
+  ASSERT_TRUE(ErrMsg);
+  EXPECT_EQ(*ErrMsg, "connection closed");
+}
+
+TEST(InProcessEPCTest, DisconnectDrainsPendingCallWrapperHandlers) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+  auto EPC = std::move(*EPCExp);
+
+  // Mock receives the call but never sends a result.
+  IPCA.setOnCallWrapper([](uint64_t, void *, shared::WrapperFunctionBuffer) {});
+
+  std::optional<std::string> ErrMsg;
+  std::string Payload = "payload";
+  EPC->callWrapperAsync(ExecutorAddr(0x42),
+                        ExecutorProcessControl::RunInPlace()(
+                            [&](shared::WrapperFunctionBuffer R) {
+                              if (const char *Msg = R.getOutOfBandError())
+                                ErrMsg = Msg;
+                            }),
+                        ArrayRef<char>(Payload.data(), Payload.size()));
+
+  ASSERT_FALSE(ErrMsg) << "Handler fired before disconnect";
+
+  cantFail(EPC->disconnect());
+
+  ASSERT_TRUE(ErrMsg);
+  EXPECT_EQ(*ErrMsg, "disconnected");
+}
+
+TEST(InProcessEPCTest, DestructorDrainsPendingCallWrapperHandlers) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+  auto EPC = std::move(*EPCExp);
+
+  IPCA.setOnCallWrapper([](uint64_t, void *, shared::WrapperFunctionBuffer) {});
+
+  std::optional<std::string> ErrMsg;
+  std::string Payload = "x";
+  EPC->callWrapperAsync(ExecutorAddr(0x42),
+                        ExecutorProcessControl::RunInPlace()(
+                            [&](shared::WrapperFunctionBuffer R) {
+                              if (const char *Msg = R.getOutOfBandError())
+                                ErrMsg = Msg;
+                            }),
+                        ArrayRef<char>(Payload.data(), Payload.size()));
+
+  ASSERT_FALSE(ErrMsg);
+
+  // Drop the IPEPC without an explicit disconnect call: the destructor must
+  // still drive doDisconnect and drain the pending handler.
+  EPC.reset();
+
+  ASSERT_TRUE(ErrMsg);
+  EXPECT_EQ(*ErrMsg, "disconnected");
+}
+
+TEST(InProcessEPCTest, DoubleDisconnectIsIdempotent) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+  auto EPC = std::move(*EPCExp);
+
+  cantFail(EPC->disconnect());
+  EXPECT_FALSE(IPCA.isConnected());
+
+  // Second disconnect (here: via destructor) must not re-fire any handlers or
+  // double-release the connection -- the test would deadlock/segfault if it
+  // did.
+  EPC.reset();
+  EXPECT_FALSE(IPCA.isConnected());
+}
+
+namespace {
+
+// Custom EPC-attached ES that hands the EPC ownership to the session but
+// retains a raw pointer for tests that need to drive the EPC directly.
+struct SessionFixture {
+  SessionFixture(std::unique_ptr<InProcessEPC> EPC)
+      : EPCPtr(EPC.get()), ES(std::move(EPC)),
+        JD(ES.createBareJITDylib("TestJD")) {}
+
+  ~SessionFixture() { cantFail(ES.endSession()); }
+
+  InProcessEPC *EPCPtr;
+  ExecutionSession ES;
+  JITDylib &JD;
+};
+
+} // namespace
+
+TEST(InProcessEPCTest, ReturnWrapperResultForInvalidCallIdIsReported) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+
+  SessionFixture Fix(std::move(*EPCExp));
+
+  std::string CapturedErr;
+  Fix.ES.setErrorReporter(
+      [&](Error E) { CapturedErr = toString(std::move(E)); });
+
+  IPCA.returnWrapperResult(
+      /*CallId=*/0xdead, shared::WrapperFunctionBuffer::copyFrom("ignored", 7));
+
+  EXPECT_NE(CapturedErr.find("invalid call id"), std::string::npos)
+      << "Expected invalid-call-id report, got: " << CapturedErr;
+}
+
+TEST(InProcessEPCTest, JITDispatchSuccess) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+
+  SessionFixture Fix(std::move(*EPCExp));
+
+  constexpr ExecutorAddr TagAddr(0xCAFEBABE);
+  auto Tag = Fix.ES.intern("echo_tag");
+  cantFail(Fix.JD.define(
+      absoluteSymbols({{Tag, {TagAddr, JITSymbolFlags::Exported}}})));
+
+  ExecutionSession::JITDispatchHandlerAssociationMap Assocs;
+  Assocs[Tag] = [](ExecutionSession::SendResultFunction SendResult,
+                   const char *ArgData, size_t ArgSize) {
+    SendResult(shared::WrapperFunctionBuffer::copyFrom(ArgData, ArgSize));
+  };
+  cantFail(Fix.ES.registerJITDispatchHandlers(Fix.JD, std::move(Assocs)));
+
+  std::optional<std::string> Result;
+  std::optional<uint64_t> RxCallId;
+  IPCA.setOnReturnJITDispatchResult(
+      [&](uint64_t CallId, shared::WrapperFunctionBuffer ResultBytes) {
+        RxCallId = CallId;
+        Result = std::string(ResultBytes.data(), ResultBytes.size());
+      });
+
+  auto SentCallId =
+      IPCA.callJITDispatch(TagAddr.toPtr<void *>(),
+                           shared::WrapperFunctionBuffer::copyFrom("ping", 4));
+
+  ASSERT_TRUE(SentCallId);
+  ASSERT_TRUE(Result);
+  EXPECT_EQ(*Result, "ping");
+  ASSERT_TRUE(RxCallId);
+  EXPECT_EQ(*RxCallId, *SentCallId);
+}
+
+TEST(InProcessEPCTest, JITDispatchUnknownHandler) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+
+  SessionFixture Fix(std::move(*EPCExp));
+
+  std::optional<std::string> ErrMsg;
+  IPCA.setOnReturnJITDispatchResult(
+      [&](uint64_t, shared::WrapperFunctionBuffer R) {
+        if (const char *Msg = R.getOutOfBandError())
+          ErrMsg = Msg;
+      });
+
+  auto SentCallId =
+      IPCA.callJITDispatch(reinterpret_cast<void *>(0xdeadbeef),
+                           shared::WrapperFunctionBuffer::copyFrom("x", 1));
+  ASSERT_TRUE(SentCallId);
+
+  ASSERT_TRUE(ErrMsg)
+      << "Expected ReturnJITDispatchResult to deliver an OOB error";
+}
+
+TEST(InProcessEPCTest, JITDispatchAfterDisconnectIsDropped) {
+  MockIPCA IPCA;
+  MockBootstrapInfoAccess BIA;
+  auto EPCExp = createIPEPC(IPCA, BIA);
+  ASSERT_THAT_EXPECTED(EPCExp, Succeeded());
+
+  SessionFixture Fix(std::move(*EPCExp));
+
+  bool ResultFired = false;
+  IPCA.setOnReturnJITDispatchResult(
+      [&](uint64_t, shared::WrapperFunctionBuffer) { ResultFired = true; });
+
+  // Tear down the connection. After this the controller side's
+  // EnterMessageScope returns 0 and CallJITDispatch is never invoked.
+  cantFail(Fix.EPCPtr->disconnect());
+
+  auto SentCallId =
+      IPCA.callJITDispatch(reinterpret_cast<void *>(0xdeadbeef),
+                           shared::WrapperFunctionBuffer::copyFrom("x", 1));
+
+  EXPECT_FALSE(SentCallId)
+      << "callJITDispatch should be dropped after disconnect";
+  EXPECT_FALSE(ResultFired);
+}



More information about the llvm-commits mailing list