Pointers and buffers
On this page
A pointer needs a fact about what it carries. Facts go on a function's params, keyed by C name or by 1-based position, or on its returns:
functions: {
GetCodepoint: { params: { codepointSize: { out: true } } },
UpdateCamera: { params: { camera: { inout: true } } },
DrawTriangleStrip: { params: { points: { array: { length: "pointCount" } } } },
LoadFileData: { returns: { bytes: { length: "dataSize" }, free: "UnloadFileData" } },
LoadFileText: { returns: { string: true, free: "UnloadFileText" } },
},outmarks a pointer the function writes through, andinoutalso passes the current value in. A function with outputs returns an object with each output and the C result asresult, soGetCodepoint("✓")returns{ result: 10003, codepointSize: 3 }. A status code stays inresult; the binding does not throw for it.bytespasses anArrayBufferor a view without copying, andarraycopies a JavaScript array into C memory for the call.lengthnames the parameter that carries the count, which disappears from the JavaScript signature. When it names a pointer to the count, ascompressin zlib takesdestLen, the function receives the buffer's size, and the count it writes there returns as an output. Shared buffers are rejected, as are immutable buffers where C may write. While C uses a buffer, transferring or resizing it, including from a callback, throws aTypeError.- On a result,
bytesandarraycopy the C memory into aUint8Arrayor an array.lengthnames an integer parameter, or a pointer the function writes the count through. stringmarks achar *orunsigned char *as UTF-8 text.freenames the function that releases a string, buffer, or array result after it is copied.
When C sizes a buffer from several parameters, write its length as their product:
functions: {
fill_pixels: { params: { pixels: { bytes: { length: "width * height * 4" } } } },
}fill_pixels(width, height, pixels) keeps every factor in its signature. Before C runs,
the binding multiplies the converted factors and literals and throws a RangeError when
the buffer is shorter, a factor is negative, or the product overflows. A longer buffer
passes unchanged, so one allocation can serve several sizes. On an array, the product
counts elements. Products apply to input buffers and arrays of functions, not to results,
outputs, strings, callbacks, or fields.
Share and compute counts
Input arrays, byte spans, and counted strings can share an integer length parameter.
The binding checks every input length before entering C. Counted strings contribute
their UTF-8 byte length. Shared counts must be integer inputs; writable counts and
output capacities keep separate parameters.
Arrays of handles
For a flat array of owned pointer handles, declare the element type's destructor
and the table's free function. The returned table must remain stable until free
runs. Its cleanup releases the table; each distinct element transfers to its handle.
Repeated pointers preserve identity. Conversion failures release new elements,
including elements not yet converted, and preserve existing handles' ownership.
Use borrowed: true on a returned or output pointer array when C keeps owning its
pointees. Its free function releases only the outer table. Borrowed output aliases
preserve any ownership already held by an existing handle.
Typed arrays
Const arithmetic-pointer inputs also accept the matching typed array. For example,
const double * accepts Float64Array, and const uint64_t * accepts BigUint64Array.
Use the generated SDK for the library's exact native widths. Synchronous calls lend
the view's current storage, including its offset, after argument getters finish.
C callbacks can observe JavaScript element writes, while resizing or transferring
the backing buffer is blocked until C returns. C must not retain the pointer.
Compiled code hands a matching fixed-length view's storage straight to a function
of a library that registers no callbacks, so typed arrays are the faster choice for
calls in hot loops.
Async calls snapshot numeric views before scheduling worker execution, so later
JavaScript writes and transfers cannot affect the native input. Ordinary arrays
keep their copy semantics. Mutable pointers and fixed array rows still use copies.
To let C fill a reusable typed array, mark a mutable arithmetic pointer writable:
functions: {
fill_samples: { params: { samples: { array: { length: "count", writable: true } } } },
}fill_samples(samples) then lends a matching Float64Array to C, which writes its
elements in place. The view keeps the lending rules above and must not be backed by
an immutable buffer. Writable views are synchronous; a function with an async
twin rejects them. Ordinary arrays still pass a copy, so C's writes to them are discarded.
Rows and fixed dimensions
Counted arrays preserve fixed inner dimensions. A parameter such as
const int (*rows)[3] with array: { length: "count" } takes an array of
three-element tuples; only the outer length supplies count. Numeric and record
rows work through synchronous calls, workers, and callbacks. Inputs use
native copies, and C-allocated results copy every row before free runs. A shape
fact applies to each union leaf through the fixed dimensions.
For fixed character rows, choose a representation in the array fact:
array: { length: "count", element: { string: true } } maps char (*)[N] to
strings. Each row must contain a NUL within its declared bound; input UTF-8 bytes
must leave room for that terminator. { bytes: true } maps each row to an exactly
sized byte view and returns copied Uint8Array values. These formats also apply to
rows within deeper fixed grids, caller output buffers, inout arrays, and callbacks.
Caller-allocated outputs
For caller-allocated array outputs, name both the written count and a capacity input:
functions: {
fill_rows: {
params: {
rows: { out: true, array: { length: "written", capacity: "capacity" } },
},
},
}The capacity stays in the JavaScript signature. The binding allocates aligned storage
and returns only the prefix C reports through written. To return the entire buffer,
use the same integer input for length and capacity. Inout arrays use their original
JavaScript length as the capacity and return a native snapshot without changing the
input. Invalid capacities and written counts throw a RangeError; a zero-capacity
buffer returns an empty array. Caller-owned buffers cannot declare free.
Record elements can transfer independently owned pointer fields using their
handle: { owned: true } field facts, including fixed inner rows and tagged payloads.
Only the written prefix transfers ownership. Invalid written counts trigger cleanup
within the allocated capacity, preserving existing input ownership. Inout handles stay
pinned through later getters, callbacks and worker execution.
Caller buffers of pointer handles require an explicit borrowed choice alongside
array: false transfers independently destructible pointees to their handles;
true leaves them owned by C. Null slots and repeated pointers preserve their values
and identity. Only the written prefix participates in ownership transfer, including
inout buffers. Inout pointer elements must be non-null handles.
Counted text
Use string: { length: "byteCount" } for UTF-8 pointer/count pairs. The count
measures encoded bytes and disappears from input signatures. Counted text permits
embedded NUL bytes; string: true remains NUL-terminated and rejects them on input.
functions: {
copy_text: {
params: { text: { string: { length: "length" } } },
returns: { string: { length: "written" }, free: "text_free" },
},
}A result's length comes from an integer parameter or a count the function writes
through a pointer. Results copy exactly that many UTF-8 bytes, preserving NULs and
replacing invalid sequences. Negative or excessive lengths throw before reading
the buffer; free still releases an owned result when conversion fails. The fact
must describe the C buffer's actual allocation.
Counted text works with ordinary functions, async twins, and callbacks.
In callback.params, length names the count's 1-based parameter position.
Queued notifications copy exactly that many bytes before the C callback returns;
waiting callbacks borrow the source until JavaScript conversion finishes. Negative
or excessive counts reject before a foreign thread copies the buffer. Counted text
cannot accompany bytes or array.
With nullable: true, a null input passes NULL and a zero count; an empty string
passes a non-null pointer and a zero count.
An out string names an integer count pointer that C fills. The output object contains
the decoded text and updated count. An inout string initializes its count from the
encoded input and decodes the updated pointer/count pair, so C may advance a cursor
within that encoding. Both forms support async twins. Input encodings are borrowed:
C must not free or reallocate them. Use free only when the returned pointer owns a
separate C allocation.
Array inputs convert in index order. A conversion error prevents later elements' getters and Proxy traps from running.
Fixed arguments
constantpasses a C macro, enumerator, or integer for a parameter, such asSQLITE_TRANSIENTfor a destructor or0for a reserved argument. The parameter disappears from the JavaScript signature, and the C compiler checks the value's type.sizeofpasses the C compiler's size of a complete struct or union, omitting the integer parameter from JavaScript. Compilation fails if its size does not fit the parameter.endpasses the end of a NUL-terminated string parameter, for APIs such as Dear ImGui'sImFont_CalcWordWrapPositionthat read text as a[text, text_end)range:text_end: { end: "text" }points at the terminator of the same UTF-8 encoding and disappears from the JavaScript signature. A null string passesNULLfor both.
Each fact names the C declaration it describes. A fact that names nothing in the headers, or that does not fit the declaration, fails the build.
Type void pointers
Some functions take a void * and an argument that says what it points at, as Dear
ImGui's igSliderScalar takes an ImGuiDataType. Give such a function a variant per
type. Variants of a function that is not variadic have no args; their params replace
the function's facts for the parameters they name:
functions: {
scalar_step: {
variants: {
scalar_step_double: {
params: { type: { constant: "DATA_DOUBLE" }, value: { pointee: "double", inout: true } },
},
scalar_step_int: {
params: { type: { constant: "DATA_INT" }, value: { pointee: "int", inout: true } },
},
},
},
},pointee names the C type the pointer addresses; the parameter then binds as a pointer
to that type, const for a const void *, so other facts such as inout and array
apply. constant fixes the selector, so each export can only pass the type it names.
Like variadic functions, a function with variants exports only its variants, and
dispatch adds a runtime selector over them.