std.ffi
std.ffi contains the accepted native-FFI boundary types. The checked-in SDK does not implement this target surface yet.
Use ordinary counted str, std.bytes.Bytes, or slices when a foreign operation accepts a pointer and a byte size. Use CStr only when the actual C ABI contract requires a pointer to a NUL-terminated byte string.
CStr
CStr is one nominal role family:
ffi.CStr in L
^ffi.CStrCStr in L is a borrowed immutable view. ^CStr owns the same representation in the built-in allocation domain. There is no second CString type.
Every value proves all of the following:
ptr[0..size)is readable content for the value's region;ptr[size]is readable and equals zero;- no byte in
ptr[0..size)is zero; sizeexcludes the terminator; and- the bytes are not implicitly UTF-8 text.
The borrowed representation is {ptr, size}. An owning value is immutable, owns an exact size + 1 byte buffer, has no public capacity or growth API, and releases that buffer through the built-in domain. The canonical empty owner may use static one-zero-byte storage and no-op drop.
Static Literals
The closed c"..." literal form creates a program-lifetime CStr without allocation:
let mode = c"rb";
let empty = c"";It is a byte literal, never interpolates, and rejects every decoded zero byte. Ordinary source characters and Unicode escapes contribute their UTF-8 bytes; \xNN contributes the exact byte, so non-UTF-8 C strings remain expressible. There are no raw, multiline, interpolated, or owning c literal variants.
Owning Construction
Construction from counted bytes explicitly allocates the terminated owner:
import std.bytes;
import std.ffi;
let raw: bytes.Bytes in _ = input;
let name: ^ffi.CStr = try ffi.CStr(raw);The constructor rejects CStrError.InteriorNul, allocates exactly size + 1, copies the content once, and writes the terminator. Allocation failure is std.mem.AllocError.OutOfMemory. Empty input uses the canonical empty owner.
Ordinary text may contain U+0000 and no str role promises terminated storage. Dynamic text therefore follows the same checked allocating constructor as any other counted bytes:
let text: str = make_text();
let native: ^ffi.CStr = try ffi.CStr(bytes.Bytes(text));Construction rejects interior NUL. There is no O(1) ^str projection, hidden scan, borrowed conversion, or special complete-owner overload.
Bounded Foreign Input
find searches for the first NUL only inside an existing initialized, region-tied byte view:
let value: ffi.CStr in _ =
ffi.CStr.find(storage) require FfiError.Unterminated;Its conceptual signature is:
fn CStr.find<region L>(data: bytes.Bytes in L) -> CStr? in L;The result caches the content size. A missing terminator returns null; a NUL at the first byte produces the valid empty C string. Bytes after the first NUL are outside the result. A null raw pointer is handled before construction and is never a distinguished CStr value.
Foreign storage first receives a real bound and lifetime through the accepted owner-anchored memory view:
let initialized: [u8] in _ = unsafe {
mem.view(ptr, readable_bound, foreign_owner)
};
let value = ffi.CStr.find(bytes.Bytes(initialized));There is no public unbounded from_ptr, from_cstr, from_raw, adopt, or into_raw operation. A pointer-only foreign producer must be handled by the specific unsafe wrapper that knows its validity, extent, lifetime, and release contract. Foreign ownership remains a nominal resource with its matching release; conversion into ^CStr is an explicit copy.
Access And Text Validation
The minimal borrowed surface is:
value.ptr
value.size
value.bytes()
try value.text()ptr is the read-only raw pointer expected by a C declaration. bytes() is a region-tied bytes.Bytes view excluding the terminator. text() validates strict UTF-8 and returns an allocation-free str in L, or reports std.unicode.utf8.UnicodeError.Invalid. There is no bytes_with_nul helper; the terminator proof is carried by CStr itself.
Raw C ABI
CStr is not passed to C by value and the compiler performs no implicit ABI conversion:
import std.ffi;
extern library "c" {
abi "C";
fn puts(value: *u8) -> i32;
}
fn print(value: ffi.CStr) {
unsafe {
_ = puts(value.ptr);
}
}
print(c"hello");The raw declaration remains pointer-shaped. The safe Zynx wrapper accepts the nominal proof. When C accepts (pointer, length), the wrapper instead keeps the ordinary counted view and does not create a CStr.
C Callbacks
C callbacks use the language-level function-pointer type, not a nominal std.ffi wrapper and not an erased Zynx callable:
type Visit = extern "C" fn(*mut void, i32) -> i32;
extern "C" fn visit_trampoline(context: *mut void, value: i32) -> i32 {
// Explicit context recovery belongs to this callback.
return 0;
}
extern "C" fn c_visit(callback: Visit, context: *mut void) -> i32;Visit is non-null; a C contract that permits NULL uses Visit?. Every indirect C-function-pointer call is unsafe. Ordinary Zynx functions and closures, including noncapturing closures, never convert to this type. Stateful callbacks carry the exact *mut void context required by their foreign API; the compiler does not box an environment or manufacture a trampoline.
A safe synchronous wrapper may lend stack context only for a documented no-retain call. A stored callback instead requires a nominal registration owner specific to that API. Drop must unregister, wait until no callback is active or can begin, and only then destroy context state. If the foreign API cannot provide this quiescence contract, no general safe retained wrapper exists. Foreign-thread callbacks additionally require explicit synchronization and exact Send + Sync state. Errors, suspension, ownership, and unwinding never cross the C callback signature; wrappers translate them to the API's explicit status or completion protocol.
Bindgen maps a C function-pointer typedef to an extern "C" fn alias and uses its nullable form for unannotated C pointer positions. C-layout callback tables store those aliases plus explicit context fields; they are not Zynx vtables. There is no automatic WebAssembly Component Model mapping. WIT callbacks use typed imports/exports and resources; a core-Wasm function table remains an adapter detail.
Foreign Output And Wasm
There is no mutable CStr. Existing initialized foreign output uses &[u8]; fresh contiguous output uses OutputSpan<u8>. After the exclusive write ends, the initialized result may pass through bounded CStr.find.
CStr is a native-FFI concept, not a WebAssembly Component Model string. Component strings remain Unicode pointer-plus-byte-size values. A boundary chooses cstr.text() for checked text or cstr.bytes() for a byte list; neither conversion is implicit.
API Reference
| Item | Contract |
|---|---|
CStr in L | Immutable borrowed terminated byte string tied to region L. |
^CStr | Immutable exact owning terminated byte string. |
CStrError.InteriorNul | Input content contained a zero byte. |
c"..." | Static allocation-free C-string literal with no interpolation or NUL. |
CStr(data) | Checked allocating construction from bytes.Bytes; returns ^CStr. |
CStr.find(data) | Bounded first-NUL search returning CStr? in L. |
.ptr | Read-only raw pointer to the first content byte or terminator. |
.size | Content byte count, excluding NUL. |
.bytes() | Allocation-free content bytes, excluding NUL. |
.text() | Allocation-free checked strict-UTF-8 view. |