0.31-4
✨ What's New ✨
🕸️ A new WASM flavour: wasm2
uniffi-bindgen-react-native gains a second WASM target, built on the same player architecture as the Node.js runtime shipped in 0.31.0-3. There is no shim crate to generate and no per-library glue to compile: ubrn build wasm2 compiles your crate once for wasm32-unknown-unknown, and the bindgen reads the UniFFI metadata straight out of that same .wasm, so the second native-only cargo build disappears. The generated wrapper opens no module itself, which leaves it environment-neutral — the same bindings bundle for Node, browsers and React Native alike.
Getting started: your crate needs to be a cdylib, link the new uniffi-runtime-wasm helper crate, and enable the uniffi_core feature that drops Send + Sync on exported objects. ubrn build wasm2 checks all three up front rather than letting cargo fail later. The player runtime is published to npm as @ubjs/wasm.
- The player runtime, plus the
uniffi-runtime-wasmhelper crate consuming cdylibs link (#418). - Post-link processing of the wasm module — staging, a growable function table for callback trampolines, and dead-export stripping — and the crate-manifest queries the build validates (#424).
- The
Wasm2ABI flavour in the bindgen, exportingPLAYER_DEFINITIONSandsetNativeModulefrom the per-module wrapper, with a generatedindex.tsdoing the opening (#425). - The
ubrn build wasm2andubrn generate wasm2subcommands, aliasedweb2(#426). - The full fixture suite now runs against the flavour (#427) in CI (#428). As with the existing
wasmflavour, thefuturesfixture is excluded: itsTimerFuturewakes itself withstd::thread::spawn, which single-threaded wasm32 cannot do.
Other new features
bindings.typescript.forceAsyncinuniffi.tomlgives chosen types and functions anasync/Promisesurface in TypeScript without making them async in Rust, so call sites can be migrated toawaitahead of any real off-main-thread work. Set it totruefor the whole crate, or to a list of names. See the reference — in particular, it moves no work off the main thread (#408).bindings.typescript.strictTypeCheckingdrops the// @ts-nocheckheader from generated files sotscchecks them (#408).- Lifting and lowering now share a cursor instead of creating a
DataViewper value, which speeds up types made of many small reads and writes — large or deeply nested records, recursive enums — on every backend (#413). - The supported
uniffi-rsversion is relaxed from=0.31.0to=0.31, so 0.31.x point releases no longer need a matching release here (#431).
🦊 What's Changed
Memory leaks fixed
Four leaks affecting the N-API backend, present in 0.31.0-3. Upgrading is recommended for anyone making calls in a loop.
- The
RustBufferreturned by an async call was never freed, leaking one buffer per async call returning a string, byte array, record or list. Thewasmflavour was unaffected (#420). - The N-API runtime allocated every
RustBufferargument twice and freed only one of the two, leaking a copy of each buffer passed into Rust — roughly 2.4 KB per frame. Lowered arguments are now library-owned rather than copied (#432). Thank you @1egoman! - Callback trampolines were rebuilt on every call that marshalled a callback argument, instead of once per callback type as intended.
rust_future_pollmarshals its continuation on every poll, so everyawaitof a uniffi async function paid this — each construction leaking a pinnednapi_ref, aCallbackUserData, and a freshThreadsafeFunctionlibuv handle. Trampolines are now cached per callback type (#440). - Callback interfaces implemented in TypeScript leaked their whole serialized return value on each invocation, for any method returning a record, enum,
Vec,Optionor byte array. #432 taught the argument path to adopt library-owned buffers; the callback-return path still copied, orphaning the original.Stringreturns were never affected — their converter producesTextEncodermemory rather than a library allocation (#442).
Alongside those, the runtime now refuses to free a buffer it cannot prove the library owns, rather than falling back to guessing the capacity from byteLength. Generated code always marks the buffers it frees, so this is hardening rather than a user-visible fix (#441).
Fixes
- N-API:
is_js_threadis now answered per callback rather than per process. Calling a library from a Nodeworker_threadsworker — which is what Vitest and Jest do by default — hung on the first call, taking downworker.terminate()andprocess.exit()with it. Every async call was affected, not just calls with a callback of your own (#433, fixing #436). Thank you @stupside! - Escape
*/in generated TypeScript docstrings. TypeScript doesn't allow nested block comments, so a*/in a doc comment closed the docstring early and the rest was parsed as code, failing the build (#438). Thank you @liamiepops! - Export the
./package.jsonsubpath from the root package. The generated Android CMake resolves the package root withrequire.resolve('uniffi-bindgen-react-native/package.json'), which failed withERR_PACKAGE_PATH_NOT_EXPORTEDwhere Node enforces theexportsmap (#407, fixing #404). Thank you @DeyLak! - Android: replace the deprecated
TurboReactPackagewithBaseReactPackagein the generated Kotlin and Java packages (#411, fixing #410). Thank you @ANAMASGARD! - Emit
opt-level = 3rather thanopt-level = "3"in the generated WASM template's release profile (#401). Thank you @MrCreativ3001!
CI
- The React Native compatibility matrix is now date-derived: React Native versions,
create-react-native-libraryversions and runner images are tied together by date, covering the last 12 months of React Native. Per-PR checks run the latest versions only; the historical sweep runs nightly. This fixes the frequent build breakages caused by old React Native versions no longer building onmacos-latest(#419). - Node 24 in CI (#439).
⚠️ Breaking Changes
- The N-API
RustBufferfixes change the contract between the generated bindings and the runtime: regenerate your bindings and upgrade@ubjs/nodetogether. A new runtime with old bindings, or the reverse, will not behave correctly (#420, #432).
Full Changelog: 0.31.0-3...0.31-4