@ubjs/node
v0.31.0-3
Published
Call Rust libraries from Node.js without hand-written glue — the N-API runtime backend for uniffi-bindgen-react-native.
Downloads
1,871
Maintainers
Readme
uniffi-runtime-napi
Call Rust libraries from Node.js — without writing any glue code by hand.
What it does
You have a Rust library compiled as a .dylib or .so. You want to call its functions from JavaScript. Normally, you'd write C bindings, or use a code generator to produce a native addon for each library.
uniffi-runtime-napi takes a different path. It loads your Rust library at runtime, looks up its functions by name, and calls them directly. You describe the function signatures in JavaScript, and uniffi-runtime-napi handles the rest: marshalling values across the boundary, managing memory, and routing callbacks between threads.
Why this approach
UniFFI uses a small, fixed set of FFI types — integers, floats, byte buffers, callbacks, structs. That set doesn't change no matter which Rust crate you're wrapping. So instead of generating a new native addon for each library, you can build one addon that handles all the types, and drive it with data.
This has three benefits:
- Faster development — change a type description in JS, reload. No Rust recompile for the binding layer.
- Simpler packaging — one prebuilt native addon works with any UniFFI library. Ship the
.dyliband the generated JS, nothing else. - Easier to add new targets — writing a bindgen for Bun or Deno means generating JS that talks to the same uniffi-runtime-napi addon.
The approach is inspired by node-ffi-rs, which proved that runtime FFI from Node.js works well in practice. uniffi-runtime-napi narrows the scope to UniFFI's calling conventions, which lets it handle things ffi-rs couldn't — struct-by-value passing, cross-thread callback dispatch, and BigInt for 64-bit values.
This is the FFI backend for uniffi-bindgen-react-native. A code generator produces the JavaScript type descriptions; uniffi-runtime-napi executes them.
How it works
Three steps:
1. Open a library
const mod = UniffiNativeModule.open("/path/to/libfoo.dylib");This calls dlopen under the hood.
2. Register your functions
const nm = mod.register({
symbols: {
rustbuffer_alloc: "uniffi_foo_rustbuffer_alloc",
rustbuffer_free: "uniffi_foo_rustbuffer_free",
rustbuffer_from_bytes: "uniffi_foo_rustbuffer_from_bytes",
},
structs: {},
callbacks: {},
functions: {
uniffi_foo_fn_add: {
args: [FfiType.Int32, FfiType.Int32],
ret: FfiType.Int32,
hasRustCallStatus: true,
},
},
});For each function, uniffi-runtime-napi looks up the symbol, builds a call descriptor using libffi (a C library that calls functions whose signatures aren't known until runtime), and returns a callable JavaScript function.
3. Call them
const status = { code: 0 };
const result = nm.uniffi_foo_fn_add(3, 4, status);
// result === 7Arguments go in as JavaScript values, come out as JavaScript values. Errors from Rust land in the status object.
What crosses the boundary
| JS side | Rust side |
|---------|-----------|
| number | i8–i32, u8–u32, f32, f64 |
| BigInt | i64, u64, object handles |
| Uint8Array | RustBuffer (byte buffers) |
| Function | callback pointers |
| Object | VTable structs (trait implementations) |
Callbacks and threads
Rust code can call back into JavaScript. uniffi-runtime-napi handles two cases:
- Same thread: calls the JS function directly (fast).
- Different thread: serializes the arguments, dispatches to the main thread, blocks until the JS function returns.
This matters for async Rust code that runs work on background threads but needs to call foreign trait methods defined in JS.
Building
# Build the native addon
npm run build
# Run tests (builds test fixtures first)
cd fixtures/test_lib && cargo build && cd ../..
cd fixtures/uniffi-fixture-simple && cargo build && cd ../..
npm testRequires Rust, Node.js, and a C compiler (for libffi).
Project structure
src/
lib.rs — entry point, library open/close
register.rs — parses definitions, builds CIFs, creates JS functions
ffi_type.rs — the type system: every FFI type as an enum
cif.rs — maps types to libffi descriptors
marshal.rs — converts values between JS and C
callback.rs — callback trampolines (same-thread + cross-thread)
structs.rs — VTable struct building for foreign traits
fn_pointer.rs — wraps C function pointers as callable JS functions
library.rs — dlopen/dlsym wrapper
fixtures/ — test libraries written in Rust
tests/ — Node.js test suite
docs/ — API reference and design notesStatus
Early development. Scalars, buffers, callbacks, cross-thread dispatch, VTables, and async Rust futures all work and are tested.
