# How to define MLIR types for Julia?

**URL:** https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340
**Category:** General Usage
**Created:** [December 3, 2025, 9:27pm UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340 "2025-12-03T21:27:51Z")
**Posts on this page:** 10
**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: [December 3, 2025, 9:27pm UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/1 "2025-12-03T21:27:51Z")

</div>

Does anyone know if there are dialects made for julia and MLIR, not wrappers for MLIR dialects but compiled to MLIR that define julias types and ops for MLIR in C++ files. The problem: MLIR types need their storage class fully defined before  
registration, but I have a circular dependency:

- JLCSTypes.h includes the typedef classes
- But the storage definition is in JLCSTypes.cpp.inc
- JLCSDialect.cpp includes JLCSTypes.h then tries to register the type
- Storage is incomplete at registration time. Do I split the files and try resolving the includes seperatley. My MLIR starting cpp, h, td, cmake files are in examples of RepliBuild.jl repo.

---

<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: [December 4, 2025, 7:54am UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/2 "2025-12-04T07:54:13Z")

</div>

```julia-auto
#!/usr/bin/env julia

# Test JLCS dialect using MLIR.jl bindings

using Pkg
Pkg.activate(joinpath(homedir(), ".julia", "dev", "MLIR"))

using MLIR
using MLIR.API

# Path to our compiled JLCS dialect
const libJLCS_path = joinpath(@ __DIR__ , "Mlir", "build", "libJLCS.so")

# Check library exists
if !isfile(libJLCS_path)
error("JLCS library not found at: $libJLCS_path")
end
println("✓ Found JLCS library: $libJLCS_path")

# Load JLCS dialect
@assert dlopen(libJLCS_path, RTLD_GLOBAL) != C_NULL "Failed to load JLCS library"

# Create MLIR context
ctx = API.mlirContextCreate()
@assert ctx.ptr != C_NULL "Failed to create MLIR context"

# Register JLCS dialect
ccall((:registerJLCSDialect, libJLCS_path), Cvoid, (API.MlirContext,), ctx)

# Create module
loc = API.mlirLocationUnknownGet(ctx)
mod = API.mlirModuleCreateEmpty(loc)
@assert mod.ptr != C_NULL "Failed to create module"
println("✓ Created MLIR module")

# Print module
println("\nEmpty module:")
op = API.mlirModuleGetOperation(mod)
API.mlirOperationDump(op)

# Cleanup
API.mlirContextDestroy(ctx)

```

---

<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: [December 4, 2025, 7:57am UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/3 "2025-12-04T07:57:50Z")

</div>

I figured out how to compile julia dialects to MLIR, and I can resolve ffi gen for Inheritance, Virtual methods, nested callbacks, and execute c++ without a wrapper, this goes Julia \rightarrow DWARF \rightarrow MLIR \rightarrow LLVM \rightarrow C++. Full ffi gen and execution with zero writing of any c++

The MLIR dialect is simply a carrier of this ABI fidelity.

When Julia “calls” a C++ this call, `jlcs.vcall`, it isn’t FFI.  
It’s a direct LLVM IR call to a function pointer with:

- correct ABI
- correct calling convention
- correct registers
- correct stack frame layout

Identical to what the C++ compiler itself would generate.

---

<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: [December 4, 2025, 9:18pm UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/4 "2025-12-04T21:18:51Z")

</div>

```julia-auto
C++ Code Julia Code
    ↓ ↓
MLIR IR (JLCS Dialect) ← Same IR → MLIR IR (Julia types)
    ↓ ↓
        Unified LLVM IR (no boundary)
                ↓
        Native Machine Code
                ↓
        Direct Execution

```

---

<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: [December 5, 2025, 8:36pm UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/5 "2025-12-05T20:36:19Z")

</div>

| **Stage** | **Component (File)** | **Function** | **Output** |
| --- | --- | --- | --- |
| **1. ABI Extraction** | `DWARFParser.jl` | Reads vtable offsets and virtual method slots from debug info. | Structured DWARF data (ABI facts). |
| **2. IR Generation** | `JLCSIRGenerator.jl` | Creates the `jlcs.virtual_call` operation with ABI facts as attributes. | **JLCS MLIR IR** text. |
| **3. Lowering** | `LowerToLLVMPass.cpp` | Consumes the high-level `jlcs.virtual_call` op and rewrites it into LLVM IR memory and call ops using the ABI facts. | Low-level **LLVM IR**. |
| **4. Execution** | **MLIR JIT Engine** | Compiles the LLVM IR to native code and returns a Julia-callable function pointer. | **Native Machine Code (MC)**. |

---

<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: [December 6, 2025, 11:46pm UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/6 "2025-12-06T23:46:55Z")

</div>

````julia-auto
// ====================================================================
// Virtual Method Call Operation
// ====================================================================
//
 4. Virtual Call Op (Call C++ virtual method via vtable)
def VirtualCallOp : JLCS_Op<"vcall"> {  
  let summary = "Call a C++ virtual method through vtable dispatch.";
  let description = [{
    Calls a C++ virtual method by:
    1. Reading the vtable pointer from the object (at vtable_offset)
    2. Loading the function pointer from vtable[slot]
    3. Calling the function with the object pointer + arguments

    Example:
    ```mlir
    %result = jlcs.vcall @Base::foo(%obj)
      { vtable_offset = 0 : i64, slot = 0 : i64 }
      : (!llvm.ptr) -> i32
    ```
  }];

  let arguments = (ins
    SymbolRefAttr:$class_name, // Class name (e.g., @Base)
    Variadic<AnyType>:$args, // Arguments (first is always object pointer)
    I64Attr:$vtable_offset, // Offset of vptr in object
    I64Attr:$slot // Vtable slot index
  );

  let results = (outs Optional<AnyType>:$result);

  let skipDefaultBuilders = 1;
  let builders = [
    OpBuilder<(ins "SymbolRefAttr":$class_name, "ValueRange":$args,
                   "IntegerAttr":$vtable_offset, "IntegerAttr":$slot,
                   "Type":$resultType)>
  ];

  let extraClassDeclaration = [{
    VirtualCallOp(::mlir::Operation *op) : Op(op) {}

    // Helper to get the object pointer (first argument)
    Value getObject() { return getArgs()[0]; }
  }];
}

````

#

---

<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: [December 9, 2025, 4:05pm UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/7 "2025-12-09T16:05:55Z")

</div>

## Introduction

This guide teaches Julia developers how to create custom MLIR dialects for advanced FFI scenarios. The JLCS dialect demonstrates:

- **C-ABI struct manipulation** (field access by byte offset)
- **Virtual method dispatch** (vtable-based calls)
- **Strided array operations** (cross-language arrays)
- **Complete LLVM lowering** (executable code generation)
- **JIT compilation** (runtime code execution)

### Why MLIR for Julia FFI?

Traditional Julia FFI (`ccall`) works well for simple C functions, but struggles with:

- C++ virtual methods and inheritance
- Complex struct layouts with padding
- STL containers with implementation-defined layouts
- Cross-language optimization opportunities

MLIR provides:

- **Custom IR** tailored to your FFI needs
- **Transformation passes** for optimization
- **Direct LLVM lowering** for native performance
- **Type-safe operations** verified at IR level

Full Introduction and Julia and MLIR findings [RepliBuild.jl/docs/mlir at main · obsidianjulua/RepliBuild.jl · GitHub](https://github.com/obsidianjulua/RepliBuild.jl/tree/main/docs/mlir)

# Examples will need specific toolchain versions, but docs are setup to be readable for curiosity.

---

<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 15, 2026, 12:30am UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/10 "2026-07-15T00:30:42Z")

</div>

**Update, July 2026** — the posts above are from December, when the dialect was in  
the semi-early stages of being developed seriously. It’s now been through five  
releases of hardening, RepliBuild is landing in General as v3.0.0, and a couple of  
things I wrote back then deserve corrections now that they’ve been battle-tested.  
Since this thread apparently gets read (and quoted) as reference material for  
“MLIR for FFI”, here’s the honest current state.

## Correction first: “verified at IR level”

I wrote that JLCS gives you “type-safe operations verified at IR level.” As of  
today **no JLCS op has a `hasVerifier`**. Two ops segfault during lowering if you  
hand them malformed IR — `jlcs.scope` with mismatched managed\_ptrs/destructors  
arity, and `jlcs.marshal_arg` with mismatched memberTypes/juliaOffsets — instead  
of diagnosing. They’re tracked as two `@test_broken` entries in the test suite.  
This isn’t reachable from the production DWARF→codegen path (the producers  
co-generate the arrays, so they can’t disagree), but if you’re hand-writing JLCS  
IR: there are no guardrails yet. Verifiers are near the top of the roadmap  
precisely because this thread’s description got ahead of the implementation.

## What actually changed since December

**`jlcs.vcall` emits all the way to LLVM IR now.** This one’s a useful war story  
for anyone hand-building `llvm.call` in a lowering. The op _lowered_ cleanly but  
SIGSEGV’d inside `translateModuleToLLVMIR` at emit time. Root cause: my  
`VirtualCallOpLowering` built the indirect call via a raw `OperationState` and set  
`operandSegmentSizes = {1, nArgs, 0}` — three entries. But `llvm.call` carries  
`AttrSizedOperandSegments` with **two** operand groups (`callee_operands`,  
`op_bundle_operands`); for an indirect call the callee pointer is the _first  
element of `callee_operands`_, so the correct value is `{1 + nArgs, 0}`. The  
translator split a 3-entry array against a 2-segment op and walked off the end.  
Fix: use the dedicated indirect-call builder `CallOp(LLVMFunctionType, ValueRange)` and let it set `operandSegmentSizes` and `var_callee_type` itself.  
The op definition from my earlier post is unchanged — the lowering was the bug.

**Honest op liveness map** , because “the dialect has 12 ops” says nothing about  
what’s real:

- _Emitted by the production pipeline:_ `jlcs.type_info`, `jlcs.ffe_call`,  
`jlcs.try_call` (C++ exceptions → landing pad → Julia `CxxException`),  
`jlcs.marshal_arg`/`marshal_ret`, and the `!jlcs.c_struct` type.
- _Exercised only by hand-written test IR:_ `jlcs.vcall`, `ctor_call`/`dtor_call`,  
`scope`/`yield` (region RAII — lowers correctly, reverse-order destruction  
confirmed, but no DWARF-driven producer emits it yet).
- _Functional but producer-less:_ `jlcs.load/store_array_element` +  
`!jlcs.array_view`. They parse and lower cleanly; nothing generates them.

**Toolchain:** the dialect rebuilds clean against LLVM/MLIR 22.1.6 with zero  
TableGen or C++ source changes from 22.1.5 — patch bumps within a minor have been  
non-events. The dual-LLVM setup (Julia’s internal libLLVM for the C path, system  
MLIR for the dialect) is deliberate and documented.

**Outside the dialect but relevant to the FFI story:** the C generator now runs an  
exact-layout proof before emitting any struct — every member typed with known  
size/alignment, then the emitter _proves_ Julia’s layout reproduces each DWARF  
offset and the total byte size. Proof passes → named fields; any doubt → opaque  
byte blob; ABI crossings that can’t be made safe refuse loudly at the call site  
instead of corrupting. “Exact or opaque, never approximate” turned out to be the  
load-bearing design rule of the whole project.

## What’s being worked on

- **Op verifiers** — `scope` and `marshal_arg` arity checks first (see above).
- **Multiple-inheritance `this`-adjustment** — the vcall emit fix unblocked  
observing the secondary-base case: the vtable is read from the right offset but  
`this` still passes unadjusted. Remaining work is a `this_offset` on `vcall`  
plus a multi-base offset table on `type_info`.
- **Per-function bitcode slicing** — `Base.llvmcall` embeds the whole linked  
module per call site, which works at toy scale and falls over at whole-library  
scale (and duplicates file-local statics, so mixed-tier dispatch can diverge on  
internal state). Slicing is the fix; until then production configs pin `ccall`.
- **Producers** for scope-RAII and the strided-array ops — the dialect side works,  
the DWARF-driven generation doesn’t exist yet.

Docs (readable for curiosity, examples need specific toolchain versions):  
[GitHub - obsidianjulua/RepliBuild.jl · GitHub](https://github.com/obsidianjulua/RepliBuild.jl) — full changelog  
including the v3.0.0 “breaking changes since v2.5.7” section is in the repo.  
RepliBuild Hub (prebuilt configs for lua/sqlite/cjson/box2d/etc.):  
[GitHub - obsidianjulua/RepliBuild-Hub: Stores the toml configurations for the RepliBuild.jl FFE generator · GitHub](https://github.com/obsidianjulua/RepliBuild-Hub)

---

<div class="post-metadata">

### Author: ![yolhan\_mannes](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/yolhan_mannes/32/220485_2.png) [@yolhan\_mannes](https://discourse.julialang.org/u/yolhan_mannes)
#### Post date: [July 15, 2026, 7:52am UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/11 "2026-07-15T07:52:21Z")

</div>

Hi, nice experiment, if you want to make your effort go to julia for now I think [GitHub - maleadt/IRStructurizer.jl: Pattern-matching structured control flow in Julia's SSA IR. · GitHub](https://github.com/maleadt/IRStructurizer.jl) is the closest to MLIR IR form, hopefully, this will lead to easier direct translation in the future it is used only by CuTile.jl but please try to build over it since it already goes around a lot of the painpont you’re going to go against with the standard julia IR.  
Wanna also note Reactant.jl which do way more than just wrapping MLIR.

---

<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 15, 2026, 12:57pm UTC](https://discourse.julialang.org/t/how-to-define-mlir-types-for-julia/134340/12 "2026-07-15T12:57:12Z")

</div>

Thanks — both are great projects, but they point the opposite direction from RepliBuild. Reactant and cuTile/IRStructurizer lower _Julia_ code into MLIR for accelerated execution; RepliBuild starts from a _compiled C/C++ binary_, reads its DWARF, and uses MLIR only at the FFI boundary — the JLCS dialect compiles ABI marshalling thunks (sret, vtable dispatch, exception-safe calls) that `ccall` can’t express. The IR is generated from debug metadata, never from Julia IR, and thunks are straight-line code, so there’s no control-flow structurization problem to go around. That said, Reactant’s vendored-MLIR JLL is a model I’m looking at for killing RepliBuild’s system-MLIR requirement — so the pointer is appreciated, just for a different reason than intended.
