# \[ANN\] RepliBuild.jl - A full C/C++ interop toolkit for tiered FFI generation

**URL:** <https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122>\
**Category:** Package Announcements\
**Created:** [March 10, 2026, 5:42pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122 "2026-03-10T17:42:57Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [March 10, 2026, 5:42pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/1 "2026-03-10T17:42:57Z")

</div>

RepliBuild.jl, a declarative compiler bridge designed to automatically generate hyper-optimized FFI bindings directly from C and C++ source code.  
​Instead of relying on fragile header parsing, RepliBuild drives a local compilation pipeline and extracts structural DWARF debug metadata directly from the generated objects. It uses this pure structural data to automatically synthesize a tiered FFI boundary, generating the safest and fastest possible dispatch for every function.  
​Tiered FFI Generation  
​RepliBuild classifies and routes functions automatically based on their complexity:  
​Zero-Cost Abstractions (Base.llvmcall): When cross-language Link-Time Optimization (LTO) is enabled, the compiler emits LLVM Bitcode (.bc). The generated Julia wrapper dynamically loads this bitcode at parse-time, routing execution entirely through Base.llvmcall instead of standard ccall. This allows Julia’s JIT to seamlessly inline C/C++ code directly into Julia hot loops.  
​Tier 1 Safe ccall & GC Preservation: For standard FFI boundaries, it emits highly structured ccall bindings. It automatically generates idiomatic mutable struct wrappers equipped with GC-traced finalizers and Base.unsafe\_convert methods to guarantee memory safety across the boundary.  
​Tier 2 MLIR / AOT Thunks: For complex C++ paradigms (like virtual method dispatch, packed structs, and large struct returns), execution is routed transparently through an MLIR JIT or statically compiled Ahead-Of-Time (AOT) thunks.

Declarative Workflow & Caching  
​There is zero manual wrapper boilerplate required. You define the project in a single TOML file:  
​replibuild.toml

```julia-auto
[dependencies.lua]
type = "git"
url = "https://github.com/lua/lua.git"
tag = "v5.4.6"

[compile]
aot_thunks = true

[link]
enable_lto = true

[wrap]
language = "c"

```

On the julia side

```julia-auto
# Resolves dependencies, builds, wraps, and globally caches the artifacts
RepliBuild.register("replibuild.toml") 

# Instantly loads the cached module and LLVM bitcode
Lua = RepliBuild.use("lua_wrapper") 

```

RepliBuild features aggressive project-level content hashing. Successive calls to RepliBuild.use() load the cached payload instantly, completely bypassing the compilation tax and dropping Time-To-First-Plot (TTFP) to near zero.  
​Current Status  
​The architecture is heavily modularized into independent C and C++ generators:  
​The C Pipeline is highly stable, defaults to LTO, and automatically falls back to Julia’s internal LLVM (Clang\_unified\_jll) to guarantee strict llvmcall compatibility.  
​The C++ Pipeline natively handles deep layout constraints like bitfields, unions, and template instantiation. (Note: Advanced C++ bridging currently requires a local LLVM 21 toolchain).  
​You can find the repository, documentation in the julia registry or the public repo.  
​Feedback, edge-case testing, and issue reports are highly welcome! This isnt a half baked toy. I worked on this for a year now with prior experience with the mlir jit. Have fun.

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [March 10, 2026, 6:00pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/2 "2026-03-10T18:00:40Z")

</div>

Also started working on the rustc generator will commit the first skeleton of the generator, rust dwarf is alot cleaner and I think RepliBuild.jl and julia will eventually handle rust better overall.

EDITED: The rustc intergration went really smooth, it will be awhile before any wrapping of native rust without the export shims like RepliBuild.jl can do for c and cpp, the rust compiler is very different. The v2.5.0 and up will be just stabalizing cpp features and hardening the rust intergration and anything the julia community can poke holes in that I can patch right away.

---

<div class="post-metadata">

**Author:** ![oschulz](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/oschulz/32/2998_2.png) [@oschulz](https://discourse.julialang.org/u/oschulz)\
**Post date:** [March 10, 2026, 10:12pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/3 "2026-03-10T22:12:09Z")

</div>

CCing @grasph and @peremato here, who have also done work regard to auto-generation of C++ bindings.

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [March 10, 2026, 11:30pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/4 "2026-03-10T23:30:22Z")

</div>

Wanna colaborate, you could send some details of what your working with, if its dwarf then I had to write a custom dwarf parser because julia just doesnt have one… Im intrested, do you compile into a dialect or merge ir anywhere im your pipeline. If i can help just let me know.

---

<div class="post-metadata">

**Author:** ![grasph](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/grasph/32/25684_2.png) [@grasph](https://discourse.julialang.org/u/grasph)\
**Post date:** [March 11, 2026, 8:58am UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/5 "2026-03-11T08:58:21Z")

</div>

Hello @obsidianjulua,

Very interesting work. The work Oliver is mentioning is [WrapIt!](https://github.com/grasph/wrapit). It has been used to wrap large C++ framerwork, like CERN ROOT ([GitHub - grasph/wrapit: Automatization of C++--Julia wrapper generation · GitHub](https://github.com/grasph/wrapit)), Geant4 ([GitHub - JuliaHEP/Geant4.jl: Julia bindings to the Geant4 simulation toolkit · GitHub](https://github.com/JuliaHEP/Geant4.jl)), and Pythia8 ([GitHub - JuliaHEP/PYTHIA8.jl: Pythia8 Julia interface · GitHub](https://github.com/JuliaHEP/Pythia8.jl)).

The Julia/C++ interface is performed using [CxxWrap](https://github.com/JuliaInterop/CxxWrap.jl). The WrapIt! tool parses header files using LLVM/Clang to generate the wrapper code for CxxWrap.

I see that with your tool, we just provide a C++ project directory. How does the tool know how the C++ code is built and which products must be bound to Julia ?

Philippe.

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [March 11, 2026, 12:24pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/6 "2026-03-11T12:24:05Z")

</div>

To answer your questions: RepliBuild does not try to guess or reverse-engineer an existing build system like CMake. Instead, it acts as the build orchestrator and metadata extractor simultaneously, driven entirely by a declarative replibuild.toml file.  
​Here is how it handles those two steps:  
​1. How does it know how the C++ code is built?  
The user defines the build explicitly in the replibuild.toml. You point RepliBuild at local source files or a remote Git repository, and supply the necessary include paths, compiler flags, and definitions.  
​RepliBuild essentially acts as a localized compiler driver. It invokes clang (or rustc for Rust projects) on those sources directly. We bypass the host’s CMake/Make systems to guarantee we have full control over the compilation flags—specifically, ensuring that rich DWARF debug metadata is generated during the build.  
​2. How does it know which products must be bound to Julia?  
This is where RepliBuild fundamentally diverges from the header-parsing approach.  
​Instead of parsing the C++ text headers via Clang ASTs to generate a CxxWrap C++ shim, RepliBuild analyzes the DWARF debug metadata of the compiled objects. The DWARF tree contains the absolute, ground-truth structural layout of the code exactly as the compiler synthesized it: including vtable offsets, struct padding, bitfields, and template instantiations.  
​RepliBuild reads this structural data and automatically generates pure-Julia ccall or llvmcall wrappers for the public ABI. If something is present in the DWARF metadata (and not explicitly filtered out in the TOML configuration), it gets mapped into the Julia TypeRegistry and bound automatically.  
​Because we have this deep structural data, we don’t need an intermediate C++ wrapping layer—the generated Julia code can marshal the memory layout perfectly and execute cross-language LTO directly through Julia’s JIT.  
​I would love to hear your thoughts on this DWARF-first approach compared to WrapIt!'s header parsing! Also this just lets you force another build system to enforce debug flags and RepliBuild just wrap the binary, things are different in the IR, if your ever going to use pure julia then all the data has to be exposed to julia jit to line all the code and wrase the ffi boundry, if your relying on just header parsing then youll never wrap from julia and always need to write code in another language which is just architecturally wrong.

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [March 11, 2026, 12:34pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/7 "2026-03-11T12:34:15Z")

</div>

This does create executables, shared libs, create projects, even register wrappers but I promise those are just supplimentary features to secure the source. This is not a build system.

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [March 12, 2026, 4:45am UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/9 "2026-03-12T04:45:43Z")

</div>

# RepliBuild Hub

Community-maintained registry of `replibuild.toml` configs for popular C/C++ libraries. Search, fetch, and build wrappers directly from Julia — no manual setup required.

**Repository:** [github.com/obsidianjulua/RepliBuild-Hub](https://github.com/obsidianjulua/RepliBuild-Hub)

## Usage

```julia
using RepliBuild

# Search available packages
RepliBuild.search("lua")

# Install and use — one call does everything
Lua = RepliBuild.use("lua")
Lua.luaL_newstate()

```

`use()` checks your local registry first. On a miss, it fetches the TOML from the hub, registers it locally, then runs the full pipeline: dependency resolution → compile → link → DWARF introspect → wrap → load.

Subsequent calls are cached — rebuild only happens when the TOML or source content changes.

## Hub Repository Structure

```julia-auto
RepliBuild-Hub/
  index.toml # package listing (used by search())
  packages/
    lua/
      replibuild.toml
    sqlite/
      replibuild.toml
    cjson/
      replibuild.toml

```

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [March 28, 2026, 11:53am UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/10 "2026-03-28T11:53:20Z")

</div>

### New: DAG Diff — Structural Mismatch Detection Between C++ and Julia IR

Added a DAG-based structural diff algorithm that compares C++ layouts (DWARF ground truth) against Julia’s inferred alignment rules. This extends the existing per-function heuristics in `DispatchLogic.jl` — heuristics catch the obvious cases (packed returns, unions, STL), while DAGDiff catches what point-wise checks miss: transitive layout drift through by-value containment chains.

**Algorithm:**

1. Build C++ graph from DWARF metadata (struct sizes, member offsets, containment edges)
2. Build Julia graph by computing `min(sizeof(field), 8)` aligned layouts from the same members
3. Parallel walk — match nodes structurally, record size and per-member offset mismatches
4. Propagate mismatches transitively through by-value containment (if Inner is packed and Outer contains Inner by value, Outer is also mismatched)
5. Flag functions that pass or return mismatched types by value
6. Topo-sort (Kahn’s algorithm) all thunk sites for safe lowering order — types before the functions that depend on them

**Integration:**

- `DAGDiff.needs_dag_thunk(symbol, result)` queries the mismatch map — wrapper generators check this alongside existing heuristics, routing to MLIR thunks if either fires
- Backward compatible: `needs_dag_thunk(_, nothing)` returns `false` when DAG diff is not computed
- Wired into both C and C++ generator dispatch sites in `GeneratorC.jl` and `GeneratorCpp.jl`

**Visualization:**

- `export_dot(result, path)` — Graphviz DOT export with mismatch color-coding (red = layout mismatch, orange = function needs thunk, gray = safe)
- `render_dot(result, path)` — renders DOT to SVG/PNG/PDF via the `dot` command
- Per-member offset annotations, containment edges, propagation edge coloring
- Three view modes: `:diff` (both graphs overlaid), `:cpp` (DWARF only), `:julia` (inferred alignment only)

**TOML configuration:**

```toml
[wrap]
dag = true # exports DAG graphs to <project_root>/dag/

```

When enabled, the wrap stage automatically exports `diff.svg`, `cpp.svg`, `julia.svg`, and `diff.dot` to a `dag/` folder in the project root.

**Files:**

- `src/IRGen/DAGDiff.jl` — New module (~780 lines): graph types, builders, diff algorithm, topo-sort, query API, DOT visualization
- `src/Builder/ConfigurationManager.jl` — Added `dag::Bool` to `WrapConfig`
- `src/Wrapper/Generator.jl` — DAG diff computed before wrapper generation; graphs exported when `dag=true`
- `src/Wrapper/C/GeneratorC.jl`, `src/Wrapper/Cpp/GeneratorCpp.jl` — Dispatch sites augmented with `needs_dag_thunk` check
- `test/dag_test/` — 178 tests covering graph building, structural diff, transitive propagation, topo-sort, query API, DOT export, and a rendered gallery of 7 scenarios

**Stress test results (73 functions, `test/stress_test/`):**

- 25 mismatches detected: 14 types (vtable offsets on polymorphic classes, compound struct padding, bool alignment, STL internals), 5 functions routed to thunks (`compute_lu`, `compute_qr`, `compute_eigen`, `solve_ode_rk4`, `solve_ode_adaptive`)

- Transitive propagation working: `uniform_real_distribution<double>` flagged solely because it contains `param_type` by value.

 ![containment_julia](https://global.discourse-cdn.com/julialang/original/3X/d/a/da5ee5ef65ba8edb77103eca89f09bc66badab51.svg)

 ![deep_chain](https://global.discourse-cdn.com/julialang/original/3X/7/e/7eafc3689ea06f5d563b76628a34706f2cf9878b.svg)  
 ![mixed](https://global.discourse-cdn.com/julialang/original/3X/d/d/dd188aaaf6c4bf695026579f1ac90116dbf55a86.svg)

## New operations

### `jlcs.marshal_arg` — Julia-aligned struct → C-packed value

**Defined in:** `src/mlir/JLCSOps.td`

**Lowering:** `src/mlir/impl/JLCSPasses.cpp` (`MarshalArgOpLowering`)

Reads a Julia-aligned struct through a pointer and reassembles its fields into a C-packed LLVM struct value ready to pass to an external function.

Before v2.5.6, `FunctionGen.jl` emitted this as an inline sequence of `arith.constant` / `llvm.getelementptr` / `llvm.load` / `llvm.insertvalue` operations directly in the thunk IR. That sequence was correct but verbose, hard to pattern-match, and scattered the layout-mismatch logic across generated IR text rather than in a verifiable dialect op.

`jlcs.marshal_arg` lifts the whole pattern into a single named operation that the MLIR verifier can check, the pass pipeline can recognise, and the DOT visualiser can annotate.

**MLIR syntax:**

```mlir

%packed = jlcs.marshal_arg %ptr

{ memberTypes = [i32, f64], juliaOffsets = [0 : i64, 8 : i64] }

: (!llvm.ptr) -> !llvm.struct<packed (i32, f64)>

```

**Lowering (`MarshalArgOpLowering`):**

1. Emit `llvm.mlir.undef` for the packed result type.

2. For each member `i`:

- Create an `arith.constant` for `juliaOffsets[i]`.

- `llvm.getelementptr` on `srcPtr` using `i8` element type (byte-addressed).

- `llvm.load` with `alignment = 1` — unaligned because Julia’s padding may differ from C’s.

- `llvm.insertvalue` at position `[i]` into the accumulator struct.

1. Replace the op with the final struct value.

**Effect on generated thunk IR:** A 6-line-per-member inline block collapses to one line:

before (v2.5.5, 2 members)

```mlir

%s_undef_1 = llvm.mlir.undef : !llvm.struct<packed (i32, f64)>

%off_1_1 = arith.constant 0 : i64

%field_ptr_raw_1_1 = llvm.getelementptr %val_ptr_1[%off_1_1] : (!llvm.ptr, i64) -> !llvm.ptr, i8

%field_val_1_1 = llvm.load %field_ptr_raw_1_1 {alignment = 1 : i64} : !llvm.ptr -> i32

%s_packed_1_1 = llvm.insertvalue %field_val_1_1, %s_undef_1[0] : !llvm.struct<packed (i32, f64)>

%off_1_2 = arith.constant 8 : i64

%field_ptr_raw_1_2 = llvm.getelementptr %val_ptr_1[%off_1_2] : (!llvm.ptr, i64) -> !llvm.ptr, i8

%field_val_1_2 = llvm.load %field_ptr_raw_1_2 {alignment = 1 : i64} : !llvm.ptr -> f64

%s_packed_1_2 = llvm.insertvalue %field_val_1_2, %s_packed_1_1[1] : !llvm.struct<packed (i32, f64)>

```

after (v2.5.6)

```julia-auto
%packed_1 = jlcs.marshal_arg %val_ptr_1

{ memberTypes = [i32, f64], juliaOffsets = [0 : i64, 8 : i64] }

: (!llvm.ptr) -> !llvm.struct<packed (i32, f64)>

```

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [July 18, 2026, 8:01pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/11 "2026-07-18T20:01:22Z")

</div>

## RepliBuild v3.0.1 — the inheritance-ABI + Tier-2-correctness release

No breaking changes. Wrappers are fingerprinted against the generator, so every  
cached build regenerates automatically on the next `use()`/`wrap()`.

### C++ inheritance, end to end

- **Multiple inheritance** : base subobject offsets extracted from DWARF, derived  
layouts flattened correctly, and `<Derived>_as_<Base>` upcast helpers emitted —  
methods of a non-primary base are now callable on derived objects with the  
correct `this`.
- **Virtual dispatch honors overrides** : Tier-2 virtual method calls go through  
the vtable (new `jlcs.vcall` producer with exception-safe lowering) instead of  
statically calling the named class’s implementation. This also fixes virtual  
instance methods being entirely uncallable through Tier-2 dispatch.
- **Virtual inheritance** : `<Derived>_as_<VBase>` upcasts resolve the vbase  
offset through the object’s vtable at runtime — correct for every dynamic  
type, diamond-proven (shared single base, overrides reached through the vbase  
vtable).

### Tier-2 ABI correctness (found and fixed driving pugixml)

- **SysV small-struct classification** : ≤16-byte register-class struct  
returns/args coerce per-eightbyte (INTEGER→i64, SSE→f64) instead of forcing  
sret — by-value handle returns like `pugi::xml_node` are now correct.
- **Nested packed structs** no longer crash module load: dialect struct aliases  
are inlined inside LLVM struct bodies before translation.
- **JIT pre-flight guard** : a module with an LLVM-incompatible type disables  
Tier 2 with a catchable, type-naming error instead of segfaulting the process.

### Extraction & config robustness

- DWARF parser: members following a nested type definition are no longer  
dropped; base-class offsets and virtuality extracted from inheritance DIEs.
- `discover(force=true)` preserves hand-curated TOML sections  
(`[types].templates`, `[wrap].varargs/macros/shim_headers/cstring_owned`)  
instead of silently destroying them.

### Ingest, honestly labeled (issue #4)

`ingest()` is experimental and C-only; C++ ingest now warns loudly at both  
entry paths instead of emitting unusable wrappers, and the docs describe the  
actual support matrix plus how to load and use a generated wrapper.

> <https://github.com/obsidianjulua/RepliBuild.jl/blob/main/docs/src/inheritance-abi.md>

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [August 23, 2026, 2:02pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/12 "2026-08-23T14:02:45Z")

</div>

## RepliBuild v3.3.2 — and a plainer re-introduction

The first post in this thread was dense, and rereading it five months later it  
buried the part people actually ask me about: why there is an MLIR dialect  
inside a binding generator. So this is less a changelog and more a second  
attempt at explaining the thing.

### What it does

You point it at C or C++ source. It compiles the library itself, then reads the  
**compiled binary’s** debug info to find out what the compiler actually  
produced — every struct’s real field offsets, every function’s real signature,  
every vtable — and writes a Julia module from that.

The difference from Clang.jl, SWIG or bindgen is one step. They read the headers  
and trust them. RepliBuild reads the object file. A header describes intent; the  
binary is what your CPU is going to run. Most of the time those agree and it  
doesn’t matter. When they don’t — packed structs, bitfields, small structs  
passed in registers, anything C++ inheritance touches — a header-based tool  
gives you a binding that looks correct and corrupts memory on the third call.

The other half of the design is what happens when it _can’t_ prove a call is  
safe. It refuses, at that call site, with a message explaining why, and the  
other 5,000 functions in the library still work. I would rather hand you a  
module with a hole in it than one that looks complete.

### Three ways to make a call

| Tier | How | Status |
| --- | --- | --- |
| **3** | `ccall` into the `.so` | The default. Every Hub config uses this. |
| **2** | An MLIR thunk | The C++ cases `ccall` can’t express: virtual dispatch, large or packed struct returns, exception-safe calls. |
| **1** | `Base.llvmcall` on a per-function bitcode slice | Experimental, C only, off by default. |

The original post led with Tier 1 as the headline feature. That was optimistic.  
It does work — Lua runs 190 of them, and it lets Julia’s JIT inline C into a hot  
loop — but it is opt-in, C-only, and I am not calling it production. The tier  
that actually carries C++ is Tier 2, which is the rest of this post.

### The part nobody expects: MLIR

MLIR normally turns up in compilers and ML frameworks. Here it does something  
small and boring, which is exactly why it works.

To call a C++ method from Julia, something has to shuffle the arguments into the  
precise shape the C++ ABI expects: `this` first, small structs split across  
registers, large ones passed by a hidden pointer, and a dozen other rules. Every  
wrapper tool solves this somehow. The usual answers are to hand-write a C shim,  
or to work it out at runtime from a signature string.

RepliBuild writes a tiny program that does the shuffling, and compiles it.

That program is written in a purpose-built MLIR dialect — ops like _call this  
virtual method through vtable slot 4_, _marshal this Julia-aligned struct into  
its C-packed form_ — then lowered to LLVM and run. Four things fall out of using  
an IR instead of generating C text:

- The offsets in the marshalling code and the offsets in the Julia wrapper are  
the same DWARF numbers, read once. They cannot drift apart.
- The ops carry verifiers, so a malformed thunk fails when the module is parsed  
rather than when you call it.
- The x86-64 SysV rules live in one readable pass instead of being implied by a  
code generator.
- The dialect is written to disk and the JIT registers debug info pointing at  
it — so **gdb stops inside the generated MLIR by file and line** , and  
`disassemble /s` interleaves the dialect ops with the machine code they became.  
When a foreign call goes wrong at 2am, that is the thing you want.

No optimization, no graph rewriting, no clever passes. It is a compiler IR doing  
the one job that is normally done with string templates.

### New in 3.3: those thunks now compile ahead of time

The cost of the above: that MLIR module had to be built and JIT’d in **every  
process** that loaded the wrapper. On a small library nobody notices. llama.cpp  
has 3,686 functions, and it was 25 seconds of every single startup.

`aot_thunks = true` in the TOML compiles the thunks once, at build time, into a  
companion `.so`. Loading the wrapper afterwards is a `dlopen`.

| llama.cpp wrapper | JIT | AOT |
| --- | --- | --- |
| wrapper load | 24.83s | **5.38s** |
| test suite (33 tests) | 35.4s | **1.0s** |
| JIT engines spun up at load | 1 | **0** |

The flag had existed for a long time and was switched off in every package,  
because it was broken in four ways that only appear at scale — the library I  
developed it against has 283 functions and triggered none of them. If you like  
that sort of thing the changelog has the full autopsy; the short version is one  
regex that exhausted PCRE outright, two separate duplicate-symbol bugs, and an  
rpath that let a vendored copy load **two** copies of a 41 MB library into one  
process, which then abort at exit while tearing down each other’s state.

This was the most atrocious bug I have ever dealt with in my life.

That last one is the one worth passing on. The package’s own test suite was  
green — 33/33, exit 0 — the entire time, because in the build tree the two paths  
happen to name the same file. Only a _consumer_ vendoring the artifacts ever saw  
it. A generated wrapper isn’t proven by the tests that ship next to it.

### What it looks like

llama.cpp is in the Hub now, and the reference example is a chat client built on  
it:

```julia
using LlamaChat

list_models() # what's on the box
load("qwen3-coder") # load it and start talking

```

```julia-auto
julia> load("qwen3-coder")
qwen3-coder · 32768 ctx · /help for commands

>>> write a haiku about pointers
…
[9 prompt tok @ 43.2/s · 31 gen tok @ 20.1/s]

>>> /exit
(unloaded)

julia>

```

Replies render as markdown in the terminal, code blocks and all. `/exit` frees  
the context and the weights before it returns, so the session you come back to  
is holding nothing.

That package used to take **32.4 seconds** to load. It now takes **0.24** — the  
package, that is; the weights still take as long as the weights take.

### Where I’m at, honestly

- **x86-64 Linux.** ABI classification is SysV only. Win64 and AArch64 are not  
modeled, and I would rather say that than pretend otherwise.
- **C libraries need nothing but Julia** — clang comes from a JLL and everything  
else runs in-process on Julia’s own libLLVM. **C++ and Tier 2 need a system  
LLVM + MLIR install.** It is by far the largest dependency in the project, and  
a C-only library never touches it. `check_environment()` tells you which tiers  
your machine supports.
- **Just over twenty libraries in the Hub** — lua, sqlite, zlib, cJSON, box2d,  
curl, pcre2, imgui, blake3, llama.cpp and the rest. `RepliBuild.search("json")`  
to browse, `RepliBuild.use("lua")` to get a loaded module.
- **Parked, deliberately:** Tier 1 llvmcall (works, unproven at library scale)  
and the old whole-module LTO payload (it embeds the entire linked module per  
call site, which does not survive contact with a real library).
- The known-unbuilt list lives in the repo and stays honest. It is not short.

Holes, edge cases and “this library breaks it” reports are the most useful thing  
anyone can send me. If a wrapper refuses a call it shouldn’t, that’s a bug I want  
to hear about — the refusals are the design, but a _wrong_ refusal is not.

I rewrote this with claude but I verified everything and most is verbatim from my recent release notes I spent all night making for the 3.3.1 AOT patch, much love.

ABI reconstruction + ABI adaptation compiler between Julia and foreign native code is a more accurate description, think of it as a way to call a cpp function using types extracted from dwarf and giving it to mlir to repack as a thunk that julia can call, rules for that are written once in the dialect and not in the julia wrapper or shimmed into the cpp source, were moving the IR around.

I really need help vendoring my dialect, I really cant figure this part out, Enzyme does it but its under tha same c api version that ships with julias llvm, this needs a seperate mlir version(latest) and i dont want users to have to build or AUR the mlir package for the small dialect its a issue for me.

---

<div class="post-metadata">

**Author:** ![MDSW](https://avatars.discourse-cdn.com/v4/letter/m/94ad74/32.png) [@MDSW](https://discourse.julialang.org/u/MDSW)\
**Post date:** [August 23, 2026, 3:27pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/13 "2026-08-23T15:27:28Z")

</div>

If I have an existing C++ codebase for a simulation, and I want to extract components of that so that I can maintain component-level correctness assurances (avoiding wholesale translation and revalidation), will this allow me to pull out the specific components (and, as needed, their supporting class hierarchies), and collapse them into something instantiable and callable within a Julia runtime?

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [August 23, 2026, 3:38pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/14 "2026-08-23T15:38:10Z")

</div>

Its exactly why I built this

Nothing is translated. Your C++ is compiled by clang, and the Julia module is a calling convention over the resulting .so, with the marshalling derived from the DWARF the compiler emitted. There’s no ported logic to revalidate.

it’s a rebuild. RepliBuild compiles your sources itself with flags you declare in a TOML — it replaces your build for the subset you extract rather than consuming it. So it’s your validated source through a compiler you specify, not your validated binary. You’d re-establish on the built artifact; what you never do is revalidate a translation.

Selection is per translation unit, not per symbol — you name source files and exclude paths, and everything with debug info in what you compiled gets wrapped. Class hierarchies come through: base subobject offsets from DWARF, _as_ upcasts so methods of a non-primary base get the right this, and virtual dispatch through the vtable so overrides are honored. Ctor/dtor pairs become GC-finalized handles, so objects are instantiable from Julia and destructed when Julia drops them. box2d, box2d3, imgui, tinyxml2, pugixml and llama.cpp are all wrapped this way in the Hub.

Two things that would stop you: x86-64 Linux only, and templates need their instantiations declared — an uninstantiated template emits no code, so it isn’t in the DWARF. On numerics-heavy code that’s usually the largest piece of work.

Tell me the library and I’ll do a local test case and upload it to the Hub vendored — thunks built, you git pull and it loads. Happy to walk you through the MLIR dialect setup separately; that part is cmake plus a script in the repo.

Im also only lowering to the llvm dialect, I havent experimented with lowering the JLCS dialect to a gpu backend yet but I really want to but thats the fast path stuff I can optimize later or try to use Enzyme when I close the opaque ccall into the dialect sorta thing.

You dont need extern c or anything, the pkg is marshalling the cpp sysv abi directly.

> <https://github.com/obsidianjulua/RepliBuild-Hub/blob/main/packages/box2d/replibuild.toml>

This is an example of wrapping a simulation cpp library, you fill out the toml, even the git repo if you want RepliBuild to pull it from git to a location. Then call `build("replibuild.toml")` the wrap it with `wrap("replibuild.toml")` the wrappers are completely generated by the replibuild.toml, 0 source edits, include(“wrapper.jl”) and start calling the library or build a pkg using the wrapper.

To instatiate templates do this for any in the replibuild.toml

```toml
[types]
allow_unknown_enums = false
strictness = "warn"
allow_unknown_structs = true
allow_function_pointers = true
templates = ["std::vector<int>", "std::string", "std::map<int, int>"]
template_headers = ["<vector>", "<string>", "<map>"]

```

---

<div class="post-metadata">

**Author:** ![Palli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/palli/32/3380_2.png) [@Palli](https://discourse.julialang.org/u/Palli)\
**Post date:** [August 24, 2026, 1:20pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/16 "2026-08-24T13:20:00Z")

</div>

> The learning curve is one sentence: **do the opposite of every other wrapper tool.**

This made me laugh. But it seems like a big deal, thank you for making this. You know of no other such tools for languages other than Julia? This depends on LLVM, for e.g. C++, and is it then fully portable to e.g. Windows?

I though it trivial to call C, i.e. an .so/.dll from Julia, with ccall, meaning you don’t need SWIG or similar. But to call C++ you need extern C API for, or you get name mangling. Do I understand correctly that this is then not a problem for you, because of “extracts structural DWARF debug metadata directly from the generated objects”?

> [@obsidianjulua](#):
>
> This allows Julia’s JIT to seamlessly inline C/C++ code directly into Julia hot loops.

This seems like a big deal, not possible (with current tools) for any other language?

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [August 24, 2026, 3:37pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/17 "2026-08-24T15:37:14Z")

</div>

Its the real deal, I implement a llvmcall bitcode slicing mechanic for c libraries, and yes the DWARF is extracted, and used immutable to build the wrapper or give to mlir to generate a thunk for cpp libraries thats the super simple of it. My docs are extensive, changelog shows every pitfall I hit learning about how to do this because no other language but julia can actually do this right and really fast. Ive experimented with c hot loops through llvmcall and im positive a backend like enzyme can diff the lto c code julia llvmcalls. This is the compiler wrapper builder that abuses those mechanics to make a usable wrapper.

Im still working on the opaque call into the custom dialect because its not written in julia like enzyme, i wrote my dialect in c++

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [August 24, 2026, 5:12pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/18 "2026-08-24T17:12:00Z")

</div>

Windows doesnt have dwarf, I can support macOS easily I just havent done the tool calling conversions for extraction of debug. The dialect is close to language agnostic and qny language that supports ffi or dlopen or c calling mechanics can dlopen the thunks and load them. Julia is just the best at it

DW\_AT\_linkage\_name and the full type graph, everything is in DWARF

mingw/clang - apparently this is compatible but someone familiar with windows would need to tell me if the DWARF is present when compiled by this toolchain.

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [August 24, 2026, 10:37pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/19 "2026-08-24T22:37:09Z")

</div>

x86\_64-w64-windows-gnu (mingw-w64)

This is cross compatible from what I can tell, but not the msvc PDB though. I did some looking and that compiler gnu I can support.

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [August 26, 2026, 3:09am UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/20 "2026-08-26T03:09:35Z")

</div>

![2026-08-25-215311_hyprshot](https://global.discourse-cdn.com/julialang/original/3X/6/8/68e11b91105c5c8cf56680329e3bae994eaed1d5.png)  
 ![2026-08-26-120706_hyprshot](https://global.discourse-cdn.com/julialang/original/3X/3/b/3b6a13aaeb0acd9699685c14ff6187c4823ca33b.png)

This is the way it look in the Repl. Lua, cjson, llamma.cpp, imgui, box2d, and so on. You start with your source, and a replibuild.toml thats it, you dont even edit the source, dont add extern c, MLIR is marshaling for julia. best user manual there is right here.

```julia-auto
function hello_message_ptr()::Cstring
    return RepliBuild.JITManager.invoke_aot(THUNKS_HANDLE[], "_mlir_ciface__Z13hello_messagev_thunk", Cstring)
end

```

just call the thunk, but the this part is built for you, you dont write this thunk call either thats in the generated wrapper.

This is the full list of fully wrapped libraries fully functional, c uses ccall and llvmcall, cpp uses ccall and MLIR thunks.

`blake3, box2d, box2d3, cglm, cjson, curl, duktape, imgui, llamacpp, lua, lz4, miniaudio, miniz, mpack, pcre2, pugixml, sqlite, tinyxml2, tomlc17, xxhash, zlib`

> **[GitHub - obsidianjulua/RepliBuild-Hub: Stores the toml configurations for the...](https://github.com/obsidianjulua/RepliBuild-Hub/tree/main)**
>
> main

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [September 6, 2026, 2:50am UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/21 "2026-09-06T02:50:49Z")

</div>

**RepliBuild runs on Windows.** Target is `x86_64-w64-windows-gnu` — mingw under  
MSYS2 CLANG64, a native host build, not a cross-compile from Linux and not MSVC.  
All three tiers are live: the C bucket compiles and links, the JLCS dialect  
builds and loads as `libJLCS.dll`, and MLIR AOT thunks work with C++ exceptions  
crossing them intact.

 ![2026-09-05-214405_hyprshot](https://global.discourse-cdn.com/julialang/original/3X/2/2/22903460ed668dbfa800ff3adfb7267215089f76.png)

---

<div class="post-metadata">

**Author:** ![obsidianjulua](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/obsidianjulua/32/221495_2.png) [@obsidianjulua](https://discourse.julialang.org/u/obsidianjulua)\
**Post date:** [September 13, 2026, 5:55pm UTC](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122/22 "2026-09-13T17:55:01Z")

</div>

![2026-09-13-095123_hyprshot](https://global.discourse-cdn.com/julialang/original/3X/e/c/ec029f4a90867495ffce9d8fc6baa46e700c46fd.png)  
 ![2026-09-13-094715_hyprshot](https://global.discourse-cdn.com/julialang/original/3X/e/c/ec6232ae5f20cbe199ccc3030a21b9d31cb3a9eb.png)

RepliBuild.jl is Julia 1.13 stable

Looking to vendor the dialect as JLCS\_jll, removing the per system build, Ill keep developing against the newest major MLIR for support since architecture supports compiling the thunks regardless of Mlir or Llvm version for cxx features, C always uses Julia’s internal LLVM — the JLL clang is version-locked to it (20.1.8 here), and the C bucket needs a sysroot for libc headers: system paths on Linux, MSYS2 CLANG64 on Windows. Only C++ diverges, running against the newest system MLIR (22.1.8) for Tier-2 thunks.

Toolchain checks and error messaging have been greatly enhanced this version. Gdb file/line support for thunks is live, any thunk mlir executes or compiles for julia is walkable with gdb with line and error feedback.

Core engine for Linux and Windows is stable, the classifier for MacOS is coming I just need some time, Im currently learning BinaryBuilder and cross platform toolchaining and Linux and Windows is enough for me right now, the MacOS classifier is doable nothing blocks it other than me doing it then deploying and testing it. Very soon

Tier1 calls AKA Base.llvmcall is supported but volatile, C libraries will generate a C wrapper mixed with ccall and llvmcall if the `[wrap.tier1] enable` is set and the bitcode slicing mechanics with fallback to ccall but its experimental and needs per wrapper iterations. Works but with caution.. the ccall and no LTO is the more stable but slower path and cant reach Enzyme for anyone that wants to try, I havent but It should.. let me know.

If you stay on a earlier version of julia everything is compatibly from 1.12 and llvm 18, even the thunks load from a julia 1.12 compiled by julia 1.13

Anyone intested in 1,14 nightly! Ill be developing the toolchain matched version of replibuild where julia provides the dialect and mlir\_jll version locked with julias internal, That will be my in between development and I cant promise it will stay updated with that nightly but I will try. The 1.13 comes first

[Next page](https://discourse.julialang.org/t/ann-replibuild-jl-a-full-c-c-interop-toolkit-for-tiered-ffi-generation/136122.md?page=2)
