@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 +28 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/adapters/jsi.cpp +433 -0
- package/adapters/web.cpp +268 -0
- package/crate/Cargo.lock +7 -0
- package/crate/Cargo.toml +7 -0
- package/crate/src/lib.rs +1186 -0
- package/include/crossbind_embind.h +53 -0
- package/package.json +25 -0
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.)
|