From 4458b606e6d1951e84f7e5234f6960cf7ed31be9 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev Date: Mon, 12 Oct 2026 10:57:26 +0000 Subject: [PATCH 1/2] Export CppInterOp's dispatch entry point from libCore. CppInterOp clients such as cppjit attach to an interpreter through Cpp::LoadDispatchAPI(), which loads a library and looks up CppGetProcAddress in it. In ROOT that entry point lives in libCling, which TROOT loads RTLD_LOCAL and only when the interpreter starts. A client running next to ROOT cannot reach it without knowing libCling's path and starting ROOT first, so it loads its own CppInterOp and LLVM instead: a second interpreter, more memory, and two LLVMs in one process. Export CppGetProcAddress from libCore and forward it to libCling's through the handle TROOT already holds, starting the interpreter on first use. A client can then pass libCore, found through $ROOTSYS, to Cpp::LoadDispatchAPI() and share ROOT's interpreter; libCling stays loaded once and RTLD_LOCAL. PyROOT is unaffected and keeps loading the dispatch API from libCling. The test loads libCore the way such a client does, declares a variable through the dispatched Process() and reads it back through gInterpreter. --- core/base/src/TROOT.cxx | 19 +++++++++++++ core/metacling/test/CMakeLists.txt | 6 +++++ core/metacling/test/TClingDispatchTests.cxx | 30 +++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 core/metacling/test/TClingDispatchTests.cxx diff --git a/core/base/src/TROOT.cxx b/core/base/src/TROOT.cxx index 4603c0bf6979e..40b5d0f036a2c 100644 --- a/core/base/src/TROOT.cxx +++ b/core/base/src/TROOT.cxx @@ -2320,6 +2320,25 @@ void TROOT::InitInterpreter() fInterpreter->Initialize(); } +//////////////////////////////////////////////////////////////////////////////// +/// Return the CppInterOp API function `funcName` of ROOT's interpreter, starting +/// the interpreter if needed. libCling implements this lookup but is loaded +/// RTLD_LOCAL; exporting it from libCore lets a CppInterOp client such as cppjit +/// pass libCore to Cpp::LoadDispatchAPI() and share ROOT's interpreter instead of +/// loading its own LLVM. + +extern "C" void (*CppGetProcAddress(const char *funcName))() +{ + using GetProcAddress_t = void (*(const char *))(); + static GetProcAddress_t *getProcAddress = [] { + TInterpreter::Instance(); // Sets gInterpreterLib. + auto f = reinterpret_cast(dlsym(gInterpreterLib, "CppGetProcAddress")); + // In rootcling gInterpreterLib is RTLD_DEFAULT, which may find this function. + return f == &CppGetProcAddress ? nullptr : f; + }(); + return getProcAddress ? getProcAddress(funcName) : nullptr; +} + //////////////////////////////////////////////////////////////////////////////// /// Helper function used by TClass::GetClass(). /// This function attempts to load the dictionary for 'classname' diff --git a/core/metacling/test/CMakeLists.txt b/core/metacling/test/CMakeLists.txt index eaec1bfd31863..c8177c58f829e 100644 --- a/core/metacling/test/CMakeLists.txt +++ b/core/metacling/test/CMakeLists.txt @@ -32,6 +32,12 @@ ROOT_ADD_GTEST( ROOT_ADD_GTEST(TClingLoadUnloadFile TClingLoadUnloadFileTests.cxx LIBRARIES Core RIO) +if(NOT MSVC) + ROOT_ADD_GTEST(TClingDispatch TClingDispatchTests.cxx LIBRARIES Core) + target_compile_definitions(TClingDispatch PRIVATE ROOT_LIBCORE="$" + ROOT_LIBCLING="$") +endif() + if(pyroot) ROOT_ADD_PYUNITTEST(MetaClingTests MetaClingTests.py) endif() diff --git a/core/metacling/test/TClingDispatchTests.cxx b/core/metacling/test/TClingDispatchTests.cxx new file mode 100644 index 0000000000000..87747191ed2f5 --- /dev/null +++ b/core/metacling/test/TClingDispatchTests.cxx @@ -0,0 +1,30 @@ +#include "TInterpreter.h" + +#include "gtest/gtest.h" + +#include + +using GetProcAddress_t = void (*(const char *))(); + +// A CppInterOp client such as cppjit loads the library that exports +// CppGetProcAddress and looks the API up by name. Through libCore it must get +// the functions of ROOT's interpreter. +TEST(TClingDispatch, CppGetProcAddressFromLibCore) +{ + void *libCore = dlopen(ROOT_LIBCORE, RTLD_LAZY | RTLD_LOCAL); + ASSERT_NE(libCore, nullptr) << dlerror(); + auto getProcAddress = reinterpret_cast(dlsym(libCore, "CppGetProcAddress")); + ASSERT_NE(getProcAddress, nullptr) << dlerror(); + + auto process = reinterpret_cast(getProcAddress("Process")); + ASSERT_NE(process, nullptr); + EXPECT_EQ(process("int TClingDispatchValue = 42;"), 0); + EXPECT_EQ(gInterpreter->Calc("TClingDispatchValue"), 42); + EXPECT_EQ(getProcAddress("NoSuchFunction"), nullptr); + + void *libCling = dlopen(ROOT_LIBCLING, RTLD_LAZY | RTLD_LOCAL | RTLD_NOLOAD); + ASSERT_NE(libCling, nullptr) << "libCling should be loaded once, by ROOT"; + auto fromLibCling = reinterpret_cast(dlsym(libCling, "CppGetProcAddress")); + ASSERT_NE(fromLibCling, nullptr) << dlerror(); + EXPECT_EQ(getProcAddress("Process"), fromLibCling("Process")); +} From 7b0c6e3d1cf07dcd4bdf584fa8ebe951fc5486c5 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev Date: Tue, 13 Oct 2026 05:19:46 +0000 Subject: [PATCH 2/2] [core] Link the TClingDispatch test against libdl The test calls dlopen() and dlsym() itself. With glibc older than 2.34, for instance on alma8, these live in libdl, which must be linked explicitly. --- core/metacling/test/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/metacling/test/CMakeLists.txt b/core/metacling/test/CMakeLists.txt index c8177c58f829e..092d9a9638fbc 100644 --- a/core/metacling/test/CMakeLists.txt +++ b/core/metacling/test/CMakeLists.txt @@ -33,7 +33,7 @@ ROOT_ADD_GTEST( ROOT_ADD_GTEST(TClingLoadUnloadFile TClingLoadUnloadFileTests.cxx LIBRARIES Core RIO) if(NOT MSVC) - ROOT_ADD_GTEST(TClingDispatch TClingDispatchTests.cxx LIBRARIES Core) + ROOT_ADD_GTEST(TClingDispatch TClingDispatchTests.cxx LIBRARIES Core ${CMAKE_DL_LIBS}) target_compile_definitions(TClingDispatch PRIVATE ROOT_LIBCORE="$" ROOT_LIBCLING="$") endif()