Exporting Z through C
Interop works in both directions. export c function keeps a function callable
from Z while additionally publishing a stable C symbol:
function double(value: i32): i32 { return value * 2;}
export c function z_math_add(left: i32, right: i32): i32 { return left + right;}
export c function z_math_answer(): i32 { return z_math_add(double(10), 22);}The C name is exact and intentionally explicit. Ordinary Z callers invoke the Z implementation directly; they do not cross the generated foreign wrapper.
Supported scalar ABI
Section titled “Supported scalar ABI”| Z | Generated C |
|---|---|
i8…i64 |
int8_t…int64_t |
u8…u64 |
uint8_t…uint64_t |
isize, usize |
intptr_t, uintptr_t |
f32, f64 |
float, double |
boolean |
bool |
void |
void |
An owned Z String input projects as borrowed const char * at the C ABI:
export c function z_text_byte_length(text: String): usize { return text.byteLength;}The wrapper validates non-null NUL-terminated UTF-8, copies it once into owned
Z storage, and runs ordinary Z cleanup on every path. Z-to-Z calls pass the
String directly and make no boundary copy. Returning an owned String to C
remains unsupported until the public ABI can state allocator and release rules.
Borrow arbitrary bytes without copying
Section titled “Borrow arbitrary bytes without copying”Use a read-only Slice<u8> when the input is bytes rather than promised text:
export c function byteCount(bytes: Slice<u8>): usize { return bytes.length;}The generated C header exposes a pointer and length, not Z’s slice layout:
Z_API uintptr_t byteCount(const uint8_t *bytes_data, uintptr_t bytes_length);There is no allocation, byte copy, UTF-8 validation, or NUL scan in this
projection. Embedded NUL and invalid UTF-8 remain ordinary bytes. Each slice
expands to its own adjacent pair; ordinary Z callers still pass a single slice
or compatible array. Slice is already borrowed, so omit the in prefix.
The caller keeps the byte range valid and unmodified until the synchronous call
returns. (NULL, 0) is empty; (NULL, positive_length) traps. The wrapper
cannot verify arbitrary pointer validity or concurrent host mutation. Z code
cannot mutate or retain the slice; explicitly copy into owned byte storage if
the data must survive the call.
Mutable slices, other element types, returned slices, and async exports remain
unsupported. Receiving bytes never implicitly converts them to String.
Publish C-layout values deliberately
Section titled “Publish C-layout values deliberately”Ordinary Z structs do not silently become public native layouts. Mark the representation explicitly:
c struct ZMathPoint { x: i32; y: i32;}
export c function z_math_translate( point: ZMathPoint, dx: i32, dy: i32): ZMathPoint { return ZMathPoint({ x: point.x + dx, y: point.y + dy, });}Fixed-representation enums retain nominal Z matching while publishing an exact integer representation:
c enum ZMathStatus: i32 { negative, zero, positive,}The wrapper validates enum inputs from C before they enter safe Z, because a C caller can manufacture an integer outside the declared variants.
The first c struct tier accepts public mutable scalar or nested C-layout
fields and rejects cleanup-bearing, generic, private, readonly, class, or
executor-affine layouts that a plain C header could not preserve safely.
Build a static or shared library
Section titled “Build a static or shared library”Project configuration chooses the artifact independently from source ABI:
{ "target": { "kind": "static-library", "name": "zmath", "entry": "library.zs" }}z build . --releaseThe static project publishes:
build/libzmath.abuild/zmath.hUse "shared-library" to publish a .dylib, .so, or .dll plus the same
checked header contract. Executables remain the default when kind is omitted.
Generated runtime lifecycle
Section titled “Generated runtime lifecycle”The public header includes target-scoped initialization and shutdown functions. A native host initializes before its first exported call and shuts down only after exported calls, native workers, and registered callbacks have drained.
Every public wrapper enters and leaves the bounded Z runtime. This makes foreign-created threads, ARC state, callbacks, and future runtime-owned roots safe without charging ordinary Z-to-Z calls for foreign-boundary accounting.
A project may configure one cleanup-bearing runtime root:
{ "target": { "kind": "static-library", "name": "application_core", "entry": "library.zs", "runtime": { "initialize": "initializeRuntime" } }}The initializer’s owner remains alive until host shutdown, providing a natural
home for application identity, Once<T>, callback registrations, and other
embedding-lifetime resources.
A root such as OnceLifetime<Engine> can be thread-independent when Engine
is a compiler-proven shareable class (for example, a readonly Mutex<Registry>
field). The host can initialize and shut down that library on worker threads
without main-thread callbacks if both generated requirement macros are zero.
Main-affine classes elsewhere in the library still keep those requirements;
a shareable root does not override them.
Why the generated wrapper exists
Section titled “Why the generated wrapper exists”The compiler keeps internal Z symbols private and places the stable ABI in a small public wrapper. That wrapper is where it can:
- validate hostile C inputs;
- create owned Z values from foreign representations;
- enter and leave the embedded runtime;
- preserve future compiler freedom over internal names and calling conventions.
The generated header includes an extern "C" guard, so the same library is
directly consumable from C++, Objective-C++, and Swift’s C importer.
Runnable examples: static library, borrowed bytes, shared library, and embedded runtime root.
Continue with the reference map for exact ABI restrictions and embedding lifecycle details.