@crossbind/core-embind-rust 2.0.0-beta.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # @crossbind/core-embind-rust
2
+
3
+ ## 2.0.0-beta.50
4
+
5
+ ### Patch Changes
6
+
7
+ - Aligned on the 2.0.0-beta.50 baseline. No source change; the version exists so every package in
8
+ the workspace names the same release.
9
+
10
+ ## 2.0.0-beta.33
11
+
12
+ ### Minor Changes
13
+
14
+ - Initial release: the Rust binding layer for crossbind. A pure-Rust producer crate
15
+ (`embind-rs`) speaks a stable flat C ABI (`include/crossbind_embind.h`); per-host
16
+ adapters materialise the registrations in emscripten embind on web
17
+ (`adapters/web.cpp`) and in embind-jsi on iOS/Android (`adapters/jsi.cpp`) —
18
+ the same Rust archive works on every platform.
19
+ - Supported surface: structs with `impl` methods (constructors, `Self` returns),
20
+ `&str`/`&String` parameters and `String` returns, `i32`/`f64`/`bool`,
21
+ `i64`/`u64` as `BigInt`, `Option<T>` in both directions, `Result<T, E>` as JS
22
+ throws, `impl Display` as `toString()`, free functions, `&Class` parameters,
23
+ enums, smart pointers and value objects.
24
+ - The engine does not depend on this package: apps (or the bundler plugins,
25
+ which carry it as a dependency) declare it, and crossbind resolves it from the
26
+ consumer. Bridge crates are generated from plain Rust source — crates.io
27
+ crates via the `cargo:` import scheme, app-local `.rs` files, or whole-crate
28
+ packages via `export.type: 'cargo'`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Buğra Sarı and crossbind contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,344 @@
1
+ # @crossbind/core-embind-rust
2
+
3
+ Rust **producer** for the crossbind embind registration protocol. Write an idiomatic Rust
4
+ struct, register it, and it surfaces in JS as a real class - on web through emscripten's
5
+ embind, on mobile through [`@crossbind/core-embind-jsi`](../core-embind-jsi) - **with no
6
+ generated C++ wrapper and no cbindgen header**. The Rust side talks straight to the
7
+ registration ABI that both hosts already consume.
8
+
9
+ > **Status: research / spike.** The architecture is validated end to end (see the matrix
10
+ > below), but this is not yet a general-purpose package: a single class shape, hand-rolled
11
+ > arities, and registration structures that leak by design. See "Roadmap".
12
+
13
+ ## How it fits
14
+
15
+ embind is a *protocol*: a producer emits `_embind_register_*` calls, and each host has a
16
+ *consumer* that turns them into JS classes (embind-js on web, embind-jsi on mobile). This
17
+ package adds a **second producer** - Rust - next to C++. Consumers are untouched.
18
+
19
+ ```
20
+ PRODUCERS PROTOCOL (flat C-ABI) CONSUMERS (per host)
21
+ C++ (embind macros) ──┐ ┌── web embind-js
22
+ Rust (this package) ──┼── crossbind_embind_register_* ─┤
23
+ │ └── mobile embind-jsi
24
+ adapters/{web,jsi}.cpp
25
+ ```
26
+
27
+ - `crate/` - the Rust runtime (`embind-rs`): a `WireType` trait (i32, f64, bool, String) plus a
28
+ builder API mirroring embind's `class_`. See `demo/` for usage.
29
+ - `include/crossbind_embind.h` - the stable flat C-ABI the crate binds to (raw function
30
+ pointers, 10-param superset). Producers never touch a host's internals.
31
+ - `adapters/web.cpp` - passthrough to emscripten's `_embind_*` (10-param, identical).
32
+ - `adapters/jsi.cpp` - wraps the raw invokers into `jsi::Function` (via
33
+ `createFromHostFunction`) and drops `isNonnullReturn` for embind-jsi's 9-param API.
34
+ Compiled inside the React Native native module; shape-validated by `e2e/jsi-shape-check.cpp`.
35
+
36
+ ## Demo: plain Rust in, JS classes out
37
+
38
+ The user's crate is **plain Rust** - no embind-rs dependency, no macro line, nothing
39
+ crossbind-specific. Exactly like C++ (where the `.h` is enough and the toolchain generates the
40
+ `.i.cpp` bridge), the engine parses the crate's pub surface and generates a **companion bridge
41
+ crate** at `<pkg>/.crossbind/bridge-crate/` that depends on the untouched user crate by path:
42
+
43
+ ```rust
44
+ // demo/src/lib.rs - nothing else needed.
45
+ pub struct RustyCounter { value: i64, log: Vec<String> }
46
+
47
+ impl RustyCounter {
48
+ pub fn new(start: i32) -> Self { /* constructor */ }
49
+ pub fn increment(&mut self, by: i32) -> i32 { /* method */ }
50
+ pub fn add_span(&mut self, from: i32, to: i32) -> i32 { /* JS name: addSpan */ }
51
+ pub fn describe(&mut self, prefix: String) -> String { /* String in AND out */ }
52
+ }
53
+
54
+ pub struct Widget { size: i32 }
55
+ impl Widget {
56
+ pub fn create(size: i32) -> Self { /* factory -> smart_ptr + Widget.create(n) in JS */ }
57
+ pub fn area(&mut self) -> i32 { /* .. */ }
58
+ }
59
+ ```
60
+
61
+ ```ts
62
+ // Typed package import - the exact .h experience, for Rust (React Native / metro):
63
+ import { initNative, RustyCounter, Widget, Mode, RustIntVector } from '@crossbind/embind-rust-demo';
64
+
65
+ await initNative(); // boots once app-wide, then binds this module's exports
66
+ const c = new RustyCounter(10);
67
+ c.increment(5); c.increment(27);
68
+ c.addSpan(2, 10); // +8 - JS names are camelCased automatically
69
+ c.describe('count'); // "count=50 (log: +5 +27 span 2..10)"
70
+ c.setMode(Mode.Fast);
71
+ c.delete();
72
+ const w = Widget.create(6); w.area(); // 36 ; w.delete() frees the Rust Box
73
+ ```
74
+
75
+ How the import works (mirrors the C++ `.h` flow): the metro resolver maps the bare package
76
+ name to the crate's `src/lib.rs`; the transformer parses that surface and emits the same
77
+ proxy-module shape as a `.h` import (`export let` per symbol, bound when an `init()` resolves).
78
+ The package build also emits `dist/js/index.d.ts` (wired via package.json `types`), so the
79
+ import is fully typed in the editor. Booting is guarded app-wide: every proxy module registers
80
+ its bindings on import, so one `init()` from `crossbind` starts the JSI lib once and binds all of
81
+ them. Calling another module's `initNative` is a no-op after the first call.
82
+
83
+ ### App-local .rs files (no package needed)
84
+
85
+ An app can also import its own Rust file, exactly like its own header:
86
+
87
+ ```ts
88
+ import { initNative, Counter } from './native/counter.rs';
89
+ ```
90
+
91
+ On import the toolchain synthesizes a self-contained bridge crate under
92
+ `<project>/.crossbind/rust-bridges/<name>/` (the user file is embedded via `#[path] mod user;` -
93
+ no copy) plus a `counter.rs.d.ts` next to the source for full editor typing. The native builds
94
+ bundle every app-local surface into ONE super staticlib (`rust-bridges/_app_super`) and link it
95
+ into the app lib (iOS: merged into the force_loaded react-native-crossbind.a; Android: whole-archive).
96
+
97
+ App-local surfaces can use UPSTREAM crates directly - the C++-style model where the app writes
98
+ its own thin surface over a linked library and no crossbind package is involved. Declare the crates
99
+ in the app config and just `use` them:
100
+
101
+ ```js
102
+ // crossbind.config.mjs
103
+ export default {
104
+ export: { bindings: { cargoDependencies: { geo: '0.29', wkt: '0.11' } } },
105
+ ...
106
+ };
107
+ ```
108
+
109
+ ```rust
110
+ // src/native/geo_surface.rs - cargo fetches geo/wkt from crates.io, no package needed.
111
+ use geo::{ConvexHull, ...};
112
+ pub struct Hull { ... }
113
+ ```
114
+
115
+ This packageless flow is the canonical model (a Rust user writes their own surface, exactly
116
+ like writing native.h over a linked C library). Packaging a curated surface as a
117
+ `@crossbind/port-*-rs` stays possible for JS-only consumers - it is four files
118
+ (package.json + crossbind.config.mjs + crate/Cargo.toml + crate/src/lib.rs) - but this repo
119
+ ships none.
120
+
121
+ ### Direct crate import (no surface file at all)
122
+
123
+ When an upstream crate's own API already fits the grammar, skip even the surface file: any
124
+ name declared in `cargoDependencies` can be imported through the `cargo:` scheme (the
125
+ node:/npm:/jsr: convention for a non-npm store), and the bridge is generated from the crate's
126
+ OWN multi-file source (types from lib.rs; inherent impls and `impl Display` collected across
127
+ its `mod` files, following the cargo-resolved feature set):
128
+
129
+ ```js
130
+ // crossbind.config.mjs
131
+ export default {
132
+ export: { bindings: { cargoDependencies: { uuid: '{ version = "1", features = ["v4"] }' } } },
133
+ };
134
+ ```
135
+
136
+ ```js
137
+ import { initNative, Uuid } from 'cargo:uuid'; // the crates.io crate, untouched
138
+ await initNative();
139
+ const u = Uuid.newV4(); // v4.rs (feature-gated module)
140
+ `${u}`; // impl Display -> toString()
141
+ Uuid.parseStr('nope'); // Result::Err -> throws
142
+ ```
143
+
144
+ The scheme makes the store explicit, so npm packages of the same name (`uuid`, `semver`)
145
+ never collide with the crate import, the ambient types (`declare module 'cargo:uuid'`) never
146
+ clash with `@types/*`, and an undeclared crate is a hard error instead of a silent npm
147
+ fallthrough.
148
+
149
+ Root re-exports are followed too - `pub use path::{X, Y};` marks module types as surface, and
150
+ `pub use path::*;` makes that module root-like - so re-export-style crates work as well:
151
+ `cargo:semver` yields `Version`/`VersionReq` with `req.matches(version)` (a class-typed
152
+ parameter), and `cargo:regex` yields `new Regex(pat)` (throws on a bad pattern) plus `isMatch`.
153
+
154
+ Out-of-grammar items are skipped with a log line (uuid's `as_u128`, slice APIs, ...), so the
155
+ generated class is the crate's *bindable subset*. The first import runs `cargo metadata`
156
+ (which fetches the crate).
157
+
158
+ ### Rust archive linking rule (why no force_load)
159
+
160
+ Every Rust staticlib bundles its own libstd, so FULLY loading two of them into one binary
161
+ duplicates thousands of std symbols. The rule: **at most one Rust archive is fully loaded**
162
+ (the app super staticlib); every generated package bridge instead exports a
163
+ `crossbind_keep_<libName>()` symbol, is built with `codegen-units = 1` (keep symbol and init-array
164
+ constructor share one object), and is linked lazily with that symbol pinned
165
+ (iOS `-Wl,-u,_crossbind_keep_<lib>`, Android `-Wl,--undefined=crossbind_keep_<lib>`) - the registration
166
+ object is pulled, libstd stays lazy and deduplicates. Manual-bindings crates (no keep symbol)
167
+ still fall back to force_load/whole-archive and are therefore safe only as the app's single
168
+ Rust archive.
169
+
170
+ Web specifics: wasm-ld's `-u` does NOT pull archive members, so the wasm link pins keep symbols
171
+ with `-Wl,--export=crossbind_keep_<lib>` instead; and any Rust archive in the link makes `buildWasm`
172
+ compile `adapters/web.cpp` into the app wasm (it provides the flat `crossbind_embind_*` C-ABI - on
173
+ mobile the RN native module plays that role). Worker mode works for classes too: comlink's
174
+ construct support plus the runtime's embind transfer handlers proxy instances across the
175
+ boundary, so the usage is simply `const c = await new RustyCounter(10); await c.increment(5);`
176
+ - every binding returns a Promise in worker mode, exactly like C++ classes.
177
+
178
+ Generation rules (`crossbind/src/utils/rustBridgeGen.js`, run by `buildCargo` before cargo):
179
+
180
+ | you write | you get |
181
+ |---|---|
182
+ | `#[repr(i32)] pub enum Mode { Slow = 0, .. }` | embind enum |
183
+ | `#[repr(C)] #[derive(..Default, Copy..)] pub struct Point { pub x: i32, .. }` | value object `{x, ..}` |
184
+ | `pub fn new(..) -> Self` | constructor (max 3 args) |
185
+ | other `pub fn ..(..) -> Self` | `smart_ptr` + static factory (max 2 args) |
186
+ | `pub fn m(&mut self / &self, ..) -> R` | method (max 4 args), JS name camelCased |
187
+ | `text: &str` / `text: &String` parameter | `string` on the wire, borrowed at the call site |
188
+ | `i64` / `u64` param, return or field | JS **BigInt** (full 64-bit range, sign-correct) |
189
+ | top-level `pub fn f(..) -> R` | module-level free function (max 4 args), camelCased |
190
+ | `impl Display for C` | `toString()` on the class (so `` `${obj}` `` formats) |
191
+ | `-> Result<T, E>` (ctor, factory, method or free fn; `E: Display`) | Err becomes a **thrown JS exception** |
192
+ | `-> Option<Self>` (factory) | None becomes **JS null** (typed `X \| null`) |
193
+ | `-> Option<i32 / f64 / bool / String>` (method or free fn) | Some -> value, None -> **undefined** (typed `T \| undefined`) |
194
+ | `Option<i32 / f64 / bool / String>` parameter | JS undefined/null -> None (typed `T \| null \| undefined`) |
195
+ | `other: &SomeClass` parameter | pass another bound instance (borrowed for the call) |
196
+ | `export.bindings.vectors: [{ of: 'i32', name: '..' }]` in crossbind.config.mjs | `register_vector` |
197
+
198
+ Anything outside this surface is skipped **with a log line** (e.g. `Option` of an enum or a
199
+ value object, `Option` parameters, `Result<Option<..>>`). The bridge crate dodges the orphan rule with
200
+ `#[repr(transparent)]` newtype wrappers + shim fns, so the user's types never need to
201
+ implement `WireType`. A crate that calls `embind_rs::bindings!` itself opts out of generation
202
+ and is built directly (the manual API below stays fully supported).
203
+
204
+ `constructorN` / `functionN` (N = 0..4) are generated by `macro_rules!`; registration data
205
+ (typeids, argType arrays, signature strings) lives in one process-lifetime arena rather than
206
+ scattered `Box::leak`.
207
+
208
+ ## Test
209
+
210
+ `pnpm e2e:prod` runs two legs (needs Rust + the emscripten target + a local emsdk):
211
+
212
+ 1. **web** - `demo` crate → wasm, linked with `adapters/web.cpp` + `-lembind`, called from
213
+ node. The class is materialized by **unmodified embind-js**.
214
+ 2. **mobile-shape** - same crate compiled native, run against a jsi-*shaped* mock consumer
215
+ that mirrors embind-jsi's real signatures. Validates the adapter's wrapping /
216
+ marshalling / param-dropping.
217
+
218
+ ## Status matrix
219
+
220
+ | Path | State |
221
+ |---|---|
222
+ | web: Rust → flat ABI → embind-js (emsdk 5.0.3 & 6.0.2) | ✅ tested, runs (full demo incl. f64 + string) |
223
+ | mobile: adapter shape vs real embind-jsi signatures | ✅ shape-validated (native mock) |
224
+ | mobile: real Hermes / device smoke (iOS simulator, RN app) | ✅ GREEN 32/32 — ctor, N-arity, bool, enum, string both ways, value object both ways, vector, smart_ptr factory, f64 both ways, delete, app-local .rs, idioms, BigInt, Display, free fns, Option both ways, class params, semver/regex imports |
225
+ | mobile: real Hermes / device smoke (Android emulator, same RN app) | ✅ GREEN 32/32 — same demo, same adapter; cargo dep joins the cmake depends graph, keep-symbol linked; a NEW bare-crate import now links in the FIRST build (rust set busts the CMake configure) |
226
+ | app-local `.rs` import (`./native/counter.rs`, both mobile platforms) | ✅ GREEN — synthesized bridge crate + app super staticlib + typed `counter.rs.d.ts` |
227
+ | typed package import (`import { X } from '@crossbind/embind-rust-demo'`) | ✅ GREEN on iOS, Android AND web (vite) — resolver + proxy module + generated d.ts; web verified by playwright on chromium/firefox/webkit |
228
+ | real-world upstream crate, packageless (`geo` via `cargoDependencies` + app-local surface) | ✅ GREEN — Hull (ConvexHull) on iOS + Android (26/26 smoke) AND in the browser (worker-mode playwright, 3 browsers): the same surface .rs serves all three platforms — a wasm output upstream geo never shipped |
229
+ | Rust idioms: `&str`/`&String` params, `Result` → JS exception, `Option<Self>` → null | ✅ GREEN — e2e (web + jsi-mock legs), worker-mode playwright on 3 browsers, iOS sim + Android emulator (throw message, null, borrowed str all asserted) |
230
+ | i64/u64 → BigInt, `impl Display` → toString(), top-level free functions | ✅ GREEN — same full bar (e2e both legs, 3-browser worker playwright, both devices); u64::MAX round-trips exactly, free `Result` fns throw |
231
+ | DIRECT crate import (`import { Uuid } from 'cargo:uuid'` — untouched crates.io source, multi-file parse, feature-gated modules) | ✅ GREEN — worker-mode playwright on 3 browsers + iOS sim + Android emulator 26/26 (newV4 format, parse roundtrip, Display toString, Err throws) |
232
+ | `Option<i32/f64/bool/String>` returns → value-or-undefined | ✅ GREEN — e2e both legs (mock reads the nullable cell natively), 3-browser worker playwright, both devices |
233
+ | Option params + `&SomeClass` params + re-export following (`semver` matches, `regex` throwing ctor as DIRECT imports) | ✅ GREEN — e2e both legs, 3-browser worker playwright, Android 32/32 + iOS 32/32 — via the explicit cargo: scheme, so npm names never collide |
234
+ | Rust crate → real mobile archives (android/ios arm64) | ✅ compiles; iOS and Android linked+run |
235
+
236
+ ### Wire contract notes (native vs wasm)
237
+
238
+ - Sliced invokers: embind's `getDynCaller(slice=true)` drops the leading target-fn arg for
239
+ constructor/method/static invokers, so the jsi adapter bakes the target at registration and
240
+ prepends it when dispatching. Direct fns (getActualType/dtor/smart_ptr/value_object) map 1:1.
241
+ - `'s'` (std::string): embind-jsi passes strings as `jsi::String`, not a pointer — the jsi adapter
242
+ marshals jsi::String ↔ the crate's `[u32 len][bytes]` buffer. On wasm the wire IS that pointer,
243
+ so the web adapter maps `'s'`→`'p'` in dynCall signatures (`sigForWasm`).
244
+ - `'d'` (f64): on native the crate carries doubles as bit patterns in `u64`
245
+ (`f64::to_bits`/`from_bits`, cfg-gated), keeping every invoker C signature integer-class — so the
246
+ adapter's uint64-slot bounded dispatch is ABI-correct on every native ABI with no libffi and no
247
+ per-arch assembly (Apple ships libffi as private API on iOS, so this also stays App-Store-safe).
248
+ On wasm the wire stays a real f64 (embind-js dyncalls the invoker directly).
249
+ - Errors (`Result::Err`): Rust cannot unwind across the C-ABI, so shims call
250
+ `embind_rs::raise_err(msg)` → the adapter-owned `crossbind_embind_raise_error`. On wasm that import
251
+ is an `EM_JS` that throws a real JS `Error` immediately (the invoker never returns); on native
252
+ the jsi adapter parks the message in a thread-local and throws `jsi::JSError` right after the
253
+ invoker returns its dead sentinel value. `Option<Self>` needs no escape: a null pointee maps to
254
+ JS null by embind's own smart-pointer rules on both consumers.
255
+ - `'j'`/`'u'` (i64/u64): both are 64-bit integer-class slots, so bounded dispatch holds; the jsi
256
+ adapter converts BigInt ↔ raw 64 bits (signed read for 'j', unsigned for 'u' — sign lives in
257
+ the sig char, the registered typeid carries it for embind itself). emscripten's dynCall alphabet
258
+ only has 'j', so `sigForWasm` maps 'u' → 'j' on web.
259
+ - JSON values (`serde_json::Value`): the registered arg/ret type is emval (`typeid(val)` on both
260
+ consumers), so the host marshals a real JS value to a handle; the bridge's `__CrossbindJson` wire
261
+ turns the handle into `[u32 len][bytes]` JSON text through the adapter's host-JSON codec
262
+ (`crossbind_emval_json_to_handle` / `crossbind_emval_handle_to_json`) and serde maps text ↔ `Value`.
263
+ A deep copy by design (JSON semantics: `undefined`/functions cross as null); the handle slot is
264
+ integer-class (`'i'`), so wasm dynCall and native bounded dispatch both hold unchanged.
265
+ - Shared ownership (`Arc<T>`): Arc-as-intrusive - the smart-pointer wire IS the `Arc::into_raw`
266
+ pointer, one strong count per JS handle. `.smart_ptr_shared` registers sharing INTRUSIVE with
267
+ share = `Arc::increment_strong_count` and destructor = `Arc::from_raw` drop; `Arc<T>`
268
+ params/returns cross through per-class `TShared` wrappers (give a count on to_wire, add one
269
+ on from_wire). Shared classes allocate only via Arc factories and keep `&self` methods - the
270
+ generator hard-errors on Box-allocating shapes that would corrupt the shared delete().
271
+ - Live JS values (`embind_rs::JsValue`/`JsFunction`): the registered type is emval, the wire is
272
+ an OWNED handle (params arrive owned - Drop decrefs; returns transfer the count). The crate
273
+ talks to the host through `crossbind_v_*` hooks: EM_JS over `Emval` on web, fork-JS helpers with
274
+ BigInt handles on native (never the fork's wasm-heap emval string plumbing). `JsFunction`
275
+ calls catch the JS throw host-side and surface it as `Err(message)`; handles are
276
+ thread-affine, so retained callbacks stay on the JS thread (`thread_local!` storage).
277
+ Worker-backed runtimes (wasm `mt` default / `useWorker: true`) are out: functions cannot
278
+ cross the worker boundary and identity does not survive structured cloning - the JSON
279
+ value surface is the worker-safe alternative.
280
+ - Free functions ride `_embind_register_function` (argTypes = `[ret, args..]`, no `this`); both
281
+ consumers slice the target like methods, so the jsi adapter bakes + prepends there too.
282
+ - Public names are a single embind namespace shared with every linked C++ package — demos must not
283
+ claim generic names (`VectorInt` collided with a linked package; the demo uses `RustIntVector`).
284
+
285
+ ## Roadmap (toward C++/embind parity)
286
+
287
+ 1. ~~Ownership: replace `Box::leak` with a real registration-lifetime store.~~ ✅ arena.
288
+ 2. ~~Arities: N args.~~ ✅ `macro_rules!` generates `constructorN`/`functionN` (0..4).
289
+ ~~Ergonomics~~ ✅ superseded by toolchain generation: plain Rust in, companion bridge crate
290
+ out (`rustBridgeGen`), so no proc-macro is needed.
291
+ 3. Type universe: ~~primitives (i32, f64, bool, String, void)~~ ✅, ~~smart_ptr / `.create`~~ ✅
292
+ (sharing NONE, identity-over-Box), ~~C-like enums~~ ✅ (`enum_::<E>().value(..)`),
293
+ ~~value objects~~ ✅ (`value_object_::<T>().field::<F>(name, offset_of!(..))`),
294
+ ~~vector~~ ✅ (`register_vector::<T>(name)`), ~~`&str`/`&String` params~~ ✅,
295
+ ~~`Result<T, E>` → JS exception~~ ✅ (`raise_err` + adapter error escape),
296
+ ~~`Option<Self>` factories → null~~ ✅ (null smart pointer, no `register_optional` needed),
297
+ ~~i64/u64 → BigInt~~ ✅ ('j'/'u' slots), ~~free functions~~ ✅ (`embind_rs::fnN` +
298
+ `crossbind_embind_register_function`), ~~`impl Display` → toString()~~ ✅,
299
+ ~~scalar/String `Option` returns~~ ✅ (wasm: emval handle into embind's own optional type;
300
+ native: nullable heap cell + the fork's identity jsiValue type - no fork JS changes),
301
+ ~~`Option` parameters~~ ✅ (wasm: `val::take_ownership` readers; native: adapter-owned
302
+ cells), ~~class-typed parameters~~ ✅ (per-class Ref wrappers over the class pointer wire),
303
+ ~~`serde_json::Value` params/returns~~ ✅ (deep JSON copy through the host JSON codec; web
304
+ e2e + iOS/Android device smoke 36/36 each).
305
+ ~~shared ownership (`Arc<T>`)~~ ✅ (Arc-as-intrusive: `.smart_ptr_shared` registers embind's
306
+ INTRUSIVE policy, share bumps the strong count and delete() drops one; factories return
307
+ `Arc<Self>`, methods stay `&self`; web e2e + iOS/Android device smoke 40/40 each),
308
+ ~~live JS values~~ ✅ (`embind_rs::JsValue`/`JsFunction`: identity-preserving emval handles,
309
+ get/set/prims, callbacks with JS-throw -> `Err` and retained callbacks; web e2e +
310
+ iOS/Android device smoke 45/45 each - the one deliberate embind-rs import in user code).
311
+ Still open: proc-macro ergonomics superseded by toolchain generation.
312
+ 4. ~~Trigger: move registration into an `.init_array` ctor; shrink the C++ shim.~~ ✅
313
+ `embind_rs::bindings! { .. }` registers from a platform init-array constructor (no C++
314
+ trigger); the shim is now just typeid getters + passthroughs. The consumer staticlib is
315
+ whole-archived so the constructor is pulled in.
316
+ 5. ~~Real device smoke via React Native.~~ ✅ iOS simulator AND Android emulator, full demo 23/23
317
+ each (see status matrix). Still open: version discipline, `#[crossbind::bind]` proc-macro ergonomics.
318
+
319
+ ## Distribution (version discipline)
320
+
321
+ The producer crate and both adapters ship in THIS npm package, so the whole binding contract
322
+ (generator JS in `crossbind`, `embind-rs`, `adapters/*.cpp`) moves on a single npm version axis -
323
+ no cross-registry skew. The engine never depends on this package (same direction rule as
324
+ `core-embind-jsi`): the consumer declares it and the engine only resolves it -
325
+
326
+ - React Native apps get it transitively via `@crossbind/plugin-react-native`;
327
+ - web apps via `@crossbind/plugin-vite` / `@crossbind/plugin-rollup`;
328
+ - standalone cargo packages declare `@crossbind/core-embind-rust` in their devDependencies
329
+ (see `demo/package.json`).
330
+
331
+ Generated bridge manifests carry a `# embind-rs from @crossbind/core-embind-rust <version>`
332
+ stamp and a machine-local path into the resolved install; they are per-machine build
333
+ artifacts under `.crossbind/` and are regenerated on every build. A crates.io release of the
334
+ crate is a 1.0-era option for hand-written `bindings!` crates; the toolchain path stays
335
+ npm-resolved either way.
336
+
337
+ ## Building as a crossbind package
338
+
339
+ Rust is a first-class crossbind package via the engine's `buildType: 'cargo'` (no per-project
340
+ script). A package declares `export: { type: 'cargo', libName: ['demo'], crate: '.' }`; crossbind
341
+ runs `cargo build` on the host for each platform's triple (`crossbind build -p ios` →
342
+ aarch64-apple-ios + -sim), stages the `.a` as a normal prebuilt, and its usual pipeline
343
+ produces the xcframework / links it. `demo/crossbind.config.mjs` is the working example. (iOS is
344
+ clean on the host; wasm needs a sourced emsdk, android an NDK / cargo-ndk.)