Internals & Dispatch

Developer documentation

This page is for contributors. The user manual starts at Home. Sister pages: JLCS / MLIR, Inheritance ABI, Developer index.

RepliBuild compiles C/C++ with Clang, reads the DWARF debug metadata the compiler emits about its own output, and generates Julia bindings that are correct by construction — struct offsets, enum underlying types, vtable slots, base-subobject offsets, and calling conventions all come from the compiler's own record rather than being guessed. Where DWARF is incomplete (enums the optimizer folded away, macro definitions, function-pointer typedefs) the Clang.jl AST fills the gap; where Julia's computed struct alignment disagrees with the DWARF size, the struct is treated as packed and routed away from ccall.

This page is the technical reference for how that pipeline is assembled — the stages, the three dispatch tiers, and the modules behind each.

Tier 2 — the MLIR/JLCS marshalling layer — has its own page: ABI Marshalling as Compiler IR covers the dialect's types and ops, the SysV lowering, the thunk calling contract, source-level debugging, and the failure classes the design exists to make loud. This page describes where it sits; that one describes what it is.

The pipeline

Source becomes a loadable Julia module in seven stages, each owned by one module:

  1. DependencyResolver — resolve the [dependencies] table (git / local / system) and merge the external sources into the build graph.
  2. Discovery — scan files and the #include graph; write or refresh replibuild.toml.
  3. Compiler — Clang compiles each translation unit to LLVM IR, cached on source mtime plus a compile fingerprint (flags, defines, include dirs, LLVM version, target triple).
  4. Linker — link, optimize, and assemble the IR into the .so (plus LTO bitcode when enable_lto is on).
  5. DWARFParser — llvm-dwarfdump + nm produce ClassInfo / VtableInfo and the compilation_metadata.json layout facts.
  6. Wrapper generator — DWARF + symbols drive the C or C++ generator to emit the Julia module.
  7. JITManager — at module load, stand up a per-library MLIR thunk engine for any Tier-2 calls.

For C projects the link/optimize/assemble steps run in-process on Julia's resident libLLVM, version-matched to the JLL clang that emitted the IR (no external LLVM, no DWARF-dropping version skew); a failure is a hard error unless [link] fallback = true selects the external llvm-link/opt pipeline. C++ always uses the external pipeline plus the system MLIR dialect. Only the final .ll → .so codegen shells to clang in both buckets.

Three dispatch tiers

Each wrapped function is routed to exactly one calling tier, chosen from its DWARF signature:

TierMechanismSelected when
1Base.llvmcall against a per-function bitcode slicePOD args, scalar/pointer return, [wrap.tier1] enable = true (C only)
2MLIR thunk via libJLCS.so (JIT at load, or AOT _thunks.so)packed structs, unions, large by-value struct returns, C++ classes, virtual dispatch, exceptions
3ccall into the .sothe unconditional fallback

The routing decision lives in Wrapper/DispatchLogic.jl (is_ccall_safe, is_c_lto_safe); the Tier selection logic section below lists the exact checks.

Two Tier-1 payloads, one supported

Tier 1 can carry its IR two ways, and the knobs are independent.

Per-function slices — [wrap.tier1] enable (C only, default off) is the supported path. _tier1_slice_prepass (Wrapper/C/Tier1C.jl) runs IRGen/Slicer.jl over every is_c_lto_safe non-varargs candidate, applies the hazard/size policy, and dlsym-pre-flights each slice's declarations against the real .so. A throw while slicing one function makes it an errored refusal, never cached, so that function alone stays on ccall; a failure on the whole promoted module turns Tier 1 off for the wrap. Acceptance only makes a function eligible: a call site additionally needs lto_shape_ok (no Cstring or struct crossing) and must survive the signature dedup, so _tier1_emit_slices! writes julia/slices/<mangled>.ll from the final wrapper text — only slices a call site actually reads reach disk, and TIER1_FUNCTIONS is derived in the same pass. Rests on static promotion (_promote_statics_libllvm) to guarantee every declared symbol reaches the dynamic symbol table. Decided at generation time — non-accepted functions emit plain ccall, and the wrapper exports TIER1_FUNCTIONS.

Whole-module bitcode — [link] enable_lto embeds the entire linked module at each call site: scale-limited (can crash Julia's JIT on large libraries) and it duplicates file-local static state between the embedded bitcode and the .so. Production configurations set enable_lto = false; C++ defaults to LTO off. Treat it as an experimentation path for small stateless kernels.

See Tier 1.

Wrapper

Source: src/Wrapper.jl, src/Wrapper/

The Wrapper package generates Julia FFI modules from DWARF metadata and binary symbol tables. It is structured as a two-track system: a C generator and a C++ generator, selected automatically via config.wrap.language.

Module layout

ModuleSourceRole
Wrapper.Generatorsrc/Wrapper/Generator.jlTop-level wrap_library() entry point; dispatches to C or C++ generator
Wrapper.DispatchLogicsrc/Wrapper/DispatchLogic.jlPer-function tier routing decisions (is_ccall_safe, is_c_lto_safe)
Wrapper.TypeRegistrysrc/Wrapper/TypeRegistry.jlTypeRegistry and TypeStrictness — shared type-resolution context
Wrapper.Symbolssrc/Wrapper/Symbols.jlParamInfo / SymbolInfo structs for structured symbol data
Wrapper.FunctionPointerssrc/Wrapper/FunctionPointers.jlDWARF function_ptr(...) signature to Julia @cfunction type string
Wrapper.Utilssrc/Wrapper/Utils.jlKeyword escaping, identifier sanitization shared between generators
Wrapper.C.GeneratorCsrc/Wrapper/C/GeneratorC.jlFull C wrapper generator (structs, enums, functions, LTO, thunks)
Wrapper.C.TypesCsrc/Wrapper/C/TypesC.jlC type heuristics and base type map
Wrapper.C.UtilsCsrc/Wrapper/C/UtilsC.jlC-specific identifier/format helpers
Wrapper.C.IdentifiersCsrc/Wrapper/C/IdentifiersC.jlC name sanitization
Wrapper.Cpp.GeneratorCppsrc/Wrapper/Cpp/GeneratorCpp.jlFull C++ wrapper generator (classes, inheritance flattening with subobject-offset rebasing, as_<Base>/as_<VBase> upcasts, Managed handles, virtual dispatch)
Wrapper.Cpp.TypesCppsrc/Wrapper/Cpp/TypesCpp.jlC++ type map including STL, templates, references
Wrapper.Cpp.IdentifiersCppsrc/Wrapper/Cpp/IdentifiersCpp.jlNamespace stripping, operator sanitization
Wrapper.Cpp.UtilsCppsrc/Wrapper/Cpp/UtilsCpp.jlC++ formatting helpers
Wrapper.Cpp.STLWrapperssrc/Wrapper/Cpp/STLWrappers.jlSTL container type detection and accessor generation
Wrapper.Rust.GeneratorRustsrc/Wrapper/Rust/GeneratorRust.jlExperimental Rust generator — requires extern "C" + #[repr(C)] surfaces

Language selection

wrap.language is an extensible dispatch key — "c" and "cpp" are the first two targets, with additional language generators planned:

[wrap]
language = "c"   # selects C generator + clang toolchain
language = "cpp" # selects C++ generator + clang++ toolchain (default)

discover() sets this automatically based on the scanned source files. Adding a new language means adding a generator under src/Wrapper/<Lang>/ and registering it in Wrapper/Generator.jl.

Tier selection logic

The function is_ccall_safe() in src/Wrapper/DispatchLogic.jl is the core dispatch decision. It inspects each function's DWARF metadata and returns false when the signature needs an MLIR thunk (Tier 2), true when it can be called directly (Tier 1 or Tier 3).

Choosing between the two direct tiers is a second, C-only step. is_c_lto_safe() marks a function Tier-1-eligible unless its return type is a packed struct or a by-value union — C has no templates, vtables, STL, or inheritance, so those are the only ABI hazards, and by-value parameters are fine either way. Eligibility is necessary but not sufficient: with [wrap.tier1] enable = true the function must also survive slicing, the hazard/size policy, and the dlsym pre-flight. Anything that does not, and everything when Tier 1 is off, emits ccall.

Checks performed:

  1. STL container types — Any STL type in parameters or return forces Tier 2
  2. Return type safety:
    • Template returns (contains <) → Tier 2 (unpredictable ABI)
    • Struct return by value > 16 bytes → Tier 2 (too large for ccall sret)
    • Non-POD class return → Tier 2
    • Packed struct return (DWARF size != Julia aligned size) → Tier 2
  3. Parameter type safety:
    • Union parameters → Tier 2
    • Packed struct parameters → Tier 2
  4. Exception safety — Per-function is_noexcept flag from DWARF. If absent (function may throw) and the module's may_throw setting is on, the function routes through jlcs.try_call rather than ccall.

Functions routed to Tier 2 are further divided between JIT dispatch (JITManager.invoke()) and AOT thunks (ccall to _thunks.so), controlled by the aot_thunks config flag.

Idiomatic wrapper generation

Beyond raw ccall bindings, the wrapper generator clusters related C++ functions by class name to produce idiomatic Julia types:

  1. Factory detection: Functions matching create_X, new_X, make_X, alloc_X, init_X, or returning X* are identified as constructors.
  2. Destructor detection: Functions matching delete_X, destroy_X, free_X, dealloc_X, or X_destroy are identified as destructors.
  3. Method clustering: Functions taking X* as their first parameter and associated with the same DWARF class are grouped as instance methods.

The result is a mutable struct ManagedX with a raw Ptr{Cvoid} handle, a registered finalizer calling the C++ destructor, and multiple-dispatch method proxies that pass the pointer via Base.unsafe_convert.

RepliBuild.Wrapper.extract_symbols — Method
extract_symbols(binary_path::String, registry::TypeRegistry; demangle::Bool=true, method::Symbol=:nm)

Extract symbols from compiled binary using specified method.

Methods

  • :nm - Fast, basic symbol extraction
  • :objdump - Detailed with debug info (TODO)
  • :all - Try all methods and merge (TODO)

Returns vector of SymbolInfo objects.

source
RepliBuild.Wrapper.is_enum_like — Method
is_enum_like(cpp_type::String)::Bool

Check if a C++ type looks like an enum based on naming conventions. Returns false — enum detection is unreliable by name alone (uppercase names are more likely structs/classes). Real enum identification uses DWARF metadata via _is_enum_type() in DispatchLogic.jl which checks __enum__ prefixed keys.

source
RepliBuild.Wrapper.is_struct_like — Method
is_struct_like(cpp_type::String)::Bool

Check if a C++ type looks like a struct/class based on naming conventions. Structs typically: start with uppercase, contain only alphanumeric + underscore.

source
RepliBuild.Wrapper.wrap_basic — Method
wrap_basic(config::RepliBuildConfig, library_path::String; generate_docs::Bool=true)

Generate basic Julia wrapper from binary symbols only (no headers required).

Quality: ~40% - Conservative types, placeholder signatures, requires manual refinement. Use when: Headers not available, quick prototyping, binary-only distribution.

source
RepliBuild.Wrapper.wrap_library — Method
wrap_library(config::RepliBuildConfig, library_path::String;
             headers::Vector{String}=String[],
             generate_tests::Bool=false,
             generate_docs::Bool=true,
             aot_compile=nothing)

Generate Julia wrapper for compiled library.

Wraps from the DWARF metadata (compilation_metadata.json) that build writes beside the library, and refuses when that file is missing.

With [compile] aot_thunks = true, a C++ wrap ends by compiling the thunks its call sites bind into lib<name>_thunks.<ext> (ThunkBuilder.build_aot_thunks), from the thunk set the generator has just decided. When that compile fails, the wrapper is generated again with JIT dispatch.

Arguments

  • config: RepliBuildConfig with wrapper settings
  • library_path: Path to compiled library (.so, .dylib, .dll)
  • headers: Header files the Clang.jl walk starts from. NOT unused — this is what scopes that walk; empty means "discover every header at the top level of every include dir", which is the right default only when the include dir is the API. Overrides [wrap] headers, which is the same setting from a manifest. Inert when use_clang_jl = false.
  • generate_tests: Generate test file (default: false, TODO)
  • generate_docs: Include comprehensive documentation (default: true)
  • aot_compile: The AOT stage, called as aot_compile(config, library_path; needed_symbols) -> Bool. Defaults to ThunkBuilder.build_aot_thunks; tests pass a stand-in.

Returns

Path to generated Julia wrapper file

source

Compiler

Source: src/Builder/Compiler.jl

The Compiler module handles the translation of C/C++ source code into LLVM IR and shared libraries. It oversees the entire build pipeline from dependency management down to IR optimization.

Build pipeline

  1. Auto-discovery and dependency resolution: Scans the project directory, resolving file paths and external git/local dependencies to merge into the build graph.
  2. Pre-processing (shims and templates): Dynamically generates C/C++ shim files for configured macros and explicitly instantiates templates based on replibuild.toml settings. This allows normally invisible constructs to manifest in the final binary and DWARF metadata. Macro shims are pinned to default symbol visibility, and a header-collision guard verifies each direct shim #include resolves inside the project/dependency tree — the shim TU lives under the build cache, so a bare include could otherwise fall through the -I path to a system-installed header at a different version and silently bake wrong macro values.
  3. Compilation to LLVM IR: Translates source code into .ll text format — .c via the JLL clang, .cpp via system clang++. The per-file cache is keyed on source mtime plus a compile fingerprint (flags, defines, include dirs, LLVM version, target triple); config changes can never silently reuse stale IR.
  4. IR transformation and sanitization: Strips attributes incompatible with Julia's internal LLVM JIT, removes va_start/va_end intrinsics from varargs function bodies (varargs are routed through true-variadic @ccall wrapper generation), and cleans mismatched debug metadata.
  5. Link / optimize / assemble: For C, these steps run in-process on Julia's resident libLLVM — LLVM.link! for linking, the new pass manager (default<O…>) for optimization, and in-process bitcode assembly — version-matched to the JLL clang that emitted the IR. A failure is a hard error; [link] fallback = true selects the external llvm-link/opt pipeline instead. C++ always uses the external pipeline.
  6. Static promotion (_promote_statics_libllvm, C in-process bucket, [link] promote_statics): see below.
  7. Codegen: The final .ll → .so step shells to clang/clang++.

Static promotion

Post-optimization and pre-codegen, every function or global that a Tier-1 bitcode slice may bind by declare but that cannot reach the .so's dynamic symbol table is renamed to an exported __rb_<lib>_<name> with external linkage and default visibility. The test is "not exportable", not "internal linkage": default<O…> runs no internalize pass, so it covers both internal/private linkage (file-local statics) and external-linkage-but-hidden/protected symbols — Lua's LUAI_FUNC functions and LUAI_DDEF tables are exactly the latter shape. Only internal constants stay internal: no symbol exists for a slice to bind, and read-only data has no divergence class.

The rename happens on the one module (<name>_abi.ll) that becomes both the .so and the slice source, so the two are bit-identical by construction — which is what removes the whole-module path's duplicated-static failure class rather than papering over it. The old→new map lands in compilation_metadata.json under promoted_symbols, and extract_symbols_from_binary filters __rb_* so promoted statics never surface as wrappable API.

Metadata extraction

At build time the compiler also extracts DWARF metadata into compilation_metadata.json (functions, struct definitions, enums, globals). DIE parsing is depth-aware: readelf DIE headers carry the tree depth, and member/enumerator/inheritance/template DIEs at depth d attribute to the type last seen at depth d−1 — so nested type definitions interleaved between members (routine clang output) cannot steal subsequent members from the enclosing class. The recorded emitter version is the compiler that actually produced the IR, not whichever llvm-config happens to be on the PATH.

RepliBuild.Compiler.assemble_bitcode — Method

Assemble LLVM IR text (.ll) to bitcode (.bc). Uses Julia's own libLLVM via the C API (LLVMParseIRInContext + LLVMWriteBitcodeToFile) to guarantee bitcode version compatibility with Base.llvmcall. Falls back to Clangunifiedjll, then system llvm-as.

source
RepliBuild.Compiler.compute_project_hash — Method

Compute a content hash for the entire project: replibuild.toml + source files + headers + git HEAD + the toolchain (_toolchain_identity). Returns a hex string that changes when anything relevant changes, and only then: it is SHA-256, so it is the same under every Julia release (see _key_feed!).

toolchain is a seam for tests; a build always takes _toolchain_identity.

source
RepliBuild.Compiler.create_library — Function

Create shared library from LLVM IR. Returns path to the library file.

per_tu_ir is the UNMERGED per-translation-unit IR list, used only if clang refuses the (usually merged) ir_files for running out of source locations — see the fallback inside. Callers that already hold that list should pass it; without it the fallback cannot run, because splitting a merged module is not possible after the fact.

source
RepliBuild.Compiler.extract_class_name — Method

Extract class name from method signature (only considers '::' in the function-name prefix). Example: "Calculator::compute(int, int)" -> "Calculator" Example: "sumvector(std::vector<int> const&)" -> "" (free function) Example: "bool ggufreader::read<int>(...)" -> "ggufreader" (NOT "bool ggufreader")

source
RepliBuild.Compiler.extract_dwarf_return_types — Method

Extract return types and struct definitions from DWARF debug info. Returns: (returntypes, structdefs, globalvars, typedeftable, record_abi)

  • returntypes: Dict{mangledname => {ctype, juliatype, size}}
  • structdefs: Dict{structname => {members: [{name, type, offset}]}}
  • recordabi: Dict{recordname => {byte_size, pass, empty}} for by-value records
source
RepliBuild.Compiler.extract_function_name — Method

Extract function name from demangled signature. Example: "Calculator::compute(int, int, char)" -> "compute" Example: "sumvector(std::vector<int, std::allocator<int> > const&)" -> "sumvector" Example: "bool gguf_reader::read<std::vector<int> >(...)" -> "read<std::vector<int> >"

source
RepliBuild.Compiler.extract_stl_method_symbols — Method

Extract STL method symbols from the compiled binary. Uses nm to find mangled names of template-instantiated STL methods, keeping only the ones the loader can bind (see _stl_methods_from_nm). Returns Dict mapping normalized container type -> vector of method info dicts.

source
RepliBuild.Compiler.ingest_library — Method
ingest_library(config) -> String

BYOB ("bring your own build") path: skip the whole Clang/LLVM compile pipeline and ingest a pre-built .so produced by upstream's own build system. Runs DWARF metadata extraction on the binary and produces compilation_metadata.json next to a copy of the library inside the project's output dir, so wrap() finds it where it expects.

Ingested libraries dispatch through Tier 3 (ccall) only — there's no LTO bitcode to enable Tier 1 llvmcall, and no per-file IR for Tier 2 thunks.

Requires config.ingest !== nothing (set by a [ingest] section in replibuild.toml).

source
RepliBuild.Compiler.needs_recompile — Function

Check if a source file needs recompilation.

Cache hit requires BOTH the IR being newer than the source (mtime) AND the compile fingerprint matching the one that produced it (<ir>.key sidecar). A missing key (cache from before fingerprinting) forces a recompile so the key is established. Pass compile_fingerprint="" to skip the fingerprint check (legacy mtime-only behavior).

source
RepliBuild.Compiler.parse_parameters — Function

Parse parameter types from demangled signature. Example: "add(int, int)" -> [{"type": "int", "julia_type": "Cint"}, ...] Template-aware: angle brackets in nested types do not confuse the splitter.

source
RepliBuild.Compiler.sanitize_ir_for_julia — Method
sanitize_ir_for_julia(ir_text::String) -> String

Sanitize LLVM IR text for compatibility with Julia's internal LLVM (18). Strips LLVM 19–22 attributes/instructions, debug metadata, and converts varargs function bodies to extern declarations (vastart/vaend can't be JIT-compiled). Uses inlinehint (not alwaysinline) to avoid recursive inliner explosion on large modules.

Coverage by LLVM version: 19: GEP nuw, #dbg* records, inrange(), captures(), deadonunwind, initializes(), allocptr, icmp samesign, range(), trunc nuw/nsw, zext/uitofp nneg 20: GEP nusw, or disjoint, fptrunc/fpext fast-math flags, ptrtoaddr→ptrtoint 21: (covered by 19 patterns — captures/deadon_unwind/allocptr landed here) 22: ptrtoaddr instruction

This is the single source of truth for IR compatibility — used by both the C source LTO pipeline and the MLIR AOT thunks pipeline.

source
RepliBuild.Compiler.verify_wrap_surface — Method
verify_wrap_surface(config, binary_path, metadata_path)

Guard: every function the wrapper is about to emit must be resolvable in the built library's dynamic symbol table, under the exact symbol name the wrapper will ccall.

The wrap surface and the runtime surface come from two different symbol tables and nothing has ever checked that they agree. extract_compilation_metadata correlates DWARF against extract_symbols_from_binary, which reads nm -g — the full .symtab. The generated wrapper resolves through dlsym, which sees only .dynsym (extract_symbols_nm uses nm -D for exactly this reason). A symbol present in one and absent from the other is wrapped, documented, exported — and fails at the first call. Today the two agree everywhere; the check is cheap and the disagreement is silent, which is the whole argument for asserting it.

promote_statics is what makes them disagree. It treats hidden visibility as "internal" (_promote_statics_libllvm: needs_promotion tests linkage OR hidden/protected visibility) and renames such definitions to __rb_<lib>_<name>, which by design drops them from the wrap surface — DWARF still names the original, so the renamed symbol has no DWARF match. That is correct while hidden means what the library's author meant by it.

Compile with -fvisibility=hidden and it stops meaning that. Every function the library did not annotate visibility("default") goes hidden — including public API whose export macro is a bare extern, which is lua's LUA_API and is common. Then promotion renames the entire API to __rb_* and the wrapper generates nothing, from a build that succeeded and warned about nothing. Reproduced 2026-09-15 on a library-free three-function fixture through the JLL clang: annotated_public (carrying __attribute__((visibility("default")))) survives under its own name; bare_extern_public and uses_helper both come back as __rb_fix_*. The first of those is API.

test_cache_invalidation.jl records the same interaction from the other side — "internal_helper under that flag is external-linkage-and-hidden (the LUAIFUNC shape), so static promotion re-exports it as `__rbcachetestinternalhelper` … assert names, never export counts, on anything downstream of promotion." This guard asserts names. The one count it reads is the degenerate zero.

Two rules, neither carrying a threshold:

  • R1 — collapse. The wrap surface is empty while promotion renamed symbols. That is the failure above, observed rather than inferred: something was compiled, and all of it left under a different name.
  • R2 — reachability. Every wrappable function appears in the dynamic table under its mangled name. Measured across all 31 built Hub packages 2026-09-15 — 0 offenders, including every C++ package (constructor clones and weak template instantiations are all present in .dynsym), so a non-empty offender list is news rather than noise.

Runs BEFORE save_project_hash: a build that fails this must not leave a cache marker claiming it succeeded, or the next run skips straight past the failure.

source

Configuration Manager

Source: src/Builder/ConfigurationManager.jl

The single source of truth for all build settings. Handles TOML parsing, validation, and merging into a typed RepliBuildConfig struct.

RepliBuild.ConfigurationManager.get_compile_flags — Method

The flags every translation unit is compiled with — [compile] flags plus whatever the typed keys above them resolve to.

visibility = "hidden" is folded in HERE rather than at the compile call, because this is the list compute_compile_fingerprint hashes: fold it in later and flipping the key leaves the per-file IR cache valid, so the next build serves IR compiled under the other visibility and the wrapper's API silently belongs to the previous setting.

Idempotent against the legacy spelling — a package carrying -fvisibility=hidden in flags directly (argon2, box2d3 and zstd all predate the key) gets one flag, not two.

source
RepliBuild.ConfigurationManager.CompileConfig — Type

Nested struct for [compile] section.

visibility is :default (unchanged behaviour) or :hidden — the declared form of -fvisibility=hidden. It lives in the config rather than in flags because it is not an ordinary flag: it decides what the generated wrapper's API is. Under :hidden every definition the library did not annotate visibility("default") stops reaching .dynsym, so the wrap surface narrows to the set upstream declared public — free and exact for a library that annotates, and total erasure for one that does not (Compiler.verify_wrap_surface refuses that build; VisibilityProbe.visibility_probe answers which kind a package is BEFORE anyone sets this).

Consumed by get_compile_flags, deliberately: that is what compute_compile_fingerprint hashes, so flipping this key invalidates the per-file IR cache. Appending the flag further downstream — beside the -g in compile_single_to_ir, say — would leave the fingerprint blind to it and happily serve stale IR built under the other visibility.

source
RepliBuild.ConfigurationManager.DependencyItem — Type

Nested struct for a single dependency.

tag is a MUTABLE ref — upstream can force-push a tag to different content, and a plain git checkout <tag> would fetch it with no signal. commit is the optional expected 40-hex object name for that ref: when set, DependencyResolver hard-errors if the resolved HEAD differs, which is the only check that survives a cache wipe (the sidecar marker cannot, since clean() deletes it along with the clone). Leave commit empty to keep the old trust-the-tag behaviour.

source
RepliBuild.ConfigurationManager.IngestConfig — Type

Nested struct for [ingest] section.

Presence of this section flips RepliBuild from source-build mode (compile via Clang/LLVM) to ingest mode: the user supplies a pre-built .so (built by upstream's own build system), and RepliBuild only runs DWARF metadata extraction + wrapping. Ingested libraries dispatch through Tier 3 (ccall) only — no LTO bitcode, no Tier 1 llvmcall.

source

Discovery

Source: src/Builder/Discovery.jl

Scans the filesystem to identify C/C++ source files, headers, and dependencies. Auto-detects project language (:c vs :cpp) from the scanned source extensions and sets wrap.language accordingly in the generated replibuild.toml.

RepliBuild.Discovery.discover — Function
discover(target_dir::String=pwd(); force::Bool=false, unsafe::Bool=false, build::Bool=false, wrap::Bool=false) -> String

Main discovery pipeline - scans project and generates configuration.

Process:

  1. Check for existing replibuild.toml (project identified by presence of replibuild.toml)
  2. Scan all files and categorize
  3. Detect and analyze binaries
  4. Walk AST dependencies using clang
  5. Generate or update replibuild.toml with discovered data
  6. Optionally build, or build, wrap and register (vendor the build)

Arguments

  • target_dir: Project directory (default: current directory)
  • force: Force rediscovery even if replibuild.toml exists
  • unsafe: Bypass safety checks (use with extreme caution)
  • build: Automatically run build() after discovery (default: false)
  • wrap: With build, wrap too and then register the project, which vendors the build system-wide and adds it to the Julia environment (default: false)

Safety Features

  • Discovery is scoped ONLY to target_dir and subdirectories
  • Will not scan outside the project root
  • Skips .git, build, node_modules, .cache directories

Returns

  • Path to generated replibuild.toml file

Examples

# Discover only
toml_path = RepliBuild.Discovery.discover()

# Discover and build
toml_path = RepliBuild.Discovery.discover(build=true)

# Full pipeline: discover → build → wrap
toml_path = RepliBuild.Discovery.discover(build=true, wrap=true)

# Then use the TOML path:
RepliBuild.build(toml_path)
RepliBuild.wrap(toml_path)
source

DWARFParser

Source: src/Builder/DWARFParser.jl

Parses llvm-dwarfdump output to extract structured type information from compiled binaries. This is the bridge between C++ debug metadata and Julia wrapper generation.

Data structures

TypeFieldsRole
ClassInfoname, vtable_ptr_offset, base_classes, base_offsets, virtual_bases, virtual_methods, members, sizeComplete class/struct description with byte-level layout, inheritance chain (subobject offsets), and virtual-base flags
VtableInfoclasses, vtable_addresses, method_addressesAggregate metadata for all classes in a binary
VirtualMethodname, mangled_name, slot, return_type, parametersSingle virtual method with the slot index in its declaring class's primary vtable
MemberInfoname, type_name, offsetStruct field with byte offset from struct base

Extraction targets

DWARF TagExtracted Data
DW_TAG_class_type / DW_TAG_structure_typeClass/struct name, byte size, members, virtual methods, inheritance
DW_TAG_memberField name, type, DW_AT_data_member_location (byte offset)
DW_TAG_subprogram (with virtual flag)Virtual method name, mangled name, vtable slot (DW_AT_vtable_elem_location)
DW_TAG_inheritanceBase class with subobject offset; for virtual bases, the vtable-relative offset expression parsed into vbase_vtable_offset
DW_TAG_enumeration_typeEnum definitions
DW_TAG_union_typeUnion layout
DW_TAG_variableGlobal variables
DW_TAG_typedefType aliases
RepliBuild.DWARFParser.parse_symbol_table — Method
parse_symbol_table(nm_output::String) -> Tuple{Dict{String, UInt64}, Dict{String, UInt64}}

Parse nm output to extract vtable and method addresses. Returns (vtableaddresses, methodaddresses).

source
RepliBuild.DWARFParser.parse_vtables — Method
parse_vtables(binary_path::String) -> VtableInfo

Extract complete vtable information from a binary using DWARF and symbol table.

Arguments

  • binary_path: Path to compiled binary with debug info

Returns

  • VtableInfo containing classes, vtable addresses, and method addresses
source

JLCSIRGenerator

Source: src/IRGen/JLCSIRGenerator.jl, src/IRGen/ir_gen/

Transforms parsed DWARF metadata (VtableInfo) into MLIR source text in the JLCS dialect. The generated IR is then parsed and either JIT-compiled by MLIRNative (Tier 2 JIT) or written to disk and AOT-compiled by ThunkBuilder (Tier 2 AOT). Both paths share this module — there is no separate AOT IR generator.

Submodules

ModuleSourceInputOutput
TypeUtilssrc/IRGen/ir_gen/TypeUtils.jlC++ type stringMLIR type string (f64, i32, !llvm.ptr, etc.)
StructGensrc/IRGen/ir_gen/StructGen.jlstruct metadataStruct type aliases + registration IR; aligned-vs-packed LLVM struct type strings; members laid out at their DWARF offsets with explicit padding and verified against a Julia mirror of LLVM's abiSize/abiAlign, degrading to a correctly-sized opaque region when they cannot be; packed structs nested by value in other struct bodies are inlined as byte-identical LLVM literals
FunctionGensrc/IRGen/ir_gen/FunctionGen.jlfunction or virtual method metadataexternal func.func private @mangled decl + public func.func @mangled_thunk wrapper with llvm.emit_c_interface; scope-RAII temporaries for non-trivial by-value class params
ArrayViewGensrc/IRGen/ir_gen/ArrayViewGen.jlfixed-size primitive array membersZero-copy get/set thunks through jlcs.load/store_array_element
STLContainerGensrc/IRGen/ir_gen/STLContainerGen.jlSTL method metadataAccessor thunks for size(), data(), etc.

Generation flow

generate_jlcs_ir(vtinfo, metadata; needed_symbols) produces a complete MLIR module:

  1. Struct aliases + registration: type aliases for all extracted structs (packed structs as !jlcs.c_struct, padded structs as !llvm.struct with packed members inlined as LLVM literals)
  2. Type info operations: jlcs.type_info for each class with non-empty members, carrying the DWARF-resolved destructor and the base/virtual-base tables
  3. Function thunks: func.func @mangled_thunk wrappers carrying llvm.emit_c_interface — filtered by needed_symbols (the wrapper's thunk manifest, i.e. dead-thunk elimination). Each body unpacks %args_ptr (ciface convention), emits jlcs.marshal_arg for packed-struct parameters, jlcs.scope copy-construct/destruct brackets for non-trivial by-value class parameters, jlcs.ffe_call / jlcs.try_call (per-function noexcept routing) or jlcs.vcall (virtual instance methods with scalar/pointer signatures), and jlcs.marshal_ret for packed-struct returns
  4. STL container thunks: Accessor thunks for detected STL containers (size, data, push_back, etc.)
  5. Array-view thunks: rank-1 strided accessors for fixed-size primitive array members

Slicer

Source: src/IRGen/Slicer.jl

Per-function bitcode slicing for Tier 1, on Julia's resident libLLVM. slice_library(abi_ll; targets, cache_dir) parses the promoted module once and clones it per target (LLVMCloneModule), then strips the clone to declarations: LLVMFunctionDeleteBody for every reached function, LLVMSetInitializer2(gv, NULL) for reached mutable and external constant globals, internalize + globaldce for everything unreached. Internal constants are embedded rather than declared. Every slice is verified before it is returned, and results are cached content-addressed under <cache>/slices/.

The closure is one level deep by construction — a declared function contributes no edges of its own — so slice size tracks the target function, not the library. lua_gettop cuts 15.8 MB down to 2.8 KB; luaL_openlibs lands at 6 KB. This is why max_slice_kb is a tripwire rather than a tuning knob.

Anything the Slicer cannot slice correctly comes back as a refusal with a reason, never as silently-wrong IR: a variadic target, a blockaddress into a body being deleted, alias/ifunc, or an unpromoted module (the fail-loud guard against slicing _opt.ll by mistake). Softer shapes come back as hazard flags for the generator's gate — :setjmp_family, :varargs_callee, :noinline, :weak, :inline_asm, :module_asm.

Each SliceResult also records the symbols the slice declares, post-DCE and excluding intrinsics. _tier1_preflight! in the C generator dlopens the .so RTLD_GLOBAL and dlsyms each one — the exact lookup ORC will perform at first call — because an unresolved declare does not raise: ORC prints Symbols not found: [...] and then blocks forever. A miss demotes that function to ccall; a .so that will not dlopen disables Tier 1 for the whole wrap.

ThunkBuilder

Source: src/Builder/ThunkBuilder.jl

AOT compilation path for Tier 2 thunks. When aot_thunks = true in replibuild.toml, wrap runs this module as its last step (Wrapper.wrap_library), with the thunk set the C++ generator has just decided. It drives the same JLCSIRGenerator.generate_jlcs_ir() used by the JIT path, lowers the result with MLIRNative.lower_to_llvm(), emits an object file through MLIRNative.emit_object(), and links it against the user's compiled library (clang/clang++ -shared, rpath'd to the library directory) into a companion shared library named <libname>_thunks.so, holding exactly the thunks the wrapper binds. With [link] enable_lto on it additionally emits and assembles the thunks' own LTO bitcode. An AOT failure is a warning, not a wrap failure: the wrapper is generated again with JIT dispatch.

The Julia wrapper then calls the AOT thunks rather than JITManager.invoke. There is no MLIR JIT at run time, but libJLCS is still loaded: the thunks import its exception helpers, and on Windows every thunk call goes through its guard frame. A consumer of an AOT wrapper needs libJLCS as well (Ship a package).

MLIRNative

Source: src/IRGen/MLIRNative.jl

Low-level ccall bindings to libJLCS.so, the compiled JLCS MLIR dialect shared library. Provides context management, module parsing, JIT engine creation, LLVM lowering, symbol lookup, and the object/IR emission used by the AOT path. Building the dialect (cd src/mlir && ./build.sh) is required only for the C++/Tier-2 bucket.

Two behaviours here are load-bearing beyond plain FFI plumbing. parse_module names the parse buffer after a content-hashed file it writes under the library's .debug/mlir/ — MLIR's parser stamps that name onto every op as a FileLineColLoc, and the lowering turns it into the emitted DWARF's DIFile, which is what makes a JIT'd thunk steppable in gdb. And jit_source_path falls back to a temp directory when .debug is unwritable (a read-only install), because losing co-location costs nothing while losing the source view costs the whole capability.

The dialect itself — its two types, fourteen ops, lowering pass, and calling contract — is documented in ABI Marshalling as Compiler IR.

JITManager

Source: src/IRGen/JITManager.jl

Runtime for Tier 2 dispatch: one MLIR execution engine per wrapped binary (LibraryEngine), held in a process-wide GLOBAL_JIT behind a shared, lock-free thunk cache.

Key design points

  • Per-library engines. initialize_global_jit(binary_path) is called from each generated module's __init__ and creates (or reuses) the engine for its binary. Multiple wrappers coexist in one session — previously the first wrapper won and the second library's entire Tier 2 silently died, found while composing box2d with pugixml. A per-library initialization failure degrades only that library, and a missing-symbol error names every engine that was searched.
  • Manifest-driven initialization: initialize_global_jit() reads thunk_manifest.json — the thunks the wrapper actually dispatches to — so dead thunks are never generated. Any initialization failure (including the pre-flight rejection of untranslatable IR types in libJLCS) degrades the module to "Tier 2 disabled" with ccall wrappers intact, never a process crash.
  • Symbol registration before lowering: the engine is given the library and libJLCS.so as shared libraries, and the C++ runtime EH symbols (__gxx_personality_v0, __cxa_begin_catch, __cxa_end_catch) plus the jlcs_* exception helpers are registered explicitly, since JIT'd landing pads reference them by name.
  • Lock-free hot path: _lookup_cached() reads from an @atomic snapshot of the symbol dictionary with no locking. The cache is published copy-on-write — a fresh dict is built with the new entry and atomically swapped in. Readers always see a stable, immutable snapshot.
  • Arity specialization: invoke is @generated, emitting arity-specialized code for any argument count — stack-allocated Refs and a fixed-size Ptr{Cvoid}[], allocation-free at every arity. A thunk slot holds a pointer to the argument's storage, so Ref(x) is the right shape for an isbits x; the two kinds that are already an indirection — an AbstractString, and a Base.Ref that is not a Ptr (what a caller passes for a C++ T const& parameter) — are flattened to a raw pointer first and GC-preserved across the call. Both invoke methods share one _arg_marshal_plan, because two copies is how a fix to one silently misses the other.
  • @generated return dispatch: _invoke_call resolves at compile time whether the return type is a primitive (direct ccall return) or a struct (sret buffer allocation). An unresolved Any return fails loudly with the actual cause instead of corrupting memory.
  • Exception propagation: After every Tier 2 call, _check_pending_exception() polls the thread-local exception buffer set by jlcs.try_call lowering. If a C++ exception was caught during the call, a CxxException is thrown with the original what() message.

Calling convention

All Tier 2 functions use a unified ciface calling convention:

ReturnSignature
ScalarT ciface(void** args_ptr)
Structvoid ciface(T* sret, void** args_ptr)
Voidvoid ciface(void** args_ptr)

Debug

Source: src/Debug/Debug.jl

Static inspection of what the Tier-2 pipeline actually emitted, for a package this process never built. thunks(pkg) lists the thunk symbols; mlir_body(pkg, symbol) prints the generated dialect; disassemble(pkg; symbol=…) shells to objdump -dS so dialect ops and machine code interleave; dwarf(pkg; section=…) shows the address → MLIR-line table; walk(pkg, symbol) does the common combination in one call.

The object file it disassembles only exists when the JIT's object cache was enabled, and MLIR requires that at engine-creation time — so it is read from REPLIBUILD_JIT_OBJDUMP before the wrapper loads, not passed as an argument. Nothing here links gdb or LLVM: it shells to objdump and llvm-dwarfdump. See Debugging a thunk for the live-process counterpart.

BuildBridge

Source: src/Builder/BuildBridge.jl

Low-level compiler driver that shells out to clang, clang++, llvm-link, opt, llvm-as, and nm. All subprocess invocations go through this module, providing a single point of control for toolchain interaction. It serves the C++ pipeline and the C bucket's [link] fallback = true escape hatch; the default C path links and optimizes in-process on Julia's libLLVM (see Compiler).

LLVMEnvironment

Source: src/Builder/LLVMEnvironment.jl

Detects the system LLVM/Clang toolchain by searching standard paths and version-suffixed binaries. Falls back to LLVM_full_jll when no system toolchain is found. Caches results in ~/.replibuild/toolchain.toml with a 24-hour TTL.

EnvironmentDoctor

Source: src/Builder/EnvironmentDoctor.jl

check_environment() validates both toolchain buckets: the C bucket (JLL clang + Julia's resident libLLVM — no external install required) and the C++/Tier 2 bucket (system LLVM/MLIR 21+, Clang, mlir-tblgen, CMake 3.20+, and libJLCS.so). Returns a ToolchainStatus struct indicating which tiers are available, with OS-specific install instructions for missing components.

DependencyResolver

Source: src/Builder/DependencyResolver.jl

Processes the [dependencies] table from replibuild.toml. Supports three dependency types:

TypeMechanism
gitShallow clone (--depth 1) into .replibuild_cache/deps/<name>/; re-fetches on tag change
localScanned in-place; no copying
systempkg-config --cflags to inject include paths

The exclude list is applied after scanning. Resolved source files merge into the compilation graph before the compile step.

PackageRegistry

Source: src/Builder/PackageRegistry.jl

Local package registry at ~/.replibuild/registry/. Provides:

  • register() — Store a project's build configuration
  • use() — Build + wrap + load a locally registered name, with artifact caching in ~/.replibuild/builds/<hash>/. No Hub fallback — a missing name is an error
  • search() — Query the Hub index by name, description, tags, or language (browse only; does not register)
  • list_registry() — Print all registered packages with hash, source, and build status
  • unregister() — Remove a package and clean cached builds

The build-cache key (hash_config) covers the TOML, sources, headers, and project git HEAD plus the generator fingerprint — RepliBuild's own version and git revision — so upgrading RepliBuild invalidates wrappers produced by older codegen. Cached wrappers resolve their .so sibling-first via @__DIR__, with the baked absolute path as fallback.

The REPLIBUILD_HOME environment variable overrides the default registry location; REPLIBUILD_HUB_URL points Hub operations at a private mirror.

STLWrappers

Source: src/Wrapper/Cpp/STLWrappers.jl

Detects STL container types (std::vector, std::string, std::map, etc.) in DWARF metadata and generates accessor functions. These are used by the MLIR IR generator (src/IRGen/ir_gen/STLContainerGen.jl) to produce JIT thunks for STL container methods.

ASTWalker

Source: src/Builder/ASTWalker.jl

Clang.jl-based AST walker for enum extraction. Handles enum class, hex values, namespaces, and other constructs that are difficult to extract reliably from DWARF alone. Replaces the earlier regex-based approach.

ClangJLBridge

Source: src/Builder/ClangJLBridge.jl

Integration module for Clang.jl header parsing. Used by the wrapper generator when use_clang_jl = true to supplement DWARF metadata with AST-level information.

Scaffold

Source: src/Builder/PackageRegistry.jl (scaffold_package function)

Generates a distributable Julia package from a registered RepliBuild project. The scaffolded package includes the compiled shared library, generated wrapper module, and a standard Julia Project.toml — ready for Pkg.add().