Skip to content

FFI Shim Layout

hyperpolymath edited this page Jun 1, 2026 · 1 revision

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.

The convention

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 canonical shim source

The reference shim lives in the gossamer template:

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(…).

When to copy verbatim vs. extend

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.

Why per-cartridge

Three layouts were considered (per #29 issue body):

  1. 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.zig was wrong). Self-contained: tarball or fork a single cartridge without losing its shim.
  2. Shared shim in a central location: requires every build.zig to know the registry root, breaks when the tree reorganises (which is exactly what #29 was caused by).
  3. Zig package: cleanest but requires a build.zig.zon per cartridge plus toolchain coordination. Doable later; not blocking today.

Future migration to option 3 is non-blocking and additive.

Adapter-sibling pattern

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 canonical cartridge_shim.zig source.
  • adapter/build.zig references it as b.path("../ffi/cartridge_shim.zig").

This keeps a single source of truth per cartridge without duplicating bytes across the FFI and adapter dirs.

Build verification

cd cartridges/<>/<name>/ffi
zig build test --summary all

CI (.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.

References

  • #29 — original finding (b.path("../../../ffi/zig/src/cartridge_shim.zig") resolving to nothing after the registry split-out).
  • #31 — bulk rewrite to per-cartridge layout (97 build.zig files).
  • #28zig-test workflow's fetch-depth fix; needed so the changed-cartridge diff finds a merge base.

Clone this wiki locally