[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