Skip to content

Add ABIAdapter / DispatchTrampoline and use for @ccallable / @cfunction#62245

Draft
topolarity wants to merge 5 commits into
JuliaLang:masterfrom
topolarity:ct/abi-adapter-caches
Draft

Add ABIAdapter / DispatchTrampoline and use for @ccallable / @cfunction#62245
topolarity wants to merge 5 commits into
JuliaLang:masterfrom
topolarity:ct/abi-adapter-caches

Conversation

@topolarity

@topolarity topolarity commented Jul 1, 2026

Copy link
Copy Markdown
Member

This introduces first-class types for the adapters / trampolines we already generate for @cfunction / @ccallable:

  • ABIAdapter (currently emit_abi_converter, et al)
  • DispatchTrampoline (currently the "dispatch trampoline" encoded in cfuncdata_t)

which allows them to be cached and serialized independently.

The adapter / trampoline split is designed to support:

  1. OpaqueClosure: adapter only
  2. @cfunction: trampoline + adapter (sometimes)
  3. TypedCallable: trampoline + adapter (always) + runtime cache

Since TypedCallable does not have the top-level eval / type-restrictions that @cfunction has where the ABI is fixed at lowering time, the runtime cache implemented here is required so that we can provide ABI adapters for TypedCallable construction in type-unstable / interpreted code while leaving JIT work bounded (under type-stability assumptions). It also allows us to re-use ABI adapters / trampolines across multiple @cfunction sites, altough that's a fairly marginal benefit.

TODO: Explain the cache design / trade-offs + serialization strategy + unspecialized handling (#61949)

Co-developed by Claude Opus 4.8 🤖
(draft until I sweep the latest re-factor and clean up the AI-isms + needs a full review for bugs / logic problems)

topolarity and others added 5 commits July 15, 2026 19:08
Add shared machinery for caches that key a TypeMap exactly on a
signature type and hold several records per entry: an intrusive,
append-only list chained through an atomic next field embedded in
each record, walked lock-free and mutated under the owning cache's
writelock. Keys stay packed inline in the records; a match callback
reads them directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce the ABIAdapter datatype and a process-global adapter cache
(`Core.abi_adapters`, an ABIAdapterCache with its own writelock), interning
one JIT'd ABI-adapter thunk per distinct (sigt, rt, ci, specsig,
is_opaque_closure, nargs) rather than one per call site.

Adapters serialize as first-class image code: records are fvar-wired
(jl_get_adapter_id), reset their bucket `next`, and are re-interned on load
(jl_reintern_abi_adapter). Because an adapter has no compiled-code edge of its
own, it is reported to the serializer via `ext_foreign_code` (renamed from
ext_foreign_cis) so independent adapters -- ones not owned by any trampoline --
are rooted through the pre-dump GC and serialized/reinterned in both
incremental and full/AOT images. The codegen-free fallback
(jl_jit_abi_converter_fallback) is cache-aware.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the DispatchTrampoline datatype and a process-global trampoline cache
(`Core.dispatch_trampolines`, a DispatchTrampolineCache with its own writelock),
interning one @cfunction/@ccallable dispatch record per (sigt, rt). A trampoline
holds `last_invoked` -- a Union{CodeInstance, ABIAdapter} recording its resolved
target -- plus a redundant `fptr` cache for the hot-path poll.

Trampolines serialize alongside their adapters: the record's bucket `next` is
reset and it is re-interned on load (jl_reintern_trampoline); `last_invoked`
serializes as a normal reference so the first post-load call restores `fptr`
straight from it. The resolver itself (jl_resolve_trampoline) lands with the
@cfunction migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… delete cfuncdata

Reroute @cfunction/@ccallable off the per-call-site cfuncdata global onto the
interned DispatchTrampoline + ABIAdapter caches. emit_abi_call now embeds the
interned trampoline and polls last_world/fptr, falling back to the runtime
resolver jl_resolve_trampoline (which restores fptr from the trampoline's
last_invoked, JITing/interning an ABIAdapter only when needed). The AOT path
(generate_cfunc_thunks/emit_trampoline_adapter) materializes each trampoline's
adapter record and, outside --trim, eagerly enqueues the dynamic-dispatch
("unspecialized") adapter so a codegen-free interpreter run of a non-trimmed
image can still dispatch. The obsolete cfuncdata_t struct and jl_get_abi_converter
are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a JULIA_LOAD_CODEGEN_LIB environment variable (0/no) that makes
the loader skip libjulia-codegen and install the codegen-stubs
fallbacks, exactly as when the library is absent -- so the no-codegen
configuration can be tested and debugged without hiding the library
file.

Use it to extend the cfunction adapter serialization test with a
codegen-free child process: every @cfunction call must be served by
the ABI adapters serialized into the package image, and after a
method redefinition the resolver must dispatch through the
image-serialized dynamic-dispatch adapter rather than error. A
jl_get_LLVM_VERSION probe (0 from the fallback stub) guards against
the test passing vacuously through the JIT.

Fixes JuliaLang#61949

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@topolarity topolarity force-pushed the ct/abi-adapter-caches branch from 7f4b875 to 15f39e0 Compare July 15, 2026 23:10
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