# How MLIR bridges Julia and other Languages

**URL:** https://discourse.julialang.org/t/how-mlir-bridges-julia-and-other-languages/136213
**Category:** General Usage
**Created:** [March 16, 2026, 2:59am UTC](https://discourse.julialang.org/t/how-mlir-bridges-julia-and-other-languages/136213 "2026-03-16T02:59:28Z")
**Posts on this page:** 1
**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 16, 2026, 2:59am UTC](https://discourse.julialang.org/t/how-mlir-bridges-julia-and-other-languages/136213/1 "2026-03-16T02:59:28Z")

</div>

The short is MLIR can be a sort of ABI compiler so two languages can merge into the same IR. The long part is below.

## The Problem

Julia’s `ccall` assumes C-compatible struct layouts — natural alignment, predictable padding. C++ breaks this with packed structs, virtual dispatch, RAII, and non-trivial return conventions. A `ccall` to a function returning a packed struct will segfault unless each return is dealt with the traditional way of writing the handling ourselves.

## Packed Struct Returns: The `ffe_call` Op

Consider this C++ type and function:

```cpp
#pragma pack(push, 1)
struct PackedTriplet { char tag; int value; char flag; }; // 6 bytes, no padding
#pragma pack(pop)

PackedTriplet pack_three(char tag, int value, char flag);

```

The SysV ABI returns this via `sret` pointer, not registers. Julia’s `ccall` doesn’t know that — it expects an aligned 12-byte struct (1 + 3pad + 4 + 1 + 3pad). Reading the packed 6 bytes as 12 corrupts the `int` field.

Here’s the MLIR thunk generates:

```mlir
func.func @pack_three_thunk(%args: !llvm.ptr) -> !llvm.struct<(i8, i32, i8)>
    attributes { llvm.emit_c_interface } {
  // Unpack arguments from Julia's void** array
  %c0 = arith.constant 0 : i64
  %c1 = arith.constant 1 : i64
  %c2 = arith.constant 2 : i64
  %p0 = llvm.getelementptr %args[%c0] : (!llvm.ptr, i64) -> !llvm.ptr, !llvm.ptr
  %p1 = llvm.getelementptr %args[%c1] : (!llvm.ptr, i64) -> !llvm.ptr, !llvm.ptr
  %p2 = llvm.getelementptr %args[%c2] : (!llvm.ptr, i64) -> !llvm.ptr, !llvm.ptr
  %v0 = llvm.load %p0 : !llvm.ptr -> !llvm.ptr
  %v1 = llvm.load %p1 : !llvm.ptr -> !llvm.ptr
  %v2 = llvm.load %p2 : !llvm.ptr -> !llvm.ptr
  %tag = llvm.load %v0 : !llvm.ptr -> i8
  %value = llvm.load %v1 : !llvm.ptr -> i32
  %flag = llvm.load %v2 : !llvm.ptr -> i8

  // Call C++ — returns packed struct (6 bytes, no padding)
  %packed = jlcs.ffe_call %tag, %value, %flag { callee = @pack_three }
      : (i8, i32, i8) -> !llvm.struct<packed (i8, i32, i8)>

  // Repack into aligned struct for Julia (12 bytes, with padding)
  %out = llvm.mlir.undef : !llvm.struct<(i8, i32, i8)>
  %f0 = llvm.extractvalue %packed[0] : !llvm.struct<packed (i8, i32, i8)>
  %f1 = llvm.extractvalue %packed[1] : !llvm.struct<packed (i8, i32, i8)>
  %f2 = llvm.extractvalue %packed[2] : !llvm.struct<packed (i8, i32, i8)>
  %o0 = llvm.insertvalue %f0, %out[0] : !llvm.struct<(i8, i32, i8)>
  %o1 = llvm.insertvalue %f1, %o0[1] : !llvm.struct<(i8, i32, i8)>
  %o2 = llvm.insertvalue %f2, %o1[2] : !llvm.struct<(i8, i32, i8)>

  return %o2 : !llvm.struct<(i8, i32, i8)>
}

```

The `jlcs.ffe_call` op knows the C++ function returns a **packed** `(i8, i32, i8)` — 6 bytes with no padding. It extracts each field individually and inserts them into a new **aligned** struct that Julia can read safely. In Julia\>

```julia
triplet = pack_three(UInt8('A'), Cint(999), UInt8('Z'))
@test triplet.value == 999 # just works

```

## How Arguments Cross the Boundary

Every thunk takes a single `void**` — an array of pointers to Julia `Ref` values. The IR does a double dereference: first to get the pointer from the array slot, then to load the typed value:

```mlir
// args[0] → pointer → load pointer → load i32
%slot = llvm.getelementptr %args[%idx] : (!llvm.ptr, i64) -> !llvm.ptr, !llvm.ptr
%ptr = llvm.load %slot : !llvm.ptr -> !llvm.ptr
%val = llvm.load %ptr : !llvm.ptr -> i32

```

This matches Julia’s `Ref{T}` layout — a heap-allocated box containing the value. The thunk unwraps it to get the raw primitive for C++.

## What the Dialect Handles

The dialect generates thunks for cases `ccall` can’t handle safely:

- **Packed structs** — field-by-field repack between packed and aligned layouts
- **Virtual dispatch** — vtable pointer resolution and indirect calls
- **RAII** — `jlcs.scope`/`jlcs.yield` ops for constructor/destructor pairing
- **Template specializations** — type-specific marshalling for `std::vector<T>`, etc.
