Skip to content

spike(uniffi): the Rust palette on Expo through uniffi-bindgen-react-native - #76

Open
ivanbanov wants to merge 3 commits into
rust-corefrom
rn-uniffi
Open

ivanbanov wants to merge 3 commits into
rust-corefrom
rn-uniffi

Conversation

@ivanbanov

@ivanbanov ivanbanov commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Stacks on #75 (rust-core). A spike, not a feature: what does a Rust machine cost on React Native over uniffi? Measured: too slow for a hot path, fine for a UI machine. The decision it led to: the TS engine stays the React Native default, and machines written in Rust (e.g. a dropdown) are consumed as wasm on the web and over uniffi on React Native, behind a facade like fromWasm. TODO.md carries the plan; ARCHITECTURE.md now has the target diagram, including the Rust render path (connector, crates/bindings, iced).

What it adds

  • crates/uniffi speaks the push protocol crates/wasm speaks: attach(host) with one notify(state, lo, hi) per change and timers as host calls (start_timer / cancel_timer), next to the pull API. send / send_json push; send_kind / send_payload_json pull. The crate is its own cargo workspace, so it also builds from a git worktree.
  • sandbox/uniffi: the React Native turbo module uniffi-bindgen-react-native (ubrn 0.31.0-6, matching uniffi 0.31) generates from the crate. Only package.json and ubrn.config.yaml are source; pnpm ubrn:ios builds the Rust for the simulator and generates the C++/TS glue, all ignored. Expo autolinks it into a dev build. No Swift, no Kotlin, no Android.
  • sandbox/native/rust-bench.tsx: the Expo sandbox times TS vs Rust pull vs Rust push per event in Hermes, on screen and in the Metro log.

Numbers

Release build, Hermes, iPhone 17 simulator (arm64), median of 3 runs of 50,000 events:

Event TS Rust pull Rust push
execute (no payload) 315 ns 1,596 ns 1,446 ns
query.set (JSON payload) 982 ns 6,365 ns 7,618 ns
bare call (stateIndex()) 1,130 ns
push, notify not reading 1,419 ns
  • The floor is the generated wrapper, not the engine: each method is two FFI calls (an Arc clone, then the call) plus the status object and closures the generated TS allocates per call. Push and pull cost the same; a notify back into JS adds ~0.3 µs.
  • A JSON payload adds ~5 µs: the string is lowered, then parsed by serde.
  • In the browser a Rust machine over wasm runs within ~1.3x of the TS engine. Over uniffi it is 4.5x to 8x slower than the TS engine, so the TS engine stays the React Native default.
  • Fine for a UI machine: ~11,000 payload-less events fit in one 16.67 ms frame, and a dropdown sends a handful. The facade must keep the hot path in JS: a context mirror re-read only on the fields a notify names, typed payloads instead of JSON strings.

Also in here

  • Expo's tsconfig-paths loader is turned off in app.json: it needs the TypeScript JS API, which TypeScript 7 no longer ships, so Metro would not start on this branch. The workspace packages resolve by main, so nothing is lost.
  • babel-preset-expo is an explicit devDependency of the native sandbox: pnpm does not hoist it, and the Release bundle step could not find it.

Not done, on purpose

  • No TS facade over the handle yet (fromUniffi, the twin of fromWasm), no typed payloads, and no start / stop / fire_timer in push mode. These are the next steps, listed in TODO.md.
  • Android, Swift and Kotlin are out of scope.

How to run

pnpm --filter @sandbox/uniffi ubrn:ios
cd sandbox/native && pnpm ios

🤖 Generated with Claude Code

…native

`crates/uniffi` gets the push protocol `crates/wasm` speaks: `attach(host)` with a
`notify(state, lo, hi)` per change and timers as host calls, next to the pull API
(`send`/`send_json` push, `send_kind`/`send_payload_json` pull). The crate is its own
cargo workspace so it builds from a git worktree too.

`sandbox/uniffi` is the turbo module ubrn generates from it (config only; the glue,
the TS bindings and the xcframework are built by `pnpm ubrn:ios` and ignored). The
Expo sandbox depends on it and times TS vs Rust pull vs Rust push per event in
Hermes (`rust-bench.tsx`); the findings and the numbers are in TODO.md. Along the way:
Expo's tsconfig-paths loader is off (it needs the TypeScript JS API, gone in TS 7) and
`babel-preset-expo` is an explicit devDependency (pnpm does not hoist it for the
release bundle step).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
dunky-state-machine Ready Ready Preview Oct 9, 2026 3:48pm UTC

ARCHITECTURE.md draws the two-engine shape end to end: engines synced by spec, machines
with their connectors, the bindings (wasm, uniffi) and the facades that put a Rust
machine behind the TS Machine interface, the shared bindings and the targets, and the
Rust column above the engine (a connector per machine, a shared bindings crate, one iced
target spanning macOS, Windows and Linux). TODO.md records the decision: the TS engine
stays the React Native default, Rust-written machines are consumed over wasm and uniffi
behind a facade, and lists what the facade needs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The intro says what the Rust side changes: the machine is substrate-agnostic and
language-agnostic, with a TS engine and a Rust engine on one spec, and a machine
written in Rust reaching TypeScript through wasm behind the same Machine interface.
A compact diagram, a pointer to the Rust benchmark, and links to the Rust docs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ivanbanov ivanbanov changed the title spike(uniffi): the Rust palette on Expo through uniffi-bindgen-react-native feat(uniffi): the Rust palette on Expo through uniffi-bindgen-react-native Oct 9, 2026
@ivanbanov ivanbanov changed the title feat(uniffi): the Rust palette on Expo through uniffi-bindgen-react-native spike(uniffi): the Rust palette on Expo through uniffi-bindgen-react-native Oct 12, 2026

This branch was successfully deployed

1 active deployment
Preview — 615dfc8a Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant