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
int — sqlite3_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.
#Related guides
Start and bind a library · Native data · Callbacks · Lifetimes and unsafe access · Declarations and generation