Skip to content

Share native TLS with prebuilt libraries on the in-process JIT - #1103

Open
conrade-ctc wants to merge 1 commit into
compiler-research:mainfrom
chicagotrading:pr-i-native-tls
Open

Share native TLS with prebuilt libraries on the in-process JIT#1103
conrade-ctc wants to merge 1 commit into
compiler-research:mainfrom
chicagotrading:pr-i-native-tls

Conversation

@conrade-ctc

@conrade-ctc conrade-ctc commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

The in-process ORC JIT compiles with emulated TLS. A jitted reference to a prebuilt library's thread_local then needs a __emutls_v.<sym> companion that nothing defines. std::call_once is the usual break: jitted code writes libstdc++'s __once_call, but __once_proxy reads the native copy. Defining the companions does not help, because the two sides then use different storage.

This PR adds an interim pass, before the module reaches the JIT. It turns each llvm.threadlocal.address use of an external thread_local declaration into a dlsym-based helper call. Jitted and native code then share the per-thread storage. The scope is narrow: ELF, in-process, and only symbols the dynamic linker already resolves.

Is native TLS support for the in-process JIT the intended long-term fix? If the ORC owners plan it, this pass retires.

Known gap: to run the pass between Parse and Execute, the no-value ParseAndExecute path skips clang's LastValue dump-and-clear. A value-producing input on that path no longer prints and can leave a stale LastValue for a later evaluate. An IRTransformLayer on the LLJIT would avoid the split.

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.78947% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.77%. Comparing base (9bbdbb2) to head (4d5c4da).

Files with missing lines Patch % Lines
lib/CppInterOp/CppInterOpInterpreter.h 86.11% 5 Missing ⚠️
lib/CppInterOp/Compatibility.h 95.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1103      +/-   ##
==========================================
+ Coverage   87.74%   87.77%   +0.03%     
==========================================
  Files          23       23              
  Lines        6429     6504      +75     
==========================================
+ Hits         5641     5709      +68     
- Misses        788      795       +7     
Files with missing lines Coverage Δ
lib/CppInterOp/Compatibility.h 83.97% <95.00%> (+1.78%) ⬆️
lib/CppInterOp/CppInterOpInterpreter.h 85.34% <86.11%> (+0.05%) ⬆️
Files with missing lines Coverage Δ
lib/CppInterOp/Compatibility.h 83.97% <95.00%> (+1.78%) ⬆️
lib/CppInterOp/CppInterOpInterpreter.h 85.34% <86.11%> (+0.05%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@conrade-ctc

Copy link
Copy Markdown
Collaborator Author

@vgvassilev one design point I would like your view on. To rewrite the module before it reaches the JIT, the pass splits the no-value ParseAndExecute into Parse + Execute. That drops clang's LastValue dump-and-clear on that path (see the description). An alternative is to leave ParseAndExecute intact and install the rewrite as an LLJIT IRTransformLayer transform. If that seems better to you, I am happy to rework it that way.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread lib/CppInterOp/Compatibility.h
Comment thread lib/CppInterOp/Compatibility.h
Comment thread lib/CppInterOp/Compatibility.h
Comment thread lib/CppInterOp/Compatibility.h
Comment thread lib/CppInterOp/Compatibility.h
Comment thread lib/CppInterOp/CppInterOpInterpreter.h
Comment thread lib/CppInterOp/CppInterOpInterpreter.h Outdated
Comment thread lib/CppInterOp/CppInterOpInterpreter.h
@conrade-ctc
conrade-ctc force-pushed the pr-i-native-tls branch 2 times, most recently from 1d8d921 to 10913a1 Compare September 2, 2026 13:31

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Comment thread lib/CppInterOp/CppInterOpInterpreter.h
The ORC JIT compiles with emulated TLS, so jitted code referencing a
thread_local defined in a prebuilt library resolves __emutls_v.<sym>
companions that nothing defines -- libstdc++'s __once_callable/__once_call
behind std::call_once being the canonical case. Synthesizing the companions
would still be wrong: the library's own code reads the native copies
(libstdc++'s __once_proxy reads __once_call), so the two sides would
silently diverge.

Instead, rewrite each external thread_local declaration's
llvm.threadlocal.address accesses into calls to a runtime helper returning
the calling thread's native copy via dlsym, before the module reaches the
JIT. Jitted and native code then share per-thread storage, so cross-boundary
protocols like the std::call_once machinery work unchanged. thread_locals
the JIT itself defines keep the emulated-TLS path.

Only symbols the dynamic linker already resolves are redirected; anything
else keeps its clear link-time error. Scoped to ELF and the in-process JIT
(the helper is the process's own dlsym).

Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant