This is what wrapping C++ looks like using mlir thunks:
#include <iostream>
// Returns the greeting without printing it — Cstring back to Julia.
const char* hello_message() {
return "Hello, World!";
}
// Prints the greeting, returns how many characters it wrote.
int hello_print() {
const char* msg = hello_message();
std::cout << msg << std::endl;
return 13;
}
// Takes arguments from Julia: greets `name`, `times` over.
int hello_to(const char* name, int times) {
for (int i = 0; i < times; ++i) {
std::cout << "Hello, " << name << "!" << std::endl;
}
return times;
}
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
Then the julia side wrapper to call the thunks is so clean.
export main, hello_print, hello_message, hello_to
function main()::Cint
ccall((:main, LIBRARY_PATH), Cint, (), )
end
function hello_print()
# [Tier 2] Dispatch to MLIR JIT (Complex ABI / Packed / Union)
return RepliBuild.JITManager.invoke("_mlir_ciface__Z11hello_printv_thunk", Cint)
end
function hello_message()
# [Tier 2] Dispatch to MLIR JIT (Complex ABI / Packed / Union)
return RepliBuild.JITManager.invoke("_mlir_ciface__Z13hello_messagev_thunk", Cstring)
end
function hello_to(name::Any, times::Integer)
# [Tier 2] Dispatch to MLIR JIT (Complex ABI / Packed / Union)
return RepliBuild.JITManager.invoke("_mlir_ciface__Z8hello_toPKci_thunk", Cint, name, times)
end
A foreign call is a compilation problem, so I compile it. Where a binding generator would paste a C shim or interpret a signature at runtime, I emit a small program in a purpose-built MLIR dialect, lowers it, and runs the result — ABI marshalling as first-class IR, which as far as I know nobody else does with MLIR. Nothing needs to be enabled and nothing links gdb: RepliBuild is the publisher of LLVM’s GDB JIT interface, and gdb reads the descriptor out of the inferior. clean() removes .debug, and it regenerates on the next JIT init.
spent 2 years to get to this point but Im very satisfied and love the language. Sorry the generator actually makes more code block comments than call sites but I havent bothered to change it and the generator makes a docstring per call site regardless.
I was able to wrap Llama.cpp and run a model close to native cxx through the wrapper including private exports to load and unload models, clear cahces basically the entire api surface not just curated hand written wraps, and I mean zero == 0 hand written edits to generate bindings other than my work in the actual generator…
is Linux guarded because Windows is just wierd.
The JLCS dialect (TableGen-defined, src/mlir/) models C/C++ interop semantics directly: !jlcs.c_struct\ types carrying explicit field offsets and packing, jlcs.ffe_call / jlcs.try_call\ (exception-safe invoke + landing pad), jlcs.vcall\ (vtable dispatch that honors overrides), jlcs.marshal_arg / marshal_ret (Julia-aligned ↔ C-packed), and constructor/destructor ops inside region-based RAII scopes for Itanium’s non-trivial by-value parameters.
func.func @_ZNK5Base25get_bEv_thunk(%args_ptr: !llvm.ptr) -> i32
attributes { llvm.emit_c_interface } {
%arg_ptr_1 = llvm.getelementptr %args_ptr[%idx_1] : (!llvm.ptr, i64) -> !llvm.ptr, !llvm.ptr
%val_ptr_1 = llvm.load %arg_ptr_1 : !llvm.ptr -> !llvm.ptr // slot → storage
%val_1 = llvm.load %val_ptr_1 : !llvm.ptr -> !llvm.ptr // storage → `this`
%ret_val = "jlcs.vcall"(%val_1) { class_name = @Base2, slot = 2 : i64, … } : (!llvm.ptr) -> i32
return %ret_val : i32
}
Four things fall out of choosing an IR over a shim: the struct offsets in the marshalling code and in the Julia wrapper are the same DWARF numbers, read once; the ops carry verifiers, so a malformed thunk fails at parse instead of at runtime; the x86-64 SysV rules live in one readable pass (classifySysVStruct) rather than being implied by a code generator; and because the emitted dialect is written to disk and the JIT registers DWARF pointing at it, gdb stops inside the generated MLIR by file and line — disassemble /s interleaves dialect ops with the machine code they became. Ops execute through a per-library MLIR JIT engine, or ahead-of-time in a companion _thunks.so.
Full treatment — the thesis, the op reference, the lowering, source-level debugging, and the failure classes the design exists to make loud: [ABI Marshalling as Compiler IR](ABI Marshalling as Compiler IR (MLIR/JLCS) · RepliBuild.jl)
had alot of edits but wont be using any llm genrated content at all. hope it looks nice is and is readable I dont like Markdown…