pub const RUNTIME_TS: &str = r#"// uniffi_runtime.ts — Generated by uniffi-bindgen-js. DO NOT EDIT.
//
// Shared runtime for UniFFI JavaScript bindings.
// Handles WASM loading, memory management, FFI buffer calling convention,
// and UniFFI binary serialization protocol.
// ---------------------------------------------------------------------------
// WASM Loading
// ---------------------------------------------------------------------------
async function loadWasm(wasmUrl: URL | string): Promise<WebAssembly.WebAssemblyInstantiatedSource> {
if (typeof process !== 'undefined' && process.versions?.node) {
// Node.js: read file and instantiate
const { readFile } = await import('node:fs/promises');
const { fileURLToPath } = await import('node:url');
const path = typeof wasmUrl === 'string' ? wasmUrl : fileURLToPath(wasmUrl);
const bytes = await readFile(path);
return WebAssembly.instantiate(bytes);
} else {
// Browser: prefer streaming compilation (compiles while downloading)
const url = typeof wasmUrl === 'string' ? wasmUrl : wasmUrl.href;
const resp = await fetch(url);
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
// clone() so the original Response body remains available for the fallback
return await WebAssembly.instantiateStreaming(resp.clone());
} catch (_) {
// Fallback if streaming fails (e.g., wrong MIME type)
}
}
return WebAssembly.instantiate(await resp.arrayBuffer());
}
}
// ---------------------------------------------------------------------------
// Cached TextEncoder / TextDecoder
// ---------------------------------------------------------------------------
//
// Safari 16-18 has a bug where a single TextDecoder crashes after decoding
// ~2GB of cumulative data. We track bytes decoded and replace the decoder
// before hitting the limit. See: https://bugs.webkit.org/show_bug.cgi?id=265634
const _textEncoder = new TextEncoder();
const SAFARI_TEXT_DECODER_LIMIT = 1_800_000_000; // ~1.8GB, safely under the ~2GB crash threshold
let _textDecoder = new TextDecoder('utf-8', { fatal: true });
let _textDecoderBytesDecoded = 0;
function _encodeUtf8(s: string): Uint8Array {
return _textEncoder.encode(s);
}
function _decodeUtf8(bytes: Uint8Array): string {
_textDecoderBytesDecoded += bytes.byteLength;
if (_textDecoderBytesDecoded > SAFARI_TEXT_DECODER_LIMIT) {
_textDecoder = new TextDecoder('utf-8', { fatal: true });
_textDecoderBytesDecoded = bytes.byteLength;
}
return _textDecoder.decode(bytes);
}
// ---------------------------------------------------------------------------
// FfiBufferElement Layout
// ---------------------------------------------------------------------------
//
// Each FfiBufferElement is 8 bytes (a union of all FFI primitive types).
// On wasm32, pointers are 4 bytes stored in the lower half (little-endian).
//
// Composite types use multiple elements:
// RustBuffer: 3 elements — [0]=capacity(u64), [1]=len(u64), [2]=data(ptr)
// RustCallStatus: 4 elements — [0]=code(i8), [1..3]=error_buf(RustBuffer)
// Handle: 1 element — u64
// void/(): 0 elements
//
const ELEMENT_SIZE = 8;
// RustBuffer C struct layout in linear memory (wasm32):
// offset 0: capacity (u64, 8 bytes)
// offset 8: len (u64, 8 bytes)
// offset 16: data (*mut u8, 4 bytes padded to 8)
// Total: 24 bytes
const RUST_BUFFER_STRUCT_SIZE = 24;
// RustCallStatus C struct layout in linear memory (wasm32):
// offset 0: code (i8, 1 byte + 7 padding)
// offset 8: error_buf (RustBuffer, 24 bytes)
// Total: 32 bytes
const RUST_CALL_STATUS_STRUCT_SIZE = 32;
// ForeignBytes C struct layout (wasm32):
// offset 0: len (i32, 4 bytes)
// offset 4: data (*const u8, 4 bytes)
// Total: 8 bytes
const FOREIGN_BYTES_STRUCT_SIZE = 8;
// ---------------------------------------------------------------------------
// RustBuffer descriptor (JS-side representation)
// ---------------------------------------------------------------------------
interface RustBufferDescriptor {
capacity: number;
len: number;
dataPtr: number;
}
// ---------------------------------------------------------------------------
// FinalizationRegistry (with polyfill for older environments)
// ---------------------------------------------------------------------------
const _pointerRegistryFactory = typeof FinalizationRegistry !== 'undefined'
? (cb: (held: any) => void) => new FinalizationRegistry(cb)
: (_cb: (held: any) => void) => ({
register(_target: object, _held: any, _token?: object): void {},
unregister(_token: object): void {},
});
// ---------------------------------------------------------------------------
// Panic error class
// ---------------------------------------------------------------------------
/** Error thrown when Rust code panics across the FFI boundary. */
export class UniffiPanicError extends Error {
override readonly name = 'UniffiPanicError' as const;
constructor(message: string) {
super(message);
}
}
// ---------------------------------------------------------------------------
// UniFFI Runtime
// ---------------------------------------------------------------------------
/** Descriptor for a VTable trampoline entry. */
interface VTableEntry {
params: string[];
results: string[];
fn: Function;
}
export class UniffiRuntime {
private _instance: WebAssembly.Instance;
private _memory: WebAssembly.Memory;
private _exports: Record<string, Function>;
private _scratchBase: number;
private _scratchSize: number;
private _scratchOffset: number;
private _namespace: string;
// Persistent memory for VTable structs (never reset, unlike scratch)
private _persistBase: number;
private _persistSize: number;
private _persistOffset: number;
// Indirect function table (for callback trampolines)
private _table: WebAssembly.Table | null;
// Callback handle map: JS objects keyed by u64 handle
private _nextHandle: bigint;
private _handleMap: Map<bigint, unknown>;
// Prevent leaked object handles from becoming permanent memory leaks
private _pointerRegistry: ReturnType<typeof _pointerRegistryFactory>;
// Cached DataView (invalidated when memory grows)
private _cachedDv: DataView | null;
// Async/RustFuture polling infrastructure
private _asyncCallbackIdx: number;
private _asyncPendingResolves: Map<bigint, (result: number) => void>;
private _asyncNextId: bigint;
private constructor(
instance: WebAssembly.Instance,
namespace: string,
persistBase: number,
persistSize: number,
scratchBase: number,
scratchSize: number,
) {
this._instance = instance;
this._memory = instance.exports.memory as WebAssembly.Memory;
this._exports = instance.exports as Record<string, Function>;
this._namespace = namespace;
this._persistBase = persistBase;
this._persistSize = persistSize;
this._persistOffset = 0;
this._scratchBase = scratchBase;
this._scratchSize = scratchSize;
this._scratchOffset = 0;
this._table = (instance.exports.__indirect_function_table as WebAssembly.Table) ?? null;
this._pointerRegistry = _pointerRegistryFactory(({ freeFn, handle }: { freeFn: string; handle: bigint }) => {
try { this.callFree(freeFn, handle); } catch (_) { /* best-effort */ }
});
this._cachedDv = null;
this._nextHandle = 1n;
this._handleMap = new Map();
this._asyncCallbackIdx = -1;
this._asyncPendingResolves = new Map();
this._asyncNextId = 1n;
}
/**
* Load a WASM module and initialize the runtime.
*
* @param wasmUrl URL or path to the .wasm file
* @param namespace The UniFFI namespace (used for FFI function name prefixes)
*/
static async load(wasmUrl: URL | string, namespace: string): Promise<UniffiRuntime> {
const { instance } = await loadWasm(wasmUrl);
const memory = instance.exports.memory as WebAssembly.Memory;
const exports = instance.exports as Record<string, Function>;
// Allocate scratch + persistent memory via Rust's allocator.
// We must NOT use memory.grow() from JS — Rust's allocator doesn't know
// about those pages and may allocate over them.
//
// Bootstrap: grow 1 page for temporary struct space to make the alloc call.
const bootPage = memory.grow(1);
const bootBase = bootPage * 65536;
const retBufPtr = bootBase;
const statusPtr = bootBase + 24;
new Uint8Array(memory.buffer, statusPtr, RUST_CALL_STATUS_STRUCT_SIZE).fill(0);
// Call ffi_{ns}_rustbuffer_alloc(retptr: i32, size: i64, status_ptr: i32)
// Allocate 128KB (64KB persistent + 64KB scratch)
const totalSize = 131072;
const allocFn = exports[`ffi_${namespace}_rustbuffer_alloc`];
if (!allocFn) {
throw new Error(`WASM export not found: ffi_${namespace}_rustbuffer_alloc`);
}
(allocFn as (a: number, b: bigint, c: number) => void)(retBufPtr, BigInt(totalSize), statusPtr);
const bootDv = new DataView(memory.buffer);
const code = bootDv.getInt8(statusPtr);
if (code !== 0) throw new Error('Failed to allocate scratch memory from Rust');
const dataPtr = bootDv.getUint32(retBufPtr + 16, true);
// Zero the entire region
new Uint8Array(memory.buffer, dataPtr, totalSize).fill(0);
const persistBase = dataPtr;
const persistSize = 65536;
const scratchBase = dataPtr + 65536;
const scratchSize = 65536;
return new UniffiRuntime(instance, namespace, persistBase, persistSize, scratchBase, scratchSize);
}
// -----------------------------------------------------------------------
// DataView (cached — invalidated when memory grows)
// -----------------------------------------------------------------------
/** @internal — used by generated callback trampolines */
_dv(): DataView {
const dv = this._cachedDv;
if (dv !== null && dv.buffer === this._memory.buffer) return dv;
this._cachedDv = new DataView(this._memory.buffer);
return this._cachedDv;
}
// -----------------------------------------------------------------------
// Persistent Allocator (for VTable structs that must outlive scratch resets)
// -----------------------------------------------------------------------
private _persistAlloc(bytes: number): number {
const aligned = (this._persistOffset + 7) & ~7;
const ptr = this._persistBase + aligned;
this._persistOffset = aligned + bytes;
if (this._persistOffset > this._persistSize) {
throw new Error('UniffiRuntime: persistent space exhausted');
}
return ptr;
}
// -----------------------------------------------------------------------
// Scratch Allocator
// -----------------------------------------------------------------------
scratchAlloc(bytes: number): number {
const ptr = this._scratchBase + this._scratchOffset;
this._scratchOffset += bytes;
// Align to 8 bytes
this._scratchOffset = (this._scratchOffset + 7) & ~7;
if (this._scratchOffset > this._scratchSize) {
throw new Error('UniffiRuntime: scratch space exhausted');
}
return ptr;
}
scratchReset(): void {
this._scratchOffset = 0;
}
/** Save the current scratch offset (for restoring after a callback). */
scratchSave(): number {
return this._scratchOffset;
}
/** Restore a previously saved scratch offset. */
scratchRestore(offset: number): void {
this._scratchOffset = offset;
}
// -----------------------------------------------------------------------
// Callback Handle Map
// -----------------------------------------------------------------------
/** Insert a JS callback object and return its handle. */
insertCallbackHandle(obj: unknown): bigint {
const h = this._nextHandle++;
this._handleMap.set(h, obj);
return h;
}
/** Get a JS callback object by handle. */
getCallbackHandle(h: bigint): unknown {
return this._handleMap.get(h);
}
/** Remove a callback handle (called by uniffi_free trampoline). */
removeCallbackHandle(h: bigint): void {
this._handleMap.delete(h);
}
/** Clone a callback handle (called by uniffi_clone trampoline). */
cloneCallbackHandle(h: bigint): bigint {
const obj = this._handleMap.get(h);
return this.insertCallbackHandle(obj);
}
// -----------------------------------------------------------------------
// FinalizationRegistry for leaked handles
// -----------------------------------------------------------------------
/**
* Register an object so its handle is freed if the object is garbage-collected
* without an explicit free() call. This is a safety net, not a substitute
* for deterministic cleanup.
*/
registerPointer(obj: object, freeFn: string, handle: bigint): void {
this._pointerRegistry.register(obj, { freeFn, handle }, obj);
}
/** Unregister an object from the FinalizationRegistry (called by free()). */
unregisterPointer(obj: object): void {
this._pointerRegistry.unregister(obj);
}
// -----------------------------------------------------------------------
// FFI Buffer Element Read/Write
// -----------------------------------------------------------------------
writeU8Element(ptr: number, value: number): void {
this._dv().setUint8(ptr, value);
}
readU8Element(ptr: number): number {
return this._dv().getUint8(ptr);
}
writeI8Element(ptr: number, value: number): void {
this._dv().setInt8(ptr, value);
}
readI8Element(ptr: number): number {
return this._dv().getInt8(ptr);
}
writeU16Element(ptr: number, value: number): void {
this._dv().setUint16(ptr, value, true);
}
readU16Element(ptr: number): number {
return this._dv().getUint16(ptr, true);
}
writeI16Element(ptr: number, value: number): void {
this._dv().setInt16(ptr, value, true);
}
readI16Element(ptr: number): number {
return this._dv().getInt16(ptr, true);
}
writeU32Element(ptr: number, value: number): void {
this._dv().setUint32(ptr, value, true);
}
readU32Element(ptr: number): number {
return this._dv().getUint32(ptr, true);
}
writeI32Element(ptr: number, value: number): void {
this._dv().setInt32(ptr, value, true);
}
readI32Element(ptr: number): number {
return this._dv().getInt32(ptr, true);
}
writeU64Element(ptr: number, value: bigint): void {
this._dv().setBigUint64(ptr, value, true);
}
readU64Element(ptr: number): bigint {
return this._dv().getBigUint64(ptr, true);
}
writeI64Element(ptr: number, value: bigint): void {
this._dv().setBigInt64(ptr, value, true);
}
readI64Element(ptr: number): bigint {
return this._dv().getBigInt64(ptr, true);
}
writeF32Element(ptr: number, value: number): void {
this._dv().setFloat32(ptr, value, true);
}
readF32Element(ptr: number): number {
return this._dv().getFloat32(ptr, true);
}
writeF64Element(ptr: number, value: number): void {
this._dv().setFloat64(ptr, value, true);
}
readF64Element(ptr: number): number {
return this._dv().getFloat64(ptr, true);
}
writeBoolElement(ptr: number, value: boolean): void {
this._dv().setInt8(ptr, value ? 1 : 0);
}
readBoolElement(ptr: number): boolean {
return this._dv().getInt8(ptr) !== 0;
}
writeHandleElement(ptr: number, value: bigint): void {
this._dv().setBigUint64(ptr, value, true);
}
readHandleElement(ptr: number): bigint {
return this._dv().getBigUint64(ptr, true);
}
writePtrElement(ptr: number, value: number): void {
this._dv().setUint32(ptr, value, true);
}
readPtrElement(ptr: number): number {
return this._dv().getUint32(ptr, true);
}
// -----------------------------------------------------------------------
// RustBuffer Element Read/Write (3 elements = 24 bytes in FFI buffer)
// -----------------------------------------------------------------------
writeRustBufferElements(ptr: number, rb: RustBufferDescriptor): void {
const dv = this._dv();
dv.setBigUint64(ptr, BigInt(rb.capacity), true); // element 0: capacity
dv.setBigUint64(ptr + 8, BigInt(rb.len), true); // element 1: len
dv.setUint32(ptr + 16, rb.dataPtr, true); // element 2: data ptr
}
readRustBufferElements(ptr: number): RustBufferDescriptor {
const dv = this._dv();
return {
capacity: Number(dv.getBigUint64(ptr, true)),
len: Number(dv.getBigUint64(ptr + 8, true)),
dataPtr: dv.getUint32(ptr + 16, true),
};
}
// -----------------------------------------------------------------------
// RustCallStatus Element Read/Check (4 elements = 32 bytes in FFI buffer)
// -----------------------------------------------------------------------
/**
* Check a RustCallStatus from FFI buffer elements.
* Throws if the call failed (code != 0).
*
* @param ptr Pointer to the first element of the RustCallStatus
* @param liftError Optional function to deserialize error from RustBuffer
*/
checkCallStatus(ptr: number, liftError?: (rb: RustBufferDescriptor) => Error): void {
const code = this._dv().getInt8(ptr);
if (code === 0) return; // SUCCESS
const errorBuf = this.readRustBufferElements(ptr + ELEMENT_SIZE);
if (code === 1) {
// Expected error
if (liftError && errorBuf.len > 0) {
// liftFromBuffer (called inside liftError) already frees the RustBuffer
throw liftError(errorBuf);
}
if (errorBuf.len > 0) this.freeRustBuffer(errorBuf);
throw new Error('UniFFI: expected error without error payload');
} else if (code === 2) {
// Unexpected error (panic) — read as UTF-8 string
let msg = '(unknown)';
if (errorBuf.len > 0) {
msg = this._readUtf8(errorBuf.dataPtr, errorBuf.len);
this.freeRustBuffer(errorBuf);
}
throw new UniffiPanicError(msg);
} else {
if (errorBuf.len > 0) this.freeRustBuffer(errorBuf);
throw new Error(`Unknown FFI call status: ${code}`);
}
}
// -----------------------------------------------------------------------
// RustBuffer C Struct in Linear Memory (for direct C ABI calls)
// -----------------------------------------------------------------------
/** @internal — used by generated callback trampolines */
_writeRustBufferStruct(ptr: number, rb: RustBufferDescriptor): void {
const dv = this._dv();
dv.setBigUint64(ptr, BigInt(rb.capacity), true);
dv.setBigUint64(ptr + 8, BigInt(rb.len), true);
dv.setUint32(ptr + 16, rb.dataPtr, true);
}
/** @internal — used by generated callback trampolines */
_readRustBufferStruct(ptr: number): RustBufferDescriptor {
const dv = this._dv();
return {
capacity: Number(dv.getBigUint64(ptr, true)),
len: Number(dv.getBigUint64(ptr + 8, true)),
dataPtr: dv.getUint32(ptr + 16, true),
};
}
/** @internal — used by generated async code */
_writeRustCallStatusStruct(ptr: number): void {
// Zero the entire struct
const buf = new Uint8Array(this._memory.buffer, ptr, RUST_CALL_STATUS_STRUCT_SIZE);
buf.fill(0);
}
private _checkRustCallStatusStruct(ptr: number): void {
const code = this._dv().getInt8(ptr);
if (code !== 0) {
// Read error_buf RustBuffer from offset 8
const errorBuf = this._readRustBufferStruct(ptr + 8);
let msg = `FFI buffer management call failed with code ${code}`;
if (errorBuf.len > 0) {
msg += `: ${this._readUtf8(errorBuf.dataPtr, errorBuf.len)}`;
this.freeRustBuffer(errorBuf);
}
throw new Error(msg);
}
}
private _writeForeignBytesStruct(ptr: number, len: number, dataPtr: number): void {
const dv = this._dv();
dv.setInt32(ptr, len, true);
dv.setUint32(ptr + 4, dataPtr, true);
}
/** @internal — write success status to a RustCallStatus C struct (in callback trampolines) */
_writeCallStatusSuccess(ptr: number): void {
const dv = this._dv();
dv.setInt8(ptr, 0);
// Zero error_buf
dv.setBigUint64(ptr + 8, 0n, true);
dv.setBigUint64(ptr + 16, 0n, true);
dv.setUint32(ptr + 24, 0, true);
}
/** @internal — write panic status to a RustCallStatus C struct (in callback trampolines) */
_writeCallStatusPanic(ptr: number, error: unknown): void {
const dv = this._dv();
dv.setInt8(ptr, 2); // CALL_PANIC
// Encode error message as RustBuffer in the error_buf field
const msg = error instanceof Error ? error.message : String(error);
const encoded = _encodeUtf8(msg);
try {
const rb = this.rustBufferFromBytes(encoded);
this._writeRustBufferStruct(ptr + 8, rb);
} catch (_) {
// If we can't allocate a buffer for the error message, write empty error_buf
dv.setBigUint64(ptr + 8, 0n, true);
dv.setBigUint64(ptr + 16, 0n, true);
dv.setUint32(ptr + 24, 0, true);
}
}
// -----------------------------------------------------------------------
// FFI Call (uniform (i32, i32) -> void signature)
// -----------------------------------------------------------------------
/**
* Call a UniFFI FFI buffer function.
* All FFI buffer functions have the signature (argPtr: i32, retPtr: i32) -> void.
*/
call(fnName: string, argPtr: number, retPtr: number): void {
const fn_ = this._exports[fnName];
if (!fn_) {
throw new Error(`WASM export not found: ${fnName}`);
}
(fn_ as (a: number, b: number) => void)(argPtr, retPtr);
// Invalidate cached DataView — memory may have grown during the call.
this._cachedDv = null;
}
// -----------------------------------------------------------------------
// Object handle lifecycle (direct C ABI)
// -----------------------------------------------------------------------
/**
* Clone an object handle by calling its clone function (direct C ABI).
*
* This MUST be called before every method call because the FFI scaffolding
* consumes handles: `try_lift(handle)` calls `Arc::from_raw` without
* incrementing the reference count. Without a preceding clone the first
* method call would decrement the ref-count to 0 and destroy the object.
*
* Signature: (handle: i64, status_ptr: i32) -> i64
*/
cloneObjectHandle(fnName: string, handle: bigint): bigint {
const saved = this.scratchSave();
const statusPtr = this.scratchAlloc(RUST_CALL_STATUS_STRUCT_SIZE);
this._writeRustCallStatusStruct(statusPtr);
const fn_ = this._exports[fnName];
if (!fn_) throw new Error(`WASM export not found: ${fnName}`);
const cloned = (fn_ as (a: bigint, b: number) => bigint)(handle, statusPtr);
this._cachedDv = null; // C ABI call may grow memory
this._checkRustCallStatusStruct(statusPtr);
this.scratchRestore(saved);
return cloned;
}
/**
* Free an object by calling its free function (direct C ABI).
* Checks call status and throws on error.
*/
callFree(fnName: string, handle: bigint): void {
const saved = this.scratchSave();
const statusPtr = this.scratchAlloc(RUST_CALL_STATUS_STRUCT_SIZE);
this._writeRustCallStatusStruct(statusPtr);
const fn_ = this._exports[fnName];
if (!fn_) throw new Error(`WASM export not found: ${fnName}`);
(fn_ as (a: bigint, b: number) => void)(handle, statusPtr);
this._cachedDv = null; // C ABI call may grow memory
this._checkRustCallStatusStruct(statusPtr);
this.scratchRestore(saved);
}
// -----------------------------------------------------------------------
// Async / RustFuture Polling
// -----------------------------------------------------------------------
/**
* Lazily initialize the async continuation callback in the WASM function table.
* The callback signature on wasm32 is (i64 data, i32 pollResult) -> void.
* Returns the table index of the callback.
*/
private _ensureAsyncCallback(): number {
if (this._asyncCallbackIdx >= 0) return this._asyncCallbackIdx;
if (!this._table) {
throw new Error(
'Cannot use async FFI: __indirect_function_table not exported. ' +
'Compile with RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table".'
);
}
const self = this;
const callback = new (WebAssembly as any).Function(
{ parameters: ['i64', 'i32'], results: [] },
(data: bigint, pollResult: number) => {
const resolve = self._asyncPendingResolves.get(data);
if (resolve) {
self._asyncPendingResolves.delete(data);
resolve(pollResult);
}
},
);
this._asyncCallbackIdx = this._table.grow(1);
this._table.set(this._asyncCallbackIdx, callback);
return this._asyncCallbackIdx;
}
/**
* Poll a RustFuture until it is ready.
*
* @param futureHandle Handle from the initial async scaffolding call
* @param pollFnName WASM export name for the poll function
*/
async pollToReady(futureHandle: bigint, pollFnName: string): Promise<void> {
const cbIdx = this._ensureAsyncCallback();
const pollFn = this._exports[pollFnName] as
(handle: bigint, callbackIdx: number, data: bigint) => void;
if (!pollFn) throw new Error(`WASM export not found: ${pollFnName}`);
const POLL_READY = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const pollResult = await new Promise<number>((resolve) => {
const id = this._asyncNextId++;
this._asyncPendingResolves.set(id, resolve);
// The callback may fire synchronously (for already-ready futures).
// Promise resolution is always deferred to a microtask, so this is safe.
pollFn(futureHandle, cbIdx, id);
});
this._cachedDv = null; // poll call may grow memory
if (pollResult === POLL_READY) break;
}
}
/**
* Get a WASM export function by name.
* Used by generated async code to call rust_future_complete and rust_future_free.
*/
getExport(name: string): Function {
const fn_ = this._exports[name];
if (!fn_) throw new Error(`WASM export not found: ${name}`);
return fn_;
}
// -----------------------------------------------------------------------
// RustBuffer Management (via C ABI exports)
// -----------------------------------------------------------------------
/**
* Create a RustBuffer from bytes (copies data into Rust-owned memory).
*
* Uses ffi_{ns}_rustbuffer_alloc to get Rust-owned memory, then copies
* data directly into it. This avoids putting data on the scratch allocator,
* which has a fixed 64KB budget.
*/
rustBufferFromBytes(data: Uint8Array): RustBufferDescriptor {
if (data.length === 0) {
return { capacity: 0, len: 0, dataPtr: 0 };
}
// Use scratch only for the small struct args (retBuf + status = 56 bytes)
const saved = this.scratchSave();
const retBufPtr = this.scratchAlloc(RUST_BUFFER_STRUCT_SIZE);
const statusPtr = this.scratchAlloc(RUST_CALL_STATUS_STRUCT_SIZE);
this._writeRustCallStatusStruct(statusPtr);
// Allocate Rust-owned buffer of the right size
const allocFn = this._exports[`ffi_${this._namespace}_rustbuffer_alloc`];
if (!allocFn) {
throw new Error(`WASM export not found: ffi_${this._namespace}_rustbuffer_alloc`);
}
(allocFn as (a: number, b: bigint, c: number) => void)(retBufPtr, BigInt(data.length), statusPtr);
this._cachedDv = null; // C ABI call may grow memory
this._checkRustCallStatusStruct(statusPtr);
const rb = this._readRustBufferStruct(retBufPtr);
this.scratchRestore(saved);
// Copy data directly into the Rust-owned buffer
new Uint8Array(this._memory.buffer, rb.dataPtr, data.length).set(data);
// alloc sets capacity but len=0; update len to reflect actual data
rb.len = data.length;
return rb;
}
/**
* Free a RustBuffer (returns memory to Rust allocator).
* Uses ffi_{ns}_rustbuffer_free C ABI export.
*/
freeRustBuffer(rb: RustBufferDescriptor): void {
if (rb.dataPtr === 0 && rb.len === 0 && rb.capacity === 0) return;
const rbPtr = this.scratchAlloc(RUST_BUFFER_STRUCT_SIZE);
this._writeRustBufferStruct(rbPtr, rb);
const statusPtr = this.scratchAlloc(RUST_CALL_STATUS_STRUCT_SIZE);
this._writeRustCallStatusStruct(statusPtr);
const fn_ = this._exports[`ffi_${this._namespace}_rustbuffer_free`];
if (!fn_) {
throw new Error(`WASM export not found: ffi_${this._namespace}_rustbuffer_free`);
}
(fn_ as (a: number, b: number) => void)(rbPtr, statusPtr);
this._cachedDv = null; // C ABI call may grow memory
this._checkRustCallStatusStruct(statusPtr);
}
// -----------------------------------------------------------------------
// UTF-8 helpers
// -----------------------------------------------------------------------
/** @internal — used by generated callback trampolines */
_readUtf8(ptr: number, len: number): string {
const bytes = new Uint8Array(this._memory.buffer, ptr, len);
return _decodeUtf8(bytes);
}
// -----------------------------------------------------------------------
// UniFFI Binary Serialization — Lower (JS → bytes, big-endian)
// -----------------------------------------------------------------------
//
// These functions serialize JS values into Uint8Array using the UniFFI
// binary format (big-endian). The resulting bytes are then wrapped in a
// RustBuffer via rustBufferFromBytes for passing to FFI functions.
lowerIntoBuffer(write: (w: UniFFIWriter) => void): RustBufferDescriptor {
const writer = new UniFFIWriter();
write(writer);
const bytes = writer.toBytes();
const rb = this.rustBufferFromBytes(bytes);
return rb;
}
// -----------------------------------------------------------------------
// UniFFI Binary Deserialization — Lift (bytes → JS, big-endian)
// -----------------------------------------------------------------------
liftFromBuffer<T>(rb: RustBufferDescriptor, read: (r: UniFFIReader) => T): T {
const bytes = new Uint8Array(this._memory.buffer, rb.dataPtr, rb.len);
// Copy bytes since we'll free the buffer
const copy = new Uint8Array(bytes);
this.freeRustBuffer(rb);
const reader = new UniFFIReader(copy);
return read(reader);
}
// -----------------------------------------------------------------------
// Top-level Lower/Lift: String and Bytes
// -----------------------------------------------------------------------
//
// IMPORTANT: Top-level String/Bytes use FfiConverter::lower which wraps
// raw data in a RustBuffer (no length prefix). This is different from
// the inner serialization format (i32 len + data) used by UniFFIWriter.
/** Lower a string to a RustBuffer containing raw UTF-8 bytes (no length prefix). */
lowerString(value: string): RustBufferDescriptor {
const encoded = _encodeUtf8(value);
return this.rustBufferFromBytes(encoded);
}
/** Lift a RustBuffer containing raw UTF-8 bytes to a string. */
liftString(rb: RustBufferDescriptor): string {
const bytes = new Uint8Array(this._memory.buffer, rb.dataPtr, rb.len);
const copy = new Uint8Array(bytes);
this.freeRustBuffer(rb);
return _decodeUtf8(copy);
}
// -----------------------------------------------------------------------
// Callback Interface VTable Registration
// -----------------------------------------------------------------------
/**
* Register a callback interface VTable with the Rust scaffolding.
*
* Creates WASM-typed trampoline functions via WebAssembly.Function, adds them
* to the indirect function table, writes the VTable struct to persistent memory,
* and calls the Rust VTable init function.
*
* @param name Callback interface name (for debugging)
* @param initFnName WASM export name for the VTable init function
* @param entries Array of VTable entries: [{params, results, fn}, ...]
* Order: [uniffi_free, uniffi_clone, ...methods]
*/
registerCallbackVTable(name: string, initFnName: string, entries: VTableEntry[]): void {
if (!this._table) {
throw new Error(`Cannot register callback VTable for ${name}: __indirect_function_table not exported. Compile with RUSTFLAGS="-C link-arg=--export-table -C link-arg=--growable-table".`);
}
// Create typed WASM functions from JS closures
const wasmFns: any[] = [];
for (const entry of entries) {
const wasmFn = new (WebAssembly as any).Function(
{ parameters: entry.params, results: entry.results },
entry.fn,
);
wasmFns.push(wasmFn);
}
// Grow the indirect function table and add our trampolines
const baseIdx = this._table.grow(entries.length);
for (let i = 0; i < wasmFns.length; i++) {
this._table.set(baseIdx + i, wasmFns[i]);
}
// Write VTable struct to persistent memory (4 bytes per entry = i32 function table index)
const vtablePtr = this._persistAlloc(entries.length * 4);
const dv = this._dv();
for (let i = 0; i < entries.length; i++) {
dv.setUint32(vtablePtr + i * 4, baseIdx + i, true);
}
// Call the Rust VTable init function: (vtable_ptr: i32) -> void
const initFn = this._exports[initFnName];
if (!initFn) {
throw new Error(`WASM export not found: ${initFnName}`);
}
(initFn as (ptr: number) => void)(vtablePtr);
}
}
// ---------------------------------------------------------------------------
// UniFFI Binary Format Writer (big-endian)
// ---------------------------------------------------------------------------
export class UniFFIWriter {
private _buf: DataView;
private _bytes: Uint8Array;
private _pos: number;
private _capacity: number;
constructor(initialCapacity: number = 256) {
this._capacity = initialCapacity;
this._bytes = new Uint8Array(initialCapacity);
this._buf = new DataView(this._bytes.buffer);
this._pos = 0;
}
private _ensureCapacity(additional: number): void {
const needed = this._pos + additional;
if (needed <= this._capacity) return;
let newCap = this._capacity;
while (newCap < needed) newCap *= 2;
const newBytes = new Uint8Array(newCap);
newBytes.set(this._bytes);
this._bytes = newBytes;
this._buf = new DataView(this._bytes.buffer);
this._capacity = newCap;
}
toBytes(): Uint8Array {
return this._bytes.subarray(0, this._pos);
}
writeI8(value: number): void {
this._ensureCapacity(1);
this._buf.setInt8(this._pos, value);
this._pos += 1;
}
writeU8(value: number): void {
this._ensureCapacity(1);
this._buf.setUint8(this._pos, value);
this._pos += 1;
}
writeI16(value: number): void {
this._ensureCapacity(2);
this._buf.setInt16(this._pos, value, false); // big-endian
this._pos += 2;
}
writeU16(value: number): void {
this._ensureCapacity(2);
this._buf.setUint16(this._pos, value, false);
this._pos += 2;
}
writeI32(value: number): void {
this._ensureCapacity(4);
this._buf.setInt32(this._pos, value, false);
this._pos += 4;
}
writeU32(value: number): void {
this._ensureCapacity(4);
this._buf.setUint32(this._pos, value, false);
this._pos += 4;
}
writeI64(value: bigint): void {
this._ensureCapacity(8);
this._buf.setBigInt64(this._pos, value, false);
this._pos += 8;
}
writeU64(value: bigint): void {
this._ensureCapacity(8);
this._buf.setBigUint64(this._pos, value, false);
this._pos += 8;
}
writeF32(value: number): void {
this._ensureCapacity(4);
this._buf.setFloat32(this._pos, value, false);
this._pos += 4;
}
writeF64(value: number): void {
this._ensureCapacity(8);
this._buf.setFloat64(this._pos, value, false);
this._pos += 8;
}
writeBool(value: boolean): void {
this.writeI8(value ? 1 : 0);
}
writeString(value: string): void {
const encoded = _encodeUtf8(value);
this.writeI32(encoded.length);
this._ensureCapacity(encoded.length);
this._bytes.set(encoded, this._pos);
this._pos += encoded.length;
}
writeBytes(value: Uint8Array): void {
this.writeI32(value.length);
this._ensureCapacity(value.length);
this._bytes.set(value, this._pos);
this._pos += value.length;
}
writeDuration(seconds: number): void {
// Duration = u64 secs + u32 nanos (always non-negative)
if (seconds < 0) throw new RangeError('Duration must be non-negative');
let secs = Math.floor(seconds);
let nanos = Math.round((seconds - secs) * 1_000_000_000);
// Math.round can produce 1e9 when the fractional part rounds up
if (nanos >= 1_000_000_000) { secs += 1; nanos -= 1_000_000_000; }
this.writeU64(BigInt(secs));
this.writeU32(nanos);
}
writeTimestamp(date: Date): void {
// UniFFI SystemTime wire format: i64 seconds + u32 nanos
// seconds carries the sign, nanos is the subsecond part of the absolute offset.
// For pre-epoch: -1500ms → seconds=-1, nanos=500_000_000
// (meaning abs offset = 1.5s, subtracted from epoch)
const ms = date.getTime();
const absMs = Math.abs(ms);
const sign = ms >= 0 ? 1 : -1;
const absSecs = Math.floor(absMs / 1000);
const nanos = (absMs % 1000) * 1_000_000;
this.writeI64(BigInt(sign * absSecs));
this.writeU32(nanos);
}
writeOptional<T>(value: T | null | undefined, writeInner: (w: UniFFIWriter, v: T) => void): void {
if (value === null || value === undefined) {
this.writeI8(0);
} else {
this.writeI8(1);
writeInner(this, value);
}
}
writeSequence<T>(values: T[], writeInner: (w: UniFFIWriter, v: T) => void): void {
this.writeI32(values.length);
for (const v of values) {
writeInner(this, v);
}
}
writeMap<K, V>(
map: Map<K, V>,
writeKey: (w: UniFFIWriter, k: K) => void,
writeValue: (w: UniFFIWriter, v: V) => void,
): void {
this.writeI32(map.size);
for (const [k, v] of map) {
writeKey(this, k);
writeValue(this, v);
}
}
}
// ---------------------------------------------------------------------------
// UniFFI Binary Format Reader (big-endian)
// ---------------------------------------------------------------------------
export class UniFFIReader {
private _buf: DataView;
private _pos: number;
private _len: number;
constructor(bytes: Uint8Array) {
this._buf = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
this._pos = 0;
this._len = bytes.byteLength;
}
readI8(): number {
const v = this._buf.getInt8(this._pos);
this._pos += 1;
return v;
}
readU8(): number {
const v = this._buf.getUint8(this._pos);
this._pos += 1;
return v;
}
readI16(): number {
const v = this._buf.getInt16(this._pos, false); // big-endian
this._pos += 2;
return v;
}
readU16(): number {
const v = this._buf.getUint16(this._pos, false);
this._pos += 2;
return v;
}
readI32(): number {
const v = this._buf.getInt32(this._pos, false);
this._pos += 4;
return v;
}
readU32(): number {
const v = this._buf.getUint32(this._pos, false);
this._pos += 4;
return v;
}
readI64(): bigint {
const v = this._buf.getBigInt64(this._pos, false);
this._pos += 8;
return v;
}
readU64(): bigint {
const v = this._buf.getBigUint64(this._pos, false);
this._pos += 8;
return v;
}
readF32(): number {
const v = this._buf.getFloat32(this._pos, false);
this._pos += 4;
return v;
}
readF64(): number {
const v = this._buf.getFloat64(this._pos, false);
this._pos += 8;
return v;
}
readBool(): boolean {
return this.readI8() !== 0;
}
readString(): string {
const len = this.readI32();
const bytes = new Uint8Array(this._buf.buffer, this._buf.byteOffset + this._pos, len);
this._pos += len;
return _decodeUtf8(bytes);
}
readBytes(): Uint8Array {
const len = this.readI32();
const bytes = new Uint8Array(this._buf.buffer, this._buf.byteOffset + this._pos, len);
this._pos += len;
return new Uint8Array(bytes); // copy
}
readDuration(): number {
// Duration = u64 secs + u32 nanos → number (seconds)
const secs = Number(this.readU64());
const nanos = this.readU32();
return secs + nanos / 1_000_000_000;
}
readTimestamp(): Date {
// UniFFI SystemTime wire format: i64 seconds + u32 nanos
// seconds carries the sign, nanos is the subsecond part of the absolute offset.
// Reconstruct: abs_offset = Duration(abs(seconds), nanos), then epoch ± abs_offset.
// Note: sub-second pre-epoch timestamps (0 to -1s) round-trip as positive
// because seconds=0 loses sign information. This matches Rust UniFFI behavior.
const seconds = this.readI64();
const nanos = this.readU32();
const secs = Number(seconds);
const sign = secs >= 0 ? 1 : -1;
const absMs = Math.abs(secs) * 1000 + Math.floor(nanos / 1_000_000);
return new Date(sign * absMs);
}
readOptional<T>(readInner: (r: UniFFIReader) => T): T | null {
const tag = this.readI8();
if (tag === 0) return null;
return readInner(this);
}
readSequence<T>(readInner: (r: UniFFIReader) => T): T[] {
const count = this.readI32();
const result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(readInner(this));
}
return result;
}
readMap<K, V>(
readKey: (r: UniFFIReader) => K,
readValue: (r: UniFFIReader) => V,
): Map<K, V> {
const count = this.readI32();
const result = new Map<K, V>();
for (let i = 0; i < count; i++) {
const k = readKey(this);
const v = readValue(this);
result.set(k, v);
}
return result;
}
}
"#;