Internals & Dispatch
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. It is aimed at contributors and advanced integrators, not everyday use.
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:
- DependencyResolver — resolve the
[dependencies]table (git / local / system) and merge the external sources into the build graph. - Discovery — scan files and the
#includegraph; write or refreshreplibuild.toml. - Compiler — Clang compiles each translation unit to LLVM IR, cached on source
mtimeplus a compile fingerprint (flags, defines, include dirs, LLVM version, target triple). - Linker — link, optimize, and assemble the IR into the
.so(plus LTO bitcode whenenable_ltois on). - DWARFParser —
llvm-dwarfdump+nmproduceClassInfo/VtableInfoand thecompilation_metadata.jsonlayout facts. - Wrapper generator — DWARF + symbols drive the C or C++ generator to emit the Julia module.
- 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:
| Tier | Mechanism | Selected when |
|---|---|---|
| 1 | Base.llvmcall against a per-function bitcode slice | POD args, scalar/pointer return, [wrap.tier1] enable = true (C only) |
| 2 | MLIR thunk via libJLCS.so (JIT at load, or AOT _thunks.so) | packed structs, unions, large by-value struct returns, C++ classes, virtual dispatch, exceptions |
| 3 | ccall into the .so | the 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.
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/GeneratorC.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. 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.
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
| Module | Source | Role |
|---|---|---|
Wrapper.Generator | src/Wrapper/Generator.jl | Top-level wrap_library() entry point; dispatches to C or C++ generator |
Wrapper.DispatchLogic | src/Wrapper/DispatchLogic.jl | Per-function tier routing decisions (is_ccall_safe, is_c_lto_safe) |
Wrapper.TypeRegistry | src/Wrapper/TypeRegistry.jl | TypeRegistry and TypeStrictness — shared type-resolution context |
Wrapper.Symbols | src/Wrapper/Symbols.jl | ParamInfo / SymbolInfo structs for structured symbol data |
Wrapper.FunctionPointers | src/Wrapper/FunctionPointers.jl | DWARF function_ptr(...) signature to Julia @cfunction type string |
Wrapper.Utils | src/Wrapper/Utils.jl | Keyword escaping, identifier sanitization shared between generators |
Wrapper.C.GeneratorC | src/Wrapper/C/GeneratorC.jl | Full C wrapper generator (structs, enums, functions, LTO, thunks) |
Wrapper.C.TypesC | src/Wrapper/C/TypesC.jl | C type heuristics and base type map |
Wrapper.C.UtilsC | src/Wrapper/C/UtilsC.jl | C-specific identifier/format helpers |
Wrapper.C.IdentifiersC | src/Wrapper/C/IdentifiersC.jl | C name sanitization |
Wrapper.Cpp.GeneratorCpp | src/Wrapper/Cpp/GeneratorCpp.jl | Full C++ wrapper generator (classes, inheritance flattening with subobject-offset rebasing, as_<Base>/as_<VBase> upcasts, Managed handles, virtual dispatch) |
Wrapper.Cpp.TypesCpp | src/Wrapper/Cpp/TypesCpp.jl | C++ type map including STL, templates, references |
Wrapper.Cpp.IdentifiersCpp | src/Wrapper/Cpp/IdentifiersCpp.jl | Namespace stripping, operator sanitization |
Wrapper.Cpp.UtilsCpp | src/Wrapper/Cpp/UtilsCpp.jl | C++ formatting helpers |
Wrapper.Cpp.STLWrappers | src/Wrapper/Cpp/STLWrappers.jl | STL container type detection and accessor generation |
Wrapper.Rust.GeneratorRust | src/Wrapper/Rust/GeneratorRust.jl | Experimental 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:
- STL container types — Any STL type in parameters or return forces Tier 2
- Return type safety:
- Template returns (contains
<) → Tier 2 (unpredictable ABI) - Struct return by value > 16 bytes → Tier 2 (too large for
ccallsret) - Non-POD class return → Tier 2
- Packed struct return (DWARF size != Julia aligned size) → Tier 2
- Template returns (contains
- Parameter type safety:
- Union parameters → Tier 2
- Packed struct parameters → Tier 2
- Exception safety — Per-function
is_noexceptflag from DWARF. If absent (function may throw) and the module'smay_throwsetting is on, the function routes throughjlcs.try_callrather thanccall.
For struct-graph cases where pairwise heuristics miss transitive layout mismatches (a non-packed struct that contains a packed struct, for example), src/IRGen/DAGDiff.jl performs a structural type-graph diff to surface bad cases and produces a topo-sorted lowering order for multi-type thunks.
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:
- Factory detection: Functions matching
create_X,new_X,make_X,alloc_X,init_X, or returningX*are identified as constructors. - Destructor detection: Functions matching
delete_X,destroy_X,free_X,dealloc_X, orX_destroyare identified as destructors. - 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.
RepliBuild.Wrapper.is_enum_like — Method
is_enum_like(cpp_type::String)::BoolCheck 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.
RepliBuild.Wrapper.is_function_pointer_like — Method
is_function_pointer_like(cpp_type::String)::BoolCheck if a C++ type contains function pointer syntax. Looks for patterns: (name), ()(args), (^name) (blocks)
RepliBuild.Wrapper.is_struct_like — Method
is_struct_like(cpp_type::String)::BoolCheck if a C++ type looks like a struct/class based on naming conventions. Structs typically: start with uppercase, contain only alphanumeric + underscore.
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.
RepliBuild.Wrapper.wrap_library — Method
wrap_library(config::RepliBuildConfig, library_path::String;
headers::Vector{String}=String[],
generate_tests::Bool=false,
generate_docs::Bool=true)Generate Julia wrapper for compiled library.
Always uses introspective (DWARF metadata) wrapping when metadata is available, otherwise falls back to basic symbol-only extraction with conservative types.
Arguments
config: RepliBuildConfig with wrapper settingslibrary_path: Path to compiled library (.so, .dylib, .dll)headers: Optional header files (currently unused, reserved for future)generate_tests: Generate test file (default: false, TODO)generate_docs: Include comprehensive documentation (default: true)
Returns
Path to generated Julia wrapper file
RepliBuild.Wrapper.ParamInfo — Type
ParamInfoInformation about a function parameter.
RepliBuild.Wrapper.SymbolInfo — Type
SymbolInfoComprehensive information about a symbol extracted from binary/headers.
RepliBuild.Wrapper.TypeRegistry — Type
TypeRegistryRepliBuild.Wrapper.TypeStrictness — Type
TypeStrictnessCompiler
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
- Auto-discovery and dependency resolution: Scans the project directory, resolving file paths and external git/local dependencies to merge into the build graph.
- Pre-processing (shims and templates): Dynamically generates C/C++ shim files for configured macros and explicitly instantiates templates based on
replibuild.tomlsettings. 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#includeresolves inside the project/dependency tree — the shim TU lives under the build cache, so a bare include could otherwise fall through the-Ipath to a system-installed header at a different version and silently bake wrong macro values. - Compilation to LLVM IR: Translates source code into
.lltext format —.cvia the JLLclang,.cppvia systemclang++. The per-file cache is keyed on sourcemtimeplus a compile fingerprint (flags, defines, include dirs, LLVM version, target triple); config changes can never silently reuse stale IR. - IR transformation and sanitization: Strips attributes incompatible with Julia's internal LLVM JIT, removes
va_start/va_endintrinsics from varargs function bodies (varargs are routed through true-variadic@ccallwrapper generation), and cleans mismatched debug metadata. - 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 = trueselects the externalllvm-link/optpipeline instead. C++ always uses the external pipeline. - Static promotion (
_promote_statics_libllvm, C in-process bucket,[link] promote_statics): see below. - Codegen: The final
.ll → .sostep 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.
RepliBuild.Compiler.build_type_registry — Method
Build type registry from functions. Collects all unique types used in the codebase.
RepliBuild.Compiler.compile_project — Method
Complete compilation workflow: discover sources, compile, link, create binary. This is the main entry point for building a project.
RepliBuild.Compiler.compile_to_ir — Method
Compile multiple C++ files to LLVM IR (with parallel support). Returns vector of IR file paths.
RepliBuild.Compiler.compute_project_hash — Method
Compute a content hash for the entire project: replibuild.toml + source files + git HEAD. Returns a hex string that changes when anything relevant changes.
RepliBuild.Compiler.create_executable — Function
Create executable from LLVM IR. Returns path to executable file.
RepliBuild.Compiler.create_library — Function
Create shared library from LLVM IR. Returns path to library file.
RepliBuild.Compiler.dwarf_type_to_julia — Method
Comprehensive C/C++ type to Julia type mapping. Handles all standard C/C++ types including sized integers, pointers, and qualifiers.
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")
RepliBuild.Compiler.extract_compilation_metadata — Method
Extract compilation metadata from source files and binary. This is the core of automatic wrapper generation!
RepliBuild.Compiler.extract_dwarf_return_types — Method
Extract return types and struct definitions from DWARF debug info. Returns: (returntypesdict, structdefsdict)
- returntypes: Dict{mangledname => {ctype, juliatype, size}}
- structdefs: Dict{structname => {members: [{name, type, offset}]}}
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> >"
RepliBuild.Compiler.extract_mangled_name — Method
Extract mangled symbol name from nm output.
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. Returns Dict mapping normalized container type -> vector of method info dicts.
RepliBuild.Compiler.extract_symbols_from_binary — Method
Extract symbol information from compiled binary using nm. Returns vector of symbol dictionaries with mangled/demangled names.
RepliBuild.Compiler.generate_macro_shims — Method
Generate a C/C++ shim file to instantiate requested macros as typed functions so they appear in the DWARF metadata and can be wrapped.
RepliBuild.Compiler.generate_template_instantiations — Method
Generate a dummy C++ file to explicitly instantiate requested templates so they appear in the DWARF metadata.
RepliBuild.Compiler.get_type_size — Method
Get type size in bytes from C/C++ type name.
RepliBuild.Compiler.infer_return_type — Method
Infer return type from demangled function signature using pattern matching. This is a fallback when DWARF debug info is unavailable for a given function.
RepliBuild.Compiler.ingest_library — Method
ingest_library(config) -> StringBYOB ("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).
RepliBuild.Compiler.is_project_cache_valid — Method
Check if the project-level cache is valid. Returns true if all artifacts exist and the content hash matches, meaning the build can be skipped entirely.
RepliBuild.Compiler.link_optimize_ir — Method
Link multiple LLVM IR files and optimize. Returns linked IR path (String) or original IR files (Vector{String}) on llvm-link failure.
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).
RepliBuild.Compiler.parse_function_signatures — Function
Parse function signatures from symbol information. Infers parameter types and return types from demangled names.
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.
RepliBuild.Compiler.sanitize_ir_for_julia — Method
sanitize_ir_for_julia(ir_text::String) -> StringSanitize 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.
RepliBuild.Compiler.save_compilation_metadata — Method
Save compilation metadata to JSON file next to binary. This enables automatic wrapper generation!
RepliBuild.Compiler.save_project_hash — Method
Save the project hash after a successful build.
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.create_default_config — Function
Create a default configuration and save to file.
RepliBuild.ConfigurationManager.get_build_path — Method
Get full build path (project_root + paths.build)
RepliBuild.ConfigurationManager.get_cache_path — Method
Get full cache path (project_root + cache.directory)
RepliBuild.ConfigurationManager.get_compile_flags — Method
Get compiler flags
RepliBuild.ConfigurationManager.get_include_dirs — Method
Get all include directories (from config)
RepliBuild.ConfigurationManager.get_library_name — Method
Get library output name (uses config or auto-generates)
RepliBuild.ConfigurationManager.get_module_name — Method
Get wrapper module name (uses config or auto-generates)
RepliBuild.ConfigurationManager.get_output_path — Method
Get full output path (project_root + paths.output)
RepliBuild.ConfigurationManager.get_source_files — Method
Get all C++ source files (from config)
RepliBuild.ConfigurationManager.is_cache_enabled — Method
Should use cache?
RepliBuild.ConfigurationManager.is_parallel_enabled — Method
Should run parallel compilation?
RepliBuild.ConfigurationManager.is_stage_enabled — Method
Check if a stage is in the workflow
RepliBuild.ConfigurationManager.load_config — Function
Load RepliBuildConfig from TOML file. This is the ONLY function that parses TOML - all other modules use this.
RepliBuild.ConfigurationManager.merge_compile_flags — Method
Merge compile flags into config (creates new config). Used for runtime overrides like: compile(sources, flags=["-O3"])
RepliBuild.ConfigurationManager.print_config — Method
Print configuration summary
RepliBuild.ConfigurationManager.save_config — Method
Save configuration to TOML file. Only saves user-configurable settings (not runtime data).
RepliBuild.ConfigurationManager.validate_config! — Method
Validate config and throw error if invalid.
RepliBuild.ConfigurationManager.validate_config — Method
Validate configuration and return list of errors (empty if valid).
RepliBuild.ConfigurationManager.with_discovery_results — Method
Update both source files and include dirs (creates new config). Used after discovery completes.
RepliBuild.ConfigurationManager.with_include_dirs — Method
Update include directories in config (creates new config). Used after discovery finds include paths.
RepliBuild.ConfigurationManager.with_source_files — Method
Update source files in config (creates new config). Used after discovery finds C++ sources.
RepliBuild.ConfigurationManager.BinaryConfig — Type
Nested struct for [binary] section
RepliBuild.ConfigurationManager.CacheConfig — Type
Nested struct for [cache] section
RepliBuild.ConfigurationManager.CompileConfig — Type
Nested struct for [compile] section
RepliBuild.ConfigurationManager.DependenciesConfig — Type
Nested struct for [dependencies] section
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.
RepliBuild.ConfigurationManager.DiscoveryConfig — Type
Nested struct for [discovery] section
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.
RepliBuild.ConfigurationManager.LLVMConfig — Type
Nested struct for [llvm] section
RepliBuild.ConfigurationManager.LinkConfig — Type
Nested struct for [link] section
RepliBuild.ConfigurationManager.PathsConfig — Type
Nested struct for [paths] section
RepliBuild.ConfigurationManager.ProjectConfig — Type
Nested struct for [project] section
RepliBuild.ConfigurationManager.RepliBuildConfig — Type
Main immutable configuration structure. All modules receive this struct - it's the single source of truth.
RepliBuild.ConfigurationManager.TypesConfig — Type
Nested struct for [types] section - Type validation settings
RepliBuild.ConfigurationManager.WorkflowConfig — Type
Nested struct for [workflow] section
RepliBuild.ConfigurationManager.WrapConfig — Type
Nested struct for [wrap] section
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) -> StringMain discovery pipeline - scans project and generates configuration.
Process:
- Check for existing replibuild.toml (project identified by presence of replibuild.toml)
- Scan all files and categorize
- Detect and analyze binaries
- Walk AST dependencies using clang
- Generate or update replibuild.toml with discovered data
- Optionally run build and wrap pipeline
Arguments
target_dir: Project directory (default: current directory)force: Force rediscovery even if replibuild.toml existsunsafe: Bypass safety checks (use with extreme caution)build: Automatically run build() after discovery (default: false)wrap: Automatically run wrap() after build (requires build=true, 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.tomlfile
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)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
| Type | Fields | Role |
|---|---|---|
ClassInfo | name, vtable_ptr_offset, base_classes, base_offsets, virtual_bases, virtual_methods, members, size | Complete class/struct description with byte-level layout, inheritance chain (subobject offsets), and virtual-base flags |
VtableInfo | classes, vtable_addresses, method_addresses | Aggregate metadata for all classes in a binary |
VirtualMethod | name, mangled_name, slot, return_type, parameters | Single virtual method with the slot index in its declaring class's primary vtable |
MemberInfo | name, type_name, offset | Struct field with byte offset from struct base |
Extraction targets
| DWARF Tag | Extracted Data |
|---|---|
DW_TAG_class_type / DW_TAG_structure_type | Class/struct name, byte size, members, virtual methods, inheritance |
DW_TAG_member | Field 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_inheritance | Base class with subobject offset; for virtual bases, the vtable-relative offset expression parsed into vbase_vtable_offset |
DW_TAG_enumeration_type | Enum definitions |
DW_TAG_union_type | Union layout |
DW_TAG_variable | Global variables |
DW_TAG_typedef | Type aliases |
RepliBuild.DWARFParser.export_vtable_json — Method
export_vtable_json(vtinfo::VtableInfo, output_path::String)Export vtable information to JSON for inspection or use by other tools.
RepliBuild.DWARFParser.parse_dwarf_output — Method
parse_dwarf_output(dwarf_text::String) -> Dict{String, ClassInfo}Parse llvm-dwarfdump output to extract class and vtable information.
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).
RepliBuild.DWARFParser.parse_vtables — Method
parse_vtables(binary_path::String) -> VtableInfoExtract complete vtable information from a binary using DWARF and symbol table.
Arguments
binary_path: Path to compiled binary with debug info
Returns
VtableInfocontaining classes, vtable addresses, and method addresses
RepliBuild.DWARFParser.read_vtable_data — Method
read_vtable_data(binary_path::String, vtable_addr::UInt64, num_entries::Int) -> Vector{UInt64}Read actual vtable function pointers from binary at given address.
RepliBuild.DWARFParser.ClassInfo — Type
Information about a C++ class with virtual methods
RepliBuild.DWARFParser.MemberInfo — Type
Information about a data member (field)
RepliBuild.DWARFParser.VirtualMethod — Type
Information about a virtual method
RepliBuild.DWARFParser.VtableInfo — Type
Complete vtable information from binary
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
| Module | Source | Input | Output |
|---|---|---|---|
TypeUtils | src/IRGen/ir_gen/TypeUtils.jl | C++ type string | MLIR type string (f64, i32, !llvm.ptr, etc.) |
StructGen | src/IRGen/ir_gen/StructGen.jl | struct metadata | Struct 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 |
FunctionGen | src/IRGen/ir_gen/FunctionGen.jl | function or virtual method metadata | external 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 |
ArrayViewGen | src/IRGen/ir_gen/ArrayViewGen.jl | fixed-size primitive array members | Zero-copy get/set thunks through jlcs.load/store_array_element |
STLContainerGen | src/IRGen/ir_gen/STLContainerGen.jl | STL method metadata | Accessor thunks for size(), data(), etc. |
Generation flow
generate_jlcs_ir(vtinfo, metadata; needed_symbols) produces a complete MLIR module:
- Struct aliases + registration: type aliases for all extracted structs (packed structs as
!jlcs.c_struct, padded structs as!llvm.structwith packed members inlined as LLVM literals) - Type info operations:
jlcs.type_infofor each class with non-empty members, carrying the DWARF-resolved destructor and the base/virtual-base tables - Function thunks:
func.func @mangled_thunkwrappers carryingllvm.emit_c_interface— filtered byneeded_symbols(the wrapper's thunk manifest, i.e. dead-thunk elimination). Each body unpacks%args_ptr(ciface convention), emitsjlcs.marshal_argfor packed-struct parameters,jlcs.scopecopy-construct/destruct brackets for non-trivial by-value class parameters,jlcs.ffe_call/jlcs.try_call(per-function noexcept routing) orjlcs.vcall(virtual instance methods with scalar/pointer signatures), andjlcs.marshal_retfor packed-struct returns - STL container thunks: Accessor thunks for detected STL containers (size, data, push_back, etc.)
- Array-view thunks: rank-1 strided accessors for fixed-size primitive array members
DAGDiff
Source: src/IRGen/DAGDiff.jl
Structural type-graph diff used by tier selection and IR generation when a struct may contain other structs whose layouts disagree between Julia and C++. The pairwise check in is_ccall_safe() catches direct packed-vs-aligned mismatches; DAGDiff catches the transitive cases — a non-packed struct that contains a packed struct as a field, a struct chain through a typedef alias, etc. It outputs a topo-sorted lowering order so that the MLIR thunks for dependent types are emitted in the right sequence.
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, this module 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. With [link] enable_lto on it additionally emits and assembles the thunks' own LTO bitcode. An AOT failure is a warning, not a build failure — the JIT path remains available.
The Julia wrapper then ccalls into the AOT thunks rather than calling JITManager.invoke. There is no MLIR JIT at runtime — libJLCS.so is only needed at build time for the lowering step. After AOT compilation, the user can ship the wrapped library + thunks .so without bundling LLVM/MLIR runtime libraries.
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()readsthunk_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 inlibJLCS) degrades the module to "Tier 2 disabled" withccallwrappers intact, never a process crash. - Symbol registration before lowering: the engine is given the library and
libJLCS.soas shared libraries, and the C++ runtime EH symbols (__gxx_personality_v0,__cxa_begin_catch,__cxa_end_catch) plus thejlcs_*exception helpers are registered explicitly, since JIT'd landing pads reference them by name. - Lock-free hot path:
_lookup_cached()reads from an@atomicsnapshot 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:
invokeis@generated, emitting arity-specialized code for any argument count — stack-allocatedRefs and a fixed-sizePtr{Cvoid}[], allocation-free at every arity. A thunk slot holds a pointer to the argument's storage, soRef(x)is the right shape for anisbitsx; the two kinds that are already an indirection — anAbstractString, and aBase.Refthat is not aPtr(what a caller passes for a C++T const¶meter) — are flattened to a raw pointer first and GC-preserved across the call. Bothinvokemethods share one_arg_marshal_plan, because two copies is how a fix to one silently misses the other. @generatedreturn dispatch:_invoke_callresolves at compile time whether the return type is a primitive (directccallreturn) or a struct (sretbuffer allocation). An unresolvedAnyreturn 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 byjlcs.try_calllowering. If a C++ exception was caught during the call, aCxxExceptionis thrown with the originalwhat()message.
Calling convention
All Tier 2 functions use a unified ciface calling convention:
| Return | Signature |
|---|---|
| Scalar | T ciface(void** args_ptr) |
| Struct | void ciface(T* sret, void** args_ptr) |
| Void | void 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:
| Type | Mechanism |
|---|---|
git | Shallow clone (--depth 1) into .replibuild_cache/deps/<name>/; re-fetches on tag change |
local | Scanned in-place; no copying |
system | pkg-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 configurationuse()— Build + wrap + load, with artifact caching in~/.replibuild/builds/<hash>/; on a local miss, fetches the package config from the RepliBuild-Hub community registrysearch()— Query the Hub index by name, description, tags, or languagelist_registry()— Print all registered packages with hash, source, and build statusunregister()— 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().