SCUA

How-to

Pass strings, buffers and records

Use typed declarations to move text, image bytes, numeric arrays and C records between SCUA and a native library. Counts are in elements unless an API explicitly says bytes.

These APIs are experimental and unreleased. Start with Native libraries and FFI for setup and platform support. Code fragments below assume import ffi, an open library handle, and C functions with the signatures shown in comments. Symbol names are illustrative unless linked to an executable example.

#Strings and opaque native objects

-- C: const char *name(void);              borrowed/static result
let name = ffi.bind(library, "name", "cstr", [])
-- C: char *duplicate(const char *);       caller owns result
-- C: void release_text(void *);
let duplicate = ffi.bind(library, "duplicate", "cstr", ["cstr"], "release_text")
print(ffi.call(duplicate, "hello"))

-- C: void *create(void); void destroy(void *); int use(void *);
let create = ffi.bind(library, "create", "ptr", [], "destroy")
let use = ffi.bind(library, "use", "i32", ["ptr"])
let resource = ffi.call(create)
print(ffi.call(use, resource))
ffi.close(resource)

An optional fifth bind argument names a void release(void *) function in the same library. It is valid for copied string results and supported owned opaque or typed-buffer pointer results. Never supply a release function for borrowed memory. A release whose C signature returns intsqlite3_close returns SQLITE_BUSY and refuses while statements are live — is declared as a table wherever a release name is accepted: {symbol = "sqlite3_close", result = "i32"} (as the fifth argument's release key, or as ffi.out_owned's third argument). Its result is then checked. Non-zero faults an explicit ffi.close with the symbol and the code; when the release ran from a dependency cascade, the finalizer sweep or partition teardown it is written to stderr and to host log sinks instead. SCUA's handle is gone either way, so declare the resource's dependents and close them first. A release declared by name alone is void, and a refusal it returns is invisible. Owned strings are copied then released, including when validation/copying fails. Owned pointers are released on explicit close, dependency close, library close/revocation, or partition teardown. Without a release function, pointer handles borrow the address and never free it.

C-string inputs are temporary NUL-terminated copies; native code must not retain them. Inputs and results are limited to 1 MiB excluding the terminator. Results must be readable, terminated UTF-8; invalid UTF-8 faults. Use u8 buffers for binary data, embedded NULs or other encodings. The scan limit cannot make an invalid native address safe.

When C does keep the pointer — sqlite3_bind_text with the SQLITE_STATIC destructor promises that the text outlives the call — declare the argument {type = "cstr", retained = true}. A retained argument refuses a script string at the call (its copy would be freed on return, and C would read freed memory later) and takes a native string instead: ffi.native_string(library, text) copies the text into native memory that lives as long as its handle. Close it once C is done with it, and list it under depends_on for any result that keeps it, so closing one invalidates the other. Where C offers a "copy it yourself" mode instead — SQLITE_TRANSIENT is (void *)-1 — pass the sentinel as the negative integer it is: ffi.unsafe_pointer(library, -1).

let bind_text = ffi.bind(lib, "sqlite3_bind_text", "i32", [Stmt, "i32", {type = "cstr", retained = true}, "i32", "ptr"])
let text = ffi.native_string(lib, "hello world")
ffi.call(bind_text, stmt, 1, text, -1, nil) -- a nil destructor is SQLITE_STATIC: the pointer is kept
-- step, then finalize the statement, then:
ffi.close(text)

A fault about an argument names its position, what the binding declared and what arrived — argument 2: expected i64, got string — and a misspelt descriptor key or unknown type name is quoted back. A fault raised by your own callback reaches the caller as FFI callback faulted: <its message>. Everything printed so far is written out before any native code runs, so a native crash cannot take lines that had already executed with it.

The checked APIs do not construct opaque addresses from integers, dereference them or cast them. Explicitly unsafe operations are described in resource lifetimes. Returned handles depend on their library and conservatively on every opaque-pointer/native-buffer input. Closing an input invalidates dependent handles and releases owned dependents first. Borrowed pointer results with cstr inputs are refused because they could escape temporary storage. This does not infer arbitrary C lifetimes: if another native operation frees/replaces an object, stop using its old handles. The untyped ptr declaration cannot distinguish unrelated C object types. Libraries must not retain input addresses beyond the explicitly maintained lifetimes.

Use an opaque type declaration to distinguish native object types:

let Context = ffi.opaque("Context")
let required = {type = Context, nullable = false}
let create = ffi.bind(library, "create", required, [], "destroy")
let use = ffi.bind(library, "use", "i32", [required])
let context = ffi.call(create)
ffi.call(use, context)
ffi.close(context)

Each ffi.opaque call creates a distinct identity, even with the same name. Reuse the descriptor across compatible declarations. Typed handles cannot be passed to a different opaque type or to plain ptr; plain ptr handles cannot be passed to a typed declaration. Type identities are also preserved through callback arguments and checked on callback returns. {type = T, nullable = false} works for opaque pointers, strings, typed-buffer pointers and function pointers. Nil arguments are rejected before native entry; a NULL result faults. Declarations are nullable by default. Unknown qualifier keys are errors. These checks validate the declared contract, not whether a C symbol actually has that signature.

Return lifetimes can be declared in the binding options:

let copy = ffi.bind(library, "context_copy", Context, [Context], {
    ownership = "owned", release = "context_destroy", depends_on = []
})
let child = ffi.bind(library, "context_child", Context, [Context], {
    ownership = "borrowed", depends_on = [0]
})

ownership = "owned" requires a release symbol; "borrowed" forbids one. Ownership applies to pointer and string results. Omitting it retains the existing convention: a release symbol means owned, otherwise borrowed. Release functions use the platform-default ABI and the signature void release(void *), or int release(void *) when declared with result = "i32" (see above). Owned pointer destruction happens on explicit close, parent/library invalidation or partition teardown, by default. Automatic cleanup requires an explicit ffi.autoclose opt-in. Owned string results are released immediately after copying, including on copying/validation failure.

depends_on lists zero-based native argument indices for pointer, function-pointer or pointer-bearing record results. Each selected argument must be a pointer, native buffer, function pointer or pointer-bearing record. Duplicate and out-of-range indices are rejected. Omitting the list retains every resource argument; an empty list declares independence from the call arguments. The owning library is always retained as a lifetime dependency. Borrowed results also inherit any existing source/view dependencies of the callable itself, so a returned address cannot outlive the native resource behind that borrowed callable. Closing an ordinary bound symbol does not close its results. Incorrectly omitting a real native dependency can cause use-after-free: these are trusted binding annotations, not ownership inferred from C. No ownership is transferred from input handles, and a borrowed pointer cannot escape a temporary string input.

#Plain C records and inline arrays

-- C: typedef struct { float x, y; } Vec2;
-- C: Vec2 add_vec(Vec2 a, Vec2 b);
let Vec2 = ffi.struct(["f32", "f32"])
let add_vec = ffi.bind(library, "add_vec", Vec2, [Vec2, Vec2])
print(ffi.call(add_vec, [1.0, 2.0], [3.0, 4.0]))  -- [4.0, 6.0]

-- C: struct { uint8_t tag; Vec2 point; int32_t samples[3]; double weight; };
let Record = ffi.struct(["u8", Vec2, ffi.array("i32", 3), "f64"])
print(ffi.sizeof(Record))
let records = ffi.buffer(Record, 1)
ffi.set(records, 0, [7, [1.0, 2.0], [3, 4, 5], 6.5])
-- Bind a Record* argument using ffi.pointer(Record).
ffi.close(records)

Fields use declaration-order arrays, including nested records and inline arrays. libffi computes natural ABI layout and by-value argument/result classification; padding is zeroed on input and not exposed on output. Reuse the same record descriptor when declaring a buffer and its pointer argument: separately created record descriptors are distinct types. ffi.array(T, N) describes an inline array, not a top-level C array passed by value (C has no such function parameters). Inline arrays can contain opaque/function pointers and nested pointer-containing structs in by-value records and read-only native views. Their decoded handles follow the enclosing result, callback or view lifetime rules. Use ffi.pack for SCUA-managed pointer-bearing native records and its transactional write helpers, rather than ffi.buffer. Unions with explicit member selection, explicit bit access and Clang-based header import are described in binding declarations. Packed/compiler-specific by-value layouts, flexible arrays, SIMD ABI types and long double remain open; see platform support.

For selective access without copying the whole record, use ffi.field_get(buffer, element_index, field_index) and ffi.field_set(buffer, element_index, field_index, value). Both indices are zero-based; nested record/inline-array fields return or accept their usual array representation. A failed field write leaves the entire buffer unchanged. ffi.offsetof(Record, field_index) reports the field's ABI byte offset. These checked operations access typed buffers, not arbitrary addresses.

#Named record fields

An optional second argument to ffi.struct preserves C field names:

let Vec2 = ffi.struct(["f32", "f32"], ["x", "y"])
let add = ffi.bind(library, "vector_add", Vec2, [Vec2, Vec2])
let result = ffi.call(add, {x = 1.0, y = 2.0}, {x = 3.0, y = 4.0})
print(result.x)
let vertices = ffi.buffer(Vec2, 10)
ffi.field_set(vertices, 0, "x", 12.0)
print(ffi.field_get(vertices, 0, "x"))

Named layouts accept either positional arrays or tables containing exactly the declared fields; their decoded results are named tables, including nested records. Unnamed layouts continue to use arrays. ffi.field_get, ffi.field_set and ffi.offsetof accept names or zero-based indices. Names must be distinct, nonempty NUL-free strings, at most 255 bytes. Naming a layout does not change its ABI representation. As before, types declared separately retain distinct identities.

#By-value pointer records

ffi.struct can declare opaque ("ptr" or nominal opaque types) and function-pointer fields for by-value native inputs and results, including nested structs. Pass input fields as live FFI handles, not numeric addresses. Field type identity, function signature and nullability are checked before native entry. Temporary string fields, typed-buffer fields and ordinary ffi.buffer allocations containing pointers remain unsupported; use ffi.pack and its checked write helpers.

let Entry = ffi.struct(["ptr", "i64"], ["value", "factor"])
let read_entry = ffi.bind(library, "read_entry", "i64", [Entry])
print(ffi.call(read_entry, {value = native_pointer, factor = 2}))

Pointer/function results conservatively depend on nested resource fields as well as ordinary resource arguments. A depends_on entry selecting a record argument selects its nested fields; an explicit omission is a trusted lifetime assertion. At most 16 distinct dependent resources can be retained by a call result. Marshalling does not transfer ownership or permit C to retain the temporary record storage after the call. ffi.buffer(Entry, ...) remains rejected until persistent pointer-field storage has its own dependency rules.

ffi.pack(library, RecordType, [record, ...]) creates persistent, read-only native record storage, including pointer-containing records. It can be passed to a matching read-only typed pointer parameter and supports get, read and field access. Initialization is transactional: an invalid field leaves no published buffer. At most 16 distinct native resource dependencies are tracked across the packed records. Closing any provider invalidates the packed buffer and its decoded child views; closing the packed buffer frees its bytes, not its providers. The bytes remain read-only to native writable-span parameters and raw byte writes. ffi.replace(storage, records) atomically replaces the complete contents with the same record count and updates the dependency list. Failed validation/allocation leaves the old data and views intact. Successful replacement invalidates all previously decoded views and derived handles, but keeps the storage address stable. Replacement values must use independent native resources, not the storage itself or views derived from it; such dependency cycles are rejected. ffi.set(storage, index, record) and ffi.field_set(storage, index, field, value) use the same transactional rules for a single record or field. Dependencies are tracked by field location: replacing one reference does not remove a provider still used elsewhere. Successful mutations invalidate old derived views; failed ones preserve them. Dependency metadata counts toward the native storage budget. Mutation is refused during a native callback; direct native pointer-field writes remain unsupported. These operations do not authorize native code to retain a buffer after its handle closes.

ffi.write(storage, start, records) atomically updates a range of packed records. All records are validated before any bytes or dependencies change; records outside the range are preserved. An empty write is a no-op and does not invalidate existing views. Successful nonempty writes use the same old-view invalidation and dependency-cycle rules as individual field updates.

Returned pointer/function fields are borrowed handles tied to the provider library and selected input dependencies. No ownership or release function is inferred for a field. Native code must keep the pointees valid; use a compiled wrapper for structures transferring ownership of fields. Result decoding rolls back any partially published handles on failure (including a nonnullable field unexpectedly returning NULL). Calls with pointer-record results reject temporary string and managed-array borrowing inputs, since native code could return their expiring addresses.

Owner-thread callbacks also accept/return these records. Input pointer/function fields become callback-scoped borrowed handles; capturing the record does not extend their lifetime. They and derived bindings expire when the callback exits, including on an error. Returning a record validates its pointer fields and function signatures before handing addresses to C. The native caller remains responsible for the underlying pointees' lifetimes; returning a callback input pointer does not transfer ownership. Native code retaining a returned address must arrange for its actual resource owner to remain alive. Invalid output fields fault the enclosing foreign call and the native callback result storage is zeroed.

Typed pointer fields can declare a fixed extent:

let Samples = {type = ffi.pointer("f32"), count = 3, writable = false}
let Entry = ffi.struct([Samples, "i32"], ["values", "tag"])
let echo = ffi.bind(library, "echo_entry", Entry, [Entry])
let storage = ffi.buffer("f32", 3)
ffi.write(storage, 0, [1.0, 2.0, 3.0])
let result = ffi.call(echo, {values = storage, tag = 7})
print(ffi.get(result.values, 2))
ffi.close(storage) -- result.values becomes stale too

The field value must be a matching native buffer or nullable nil, never a managed-array borrow or output slot. Its extent is fixed in the declaration, not read from a sibling count field. Returned fields are borrowed bounded views; callback input fields expire with the callback. Inline arrays can likewise contain extent-qualified typed pointers. These additions do not transfer field ownership or enable persistent writable pointer-containing record storage.

#Typed buffers and output parameters

Pointer/count pairs can be declared explicitly:

let Samples = {type = ffi.pointer("f32"), count_arg = 1, writable = true}
let scale = ffi.bind(library, "scale", "void", [Samples, "size_t", "f32"])
let Callback = ffi.fn_type("f64", [Samples, "size_t"])

count_arg is the zero-based index of the integer count argument in the native signature. For normal calls, the count must be nonnegative and no larger than the supplied native buffer; nil requires a zero count. Without an annotation, existing manually paired buffer calls keep their previous trusted-count contract. writable = false permits read-only buffers/views; the default is true. These annotations are also accepted in manifests.

A typed pointer result requires either count or count_arg. The latter declares the returned element count using an integer input argument, captured before C runs. For example:

let Integers = ffi.pointer("i32")
let identity = ffi.bind(library, "identity", {type = Integers, count_arg = 1, writable = false},
    [{type = Integers, count_arg = 1}, "i32"])
let Record = ffi.struct(["f32", "f32"], ["x", "y"])
let get_position = ffi.bind(library, "get_position",
    {type = ffi.pointer(Record), count = 1, writable = false}, [])
let position = ffi.call(get_position)
print(ffi.field_get(position, 0, "x"))
ffi.close(position) -- releases the view, not the borrowed native allocation

The result is a borrowed native buffer view, bounded to 16 MiB, supporting the usual buffer operations. Nullable NULL results become nil. Non-NULL addresses must satisfy element alignment. Views expire with their library and declared resource dependencies (all native resource arguments by default), including dependencies inherited from a borrowed callable. Pointer-bearing elements require writable = false. C must guarantee that the declared extent is valid: this is not an allocation-size probe. Calls returning such views cannot borrow managed arrays or temporary string copies. count is a nonnegative constant element count for inputs, results and callback spans. It cannot be combined with count_arg; fixed extents are checked at binding time. Explicit ownership = "borrowed" is accepted (and is the default). Closing a view invalidates any pointer or callable fields decoded from it.

Callbacks may return an existing native buffer with a matching element type and sufficient length. For example, ffi.fn_type({type = ffi.pointer("f32"), count_arg = 0, writable = false}, ["size_t"]) describes a callback returning that many floats. The count is captured before invoking the script; fixed count works too. A writable result requires writable storage, and pointer-bearing elements require a read-only declaration. Nil is valid only for a nullable result with zero count. Results are bounded to 16 MiB.

This borrows the buffer's address; it does not copy storage or transfer ownership to C. Keep the buffer and its dependencies live for every native use, including after the callback if C retains the pointer. C must not free SCUA-owned storage. Managed arrays/bytes, single-use output slots, and callback-scoped input views (including derived views) cannot be returned through this path. Allocate persistent native storage before invoking C, update it in the callback, and return its handle. Closing the callback does not close that separate buffer.

For generated bindings, the header importer accepts --pointer-result CallbackName=arg:0 and --pointer-parameter CallbackName.1=3 on named function-pointer typedefs. Direct typedefs are exported under their C names, so pass bindings.CallbackName to ffi.callback.

Fixed input spans use the same bounds checks and managed-array borrowing rules as count_arg: {type = ffi.pointer("f32"), count = 3, writable = false} describes a readable three-float input without a separate length parameter. Short buffers and NULL with a nonzero extent are rejected. A fixed callback span is still valid only during that callback; the native caller guarantees its declared extent. This annotation does not constrain unrelated integer arguments that the native function might independently use as lengths.

For a native allocation, declare ownership = "owned" and a same-library release function:

The Clang importer can emit this contract with --owned-result FUNCTION=RELEASE, alongside the required --pointer-result FUNCTION=COUNT|arg:INDEX extent. Declare the release function in the header too: default ABI, one data-pointer argument and void return. The importer checks that shape; choosing the correct destructor is still the binding author's responsibility. This option does not enable GC finalization; that requires a separate ffi.autoclose opt-in on the returned handle.

Use --result-depends FUNCTION=0,2 to declare which resource inputs own a result's lifetime, or --result-depends FUNCTION= for an independently allocated result. Omitting the option preserves the default dependencies. Only exclude an input when the native contract guarantees independence; the importer cannot infer that fact.

let create = ffi.bind(library, "create_values",
    {type = ffi.pointer("i64"), count_arg = 0}, ["size_t"],
    {ownership = "owned", release = "destroy_values"})
let values = ffi.call(create, 32)
ffi.set(values, 0, 42)
ffi.close(values)

The release symbol must have the platform-default signature void release(void *) — or int release(void *) when declared as {symbol = "…", result = "i32"} — and accept the exact returned allocation address. It runs once when the owning view closes, its dependency or library closes, or the partition is destroyed. Failure to publish a result (including memory allocation failure) also releases the native allocation, as does callback failure after C returns it. A NULL result is never passed to the release function. Explicit close and resource teardown are the default; ffi.autoclose separately opts a handle into deferred GC-driven cleanup. C remains responsible for allocating at least the declared extent. Imported bindings stay borrowed until you explicitly change their ownership contract in the manifest.

Inside a callback, an annotated pointer becomes a call-scoped native buffer view of that many elements. It supports ordinary len/get/set/read/write/field_* operations, subject to its read-only flag. Capturing the handle does not extend its lifetime: it and derived handles become stale when the callback returns. NULL with a nonzero count faults. Callback extents are limited to 16 MiB. Pointer/function elements and pointer-containing records are also supported when writable=false; writable callback spans still require pointer-free elements. Decoded pointer fields and callable elements expire with the callback view, including on failure. The C caller still guarantees valid memory for the declared extent; SCUA cannot prove a native address is readable. This is native buffer pairing; managed packed arrays can also be borrowed directly as described next.

let samples: { f32 } = [1.0, 2.0, 3.0]
ffi.call(scale, samples, 3, 2.0)
assert(samples[2] == 6.0)

Count-annotated scalar pointer parameters accept matching packed f32, f64, i32, i64, u8, u16 and u32 arrays without copying. A read-only u8 pointer also accepts immutable bytes. Boxed arrays, mismatched element kinds, writable bytes and overlapping writable spans are rejected. The declared count is checked against live length, not spare capacity; managed spans are limited to 16 MiB. An empty span passes NULL and therefore requires a nullable type.

Packed vec2, vec3 and vec4 arrays also support direct borrowing when the declared element is a record or fixed array of exactly 2, 3 or 4 contiguous f32 fields, respectively. Counts are vectors, not individual float lanes. Other record layouts are rejected. ffi.alignof(T) reports native ABI alignment; it is distinct from ffi.sizeof(T), a three-float record occupies 12 bytes but has 4-byte alignment on supported targets.

Direct managed borrowing uses SCUA's protected native-access interval. Native code must not retain the address; it expires at call return. SCUA callbacks and host API re-entry that could invalidate storage are refused while this interval is active. Use independently allocated native buffers for callback-capable calls. Pointer/function-pointer return types are refused when managed storage is borrowed, since the returned address could otherwise escape the interval. Scalar/POD results and explicit owned output slots remain available. Writes already performed by C are not rolled back if C subsequently attempts a prohibited callback. Allocation/preparation happens before addresses are acquired; the interval is released before result decoding or error allocation.

#Managed arrays with callbacks: staged calls

Use ffi.call_staged(symbol, ...args) when a call both receives managed packed arrays and invokes SCUA callbacks. It accepts the same descriptors and supported managed element types as ffi.call, but copies each declared managed span to temporary aligned native storage before C runs. Native buffers still pass directly. Normal ffi.call keeps its direct-borrow behavior.

let Span = {type = ffi.pointer("f32"), count_arg = 1}
let Callback = ffi.fn_type("i64", ["i64"])
-- C: void update(float *values, size_t count, int64_t (*callback)(int64_t));
let update = ffi.bind(library, "update", "void", [Span, "size_t", Callback])
let values: { f32 } = [1.0, 2.0, 3.0]
let callback = ffi.callback(library, Callback, fn(value) return value end)
ffi.call_staged(update, values, 3, callback)
ffi.close(callback)

Writable spans copy back only after native execution, callback checks and result conversion succeed. Before writing any destination, SCUA re-resolves every writable array and checks that its element kind and full length are unchanged. A shape/type change faults without copying back any staged spans. Callback side effects are not rolled back. Changes a callback makes inside a staged span are overwritten by successful copy-back; changes outside it remain. During the callback, the original array does not reflect C's staged writes.

Overlapping writable managed arguments are rejected before C runs. Read-only arguments are independent snapshots; pointer equality between their copies is not preserved. Immutable bytes are read-only. Staging retains the direct path's restrictions on pointer/function/typed-buffer and pointer-bearing record results, so temporary addresses cannot escape through them. C must not retain or free staging pointers, and callback input views still expire on return.

Each span is bounded to 16 MiB and all staging bytes count toward the partition's 64 MiB native storage budget. Temporary storage is freed on success or failure. This is an explicit copying API, not a zero-copy performance claim or an ownership transfer.

-- C: void scale(float *values, size_t count, float factor);
let scale = ffi.bind(library, "scale", "void", [ffi.pointer("f32"), "size_t", "f32"])
let values = ffi.buffer("f32", 3)
ffi.write(values, 0, [1.0, 2.0, 3.0])
ffi.call(scale, values, ffi.len(values), 2.0)
print(ffi.read(values, 0, 3))  -- [2.0, 4.0, 6.0]
ffi.close(values)

-- C: void query(int32_t *count, double *value);
let query = ffi.bind(library, "query", "void", [ffi.pointer("i32"), ffi.pointer("f64")])
let count = ffi.buffer("i32", 1)
let value = ffi.buffer("f64", 1)
ffi.call(query, count, value)
print(ffi.get(count, 0), ffi.get(value, 0))
ffi.close(count)
ffi.close(value)

Buffers own zero-initialized, aligned native memory outside the managed heap. They do not alias SCUA arrays. ffi.pointer(T) accepts a live buffer of exactly type T, or nil for NULL; count-annotated parameters additionally accept matching managed storage as described above. It is an argument declaration, not an operation exposing an address. Use ffi.buffer(T, 1) for an output/in-out slot. Buffers may contain numeric scalars, booleans or the records below, not strings or opaque pointers. Owned pointer-to-pointer outputs use a dedicated single-use slot:

-- C: int context_create(Context **out, int64_t value);
let Context = ffi.opaque("Context")
let create = ffi.bind(library, "context_create", "i32", [ffi.pointer(Context), "i64"])
let slot = ffi.out_owned(library, Context, "context_destroy")
let status = ffi.call(create, slot, 42)
let context = ffi.take(slot)
ffi.close(slot)
-- use context, then:
ffi.close(context)

ffi.out_owned requires an opaque or typed-buffer pointer type and a same-library, platform-default-ABI void release(void*) destructor, or {symbol = "sqlite3_close", result = "i32"} for an int-returning one whose refusal must be reported. An optional fourth argument lists parent handles: ffi.out_owned(library, Context, "context_destroy", [owner]). Omitting it declares an independent allocation. For borrowed outputs use ffi.out_borrowed(library, Context, [owner]); its explicit parent array may be empty for a library-lifetime static object. No destructor is called for a borrowed result. Up to 16 distinct resource parents may be declared.

The parent list follows the result through ffi.take. Closing any parent invalidates an unclaimed slot or claimed pointer before releasing the parent; an owned dependent is destroyed first. Incorrectly omitting a real dependency is unsafe, the binding author must know the native contract. The native function must write the slot at most once and not retain its address. The slot may be passed to one call only, in one argument position, to its owning library. It cannot be taken during that native call. Check the native status according to the C API before claiming a result; failed calls can still leave an owned pointer that the slot must release.

ffi.take(slot) transfers ownership without copying the resource. If publishing the handle fails, the slot still owns the pointer and can be retried or closed. NULL yields nil unless the declared type requires non-null. Closing an unclaimed slot, revoking/closing its library or tearing down the partition releases its pointer automatically. A taken pointer survives slot closure but remains tied to the library. Ordinary ffi.get and bitfield operations cannot access these slots; the explicitly unsafe address APIs remain outside checked guarantees.

For T **out plus a separately written count, use a typed pointer slot and supply the count when taking it:

-- C: void make_values(int64_t **out, int32_t *count);
let Values = ffi.pointer("i64")
let make = ffi.bind(library, "make_values", "void", [ffi.pointer(Values), ffi.pointer("i32")])
let slot = ffi.out_owned(library, Values, "destroy_values")
let count = ffi.buffer("i32", 1)
ffi.call(make, slot, count)
let values = ffi.take(slot, count)
ffi.close(slot)
ffi.close(count)
print(ffi.len(values))
ffi.close(values)

The second argument accepts either an integer or a live one-element native integer buffer (including size_t). A buffer's current value is copied during take; it is not retained by the resulting view. SCUA does not infer that the count and pointer were produced together. Floating-point, boolean, multi-element, output-pointer and closed count buffers are rejected. The count is in elements and must describe valid native memory; it is bounded to 16 MiB, not verified against an allocator. NULL requires count zero. A fixed count qualifier on the slot's typed pointer permits ffi.take(slot); an explicit count must match that qualifier. Invalid counts or failed view publication leave the slot's ownership intact. Successful take transfers the release function and parent dependencies to the view, not to the slot. The same pattern works with ffi.out_borrowed and explicit parents. Pointer-bearing elements require a read-only typed pointer qualifier. These nested pointer declarations support dedicated output slots, not arbitrary nested-pointer dereference or nested callback spans.

Alternatively, declare the output pairing on the binding:

let make = ffi.bind(library, "make_values", "void",
    [ffi.pointer(Values), ffi.pointer("i32")], {output_counts = [[0, 1]]})
-- Pass the owned/borrowed output slot and a one-element native integer buffer.
ffi.call(make, slot, count)
let values = ffi.take(slot)

Each pair contains zero-based pointer-output and count-output argument indices. Import it from a C header with --output-count make_values.0=1 to emit the same manifest/runtime declaration. The pointer argument must receive a dedicated typed output slot; the count argument must receive a live one-element native integer buffer, not a managed SCUA array. The count is snapshotted after C returns. Closing or changing the count buffer afterward does not alter the captured extent. An explicit count passed to take must match the snapshot. Negative/oversized native counts leave the slot owning its pointer but prevent taking it, including with an override; close the slot to release it. Pairing does not establish whether the C function reported success or prove the allocation's actual size. Native code must initialize its outputs according to its contract. Multiple pointer outputs may share one count output.

The same options table is accepted by ffi.fn_type for C function pointers returned from or passed to native code. A callable's output pairs must match the receiving descriptor. Paired descriptors cannot currently create SCUA callbacks; transferring ownership through native pointer-output parameters from a callback is not implemented.

Indices are zero-based. ffi.get/ffi.set access one element; ffi.write(buffer, start, values) copies an array (also bytes for u8); ffi.read(buffer, start, count) returns a copied array (bytes for u8). Writes validate completely before changing memory. ffi.len counts elements. SCUA checks these ranges and pointee types. C must respect the supplied allocation length: use count or count_arg to check the declared extent before entry. Those checks cannot stop native overruns. Native mutations are visible even if a later result conversion faults.

Start and bind a library · Native data · Callbacks · Lifetimes and unsafe access · Declarations and generation