This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 539a27a3 [FIX] Keep ORC JIT linked runtimes process-local (#807)
539a27a3 is described below

commit 539a27a3d70eece5805beae406efe3237fc34c27
Author: Yaxing Cai <[email protected]>
AuthorDate: Mon Sep 21 20:32:35 2026 +0800

    [FIX] Keep ORC JIT linked runtimes process-local (#807)
    
    ## Why
    
    `tvm_ffi_orcjit` currently reopens its shared library with
    `RTLD_GLOBAL`. This publishes symbols pulled from static LLVM and
    runtime archives into the host process. Besides the very large Mach-O
    export surface, those definitions can interpose with another LLVM copy
    loaded by an application.
    
    Linux has an additional wrinkle: RHEL-family compiler drivers implement
    `libstdc++.so` as a linker script that combines `libstdc++.so.6` with
    `libstdc++_nonshared.a`. JIT objects compiled by such a toolchain can
    therefore reference helpers that are absent from the shared runtime. The
    shared runtime and nonshared archive must come from the compiler that
    produced the JIT object, not from the compiler that built the wheel.
    
    ## What changed
    
    - keep the addon under the TVM-FFI loader's local scope and pin a second
    local handle with `RTLD_NODELETE`, without promoting LLVM into the
    process-global namespace
    - restrict the ELF and Mach-O dynamic API to `TVMFFIOrcJITInitialize`
    - remove the embedded `libstdc++_nonshared.a` design entirely
    - make `ExecutionSession.load_module` follow the same default toolchain
    convention as `tvm_ffi.cpp.build` / `load_inline`: `$CXX`, or `c++` when
    unset
    - query that compiler with `-print-file-name` for its `libstdc++.so.6`
    and optional `libstdc++_nonshared.a`; cache discovery by compiler
    command and allow `load_module(..., cxx="g++-14")` to override the
    compiler without exposing runtime paths
    - open compiler-selected shared runtimes with `RTLD_LOCAL` and keep one
    lightweight support JITDylib per runtime path
    - insert that support JITDylib after ORC's Platform JITDylib but before
    ProcessSymbols, preserving the platform's `__cxa_atexit` interposition
    - attach the matching nonshared archive to the user JITDylib and
    materialize a member only when the selected shared runtime does not
    define the requested symbol
    - add regression tests for compiler/runtime discovery, explicit compiler
    selection, process-global scope, and the binary export surface
    
    This reproduces the effective GCC linker-script order for each JIT load
    while preserving ORC lifetime semantics. In particular, placing the
    compiler runtime directly on the user JITDylib would override ORC's
    `__cxa_atexit` handling and leave JIT-owned exit handlers alive after
    their code was released; the intermediate support JITDylib keeps
    Platform first.
    
    ## Validation
    
    - native `manylinux_2_28` ARM64 container with LLVM 22.1.0, matching the
    LLVM version used by TVM-FFI CI
    - addon tests: 91 passed, 23 skipped under pytest-xdist; the serial run
    also passes
    - C and C++ quick-start examples, with the C++ example compiled and
    loaded through the shared `$CXX` default
    - confirmed the active GCC-toolset-14 driver selects
    `/usr/lib64/libstdc++.so.6.0.25` together with its own
    `libstdc++_nonshared.a`
    - `nm -D --defined-only`: exactly `TVMFFIOrcJITInitialize`
    - repository-pinned Ruff, clang-format 21.1.7, and cmake-format checks
    - ASF header, file-type, Python syntax, and `git diff --check`
    - hosted ORCJIT wheel matrix: Linux x86_64, Linux aarch64, macOS arm64,
    and Windows AMD64
---
 addons/tvm_ffi_orcjit/CMakeLists.txt               |  26 +--
 addons/tvm_ffi_orcjit/README.md                    |   6 +
 addons/tvm_ffi_orcjit/cmake/orcjit.lds             |   6 +
 addons/tvm_ffi_orcjit/cmake/orcjit.macos.lds       |   1 +
 addons/tvm_ffi_orcjit/pyproject.toml               |   1 +
 .../python/tvm_ffi_orcjit/__init__.py              |  35 ++--
 .../python/tvm_ffi_orcjit/session.py               |  63 ++++++-
 addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc      |  18 +-
 addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc    | 182 ++++++++++++++++++++-
 addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h     |  15 +-
 addons/tvm_ffi_orcjit/tests/test_cxx_runtime.py    |  77 +++++++++
 .../tests/test_library_visibility.py               |  54 ++++++
 12 files changed, 433 insertions(+), 51 deletions(-)

diff --git a/addons/tvm_ffi_orcjit/CMakeLists.txt 
b/addons/tvm_ffi_orcjit/CMakeLists.txt
index cda2c9c4..af79db90 100644
--- a/addons/tvm_ffi_orcjit/CMakeLists.txt
+++ b/addons/tvm_ffi_orcjit/CMakeLists.txt
@@ -155,22 +155,22 @@ endif ()
 # manylinux ABI floor and no too-recent GLIBCXX_* symbol is imported -- the 
-static-libstdc++
 # workaround the older conda-forge LLVM required is no longer needed.
 
-# Hide symbols pulled from static archives so they are not re-exported and 
cannot interpose with a
-# host process's own copies (e.g. PyTorch's bundled LLVM). Exclude 
LLVM/zlib/zstd by name -- NOT
-# --exclude-libs,ALL. The embedded liborc_rt.a is linked by the JIT at run 
time and resolves its C++
-# runtime through the process (dlsym), including archive-only helpers such as
-# _ZSt28__throw_bad_array_new_lengthv that libstdc++.so does not export; ALL 
localizes those and the
-# ORC platform then fails to materialize. This holds whether libstdc++ is 
static or dynamic. The
-# LLVM list is derived from llvm-config so it tracks the toolchain version.
+# Export only the explicit initialization API. In particular, do not publish 
symbols from the
+# statically linked LLVM/zlib/zstd archives: the addon is loaded into host 
processes that may carry
+# a different LLVM, and interposition between those copies is unsafe. On ELF, 
--exclude-libs,ALL
+# also gives archive members local binding before the version script trims the 
dynamic symbol table.
 if (CMAKE_SYSTEM_NAME MATCHES "Linux|Android|FreeBSD|NetBSD|OpenBSD" AND 
CMAKE_CXX_COMPILER_ID
                                                                          
MATCHES "GNU|Clang"
 )
-  # --exclude-libs needs exact archive filenames (no globs), so turn each 
-lLLVM* from llvm-config
-  # into libLLVM*.a and add zlib/zstd. One colon-separated list -- a -Wl comma 
list would be split.
-  set(_exclude_archives ${_llvm_libs_list} "-lz" "-lzstd")
-  list(TRANSFORM _exclude_archives REPLACE "^-l(.+)$" "lib\\1.a")
-  list(JOIN _exclude_archives ":" _exclude_libs_arg)
-  target_link_options(tvm_ffi_orcjit PRIVATE 
"-Wl,--exclude-libs=${_exclude_libs_arg}")
+  target_link_options(
+    tvm_ffi_orcjit PRIVATE "LINKER:--exclude-libs,ALL"
+    "LINKER:--version-script=${CMAKE_CURRENT_SOURCE_DIR}/cmake/orcjit.lds"
+  )
+elseif (APPLE)
+  target_link_options(
+    tvm_ffi_orcjit PRIVATE
+    
"LINKER:-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/cmake/orcjit.macos.lds"
+  )
 endif ()
 
 # ---- Find and embed liborc_rt (Linux/ELF only) ----
diff --git a/addons/tvm_ffi_orcjit/README.md b/addons/tvm_ffi_orcjit/README.md
index ce08ad8b..a8be7469 100644
--- a/addons/tvm_ffi_orcjit/README.md
+++ b/addons/tvm_ffi_orcjit/README.md
@@ -102,6 +102,12 @@ The `LLVM_PREFIX` environment variable tells CMake where 
to find LLVM. If
 LLVM is installed in a conda env or a standard system path, CMake can
 auto-discover it and `LLVM_PREFIX` is not needed.
 
+On Linux, each `load_module` call follows the same default compiler selection
+as `tvm_ffi.cpp.build`: `$CXX`, or `c++` when unset. ORC asks that compiler for
+its `libstdc++.so.6` and optional `libstdc++_nonshared.a`, then keeps both 
local
+to the new JITDylib. Discovery is cached, and `load_module(..., cxx="g++-14")`
+can select a different toolchain without supplying runtime-library paths.
+
 ## Usage
 
 ### Basic Example
diff --git a/addons/tvm_ffi_orcjit/cmake/orcjit.lds 
b/addons/tvm_ffi_orcjit/cmake/orcjit.lds
new file mode 100644
index 00000000..2b1270f4
--- /dev/null
+++ b/addons/tvm_ffi_orcjit/cmake/orcjit.lds
@@ -0,0 +1,6 @@
+{
+  global:
+    TVMFFIOrcJITInitialize;
+  local:
+    *;
+};
diff --git a/addons/tvm_ffi_orcjit/cmake/orcjit.macos.lds 
b/addons/tvm_ffi_orcjit/cmake/orcjit.macos.lds
new file mode 100644
index 00000000..854b6e44
--- /dev/null
+++ b/addons/tvm_ffi_orcjit/cmake/orcjit.macos.lds
@@ -0,0 +1 @@
+_TVMFFIOrcJITInitialize
diff --git a/addons/tvm_ffi_orcjit/pyproject.toml 
b/addons/tvm_ffi_orcjit/pyproject.toml
index c2221890..1b9abf1e 100644
--- a/addons/tvm_ffi_orcjit/pyproject.toml
+++ b/addons/tvm_ffi_orcjit/pyproject.toml
@@ -59,6 +59,7 @@ sdist.include = [
   "/LICENSE",
   "/pyproject.toml",
   "/CMakeLists.txt",
+  "/cmake/**",
   "/src/**/*.h",
   "/src/**/*.cc",
   "/src/**/*.cpp",
diff --git a/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/__init__.py 
b/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/__init__.py
index d555efd2..e1650303 100644
--- a/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/__init__.py
+++ b/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/__init__.py
@@ -32,7 +32,6 @@ Examples
 import ctypes
 import os
 import platform
-import sys
 from pathlib import Path
 
 from tvm_ffi import load_module
@@ -52,33 +51,31 @@ _LIB_PATH = [
     Path(__file__).parent / "lib" / _LIB_NAME,
     Path(__file__).parent.parent.parent / "build" / _LIB_NAME,
 ]
-_lib_dir = None
+_lib_path = None
 for path in _LIB_PATH:
     if path.exists():
         _ = load_module(str(path))
-        _lib_dir = path.parent
-if _lib_dir is None:
+        _lib_path = path
+if _lib_path is None:
     raise RuntimeError(
         f"Could not find {_LIB_NAME}. "
         f"Searched in {_LIB_PATH} and site-packages. "
         f"Please ensure the package is installed correctly."
     )
 
-# Explicitly initialize the library to register functions
-# This is needed because static initializers may not run when loaded via dlopen
-try:
-    # The dll search path need to be added explicitly in windows
-    if sys.platform.startswith("win32"):
-        os.add_dll_directory(str(_lib_dir))
-    # Load the library with ctypes and call the initialization function
-    c_lib = ctypes.CDLL(str(_lib_dir / _LIB_NAME), mode=ctypes.RTLD_GLOBAL)
-    init_func = c_lib.TVMFFIOrcJITInitialize
-    init_func.restype = None
-    init_func()
-except Exception as e:
-    import warnings
-
-    warnings.warn(f"Failed to explicitly initialize orcjit library: {e}")
+# Keep a second, process-lifetime local handle. RTLD_NODELETE is important for
+# modules pinned by keep_module_alive: their object deleters point into JIT 
code
+# owned by this DSO and may run during interpreter shutdown, after Python 
module
+# globals have otherwise released their handles. This does not promote the DSO
+# or its statically linked LLVM into the process-global symbol namespace.
+if os.name == "posix":
+    _c_lib = ctypes.CDLL(
+        str(_lib_path),
+        mode=ctypes.RTLD_LOCAL | getattr(os, "RTLD_NODELETE", 0),
+    )
+else:
+    _dll_directory = os.add_dll_directory(str(_lib_path.parent))
+    _c_lib = ctypes.CDLL(str(_lib_path))
 
 from .session import ExecutionSession, default_session
 
diff --git a/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py 
b/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py
index 66c7789f..e00e9186 100644
--- a/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py
+++ b/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py
@@ -18,6 +18,11 @@
 
 from __future__ import annotations
 
+import functools
+import os
+import shlex
+import subprocess
+import sys
 import threading
 from pathlib import Path
 from typing import TYPE_CHECKING
@@ -31,6 +36,48 @@ if TYPE_CHECKING:
     from collections.abc import Sequence
 
 
+def _query_compiler_file(command: tuple[str, ...], filename: str) -> Path | 
None:
+    """Ask a GCC-compatible driver which runtime file it would link."""
+    try:
+        result = subprocess.run(
+            [*command, f"-print-file-name={filename}"],
+            check=False,
+            capture_output=True,
+            text=True,
+        )
+    except OSError:
+        return None
+    if result.returncode != 0:
+        return None
+    output = result.stdout.strip()
+    if not output or output == filename:
+        return None
+    path = Path(output)
+    return path.resolve() if path.is_file() else None
+
+
[email protected]
+def _discover_linux_cxx_runtime(cxx: str) -> tuple[str | None, str | None]:
+    """Discover the runtime selected by one C++ driver command."""
+    command = tuple(shlex.split(cxx))
+    if not command:
+        raise ValueError("CXX must name a C++ compiler command")
+
+    shared = _query_compiler_file(command, "libstdc++.so.6")
+    nonshared = _query_compiler_file(command, "libstdc++_nonshared.a")
+    return (
+        str(shared) if shared is not None else None,
+        str(nonshared) if nonshared is not None else None,
+    )
+
+
+def _discover_cxx_runtime(cxx: str | None) -> tuple[str | None, str | None]:
+    """Follow tvm_ffi.cpp's $CXX-or-c++ default for Linux JIT objects."""
+    if not sys.platform.startswith("linux"):
+        return (None, None)
+    return _discover_linux_cxx_runtime(cxx if cxx is not None else 
os.environ.get("CXX", "c++"))
+
+
 @register_object("tvm_ffi_orcjit.ExecutionSession")
 class ExecutionSession(Object):
     """ORC JIT Execution Session.
@@ -107,6 +154,7 @@ class ExecutionSession(Object):
         objects: str | Path | bytes | bytearray | Sequence[str | Path | bytes 
| bytearray],
         name: str = "",
         keep_module_alive: bool = False,
+        cxx: str | None = None,
     ) -> Module:
         """Load one or more object files into a fresh module.
 
@@ -124,6 +172,12 @@ class ExecutionSession(Object):
         keep_module_alive : bool
             If True, pin the module in the runtime's process-global registry
             (see Notes). Defaults to False.
+        cxx : str or None
+            C++ compiler command whose runtime should resolve symbols in these
+            objects on Linux. Defaults to ``$CXX``, or ``c++`` when unset,
+            matching :func:`tvm_ffi.cpp.build`. The compiler is queried once
+            for its shared libstdc++ and optional ``libstdc++_nonshared.a``;
+            no runtime path needs to be supplied. Ignored on other platforms.
 
         Returns
         -------
@@ -170,7 +224,14 @@ class ExecutionSession(Object):
                     "load_module objects must be a path (str or Path) or 
object-file "
                     f"bytes, but got {type(obj).__name__}"
                 )
-        mod = _ffi_api.SessionLoadModule(self, normalized, name)  # type: 
ignore
+        cxx_runtime_path, libstdcxx_nonshared_path = _discover_cxx_runtime(cxx)
+        mod = _ffi_api.SessionLoadModule(  # type: ignore
+            self,
+            normalized,
+            name,
+            cxx_runtime_path,
+            libstdcxx_nonshared_path,
+        )
         if keep_module_alive:
             tvm_ffi._ffi_api.ModuleGlobalsAdd(mod)  # type: ignore
         return mod
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc 
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
index 1de989df..a9f1dc1e 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
@@ -241,10 +241,13 @@ void 
ORCJITDynamicLibraryObj::AddObjectBuffer(std::unique_ptr<llvm::MemoryBuffer
 }
 
 Module ORCJITExecutionSessionObj::LoadModule(const Array<Variant<String, 
Bytes>>& objects,
-                                             const String& name) {
+                                             const String& name,
+                                             const Optional<String>& 
cxx_runtime_path,
+                                             const Optional<String>& 
libstdcxx_nonshared_path) {
   // Hold no lock here: the callees each lock mutex_ at the leaf, and the fresh
   // dylib is unpublished until this returns, so no other thread can race it.
-  ORCJITDynamicLibrary dylib = CreateDynamicLibrary(name);
+  ORCJITDynamicLibrary dylib =
+      CreateDynamicLibrary(name, cxx_runtime_path, libstdcxx_nonshared_path);
   ORCJITDynamicLibraryObj* self = dylib.get();
 
   for (const Variant<String, Bytes>& object : objects) {
@@ -370,18 +373,17 @@ static void RegisterOrcJITFunctions() {
            []() { return ORCJITExecutionSessionObj::GlobalDefault(); })
       .def("tvm_ffi_orcjit.SessionLoadModule",
            [](const ORCJITExecutionSession& session, const 
Array<Variant<String, Bytes>>& objects,
-              const String& name) -> Module { return 
session->LoadModule(objects, name); })
+              const String& name, const Optional<String>& cxx_runtime_path,
+              const Optional<String>& libstdcxx_nonshared_path) -> Module {
+             return session->LoadModule(objects, name, cxx_runtime_path, 
libstdcxx_nonshared_path);
+           })
       .def("tvm_ffi_orcjit.SessionClearFreeSlabs",
            [](const ORCJITExecutionSession& session) -> int64_t {
              return session->ClearFreeSlabs();
            });
 }
 
-TVM_FFI_STATIC_INIT_BLOCK() {
-  // This block may not execute when loaded via dlopen on some platforms.
-  // Call TVMFFIOrcJITInitialize() explicitly if functions are not registered.
-  RegisterOrcJITFunctions();
-}
+TVM_FFI_STATIC_INIT_BLOCK() { RegisterOrcJITFunctions(); }
 
 }  // namespace orcjit
 }  // namespace ffi
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc 
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc
index 4f534dc0..cbe17859 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc
@@ -24,6 +24,8 @@
 
 #include "orcjit_session.h"
 
+#include <llvm/ExecutionEngine/Orc/EPCDynamicLibrarySearchGenerator.h>
+#include <llvm/ExecutionEngine/Orc/ExecutionUtils.h>
 #include <llvm/ExecutionEngine/Orc/LLJIT.h>
 #include <llvm/ExecutionEngine/Orc/ObjectLinkingLayer.h>
 #include <llvm/Support/Error.h>
@@ -39,6 +41,14 @@
 #include <cstdint>
 #include <mutex>
 
+#if defined(__linux__) && defined(__GLIBCXX__)
+#include <bits/functexcept.h>
+#endif
+
+#if defined(__linux__) || defined(__APPLE__)
+#include <dlfcn.h>
+#endif
+
 #include "orcjit_dylib.h"
 #include "orcjit_memory_manager.h"
 #include "orcjit_utils.h"
@@ -78,7 +88,6 @@ static LLVMInitializer llvm_initializer;
 extern "C" const char orc_rt_archive_start[];
 extern "C" const char orc_rt_archive_end[];
 #endif
-
 namespace {
 #ifdef TVM_FFI_ORCJIT_EMBED_ORC_RT
 // Zero-copy view of the embedded archive; its bytes live for the image 
lifetime.
@@ -93,6 +102,65 @@ std::unique_ptr<llvm::MemoryBuffer> 
GetEmbeddedOrcRuntimeBuffer() {
 }
 #endif
 
+#if (defined(__linux__) && defined(__GLIBCXX__)) || defined(__APPLE__)
+const char* GetAddonCxxRuntimeName() {
+#if defined(__APPLE__)
+  return "/usr/lib/libc++.1.dylib";
+#else
+  return "libstdc++.so.6";
+#endif
+}
+
+void* GetCxxRuntimeHandle(llvm::StringRef runtime_path) {
+  // Keep one process-lifetime local handle per compiler-selected runtime. This
+  // avoids incrementing the dlopen reference count on every load_module call.
+  // Leak the cache deliberately: destroying C++ objects from this DSO during
+  // process finalization can run after the C++ runtime has begun teardown.
+  struct HandleCache {
+    std::mutex mutex;
+    std::unordered_map<std::string, void*> handles;
+  };
+  static auto* cache = new HandleCache();
+  std::lock_guard<std::mutex> lock(cache->mutex);
+  std::string path = runtime_path.str();
+  auto it = cache->handles.find(path);
+  if (it != cache->handles.end()) return it->second;
+  void* handle = dlopen(path.c_str(), RTLD_LAZY | RTLD_LOCAL);
+  if (handle) cache->handles.emplace(std::move(path), handle);
+  return handle;
+}
+
+#if defined(__linux__) && defined(__GLIBCXX__)
+class LibStdCxxNonsharedGenerator final : public 
llvm::orc::DefinitionGenerator {
+ public:
+  LibStdCxxNonsharedGenerator(
+      void* shared_handle,
+      std::unique_ptr<llvm::orc::StaticLibraryDefinitionGenerator> 
archive_generator)
+      : shared_handle_(shared_handle), 
archive_generator_(std::move(archive_generator)) {}
+
+  llvm::Error tryToGenerate(llvm::orc::LookupState& lookup_state, 
llvm::orc::LookupKind kind,
+                            llvm::orc::JITDylib& jit_dylib,
+                            llvm::orc::JITDylibLookupFlags 
jit_dylib_lookup_flags,
+                            const llvm::orc::SymbolLookupSet& symbols) 
override {
+    llvm::orc::SymbolLookupSet missing_from_shared;
+    for (const auto& [name, lookup_flags] : symbols) {
+      std::string symbol_name = (*name).str();
+      if (!dlsym(shared_handle_, symbol_name.c_str())) {
+        missing_from_shared.add(name, lookup_flags);
+      }
+    }
+    if (missing_from_shared.empty()) return llvm::Error::success();
+    return archive_generator_->tryToGenerate(lookup_state, kind, jit_dylib, 
jit_dylib_lookup_flags,
+                                             missing_from_shared);
+  }
+
+ private:
+  void* shared_handle_;
+  std::unique_ptr<llvm::orc::StaticLibraryDefinitionGenerator> 
archive_generator_;
+};
+#endif
+#endif
+
 // Install ExecutorNativePlatform per the `orc_rt` selector (see the ctor doc).
 // A no-op except on Linux/ELF: macOS skips the platform (compact-unwind bug)
 // and Windows never wires up COFFPlatform, so both ignore the selector.
@@ -191,7 +259,45 @@ ORCJITExecutionSessionObj::ORCJITExecutionSessionObj(const 
Optional<Variant<Stri
           return std::make_unique<llvm::orc::ObjectLinkingLayer>(ES);
         });
 #endif
-#ifdef _WIN32
+#if (defined(__linux__) && defined(__GLIBCXX__)) || defined(__APPLE__)
+    builder.setPrePlatformSetup([](llvm::orc::LLJIT& J) -> llvm::Error {
+      auto process_symbols = J.getProcessSymbolsJITDylib();
+      if (!process_symbols) {
+        return llvm::make_error<llvm::StringError>(
+            "C++ runtime support requires a process symbols JITDylib",
+            llvm::inconvertibleErrorCode());
+      }
+
+      // The addon itself is RTLD_LOCAL, so the default process generator
+      // cannot see its C++ runtime dependency. Search that dependency through
+      // a private handle instead of promoting the addon (and its statically
+      // linked LLVM) into the process-global namespace.
+      void* cxx_runtime = GetCxxRuntimeHandle(GetAddonCxxRuntimeName());
+      if (!cxx_runtime) {
+        const char* error = dlerror();
+        return llvm::make_error<llvm::StringError>(error ? error : "failed to 
open the C++ runtime",
+                                                   
llvm::inconvertibleErrorCode());
+      }
+      
process_symbols->addGenerator(std::make_unique<llvm::orc::EPCDynamicLibrarySearchGenerator>(
+          J.getExecutionSession(), 
llvm::orc::ExecutorAddr::fromPtr(cxx_runtime)));
+
+#if defined(__linux__) && defined(__GLIBCXX__)
+      // GCC's libstdc++.so linker script may satisfy this helper from
+      // libstdc++_nonshared.a. Keep that definition local to this DSO, but 
make
+      // its address available to ORC before liborc_rt is bootstrapped. Adding
+      // an archive generator here is unsafe: materializing an archive member
+      // during bootstrap tries to register its sections before the ELF runtime
+      // has initialized.
+      llvm::orc::SymbolMap symbols;
+      symbols[J.mangleAndIntern("_ZSt28__throw_bad_array_new_lengthv")] =
+          
llvm::orc::ExecutorSymbolDef::fromPtr(&std::__throw_bad_array_new_length,
+                                                
llvm::JITSymbolFlags::Exported);
+      return 
process_symbols->define(llvm::orc::absoluteSymbols(std::move(symbols)));
+#else
+      return llvm::Error::success();
+#endif
+    });
+#elif defined(_WIN32)
     // Override ProcessSymbols setup to NOT add the default
     // EPCDynamicLibrarySearchGenerator. That generator resolves symbols to
     // absolute host-process addresses, which causes PCRel32 overflow when
@@ -260,9 +366,28 @@ ORCJITExecutionSession 
ORCJITExecutionSessionObj::GlobalDefault() {
   return *inst;
 }
 
-ORCJITDynamicLibrary ORCJITExecutionSessionObj::CreateDynamicLibrary(const 
String& name) {
+ORCJITDynamicLibrary ORCJITExecutionSessionObj::CreateDynamicLibrary(
+    const String& name, const Optional<String>& cxx_runtime_path,
+    const Optional<String>& libstdcxx_nonshared_path) {
   TVM_FFI_CHECK(jit_ != nullptr, InternalError) << "ExecutionSession not 
initialized";
 
+#if defined(__linux__) && defined(__GLIBCXX__)
+  void* cxx_runtime = nullptr;
+  if (cxx_runtime_path.has_value()) {
+    const String& runtime_path = cxx_runtime_path.value();
+    cxx_runtime = GetCxxRuntimeHandle(llvm::StringRef(runtime_path.data(), 
runtime_path.size()));
+    if (!cxx_runtime) {
+      const char* error = dlerror();
+      TVM_FFI_THROW(RuntimeError) << "Failed to open the JIT compiler's C++ 
runtime "
+                                  << cxx_runtime_path.value() << ": "
+                                  << (error ? error : "unknown dlopen error");
+    }
+  }
+#else
+  (void)cxx_runtime_path;
+  (void)libstdcxx_nonshared_path;
+#endif
+
   // Compound topology op — serialize against concurrent create / add / lookup 
/
   // teardown on this shared session (lock order: session lock first).
   std::lock_guard<std::mutex> lock(mutex_);
@@ -277,14 +402,55 @@ ORCJITDynamicLibrary 
ORCJITExecutionSessionObj::CreateDynamicLibrary(const Strin
 
   llvm::orc::JITDylib& jit_dylib =
       
TVM_FFI_ORCJIT_LLVM_CALL(jit_->getExecutionSession().createJITDylib(lib_name.c_str()));
-  // Use the LLJIT's default link order (Main → Platform → ProcessSymbols).
-  // This provides host process symbols via the ProcessSymbols JITDylib's 
generator,
-  // while ensuring the platform's __cxa_atexit interposer (in PlatformJD) 
takes
-  // precedence — so __cxa_atexit handlers are managed by the platform and can 
be
-  // drained per-JITDylib via __lljit_run_atexits at teardown.
+#if defined(__linux__) && defined(__GLIBCXX__)
+  llvm::orc::JITDylib* cxx_runtime_dylib = nullptr;
+  if (cxx_runtime) {
+    std::string runtime_path = cxx_runtime_path.value();
+    auto it = cxx_runtime_dylibs_.find(runtime_path);
+    if (it == cxx_runtime_dylibs_.end()) {
+      std::string runtime_name = "<C++ runtime " + 
std::to_string(cxx_runtime_dylibs_.size()) + ">";
+      auto& runtime_dylib = 
jit_->getExecutionSession().createBareJITDylib(std::move(runtime_name));
+      
runtime_dylib.addGenerator(std::make_unique<llvm::orc::EPCDynamicLibrarySearchGenerator>(
+          jit_->getExecutionSession(), 
llvm::orc::ExecutorAddr::fromPtr(cxx_runtime)));
+      it = cxx_runtime_dylibs_.emplace(std::move(runtime_path), 
&runtime_dylib).first;
+    }
+    cxx_runtime_dylib = it->second;
+  }
+#endif
+
+  // Start from LLJIT's default link order (Main → Platform →
+  // ProcessSymbols). On Linux, insert the compiler-selected C++ runtime after
+  // Platform and before ProcessSymbols. This keeps the platform's
+  // __cxa_atexit interposer ahead of libstdc++ while resolving other C++
+  // symbols from the same toolchain that produced the object.
+#if defined(__linux__) && defined(__GLIBCXX__)
+  auto process_symbols = jit_->getProcessSymbolsJITDylib();
+  bool added_cxx_runtime = false;
+#endif
   for (auto& kv : jit_->defaultLinkOrder()) {
+#if defined(__linux__) && defined(__GLIBCXX__)
+    if (cxx_runtime_dylib && process_symbols && kv.first == 
process_symbols.get()) {
+      jit_dylib.addToLinkOrder(*cxx_runtime_dylib);
+      added_cxx_runtime = true;
+    }
+#endif
     jit_dylib.addToLinkOrder(*kv.first, kv.second);
   }
+#if defined(__linux__) && defined(__GLIBCXX__)
+  if (cxx_runtime_dylib && !added_cxx_runtime) 
jit_dylib.addToLinkOrder(*cxx_runtime_dylib);
+
+  // Model GCC's libstdc++.so linker script without making either library
+  // process-global: only archive symbols absent from the shared library may
+  // materialize, and their sections belong to this user JITDylib's lifetime.
+  if (cxx_runtime && libstdcxx_nonshared_path.has_value()) {
+    const String& archive_path = libstdcxx_nonshared_path.value();
+    auto archive_generator =
+        
TVM_FFI_ORCJIT_LLVM_CALL(llvm::orc::StaticLibraryDefinitionGenerator::Load(
+            jit_->getObjLinkingLayer(), archive_path.c_str()));
+    jit_dylib.addGenerator(
+        std::make_unique<LibStdCxxNonsharedGenerator>(cxx_runtime, 
std::move(archive_generator)));
+  }
+#endif
 
   auto dylib_obj = 
make_object<ORCJITDynamicLibraryObj>(GetRef<ORCJITExecutionSession>(this),
                                                         &jit_dylib, 
jit_.get(), lib_name);
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h 
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h
index 6988490f..7c2ea4ef 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h
@@ -95,9 +95,13 @@ class ORCJITExecutionSessionObj : public Object {
   /*!
    * \brief Create a new DynamicLibrary (JITDylib) in this session
    * \param name Optional name for the library (for debugging)
+   * \param cxx_runtime_path Compiler-selected shared C++ runtime, if any.
+   * \param libstdcxx_nonshared_path Compiler-selected nonshared archive, if 
any.
    * \return The created dynamic library instance
    */
-  ORCJITDynamicLibrary CreateDynamicLibrary(const String& name);
+  ORCJITDynamicLibrary CreateDynamicLibrary(
+      const String& name, const Optional<String>& cxx_runtime_path = 
std::nullopt,
+      const Optional<String>& libstdcxx_nonshared_path = std::nullopt);
 
   /*!
    * \brief Load a set of objects into one fresh dynamic library and return the
@@ -114,10 +118,14 @@ class ORCJITExecutionSessionObj : public Object {
    * \param objects Array whose elements are each a \c String path or \c Bytes
    *        object-file image.
    * \param name Optional JITDylib name (auto-generated when empty).
+   * \param cxx_runtime_path Compiler-selected shared C++ runtime, if any.
+   * \param libstdcxx_nonshared_path Compiler-selected nonshared archive, if 
any.
    * \return The root module, with imports and library context fully wired (the
    *         dylib itself when there is no embedded library binary).
    */
-  Module LoadModule(const Array<Variant<String, Bytes>>& objects, const 
String& name);
+  Module LoadModule(const Array<Variant<String, Bytes>>& objects, const 
String& name,
+                    const Optional<String>& cxx_runtime_path = std::nullopt,
+                    const Optional<String>& libstdcxx_nonshared_path = 
std::nullopt);
 
   /*!
    * \brief Get the underlying LLVM ExecutionSession
@@ -207,6 +215,9 @@ class ORCJITExecutionSessionObj : public Object {
   /*! \brief Counter for auto-generating library names */
   std::atomic<int> dylib_counter_{0};
 
+  /*! \brief Compiler-selected C++ runtime search JITDylibs, keyed by 
shared-library path. */
+  std::unordered_map<std::string, llvm::orc::JITDylib*> cxx_runtime_dylibs_;
+
   /*!
    * \brief Serializes compound JITDylib operations on this shared session
    *        (create / add object / symbol lookup+init / teardown) and guards 
the
diff --git a/addons/tvm_ffi_orcjit/tests/test_cxx_runtime.py 
b/addons/tvm_ffi_orcjit/tests/test_cxx_runtime.py
new file mode 100644
index 00000000..a0b3996b
--- /dev/null
+++ b/addons/tvm_ffi_orcjit/tests/test_cxx_runtime.py
@@ -0,0 +1,77 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Tests for compiler-selected C++ runtime discovery."""
+
+from __future__ import annotations
+
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+from tvm_ffi_orcjit import ExecutionSession
+from tvm_ffi_orcjit import session as session_module
+from utils import build_test_objects
+
+
+def test_discover_linux_cxx_runtime(monkeypatch: pytest.MonkeyPatch, tmp_path: 
Path) -> None:
+    """The driver command selects both parts of the libstdc++ linker script."""
+    shared = tmp_path / "libstdc++.so.6"
+    nonshared = tmp_path / "libstdc++_nonshared.a"
+    shared.touch()
+    nonshared.touch()
+    requested = {
+        "libstdc++.so.6": shared,
+        "libstdc++_nonshared.a": nonshared,
+    }
+    commands: list[list[str]] = []
+
+    def fake_run(command: list[str], **_: object) -> 
subprocess.CompletedProcess[str]:
+        commands.append(command)
+        filename = command[-1].split("=", maxsplit=1)[1]
+        return subprocess.CompletedProcess(command, 0, 
stdout=f"{requested[filename]}\n")
+
+    monkeypatch.setattr(session_module.subprocess, "run", fake_run)
+    session_module._discover_linux_cxx_runtime.cache_clear()
+    try:
+        assert session_module._discover_linux_cxx_runtime("ccache g++-14") == (
+            str(shared.resolve()),
+            str(nonshared.resolve()),
+        )
+    finally:
+        session_module._discover_linux_cxx_runtime.cache_clear()
+
+    assert commands == [
+        ["ccache", "g++-14", "-print-file-name=libstdc++.so.6"],
+        ["ccache", "g++-14", "-print-file-name=libstdc++_nonshared.a"],
+    ]
+
+
[email protected](not sys.platform.startswith("linux"), reason="libstdc++ is 
Linux-only")
+def test_load_module_with_explicit_cxx() -> None:
+    """A caller can select the compiler runtime for one JITDylib."""
+    cxx = shutil.which("c++")
+    if cxx is None:
+        pytest.skip("default C++ compiler is unavailable")
+
+    obj_path = build_test_objects() / "cc-gcc" / "test_funcs.o"
+    if not obj_path.exists():
+        pytest.skip("GCC C++ test object is unavailable")
+
+    mod = ExecutionSession().load_module(obj_path, cxx=cxx)
+    assert mod.test_add(2, 3) == 5
diff --git a/addons/tvm_ffi_orcjit/tests/test_library_visibility.py 
b/addons/tvm_ffi_orcjit/tests/test_library_visibility.py
new file mode 100644
index 00000000..8f853313
--- /dev/null
+++ b/addons/tvm_ffi_orcjit/tests/test_library_visibility.py
@@ -0,0 +1,54 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Shared-library visibility tests for the ORC JIT addon."""
+
+from __future__ import annotations
+
+import ctypes
+import platform
+import shutil
+import subprocess
+
+import pytest
+import tvm_ffi_orcjit
+
+
[email protected](platform.system() == "Windows", reason="RTLD_DEFAULT is 
POSIX-only")
+def test_addon_is_not_in_the_process_global_scope() -> None:
+    """Loading the addon must not promote its symbols to RTLD_GLOBAL."""
+    with pytest.raises(AttributeError):
+        getattr(ctypes.CDLL(None), "TVMFFIOrcJITInitialize")
+
+
[email protected](platform.system() == "Windows", reason="nm flags differ on 
Windows")
+def test_addon_exports_only_initializer() -> None:
+    """Static LLVM and C++ runtime symbols must stay out of the dynamic API."""
+    nm = shutil.which("nm")
+    if nm is None:
+        pytest.skip("nm is unavailable")
+
+    lib_path = tvm_ffi_orcjit._lib_path
+    if platform.system() == "Darwin":
+        command = [nm, "-gjU", str(lib_path)]
+        expected = {"_TVMFFIOrcJITInitialize"}
+    else:
+        command = [nm, "-D", "--defined-only", "--format=posix", str(lib_path)]
+        expected = {"TVMFFIOrcJITInitialize"}
+
+    output = subprocess.run(command, check=True, capture_output=True, 
text=True).stdout
+    exported = {line.split()[0].split("@@", 1)[0] for line in 
output.splitlines() if line.strip()}
+    assert exported == expected

Reply via email to