From bb5106b0e7d4fe5fb6cf52271f418293995ccd52 Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Wed, 21 Oct 2026 18:14:57 +0100 Subject: [PATCH] JIT: load the GC runtime before the default library On Linux the default library leaves GC_* undefined and takes them from the process (TypeScriptRuntime links Boehm in), so it has to be loaded after the runtime or dlopen fails on GC_malloc. prepareJitProcess added the default library first; a runtime passed with --shared-libs was ahead of it anyway, which hid the bug from every test. The JIT collector test gains a Linux run that leaves the runtime for tslang to find (--tslang-lib-path). Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/defaultlib-collector.cmake | 10 +++++++++- tslang/tslang/jit.cpp | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/tslang/test/tester/defaultlib-collector.cmake b/tslang/test/tester/defaultlib-collector.cmake index a1583f337..8d13609ee 100644 --- a/tslang/test/tester/defaultlib-collector.cmake +++ b/tslang/test/tester/defaultlib-collector.cmake @@ -6,7 +6,8 @@ # a user shared library (docs/single-gc-collector-design.md). This drives tslang with the real # default library instead: # -# MODE=jit the JIT, the program alone and importing a shared library +# MODE=jit the JIT, the program alone and importing a shared library; on Linux also without +# the runtime passed in --shared-libs # MODE=compile an exe alone, and an exe importing a shared library that links the default-library DLL # # The default library is a separate build (TypeScriptCompilerDefaultLib). With none found the test @@ -131,6 +132,13 @@ if(MODE STREQUAL "jit") "${TSLANG}" --emit=jit ${common} "--shared-libs=${runtime}" "${alone}") run_program("JIT importing a shared library, default library" "${WORK_DIR}" "${TSLANG}" --emit=jit ${common} "--shared-libs=${runtime}" "${program}") + # The runtime found by tslang itself (--tslang-lib-path), as a user runs it. On Linux the + # default library takes GC_* from the process, so tslang has to load the runtime first; one + # passed with --shared-libs is ahead of it anyway, which is why the runs above never failed. + if(NOT WIN32) + run_program("JIT, default library, runtime not passed" "${WORK_DIR}/alone" + "${TSLANG}" --emit=jit ${common} "${alone}") + endif() elseif(MODE STREQUAL "compile") run("--emit=exe alone, default library" "${WORK_DIR}/alone" "${TSLANG}" --emit=exe ${common} "${alone}" -o "alone${exe_suffix}") diff --git a/tslang/tslang/jit.cpp b/tslang/tslang/jit.cpp index 4ecb0eb6e..50b012e1a 100644 --- a/tslang/tslang/jit.cpp +++ b/tslang/tslang/jit.cpp @@ -593,13 +593,14 @@ static int prepareJitProcess(CompileOptions &compileOptions, bool nativeImage) // If shared library implements custom mlir-runner library init and destroy // functions, we'll use them to register the library with the execution // engine. Otherwise we'll pass library directly to the execution engine. + std::string defaultLibFile; if (!compileOptions.noDefaultLib && !nativeImage) { // per-build subfolder (debug/release) must match the JIT compilation mode. // Keyed on --di (generate debug info): with debug info use the debug lib. // ...and per memory model, for the same reason the linker path is: the default lib // allocates the way the model it was built for allocates. See getDefaultLibSubDir. - auto defaultLibFile = getDefaultLibBinary(/*shared=*/true, compileOptions, + defaultLibFile = getDefaultLibBinary(/*shared=*/true, compileOptions, llvm::Triple(llvm::Triple::normalize(llvm::sys::getDefaultTargetTriple()))); // JIT is host-only // Named here rather than left to the loader, for the reason exe.cpp checks the link @@ -629,9 +630,7 @@ static int prepareJitProcess(CompileOptions &compileOptions, bool nativeImage) return -1; } #endif - - clSharedLibs.push_back(defaultLibFile); - } + } // add default libs in case they are not part of options #ifdef WIN32 @@ -685,6 +684,15 @@ static int prepareJitProcess(CompileOptions &compileOptions, bool nativeImage) } } + // After the runtime: on Linux the default library leaves GC_* undefined and takes them from + // the process (a collector of its own would be a second one), so they have to be loaded + // before it is, or dlopen fails on GC_malloc. A runtime passed with --shared-libs is + // already ahead of it. + if (!defaultLibFile.empty()) + { + clSharedLibs.push_back(defaultLibFile); + } + #ifdef _WIN32 // tslang.exe links the CRT statically (/MT[d]), so its libc symbols are not in // any DLL export table. The JIT's process-symbol resolver would otherwise bind