core/docs/zig/runtime-ffi
Shay 9ead270fda docs: ratify Zig native-substrate doctrine (ADR-0196)
Bring the docs/zig/** decision package and README 'Native Substrate
Direction' section into main, and record ADR-0196 (Accepted) as the
binding ratification.

The doctrine is explicitly NOT a wholesale Zig rewrite. It establishes a
ring architecture (Python = semantic source of truth; Rust = incumbent
algebra backend; Zig = Ring 1 native-substrate candidate only) and the
G0-G8 adoption gate ladder. Zig may enter only by clearing gates against
a locked reference contract; default-by-availability is forbidden.

ADR-0196 forward-references ADR-0180 as the first G1 instantiation (the
CRDT contract lock / ZC-0 slice). No Zig code is authorized.
2026-05-31 15:13:19 -07:00
..
README.md docs: ratify Zig native-substrate doctrine (ADR-0196) 2026-05-31 15:13:19 -07:00

Zig Guidance — Runtime FFI Boundary

Status: doctrine
Component: C ABI and runtime backend boundary

Zig should enter CORE through stable FFI surfaces, not through ad-hoc runtime imports. This document defines the shape of that boundary.


Why FFI discipline matters

The risk of adding Zig is not the language itself. The risk is boundary confusion:

Who owns this buffer?
Who frees this allocation?
Is this function allowed to mutate global state?
Is the backend selected intentionally?
Are errors typed or just logged?
Can replay identify which backend ran?

If those questions are ambiguous, Zig should not be merged.


ABI principles

1. C ABI first

All Zig substrate components should expose a C ABI first.

Python, Rust, Swift, or future edge binaries may bind to that ABI, but the ABI itself should not depend on Python object semantics.

2. Explicit versioning

Every library should expose:

unsigned int core_native_abi_version(void);
const char* core_native_build_id(void);

Runtime bindings must reject unknown ABI versions.

3. Caller-owned inputs

Input buffers should usually be caller-owned. Zig must treat them as borrowed unless the API explicitly copies them.

4. Explicit output ownership

If Zig allocates output, Zig must provide the matching free function.

Pattern:

int component_do_work(..., ComponentOutput** out, ComponentError* err);
void component_output_free(ComponentOutput* out);

No output allocation should be freed by Python, Rust, or libc unless the ABI explicitly says so.

5. Typed error surface

Do not print errors from Zig as the only failure signal. Return structured error codes and bounded messages.

Example:

typedef struct CoreNativeError {
    int code;
    char message[256];
} CoreNativeError;

Python bindings should translate these to typed Python exceptions.


Backend selection

Native backends must be selected explicitly.

Recommended environment variables:

CORE_NATIVE_BACKEND=python|rust|zig
CORE_CRDT_BACKEND=python|rust|zig
CORE_AUDIO_COMPILER_BACKEND=python|zig
CORE_RECALL_BACKEND=python|rust|zig

Rules:

  • absence of Zig library must not break the Python path;
  • presence of Zig library must not silently change runtime behavior;
  • backend identity must be visible in diagnostics;
  • backend identity should be recorded in replay evidence where behavior/performance could matter.

Memory ownership table

Case Preferred ownership
input NumPy matrix Python owns; pass pointer/shape/stride only after contiguity check
audio input samples caller owns unless canonicalizer explicitly copies
CRDT provenance bytes Zig may copy into arena-owned storage
CRDT delta output Zig owns; caller frees through Zig free function
AudioCompilationUnit output Zig owns; caller frees through Zig free function
error message caller passes error struct pointer; Zig writes bounded message
returned slices avoid raw borrowed slices unless lifetime is obvious and documented

Shape and dtype validation

All FFI surfaces must validate shape and dtype before computation.

Examples:

versor: exactly 32 f32 values
matrix: N x 32 f32, row-major contiguous
queries: B x 32 f32, row-major contiguous
audio samples: declared dtype, channel count, sample rate, length
edges: E x 2 i32 if graph diffusion is used

Invalid shape is a typed error, not undefined behavior.


Forbidden FFI behaviors

Zig FFI must not:

  • hold Python object pointers;
  • assume NumPy memory remains valid after caller scope ends;
  • spawn hidden background threads;
  • mutate global CORE state;
  • allocate without a free path;
  • panic across FFI boundary;
  • return borrowed memory with unclear lifetime;
  • reinterpret non-contiguous buffers as contiguous;
  • hide fallback after partial mutation;
  • suppress errors and continue.

Python binding shape

Python binding modules should be thin and boring:

core_native/zig_loader.py
core_native/crdt_zig.py
core_native/audio_zig.py
core_native/recall_zig.py

Responsibilities:

  • locate library;
  • check ABI version;
  • check buffer contiguity;
  • call C ABI;
  • translate errors;
  • free outputs;
  • expose Pythonic wrappers;
  • never define semantics not present in the reference contract.

Diagnostic command

A future CLI diagnostic should report:

core doctor --zig
  library found: yes/no
  ABI version: ...
  build id: ...
  enabled selectors: ...
  CRDT backend available: yes/no
  audio compiler backend available: yes/no
  recall backend available: yes/no
  last self-test: pass/fail

If a user asks for a required Zig backend and it is unavailable, fail fast with a clear error.


Runtime trace policy

Any turn/event affected by Zig-backed native behavior should be able to report:

native_backend: zig
native_component: crdt|audio|recall|algebra
abi_version: N
build_id: sha/hash/version

This does not mean every trace needs verbose native metadata. But replay-critical paths must not hide which substrate executed.


Recommendation

Add the FFI contract before adding serious Zig code.

A clean FFI boundary lets CORE evaluate Zig without infecting Python semantics, Rust parity work, or Workbench/operator flows.