Skip to content

Commit fc2e662

Browse files
authored
[jit] Add a layered probe chain for relocatable OOP runtime discovery. (#942)
* [jit] Add a layered probe chain for relocatable OOP runtime discovery. The bake-at-build-time install path from `[jit] Self-contain the OOP-JIT runtime in CppInterOp's distribution.` is fragile under relocation: an installed CppInterOp moved to a different prefix (rpm `--prefix`, conda relocation, AppImage) can't find the runtime parts because the macro holds the original `CMAKE_INSTALL_FULL_LIBDIR`. `configureBundledOOPRuntime` now probes a layered list, most-reliable first, before falling back to the baked path: 1. `$CPPINTEROP_RUNTIME_DIR` -- sysadmin override; trumps every other source so packagers can point CppInterOp at a runtime extracted to a separate location. 2. `<dir of libclangCppInterOp>/cppinterop-rt` -- self-DSO discovery via `dladdr` (POSIX). Works for any non-static link: if the `.so` moves, the runtime moves with it, no env-var needed. 3. `CPPINTEROP_RUNTIME_BUILD_DIR` -- in-tree test runs. 4. `CPPINTEROP_RUNTIME_INSTALL_DIR` -- baked install path; last resort when self-DSO discovery isn't available (Windows, or CppInterOp statically linked into a host binary). Windows has no self-DSO discovery yet -- a `GetModuleHandleEx` port can be added when Windows OOP support arrives. The clang resource directory (`CLANG_RESOURCE_DIR`) suffers the same bake-at-build-time-vs-relocation problem and would benefit from the same layering; left as follow-up. * [ci] Verify the OOP runtime probe chain via relocation + env-var. The layered probe chain in `configureBundledOOPRuntime` from `[jit] Add a layered probe chain for relocatable OOP runtime discovery.` adds two new lookup layers (`CPPINTEROP_RUNTIME_DIR` env-var override and `dladdr`-based self-DSO probe) that the existing in-tree test path doesn't exercise -- in-tree the baked `CPPINTEROP_RUNTIME_BUILD_DIR` probe wins first, so a regression in either new layer would be silent. After the install-layout assertion succeeds, move the install to a fresh prefix and re-run the OOP-tagged tests with `DYLD_LIBRARY_PATH`/`LD_LIBRARY_PATH` pointing at the moved tree: the `dladdr` probe must follow the relocated `.so` and find the runtime alongside it. Then stage the runtime parts at a third location, wipe the moved `cppinterop-rt/` subdir, and re-run with `CPPINTEROP_RUNTIME_DIR` set to the staged copy: only the env-var probe can win. Both runs assert that the `[CreateClangInterpreter]: --use-oop-jit requested but the bundled OOP runtime ... is missing` fallback warning does NOT appear in the test output. Negative-control verified locally -- with all probe locations wiped the warning does fire, confirming the assertion isn't dead. Filter is `CppInterOpTest/OutOfProcessJIT.Jit_StreamRedirectJIT`, the only OOP-tagged test that calls `CreateInterpreter()` before its skip check; the test itself fails with a preexisting `stdio.h not found` issue in the OOP child interpreter, which is orthogonal to the probe chain and doesn't trip the bundling warning. * [test] Skip OOP+sanitizer-incompatible reflection tests. The asan-ubsan CI row started exercising the OOP-JIT path after `[ci] Build the bundled OOP-JIT runtime in non-cling LLVM>=22 caches.` (#940) made OOP run by default for every llvm22+ row. Two tests then trip the assertion at `llvm/lib/ExecutionEngine/Orc/Core.cpp:2800`: Resolving symbol with incorrect flags ASan instruments globals with extra metadata that changes the `JITSymbolFlags` declared in the host process; when the EPC-based out-of-process executor reports back the resolved flags, they don't match. The in-process JIT path doesn't compare across an IPC boundary so it's unaffected, and other OOP tests don't define the kinds of common symbols that hit the resolution path. Skip the failing tests under OOP-when-sanitized rather than disabling the OOP fixture wholesale -- the remaining OOP tests on the asan-ubsan row keep running: - `EnumReflection_GetEnums` - `FunctionReflection_InstantiateTemplateFunctionFromString` Likely upstream LLVM bug; can be removed once the EPC symbol-flag tracking is fixed.
1 parent 2626ddd commit fc2e662

4 files changed

Lines changed: 108 additions & 12 deletions

File tree

.github/actions/Build_and_Test_CppInterOp/action.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,41 @@ runs:
100100
test -x "${INSTALL_DIR}/lib/cppinterop-rt/llvm-jitlink-executor" || \
101101
{ echo "FAIL: llvm-jitlink-executor missing or not executable"; exit 1; }
102102
echo "OK: bundled OOP runtime installed under lib/cppinterop-rt/"
103+
104+
# Probe-chain validation: relocate the install and override
105+
# CPPINTEROP_RUNTIME_DIR. Both must keep the bundled runtime
106+
# discoverable -- assert no fallback warning fires.
107+
RELOC_MOVED="${INSTALL_DIR}-moved"
108+
rm -rf "${RELOC_MOVED}"
109+
mv "${INSTALL_DIR}" "${RELOC_MOVED}"
110+
LIB_KEY=LD_LIBRARY_PATH
111+
[[ "$(uname)" == "Darwin" ]] && LIB_KEY=DYLD_LIBRARY_PATH
112+
OOP_FILTER='CppInterOpTest/OutOfProcessJIT.*'
113+
assert_no_fallback() {
114+
local desc="$1" log="$2"
115+
if echo "${log}" | grep -q "bundled OOP runtime.*is missing"; then
116+
echo "FAIL: ${desc} -- OOP fallback warning fired"
117+
echo "${log}" | tail -10
118+
exit 1
119+
fi
120+
echo "OK: ${desc}"
121+
}
122+
# 1. dladdr probe finds runtime alongside the moved .so.
123+
out=$(env "${LIB_KEY}=${RELOC_MOVED}/lib" \
124+
"${TESTS_BIN}" --gtest_filter="${OOP_FILTER}" 2>&1) || true
125+
assert_no_fallback "dladdr probe survives install relocation" "${out}"
126+
# 2. CPPINTEROP_RUNTIME_DIR override: stage runtime at a third
127+
# location, wipe the bundled subdir, point the env-var at
128+
# the staged copy.
129+
ENV_RT="${RUNNER_TEMP:-/tmp}/cppinterop-rt-env"
130+
rm -rf "${ENV_RT}"; mkdir -p "${ENV_RT}"
131+
cp "${RELOC_MOVED}/lib/cppinterop-rt/liborc_rt.a" "${ENV_RT}/"
132+
cp "${RELOC_MOVED}/lib/cppinterop-rt/llvm-jitlink-executor" "${ENV_RT}/"
133+
rm -rf "${RELOC_MOVED}/lib/cppinterop-rt"
134+
out=$(env "${LIB_KEY}=${RELOC_MOVED}/lib" \
135+
"CPPINTEROP_RUNTIME_DIR=${ENV_RT}" \
136+
"${TESTS_BIN}" --gtest_filter="${OOP_FILTER}" 2>&1) || true
137+
assert_no_fallback "CPPINTEROP_RUNTIME_DIR env-var override" "${out}"
103138
elif [[ "${{ matrix.clang-runtime }}" -lt 22 ]]; then
104139
echo "OK: lib/cppinterop-rt/ absent, expected for clang-runtime <= 21"
105140
else

lib/CppInterOp/Compatibility.h

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ inline void codeComplete(std::vector<std::string>& Results,
245245
#endif
246246

247247
#if LLVM_VERSION_MAJOR > 21 && !defined(_WIN32)
248+
#include <dlfcn.h>
248249
#include <unistd.h>
249250
#endif
250251

@@ -371,22 +372,55 @@ inline bool detectNVPTXArch(std::string& Arch) {
371372
}
372373

373374
#if LLVM_VERSION_MAJOR > 21
374-
/// Wire CppInterOp's bundled OOP runtime parts into `B`. CMakeLists.txt
375-
/// bakes `CPPINTEROP_RUNTIME_BUILD_DIR` and `CPPINTEROP_RUNTIME_INSTALL_DIR`
376-
/// at configure time; we probe the build-tree path first so an in-tree
377-
/// test run picks up the freshly staged files, then fall back to the
378-
/// install path. `UpdateOrcRuntimePathCB` is replaced with a no-op so
379-
/// the upstream resource-dir prefix check inside
375+
/// Directory containing libclangCppInterOp itself, derived via
376+
/// `dladdr` of an in-library function pointer. Returns empty when the
377+
/// platform has no self-DSO discovery (Windows -- a `GetModuleHandleEx`
378+
/// port can be added when Windows OOP support arrives).
379+
inline std::string findOwnLibraryDir() {
380+
#if !defined(_WIN32)
381+
Dl_info info{};
382+
if (!dladdr(reinterpret_cast<const void*>(&findOwnLibraryDir), &info) ||
383+
!info.dli_fname || !*info.dli_fname)
384+
return {};
385+
llvm::SmallString<256> P(info.dli_fname);
386+
llvm::sys::path::remove_filename(P);
387+
return std::string(P.str());
388+
#else
389+
return {};
390+
#endif
391+
}
392+
393+
/// Wire CppInterOp's bundled OOP runtime parts into `B`. Probes a
394+
/// layered list of candidate directories, in priority order:
395+
/// 1. `$CPPINTEROP_RUNTIME_DIR` -- sysadmin override.
396+
/// 2. `<dir of libclangCppInterOp>/cppinterop-rt` -- relocatable, follows
397+
/// the .so wherever a package manager moved it.
398+
/// 3. `CPPINTEROP_RUNTIME_BUILD_DIR` -- in-tree test runs.
399+
/// 4. `CPPINTEROP_RUNTIME_INSTALL_DIR` -- baked install path; last
400+
/// resort when self-DSO discovery isn't available (e.g. static
401+
/// link of CppInterOp into a host binary).
402+
/// `UpdateOrcRuntimePathCB` is replaced with a no-op so the upstream
403+
/// resource-dir prefix check inside
380404
/// `IncrementalExecutorBuilder::UpdateOrcRuntimePath`
381405
/// (`clang/lib/Interpreter/IncrementalExecutor.cpp`, the
382406
/// `consume_front(parent_path(D.Dir))` guard) doesn't run -- our
383407
/// runtime lives outside the host's clang resource tree.
384408
inline bool configureBundledOOPRuntime(clang::IncrementalExecutorBuilder& B) {
385-
#if defined(CPPINTEROP_RUNTIME_BUILD_DIR) && \
386-
defined(CPPINTEROP_RUNTIME_INSTALL_DIR)
387-
for (llvm::StringRef Dir :
388-
{llvm::StringRef(CPPINTEROP_RUNTIME_BUILD_DIR),
389-
llvm::StringRef(CPPINTEROP_RUNTIME_INSTALL_DIR)}) {
409+
llvm::SmallVector<std::string, 4> Candidates;
410+
if (const char* Env = std::getenv("CPPINTEROP_RUNTIME_DIR"))
411+
Candidates.emplace_back(Env);
412+
if (std::string OwnDir = findOwnLibraryDir(); !OwnDir.empty()) {
413+
llvm::SmallString<256> P(OwnDir);
414+
llvm::sys::path::append(P, "cppinterop-rt");
415+
Candidates.emplace_back(P.str());
416+
}
417+
#if defined(CPPINTEROP_RUNTIME_BUILD_DIR)
418+
Candidates.emplace_back(CPPINTEROP_RUNTIME_BUILD_DIR);
419+
#endif
420+
#if defined(CPPINTEROP_RUNTIME_INSTALL_DIR)
421+
Candidates.emplace_back(CPPINTEROP_RUNTIME_INSTALL_DIR);
422+
#endif
423+
for (const std::string& Dir : Candidates) {
390424
llvm::SmallString<256> OrcRT(Dir);
391425
llvm::sys::path::append(OrcRT, "liborc_rt.a");
392426
llvm::SmallString<256> Exec(Dir);
@@ -400,7 +434,6 @@ inline bool configureBundledOOPRuntime(clang::IncrementalExecutorBuilder& B) {
400434
};
401435
return true;
402436
}
403-
#endif
404437
return false;
405438
}
406439
#endif // LLVM_VERSION_MAJOR > 21

unittests/CppInterOp/EnumReflectionTest.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,20 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, EnumReflection_GetEnumConstantValue) {
254254
}
255255

256256
TYPED_TEST(CPPINTEROP_TEST_MODE, EnumReflection_GetEnums) {
257+
// Skip under OOP+sanitizer: upstream LLVM ORC asserts on
258+
// `Resolving symbol with incorrect flags`
259+
// (`llvm/lib/ExecutionEngine/Orc/Core.cpp:2800`) when
260+
// sanitizer-instrumented common symbols cross the EPC boundary --
261+
// the host's JITSymbolFlags don't match what the executor reports.
262+
#if (defined(__has_feature) && \
263+
(__has_feature(address_sanitizer) || \
264+
__has_feature(memory_sanitizer) || \
265+
__has_feature(thread_sanitizer))) || \
266+
defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__)
267+
if (this->IsOutOfProcess())
268+
GTEST_SKIP() << "OOP+sanitizer trips LLVM ORC symbol-flag assertion";
269+
#endif
270+
257271
std::string code = R"(
258272
enum Color {
259273
Red,

unittests/CppInterOp/FunctionReflectionTest.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,20 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, FunctionReflection_ExistsFunctionTemplate) {
702702

703703
TYPED_TEST(CPPINTEROP_TEST_MODE,
704704
FunctionReflection_InstantiateTemplateFunctionFromString) {
705+
// Skip under OOP+sanitizer: same upstream LLVM ORC
706+
// `Resolving symbol with incorrect flags` issue as
707+
// EnumReflection_GetEnums (Core.cpp:2800) -- sanitizer-instrumented
708+
// common symbols cross the EPC boundary with mismatched
709+
// JITSymbolFlags.
710+
#if (defined(__has_feature) && \
711+
(__has_feature(address_sanitizer) || \
712+
__has_feature(memory_sanitizer) || \
713+
__has_feature(thread_sanitizer))) || \
714+
defined(__SANITIZE_ADDRESS__) || defined(__SANITIZE_THREAD__)
715+
if (this->IsOutOfProcess())
716+
GTEST_SKIP() << "OOP+sanitizer trips LLVM ORC symbol-flag assertion";
717+
#endif
718+
705719
std::vector<const char*> interpreter_args = { "-include", "new" };
706720
TestFixture::CreateInterpreter(interpreter_args);
707721
std::string code = R"(#include <memory>)";

0 commit comments

Comments
 (0)