[llvm] [orc-rt] Add asCCallback utility -- C callbacks for methods. (PR #210048)

Lang Hames via llvm-commits llvm-commits at lists.llvm.org
Thu Jul 16 06:03:42 PDT 2026


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

Adds orc_rt::asCCallback<&Class::method>, which produces a C-ABI function pointer that forwards to the given member function. The first parameter of the generated trampoline is an opaque context pointer (void
* for non-const methods, const void * for const methods) that is cast back to the class type and used as the receiver:

  struct Counter { void inc(int N) { Count += N; } int Count = 0; };

  Counter C; void (*CB)(void *, int) = asCCallback<&Counter::inc>; CB(asCCallbackContext<&Counter::inc>(C), 2); // C.Count == 2

asCCallbackContext produces the matching context pointer via static_cast, so any base-class offset is applied and const-ness is preserved; pairing it with asCCallback by the same method keeps things correct under inheritance and const-qualification. (A raw cast of &C would give the trampoline the wrong `this` when method's class is a base at a non-zero offset.)

Specializations cover the const x noexcept combinations. The trampoline is always noexcept -- a C caller cannot unwind a C++ exception, so an escaping one terminates -- and return/argument types are restricted to C-compatible (non-reference, trivially copyable) types via static_assert.

>From 30ef8806243c89dda051a74da0d2d1a990b10b03 Mon Sep 17 00:00:00 2001
From: Lang Hames <lhames at gmail.com>
Date: Thu, 16 Jul 2026 22:05:32 +1000
Subject: [PATCH] [orc-rt] Add asCCallback utility -- C callbacks for methods.

Adds orc_rt::asCCallback<&Class::method>, which produces a C-ABI
function pointer that forwards to the given member function. The first
parameter of the generated trampoline is an opaque context pointer (void
* for non-const methods, const void * for const methods) that is cast
back to the class type and used as the receiver:

  struct Counter {
    void inc(int N) { Count += N; }
    int Count = 0;
  };

  Counter C;
  void (*CB)(void *, int) = asCCallback<&Counter::inc>;
  CB(asCCallbackContext<&Counter::inc>(C), 2); // C.Count == 2

asCCallbackContext produces the matching context pointer via
static_cast, so any base-class offset is applied and const-ness is
preserved; pairing it with asCCallback by the same method keeps things
correct under inheritance and const-qualification. (A raw cast of &C
would give the trampoline the wrong `this` when method's class is a base
at a non-zero offset.)

Specializations cover the const x noexcept combinations. The trampoline
is always noexcept -- a C caller cannot unwind a C++ exception, so an
escaping one terminates -- and return/argument types are restricted to
C-compatible (non-reference, trivially copyable) types via
static_assert.
---
 orc-rt/include/CMakeLists.txt      |   1 +
 orc-rt/include/orc-rt/CCallback.h  | 122 +++++++++++++++++++++++++++
 orc-rt/test/unit/CCallbackTest.cpp | 129 +++++++++++++++++++++++++++++
 orc-rt/test/unit/CMakeLists.txt    |   1 +
 4 files changed, 253 insertions(+)
 create mode 100644 orc-rt/include/orc-rt/CCallback.h
 create mode 100644 orc-rt/test/unit/CCallbackTest.cpp

diff --git a/orc-rt/include/CMakeLists.txt b/orc-rt/include/CMakeLists.txt
index 43988fdc8ede8..9663f4f3291c4 100644
--- a/orc-rt/include/CMakeLists.txt
+++ b/orc-rt/include/CMakeLists.txt
@@ -8,6 +8,7 @@ set(ORC_RT_HEADERS
     orc-rt/AllocAction.h
     orc-rt/BitmaskEnum.h
     orc-rt/BootstrapInfo.h
+    orc-rt/CCallback.h
     orc-rt/Compiler.h
     orc-rt/Error.h
     orc-rt/ExecutorAddress.h
diff --git a/orc-rt/include/orc-rt/CCallback.h b/orc-rt/include/orc-rt/CCallback.h
new file mode 100644
index 0000000000000..111c6a8cd311b
--- /dev/null
+++ b/orc-rt/include/orc-rt/CCallback.h
@@ -0,0 +1,122 @@
+//===----- CCallback.h - utility for generating C callbacks -----*- 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Utilities for generating C callback pointers from methods.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef ORC_RT_CCALLBACK_H
+#define ORC_RT_CCALLBACK_H
+
+#include <type_traits>
+
+namespace orc_rt {
+
+namespace detail {
+// asCCallback produces C-ABI function pointers, so wrapped methods may only
+// use C-compatible parameter and return types: no references, and trivially
+// copyable (which every genuine C type is). C++ types with non-trivial
+// move/copy semantics cannot cross a C boundary, so they are rejected here
+// rather than silently accommodated.
+template <typename T>
+inline constexpr bool isCCompatibleArg =
+    !std::is_reference_v<T> && std::is_trivially_copyable_v<T>;
+
+template <typename T>
+inline constexpr bool isCCompatibleRet =
+    std::is_void_v<T> || isCCompatibleArg<T>;
+
+// Four specializations cover the const x noexcept qualifier combinations on the
+// member-function pointer. `fn` is always noexcept regardless of Meth's
+// noexcept-ness; const methods take a `const void *` context and expose
+// `class_type` as `const ClassT` so asCCallbackContext stays const-correct.
+// Because every argument is a trivially copyable value, `Args` is passed on
+// directly -- std::forward would be a no-op for such types.
+template <typename MethT, MethT Meth> struct CCallbackImpl;
+
+template <typename RetT, typename ClassT, typename... ArgTs,
+          RetT (ClassT::*Meth)(ArgTs...)>
+struct CCallbackImpl<RetT (ClassT::*)(ArgTs...), Meth> {
+  using class_type = ClassT;
+  static RetT fn(void *Obj, ArgTs... Args) noexcept {
+    static_assert(isCCompatibleRet<RetT> && (... && isCCompatibleArg<ArgTs>),
+                  "asCCallback requires C-compatible (non-reference, trivially "
+                  "copyable) return and argument types");
+    return (reinterpret_cast<ClassT *>(Obj)->*Meth)(Args...);
+  }
+};
+
+template <typename RetT, typename ClassT, typename... ArgTs,
+          RetT (ClassT::*Meth)(ArgTs...) noexcept>
+struct CCallbackImpl<RetT (ClassT::*)(ArgTs...) noexcept, Meth> {
+  using class_type = ClassT;
+  static RetT fn(void *Obj, ArgTs... Args) noexcept {
+    static_assert(isCCompatibleRet<RetT> && (... && isCCompatibleArg<ArgTs>),
+                  "asCCallback requires C-compatible (non-reference, trivially "
+                  "copyable) return and argument types");
+    return (reinterpret_cast<ClassT *>(Obj)->*Meth)(Args...);
+  }
+};
+
+template <typename RetT, typename ClassT, typename... ArgTs,
+          RetT (ClassT::*Meth)(ArgTs...) const>
+struct CCallbackImpl<RetT (ClassT::*)(ArgTs...) const, Meth> {
+  using class_type = const ClassT;
+  static RetT fn(const void *Obj, ArgTs... Args) noexcept {
+    static_assert(isCCompatibleRet<RetT> && (... && isCCompatibleArg<ArgTs>),
+                  "asCCallback requires C-compatible (non-reference, trivially "
+                  "copyable) return and argument types");
+    return (reinterpret_cast<const ClassT *>(Obj)->*Meth)(Args...);
+  }
+};
+
+template <typename RetT, typename ClassT, typename... ArgTs,
+          RetT (ClassT::*Meth)(ArgTs...) const noexcept>
+struct CCallbackImpl<RetT (ClassT::*)(ArgTs...) const noexcept, Meth> {
+  using class_type = const ClassT;
+  static RetT fn(const void *Obj, ArgTs... Args) noexcept {
+    static_assert(isCCompatibleRet<RetT> && (... && isCCompatibleArg<ArgTs>),
+                  "asCCallback requires C-compatible (non-reference, trivially "
+                  "copyable) return and argument types");
+    return (reinterpret_cast<const ClassT *>(Obj)->*Meth)(Args...);
+  }
+};
+
+} // namespace detail
+
+/// Produces a C-callable function pointer that forwards to member function
+/// `Meth`. The returned pointer has signature
+///
+///   RetT (*)(CtxT *Ctx, ArgTs...) noexcept
+///
+/// where `CtxT` is `void` for non-const methods and `const void` for const
+/// methods. `Ctx` must point to the `Meth`'s class subobject (see
+/// asCCallbackContext); it is cast straight back to the class type, so a raw
+/// pointer to a derived object is NOT acceptable when the class is a base at a
+/// non-zero offset.
+///
+/// The trampoline is `noexcept`: a C caller cannot unwind a C++ exception, so
+/// an exception escaping `Meth` calls std::terminate. This matches the ORC
+/// runtime convention that methods wrapped as C callbacks do not throw.
+template <auto Meth>
+constexpr auto asCCallback = detail::CCallbackImpl<decltype(Meth), Meth>::fn;
+
+/// Returns the context pointer to pass alongside `asCCallback<Meth>`. It
+/// static_casts `&Obj` to `Meth`'s class, which (a) applies any base-class
+/// offset so the trampoline's cast recovers the correct `this`, and (b)
+/// preserves const-ness (yielding `const ClassT *` for const methods). Always
+/// obtain the context this way rather than casting `&Obj` directly, so the two
+/// halves compose correctly under inheritance and const-qualification.
+template <auto Meth, typename ObjT> inline auto *asCCallbackContext(ObjT &Obj) {
+  return static_cast<
+      typename detail::CCallbackImpl<decltype(Meth), Meth>::class_type *>(&Obj);
+}
+
+} // namespace orc_rt
+
+#endif // ORC_RT_CCALLBACK_H
diff --git a/orc-rt/test/unit/CCallbackTest.cpp b/orc-rt/test/unit/CCallbackTest.cpp
new file mode 100644
index 0000000000000..4a2f50920649a
--- /dev/null
+++ b/orc-rt/test/unit/CCallbackTest.cpp
@@ -0,0 +1,129 @@
+//===- CCallbackTest.cpp --------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Tests for orc-rt's CCallback.h APIs.
+//
+//===----------------------------------------------------------------------===//
+
+#include "orc-rt/CCallback.h"
+
+#include "gtest/gtest.h"
+
+using namespace orc_rt;
+
+namespace {
+
+// Increments an int through a pointer member. Because the pointer is const in
+// a const method but its pointee is not, even the const methods can mutate
+// observable state -- this lets the const-callback tests confirm the wrapped
+// method actually ran, and also exercises RetT / ArgTs plumbing.
+class Incrementer {
+public:
+  Incrementer(int *P) : P(P) {}
+
+  void inc() { ++*P; }
+  void incNoexcept() noexcept { ++*P; }
+  void incConst() const { ++*P; }
+  void incConstNoexcept() const noexcept { ++*P; }
+
+  // Non-void return and by-value argument.
+  int advance(int Y) {
+    *P += Y;
+    return *P;
+  }
+
+private:
+  int *P;
+};
+
+// A non-empty base placed *before* Incrementer so that the Incrementer
+// subobject lands at a non-zero offset inside DerivedIncrementer. This is what
+// makes context-pointer adjustment observable: handing the callback a raw
+// pointer to the derived object (rather than the adjusted subobject pointer)
+// would compute the wrong `this`.
+class NonEmptyBase {
+  [[maybe_unused]] void *Pad = nullptr;
+};
+
+class DerivedIncrementer : public NonEmptyBase, public Incrementer {
+public:
+  DerivedIncrementer(int *P) : Incrementer(P) {}
+};
+
+} // namespace
+
+// C-style trampolines that only know the callback as a plain function pointer,
+// mirroring how a C API would store and invoke it.
+static void invokeVoidVoid(void (*Callback)(void *Ctx), void *Ctx) {
+  Callback(Ctx);
+}
+
+static void invokeVoidVoidConst(void (*Callback)(const void *Ctx),
+                                const void *Ctx) {
+  Callback(Ctx);
+}
+
+TEST(CCallbackTest, NonConstVoidMethod) {
+  int X = 0;
+  Incrementer I(&X);
+  invokeVoidVoid(asCCallback<&Incrementer::inc>,
+                 asCCallbackContext<&Incrementer::inc>(I));
+  EXPECT_EQ(X, 1);
+}
+
+TEST(CCallbackTest, NoexceptVoidMethod) {
+  int X = 0;
+  Incrementer I(&X);
+  // A noexcept source method must match a specialization and yield a noexcept
+  // function pointer; the explicit type here asserts both.
+  void (*Callback)(void *) noexcept = asCCallback<&Incrementer::incNoexcept>;
+  invokeVoidVoid(Callback, asCCallbackContext<&Incrementer::incNoexcept>(I));
+  EXPECT_EQ(X, 1);
+}
+
+TEST(CCallbackTest, ConstVoidMethod) {
+  int X = 0;
+  const Incrementer I(&X);
+  invokeVoidVoidConst(asCCallback<&Incrementer::incConst>,
+                      asCCallbackContext<&Incrementer::incConst>(I));
+  EXPECT_EQ(X, 1);
+}
+
+TEST(CCallbackTest, ConstNoexceptVoidMethod) {
+  int X = 0;
+  const Incrementer I(&X);
+  void (*Callback)(const void *) noexcept =
+      asCCallback<&Incrementer::incConstNoexcept>;
+  invokeVoidVoidConst(Callback,
+                      asCCallbackContext<&Incrementer::incConstNoexcept>(I));
+  EXPECT_EQ(X, 1);
+}
+
+TEST(CCallbackTest, ForwardsArgumentAndReturnValue) {
+  int X = 10;
+  Incrementer I(&X);
+  int (*Callback)(void *, int) = asCCallback<&Incrementer::advance>;
+  EXPECT_EQ(Callback(asCCallbackContext<&Incrementer::advance>(I), 5), 15);
+  EXPECT_EQ(X, 15);
+}
+
+TEST(CCallbackTest, NonConstContextWithBaseOffset) {
+  int X = 0;
+  DerivedIncrementer DI(&X);
+  invokeVoidVoid(asCCallback<&Incrementer::inc>,
+                 asCCallbackContext<&Incrementer::inc>(DI));
+  EXPECT_EQ(X, 1);
+}
+
+TEST(CCallbackTest, ConstContextWithBaseOffset) {
+  int X = 0;
+  const DerivedIncrementer DI(&X);
+  invokeVoidVoidConst(asCCallback<&Incrementer::incConst>,
+                      asCCallbackContext<&Incrementer::incConst>(DI));
+  EXPECT_EQ(X, 1);
+}
diff --git a/orc-rt/test/unit/CMakeLists.txt b/orc-rt/test/unit/CMakeLists.txt
index e1b81e1ad6b1e..7485a18ed5c36 100644
--- a/orc-rt/test/unit/CMakeLists.txt
+++ b/orc-rt/test/unit/CMakeLists.txt
@@ -15,6 +15,7 @@ add_orc_rt_unittest(CoreTests
   AllocActionTest.cpp
   BitmaskEnumTest.cpp
   BootstrapInfoTest.cpp
+  CCallbackTest.cpp
   CallSPSCITest.cpp
   CallableTraitsHelperTest.cpp
   CommandLineTest.cpp



More information about the llvm-commits mailing list