# Julia can debug MLIR, kinda

**URL:** <https://discourse.julialang.org/t/julia-can-debug-mlir-kinda/138694>\
**Category:** Performance\
**Created:** [August 8, 2026, 9:56pm UTC](https://discourse.julialang.org/t/julia-can-debug-mlir-kinda/138694 "2026-08-08T21:56:59Z")\
**Posts on this page:** 4\
**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:** [August 8, 2026, 9:56pm UTC](https://discourse.julialang.org/t/julia-can-debug-mlir-kinda/138694/1 "2026-08-08T21:56:59Z")

</div>

## JIT’d thunks are debuggable in gdb, at source level

Break on a mangled thunk name and gdb stops inside the emitted MLIR, by file

and line, with list and disassemble /s working:

 ![2026-08-08-164205_hyprshot](https://global.discourse-cdn.com/julialang/original/3X/a/0/a07f546642019d2a1298146d52eb858bdb20e8c3.png)

```julia-auto
cd ~/Desktop/Projects/RepliBuild.jl
gdb -batch -nx \
  -ex 'set pagination off' -ex 'set confirm off' \
  -ex 'handle SIGSEGV nostop noprint pass' \
  -ex 'set breakpoint pending on' \
  -ex 'break _ZNK5Base15get_aEv_thunk' \
  -ex 'run' \
  -ex 'info symbol $pc' \
  -ex 'bt 5' \
  -ex 'x/6i $pc' \
  --args julia --project=. test/mi_test/verify.jl

```

I got the function setup now to use this and its simpler

```julia-auto
rbdbg _ZNK5Base15get_aEv_thunk test/mi_test/verify.jl

```

This is the break and the julia program. The mlir execution engine has a default I didnt find till recently when I started emitting valid IR…

`bool enableGDBNotificationListener = true`;  
`bool enablePerfNotificationListener = true`;

this emits the /.debug/jit/\*\*objdump which GDB can step through from julia. Cool stuff, but this basically unlocks a very high level debugger for free working on marshaling thunks for Julia.

---

<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 12, 2026, 8:04pm UTC](https://discourse.julialang.org/t/julia-can-debug-mlir-kinda/138694/2 "2026-08-12T20:04:31Z")

</div>

This is what wrapping C++ looks like using mlir thunks:

```julia-auto
#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.

```julia
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.

```mlir

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](https://obsidianjulua.github.io/RepliBuild.jl/dev/mlir))

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…

---

<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 17, 2026, 11:03am UTC](https://discourse.julialang.org/t/julia-can-debug-mlir-kinda/138694/3 "2026-08-17T11:03:53Z")

</div>

Found a cheesy way to generate the config.h files for builds as well, just have cmake generate it then include it in the toml for julia to build into the library. I did this for curl and it works perfect. I was worried about pre-processing support but this works good temporarily.

---

<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:48pm UTC](https://discourse.julialang.org/t/julia-can-debug-mlir-kinda/138694/4 "2026-08-23T14:48:43Z")

</div>

> [@obsidianjulua](#):
>
> ```julia-auto
> function hello_message()
> # [Tier 2] Dispatch to MLIR JIT (Complex ABI / Packed / Union)
> return RepliBuild.JITManager.invoke("_mlir_ciface__Z13hello_messagev_thunk", Cstring)
> end
> 
> ```

These now get compiled AOT at build time instead of when the thunk is called, making every MLIR thunk at least 4x faster to call across the board.

```julia-auto
function hello_message()::Union{String,Nothing}
    # [Tier 2] Dispatch to MLIR AOT Thunk (Complex ABI / Packed / Union)
    ptr = RepliBuild.JITManager.invoke_aot(THUNKS_HANDLE[], "_mlir_ciface__Z13hello_messagev_thunk", Cstring)
    ptr == C_NULL && return nothing
    s = unsafe_string(ptr)
    return s
end

```

Even the more complex thunks look very simple in the julia wrapper,

```julia-auto
function llama_model_bitnet_load_arch_hparams(this::Any, ml::Ref{llama_model_loader})
    # [Tier 2] Dispatch to MLIR AOT Thunk
    return RepliBuild.JITManager.invoke_aot(THUNKS_HANDLE[], "_mlir_ciface__ZN18llama_model_bitnet17load_arch_hparamsER18llama_model_loader_thunk", this, ml)
end
```
