-
-
Notifications
You must be signed in to change notification settings - Fork 1
FFI Shim Layout
Cartridges with a Zig FFI use a per-cartridge cartridge_shim.zig layout (decided in #29 / #31). This page records the convention so new contributors and machine consumers stay in sync.
Every cartridge that ships a Zig FFI also ships a cartridge_shim.zig beside ffi/build.zig:
cartridges/<…>/<name>/
└── ffi/
├── build.zig
├── cartridge_shim.zig ← here
└── <name>_ffi.zig
build.zig resolves the shim relative to its own directory:
const shim_mod = b.addModule("cartridge_shim", .{
.root_source_file = b.path("cartridge_shim.zig"),
.target = target,
.optimize = optimize,
});For cartridges that also ship an adapter/ sibling (e.g. local-coord-mcp), the adapter's build.zig reaches across into the FFI sibling:
const shim_mod = b.addModule("cartridge_shim", .{
.root_source_file = b.path("../ffi/cartridge_shim.zig"),
…
});The reference shim lives in the gossamer template:
cartridges/templates/gossamer-mcp/ffi/cartridge_shim.zig- SHA-256:
41f6406b8fbcb643b31910225ac5dba6cea3716601cd8c6ae22e063d943946d1 - 112 of 114 in-tree shims are byte-identical to it as of 2026-06-01.
The shim centralises the ADR-0006 five-symbol cartridge ABI (boj_cartridge_init / deinit / name / version / invoke): seven-code return convention, NUL-argument guards, tool-name comparison, buffer-too-small path. A typical cartridge's boj_cartridge_invoke is a short tool table plus shim.writeResult(…).
| Situation | Action |
|---|---|
| New cartridge, standard FFI shape | cp cartridges/templates/gossamer-mcp/ffi/cartridge_shim.zig <your-cartridge>/ffi/ |
| Need cartridge-specific helpers (state machines, custom invariants) | Copy the canonical shim then add helpers below the existing block; keep the upstream symbols stable. |
| Helper would be useful estate-wide | Add it to the canonical and propagate; do not fork the shim per cartridge. |
If you find yourself diverging from the canonical for anything that's not strictly cartridge-local, open an issue against this repo first.
Three layouts were considered (per #29 issue body):
-
Per-cartridge shim (chosen): each cartridge owns its shim. Smallest change at adoption time (95 of 97 affected cartridges already had a local shim — only
build.zigwas wrong). Self-contained: tarball or fork a single cartridge without losing its shim. -
Shared shim in a central location: requires every
build.zigto know the registry root, breaks when the tree reorganises (which is exactly what #29 was caused by). -
Zig package: cleanest but requires a
build.zig.zonper cartridge plus toolchain coordination. Doable later; not blocking today.
Future migration to option 3 is non-blocking and additive.
Cartridges like local-coord-mcp ship both ffi/ (raw FFI surface) and adapter/ (higher-level harness). Both modules need cartridge_shim. The convention:
-
ffi/owns the canonicalcartridge_shim.zigsource. -
adapter/build.zigreferences it asb.path("../ffi/cartridge_shim.zig").
This keeps a single source of truth per cartridge without duplicating bytes across the FFI and adapter dirs.
cd cartridges/<…>/<name>/ffi
zig build test --summary allCI (.github/workflows/zig-test.yml) runs the same command on every cartridge whose ffi/** paths changed in a PR, with fetch-depth: 0 so the merge-base diff resolves. See #28 for the workflow guarantee.