@crossbind/core-embind-rust 2.0.0-beta.58 → 2.0.0-beta.62

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/README.md CHANGED
@@ -31,7 +31,7 @@ Rust (this package) ──┼── crossbind_embind_register_* ─┤
31
31
  - `adapters/web.cpp` - passthrough to emscripten's `_embind_*` (10-param, identical).
32
32
  - `adapters/jsi.cpp` - wraps the raw invokers into `jsi::Function` (via
33
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`.
34
+ Compiled inside the React Native native module and native Node.js addons.
35
35
 
36
36
  ## Demo: plain Rust in, JS classes out
37
37
 
@@ -181,17 +181,20 @@ Generation rules (`crossbind/src/utils/rustBridgeGen.js`, run by `buildCargo` be
181
181
  |---|---|
182
182
  | `#[repr(i32)] pub enum Mode { Slow = 0, .. }` | embind enum |
183
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 |
184
+ | `pub fn new(..) -> Self` | constructor (max 7 args) |
185
+ | other `pub fn ..(..) -> Self` | `smart_ptr` + static factory (max 6 args) |
186
+ | `pub fn m(&mut self / &self, ..) -> R` | method (max 6 args), JS name camelCased |
187
187
  | `text: &str` / `text: &String` parameter | `string` on the wire, borrowed at the call site |
188
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 |
189
+ | top-level `pub fn f(..) -> R` | module-level free function (max 6 args), camelCased |
190
190
  | `impl Display for C` | `toString()` on the class (so `` `${obj}` `` formats) |
191
191
  | `-> Result<T, E>` (ctor, factory, method or free fn; `E: Display`) | Err becomes a **thrown JS exception** |
192
192
  | `-> Option<Self>` (factory) | None becomes **JS null** (typed `X \| null`) |
193
193
  | `-> Option<i32 / f64 / bool / String>` (method or free fn) | Some -> value, None -> **null** (typed `T \| null`) |
194
194
  | `Option<i32 / f64 / bool / String>` parameter | JS undefined/null -> None (typed `T \| null \| undefined`) |
195
+ | `Option<i64 / u64>` parameter (`JsBigIntOptArg<T>`) | a BigInt or safe-integer Number; undefined/null -> None (typed `bigint \| null \| undefined`) |
196
+ | `Option<&[u8] / &[f64]>` parameter (`JsViewOptArg<T>`) | the typed array, read as for `&[T]`; undefined/null -> None |
197
+ | `NonZeroU64` (and the other `NonZero` integers) parameter, also in `Option` where the integer's `Option` crosses | the integer; 0 is a thrown JS exception before the call |
195
198
  | `other: &SomeClass` parameter | pass another bound instance (borrowed for the call) |
196
199
  | `export.bindings.vectors: [{ of: 'i32', name: '..' }]` in crossbind.config.mjs | `register_vector` |
197
200
 
@@ -207,30 +210,26 @@ scattered `Box::leak`.
207
210
 
208
211
  ## Test
209
212
 
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.
213
+ The conformance kit calls the `demo` crate's classes and functions (the `rust:` checks in
214
+ `e2e/conformance/spec/run.mjs`) on every leg listed in `e2e/conformance/README.md`: through
215
+ `adapters/web.cpp` and **unmodified embind-js** on the wasm legs, through `adapters/jsi.cpp` on
216
+ React Native and native Node.js. `e2e/conformance-rust` checks each Rust construct in more depth.
217
217
 
218
218
  ## Status matrix
219
219
 
220
220
  | Path | State |
221
221
  |---|---|
222
222
  | web: Rust → flat ABI → embind-js (emsdk 5.0.3, 6.0.2 & 6.0.9) | ✅ tested, runs (full demo incl. f64 + string) |
223
- | mobile: adapter shape vs real embind-jsi signatures | ✅ shape-validated (native mock) |
224
223
  | 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
224
  | 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
225
  | app-local `.rs` import (`./native/counter.rs`, both mobile platforms) | ✅ GREEN — synthesized bridge crate + app super staticlib + typed `counter.rs.d.ts` |
227
226
  | 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
227
  | 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 |
228
+ | Rust idioms: `&str`/`&String` params, `Result` → JS exception, `Option<Self>` → null | ✅ GREEN — conformance kit on every leg, worker-mode playwright on 3 browsers, iOS sim + Android emulator (throw message, null, borrowed str all asserted) |
229
+ | i64/u64 → BigInt, `impl Display` → toString(), top-level free functions | ✅ GREEN — same full bar (conformance kit on every leg, 3-browser worker playwright, both devices); u64::MAX round-trips exactly, free `Result` fns throw |
231
230
  | 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-null | ✅ 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 |
231
+ | `Option<i32/f64/bool/String>` returns → value-or-null | ✅ GREEN — conformance kit on every leg, 3-browser worker playwright, both devices |
232
+ | Option params + `&SomeClass` params + re-export following (`semver` matches, `regex` throwing ctor as DIRECT imports) | ✅ GREEN — conformance kit on every leg, 3-browser worker playwright, Android 32/32 + iOS 32/32 — via the explicit cargo: scheme, so npm names never collide |
234
233
  | Rust crate → real mobile archives (android/ios arm64) | ✅ compiles; iOS and Android linked+run |
235
234
 
236
235
  ### Wire contract notes (native vs wasm)
package/adapters/jsi.cpp CHANGED
@@ -29,11 +29,13 @@
29
29
  #include <optional>
30
30
  #include <string>
31
31
  #include <typeinfo>
32
+ #include <unordered_map>
32
33
  #include <unordered_set>
33
34
  // Real embind-jsi declarations (correct namespaces/linkage/signatures): jsRuntime lives in
34
35
  // `emscripten`, the _embind_register_* functions and InitFunc in `emscripten::internal` with C++
35
36
  // linkage and jsi::Function& slots. Including its header avoids any hand-declaration mismatch.
36
37
  #include <emscripten/bind.h>
38
+ #include <crossbind/direct_reads.h>
37
39
  #include "../include/crossbind_embind.h"
38
40
 
39
41
  namespace jsi = facebook::jsi;
@@ -68,6 +70,12 @@ const void* crossbind_tid_optional_int() { static const char t = 0; return &t; }
68
70
  const void* crossbind_tid_optional_double() { static const char t = 0; return &t; }
69
71
  const void* crossbind_tid_optional_bool() { static const char t = 0; return &t; }
70
72
  const void* crossbind_tid_optional_string() { static const char t = 0; return &t; }
73
+ const void* crossbind_tid_optional_int64() { static const char t = 0; return &t; }
74
+ const void* crossbind_tid_optional_uint64() { static const char t = 0; return &t; }
75
+ const void* crossbind_tid_bytes_arg() { static const char t = 0; return &t; }
76
+ const void* crossbind_tid_f64s_arg() { static const char t = 0; return &t; }
77
+ const void* crossbind_tid_bytes_opt_arg() { static const char t = 0; return &t; }
78
+ const void* crossbind_tid_f64s_opt_arg() { static const char t = 0; return &t; }
71
79
 
72
80
  // Idempotent: every Rust archive in the app may ask for the same optional type.
73
81
  void crossbind_embind_register_optional(const void* opt, const void* /*inner: adapter converts*/) {
@@ -129,17 +137,62 @@ static uint8_t* crossbindWireFromString(const std::string& s) {
129
137
  std::memcpy(w + 4, s.data(), s.size());
130
138
  return w;
131
139
  }
132
- static jsi::Function crossbindHelper(const char* name) {
133
- return jsRuntime->global().getPropertyAsFunction(*jsRuntime, name);
140
+ // Helpers and constructors are looked up once per runtime: a global lookup on every call cost more
141
+ // than the call. Each bindings initialisation starts a new cache; the old one is leaked on purpose,
142
+ // because the runtime its functions belong to may already be gone.
143
+ static std::unordered_map<const char*, jsi::Function>* g_helpers = new std::unordered_map<const char*, jsi::Function>();
144
+ struct ViewNames {
145
+ jsi::PropNameID buffer;
146
+ jsi::PropNameID byteOffset;
147
+ jsi::PropNameID length;
148
+ };
149
+ static ViewNames* g_viewNames = nullptr;
150
+ static void resetHelpers() {
151
+ g_helpers = new std::unordered_map<const char*, jsi::Function>();
152
+ g_viewNames = nullptr;
153
+ }
154
+ static InitFunc g_resetHelpersOnInit(resetHelpers);
155
+
156
+ static ViewNames& viewNames(jsi::Runtime& rt) {
157
+ if (g_viewNames == nullptr) {
158
+ g_viewNames = new ViewNames{jsi::PropNameID::forAscii(rt, "buffer"), jsi::PropNameID::forAscii(rt, "byteOffset"),
159
+ jsi::PropNameID::forAscii(rt, "length")};
160
+ }
161
+ return *g_viewNames;
162
+ }
163
+
164
+ // Typed array arguments ('y' bytes, 'z' doubles; 'Y'/'Z' when optional): embind-jsi hands the
165
+ // invoker the view itself, or null for a missing optional one.
166
+ static void registerTypedArgs() {
167
+ auto& rt = *jsRuntime;
168
+ auto registerTypedArg = rt.global().getPropertyAsFunction(rt, "__crossbind_register_typed_arg");
169
+ registerTypedArg.call(rt, jsi::BigInt::fromUint64(rt, (uint64_t)(uintptr_t)crossbind_tid_bytes_arg()), "Uint8Array", false, false);
170
+ registerTypedArg.call(rt, jsi::BigInt::fromUint64(rt, (uint64_t)(uintptr_t)crossbind_tid_f64s_arg()), "Float64Array", true, false);
171
+ registerTypedArg.call(rt, jsi::BigInt::fromUint64(rt, (uint64_t)(uintptr_t)crossbind_tid_bytes_opt_arg()), "Uint8Array | null", false, true);
172
+ registerTypedArg.call(rt, jsi::BigInt::fromUint64(rt, (uint64_t)(uintptr_t)crossbind_tid_f64s_opt_arg()), "Float64Array | null", true, true);
173
+ }
174
+ static InitFunc g_registerTypedArgsOnInit(registerTypedArgs);
175
+
176
+ // `name` must be a string literal: the cache is keyed by its address.
177
+ static jsi::Function& crossbindHelper(const char* name) {
178
+ auto found = g_helpers->find(name);
179
+ if (found == g_helpers->end()) {
180
+ found = g_helpers->emplace(name, jsRuntime->global().getPropertyAsFunction(*jsRuntime, name)).first;
181
+ }
182
+ return found->second;
134
183
  }
135
184
  static thread_local std::string g_cbError;
136
185
  static thread_local bool g_hasCbError = false;
137
186
 
138
187
  extern "C" {
188
+ // Rust values holding JS handles can outlive the runtime (thread-local ones drop inside exit(),
189
+ // after the host cleared jsRuntime); their handles then have nothing left to release.
139
190
  void crossbind_v_ref(size_t h) {
191
+ if (jsRuntime == nullptr) return;
140
192
  crossbindHelper("__emval_incref").call(*jsRuntime, crossbindVHandle(h));
141
193
  }
142
194
  void crossbind_v_unref(size_t h) {
195
+ if (jsRuntime == nullptr) return;
143
196
  crossbindHelper("__emval_decref").call(*jsRuntime, crossbindVHandle(h));
144
197
  }
145
198
  // Fork emval constants: undefined = 1n (init_emval seeds 1..4, reserved = 5).
@@ -202,46 +255,54 @@ static thread_local bool g_hasPendingError = false;
202
255
 
203
256
  static thread_local std::string g_pendingErrorCode;
204
257
 
205
- // Typed arrays on the native runtime: the adapter builds the JS view and hands back its handle,
206
- // one copy in each direction.
207
- // jsi::Object carries no index setter, so the elements go into a jsi::Array first and the
208
- // TypedArray constructor copies them (`new Uint8Array([...])`).
209
- static jsi::Object typedArrayFrom(jsi::Runtime& rt, const char* ctor, jsi::Array values) {
210
- return rt.global().getPropertyAsFunction(rt, ctor)
211
- .callAsConstructor(rt, values).asObject(rt);
258
+ // Typed arrays on the native runtime: one copy in each direction, through the backing store of an
259
+ // array JavaScript allocated, so no ArrayBuffer ever wraps native memory. Copying element by element
260
+ // through jsi cost about 100 ns a byte.
261
+ static size_t typedArrayToJs(jsi::Runtime& rt, const char* ctor, const void* data, size_t length, size_t bytes) {
262
+ auto array = crossbindHelper(ctor).callAsConstructor(rt, (double)length).asObject(rt);
263
+ if (bytes) std::memcpy(array.getPropertyAsObject(rt, "buffer").getArrayBuffer(rt).data(rt), data, bytes);
264
+ return (size_t)crossbindHandleOf(crossbindHelper("__crossbind_v_from_value").call(rt, array));
265
+ }
266
+
267
+ // A view of one of `kinds` is copied straight out of its backing store; false leaves a plain array
268
+ // or another view to the element-by-element path.
269
+ static bool typedArrayFromJs(jsi::Runtime& rt, size_t h, std::initializer_list<const char*> kinds, void* out, size_t bytes) {
270
+ auto value = crossbindHelper("__crossbind_v_value").call(rt, crossbindVHandle(h));
271
+ if (!value.isObject()) return false;
272
+ auto view = value.getObject(rt);
273
+ bool isKind = false;
274
+ for (const char* kind : kinds) isKind = isKind || view.instanceOf(rt, crossbindHelper(kind));
275
+ if (!isKind) return false;
276
+ auto buffer = view.getPropertyAsObject(rt, "buffer");
277
+ if (!buffer.isArrayBuffer(rt)) return false;
278
+ auto offset = (size_t)view.getProperty(rt, "byteOffset").getNumber();
279
+ if (bytes) std::memcpy(out, buffer.getArrayBuffer(rt).data(rt) + offset, bytes);
280
+ return true;
212
281
  }
213
282
 
214
283
  extern "C" size_t crossbind_v_bytes_to_js(const uint8_t* data, unsigned len) {
215
- auto& rt = *jsRuntime;
216
- auto values = jsi::Array(rt, (size_t)len);
217
- for (unsigned i = 0; i < len; ++i) values.setValueAtIndex(rt, i, jsi::Value((double)data[i]));
218
- auto array = typedArrayFrom(rt, "Uint8Array", std::move(values));
219
- return (size_t)crossbindHandleOf(crossbindHelper("__crossbind_v_from_value").call(rt, array));
284
+ return typedArrayToJs(*jsRuntime, "Uint8Array", data, len, len);
220
285
  }
221
286
 
222
287
  extern "C" size_t crossbind_v_f64s_to_js(const double* data, unsigned len) {
223
- auto& rt = *jsRuntime;
224
- auto values = jsi::Array(rt, (size_t)len);
225
- for (unsigned i = 0; i < len; ++i) values.setValueAtIndex(rt, i, jsi::Value(data[i]));
226
- auto array = typedArrayFrom(rt, "Float64Array", std::move(values));
227
- return (size_t)crossbindHandleOf(crossbindHelper("__crossbind_v_from_value").call(rt, array));
288
+ return typedArrayToJs(*jsRuntime, "Float64Array", data, len, (size_t)len * sizeof(double));
228
289
  }
229
290
 
230
291
  extern "C" unsigned crossbind_v_typed_len(size_t h) {
231
- auto& rt = *jsRuntime;
232
- auto value = crossbindHelper("__crossbind_v_get").call(rt, crossbindVHandle(h), jsi::String::createFromAscii(rt, "length"));
233
- auto number = crossbindHelper("__crossbind_v_as_num").call(rt, value);
292
+ auto number = crossbindHelper("__crossbind_v_length").call(*jsRuntime, crossbindVHandle(h));
234
293
  return number.isNumber() ? (unsigned)number.getNumber() : 0;
235
294
  }
236
295
 
237
296
  extern "C" void crossbind_v_bytes_from_js(size_t h, uint8_t* out, unsigned len) {
238
297
  auto& rt = *jsRuntime;
298
+ if (typedArrayFromJs(rt, h, {"Uint8Array", "Uint8ClampedArray", "Int8Array"}, out, len)) return;
239
299
  auto array = crossbindHelper("__crossbind_v_to_array").call(rt, crossbindVHandle(h)).asObject(rt).asArray(rt);
240
300
  for (unsigned i = 0; i < len; ++i) out[i] = (uint8_t)array.getValueAtIndex(rt, i).getNumber();
241
301
  }
242
302
 
243
303
  extern "C" void crossbind_v_f64s_from_js(size_t h, double* out, unsigned len) {
244
304
  auto& rt = *jsRuntime;
305
+ if (typedArrayFromJs(rt, h, {"Float64Array"}, out, (size_t)len * sizeof(double))) return;
245
306
  auto array = crossbindHelper("__crossbind_v_to_array").call(rt, crossbindVHandle(h)).asObject(rt).asArray(rt);
246
307
  for (unsigned i = 0; i < len; ++i) out[i] = array.getValueAtIndex(rt, i).getNumber();
247
308
  }
@@ -280,7 +341,49 @@ static jsi::Value wireStringToJsi(jsi::Runtime& rt, uint64_t w) {
280
341
  return jsi::Value(jsi::String::createFromUtf8(rt, str));
281
342
  }
282
343
 
283
- static uint64_t readArg(jsi::Runtime& rt, const jsi::Value& v, char c) {
344
+ // Where a typed array argument's elements are: the crate's ViewWire.
345
+ struct ViewWire {
346
+ const void* data;
347
+ size_t length;
348
+ };
349
+
350
+ static crossbind::IDirectReads* directReads(jsi::Runtime& rt) {
351
+ return static_cast<crossbind::IDirectReads*>(rt.castInterface(crossbind::IDirectReads::uuid));
352
+ }
353
+
354
+ // embind-jsi's toWireType already made the argument a view of the element kind over an ArrayBuffer,
355
+ // so its elements are lent in place for the call. Reading its properties through the Node-API
356
+ // runtime cost about 440 ns, which the runtime's own interface does in one call.
357
+ static uint64_t readView(jsi::Runtime& rt, const jsi::Value& v, ViewWire& view) {
358
+ auto* reads = directReads(rt);
359
+ void* data = nullptr;
360
+ size_t length = 0;
361
+ if (reads != nullptr && reads->typedArrayElements(v, data, length)) {
362
+ view.data = data;
363
+ view.length = length;
364
+ return (uint64_t)(uintptr_t)&view;
365
+ }
366
+ auto& names = viewNames(rt);
367
+ auto object = v.asObject(rt);
368
+ auto buffer = object.getProperty(rt, names.buffer).asObject(rt);
369
+ if (!buffer.isArrayBuffer(rt)) throw jsi::JSError(rt, "a typed array argument needs an ArrayBuffer");
370
+ view.length = (size_t)object.getProperty(rt, names.length).asNumber();
371
+ view.data = view.length == 0 ? nullptr
372
+ : buffer.getArrayBuffer(rt).data(rt) + (size_t)object.getProperty(rt, names.byteOffset).asNumber();
373
+ return (uint64_t)(uintptr_t)&view;
374
+ }
375
+
376
+ static uint64_t readArg(jsi::Runtime& rt, const jsi::Value& v, char c, ViewWire& view) {
377
+ if (c == 'y' || c == 'z') return readView(rt, v, view);
378
+ if (c == 'Y' || c == 'Z') return v.isUndefined() || v.isNull() ? 0 : readView(rt, v, view);
379
+ // Optional 64-bit args ('J'/'U'): the value a plain 'j'/'u' takes, in a cell this adapter frees.
380
+ if (c == 'J' || c == 'U') {
381
+ if (v.isUndefined() || v.isNull()) return 0;
382
+ uint64_t bits = readArg(rt, v, c == 'J' ? 'j' : 'u', view);
383
+ void* cell = std::malloc(8);
384
+ std::memcpy(cell, &bits, 8);
385
+ return (uint64_t)(uintptr_t)cell;
386
+ }
284
387
  if (c == 's') {
285
388
  // embind-jsi passes std::string args as a jsi::String; build the crate's [u32 len][bytes]
286
389
  // wire buffer here (freed by the caller after dispatch - the crate's from_wire only copies).
@@ -303,7 +406,12 @@ static uint64_t readArg(jsi::Runtime& rt, const jsi::Value& v, char c) {
303
406
  // i64/u64 ('j'/'u') arrive as JS BigInt (embind bigint types), Number accepted as a courtesy;
304
407
  // both directions are raw 64-bit slots, so the unsigned bit copy is sign-correct either way.
305
408
  if (c == 'j' || c == 'u') {
306
- if (v.isBigInt()) return v.getBigInt(rt).getUint64(rt);
409
+ if (v.isBigInt()) {
410
+ auto* reads = directReads(rt);
411
+ uint64_t bits = 0;
412
+ bool lossless = false;
413
+ return reads != nullptr && reads->bigIntToUint64(v, bits, lossless) ? bits : v.getBigInt(rt).getUint64(rt);
414
+ }
307
415
  // A Number is accepted only while it is an exact integer JS can represent; anything
308
416
  // else would be truncated silently, and a 64-bit slot deserves the exact value.
309
417
  double d = v.isNumber() ? v.getNumber() : 0;
@@ -316,7 +424,7 @@ static uint64_t readArg(jsi::Runtime& rt, const jsi::Value& v, char c) {
316
424
  // adapter owns and frees after dispatch ('S' carries a plain string wire).
317
425
  if (c == 'I' || c == 'D' || c == 'B' || c == 'S') {
318
426
  if (v.isUndefined() || v.isNull()) return 0;
319
- if (c == 'S') return readArg(rt, v, 's');
427
+ if (c == 'S') return readArg(rt, v, 's', view);
320
428
  if (c == 'I') { int32_t x = (int32_t)v.getNumber(); void* p = std::malloc(4); std::memcpy(p, &x, 4); return (uint64_t)(uintptr_t)p; }
321
429
  if (c == 'D') { double x = v.getNumber(); void* p = std::malloc(8); std::memcpy(p, &x, 8); return (uint64_t)(uintptr_t)p; }
322
430
  uint8_t b = v.getBool() ? 1 : 0; void* p = std::malloc(1); std::memcpy(p, &b, 1); return (uint64_t)(uintptr_t)p;
@@ -344,10 +452,11 @@ static jsi::Value callInvoker(jsi::Runtime& rt, const std::string& sig, void* in
344
452
  // (embind sliced it off), so the jsi args a[0..] fill sig[2..]; otherwise a[0..] fill sig[1..].
345
453
  // Widest call: a baked target + `this` + six declared arguments.
346
454
  uint64_t s[8] = {0, 0, 0, 0, 0, 0, 0, 0};
455
+ ViewWire views[8];
347
456
  int base = 0;
348
457
  if (prepend) { s[0] = ctx; base = 1; }
349
458
  // A call with fewer JS args than the signature must not read past the argument array.
350
- for (int j = base; j < total && j < 8; ++j) s[j] = (size_t)(j - base) < count ? readArg(rt, a[j - base], sig[1 + j]) : 0;
459
+ for (int j = base; j < total && j < 8; ++j) s[j] = (size_t)(j - base) < count ? readArg(rt, a[j - base], sig[1 + j], views[j]) : 0;
351
460
 
352
461
  uint64_t r = 0;
353
462
  if (ret == 'v') {
@@ -380,7 +489,7 @@ static jsi::Value callInvoker(jsi::Runtime& rt, const std::string& sig, void* in
380
489
  // now that the crate has copied.
381
490
  for (int j = base; j < total && j < 8; ++j) {
382
491
  const char cj = sig[1 + j];
383
- if (cj == 's' || cj == 'S' || cj == 'I' || cj == 'D' || cj == 'B') std::free((void*)(uintptr_t)s[j]);
492
+ if (cj == 's' || cj == 'S' || cj == 'I' || cj == 'D' || cj == 'B' || cj == 'J' || cj == 'U') std::free((void*)(uintptr_t)s[j]);
384
493
  }
385
494
 
386
495
  // A shim raised (Result::Err): drop the sentinel return and surface a real JS exception.
package/adapters/web.cpp CHANGED
@@ -72,9 +72,57 @@ const void* crossbind_tid_optional_int() { static const char t = 0; return &t; }
72
72
  const void* crossbind_tid_optional_double() { static const char t = 0; return &t; }
73
73
  const void* crossbind_tid_optional_bool() { static const char t = 0; return &t; }
74
74
  const void* crossbind_tid_optional_string() { static const char t = 0; return &t; }
75
+ const void* crossbind_tid_optional_int64() { static const char t = 0; return &t; }
76
+ const void* crossbind_tid_optional_uint64() { static const char t = 0; return &t; }
77
+ const void* crossbind_tid_bytes_arg() { static const char t = 0; return &t; }
78
+ const void* crossbind_tid_f64s_arg() { static const char t = 0; return &t; }
79
+ const void* crossbind_tid_bytes_opt_arg() { static const char t = 0; return &t; }
80
+ const void* crossbind_tid_f64s_opt_arg() { static const char t = 0; return &t; }
75
81
 
76
82
  }
77
83
 
84
+ // Typed array arguments: the view is copied once into wasm memory behind a { data, length } header
85
+ // (the crate's ViewWire) and freed after the call. Another typed array or a plain array is converted
86
+ // element by element first. An optional one passes null or undefined as a null pointer.
87
+ EM_JS_DEPS(crossbind_typed_arg_deps, "$registerType,$throwBindingError,$embindRepr");
88
+ EM_JS(void, crossbind_register_typed_arg, (const void* rawType, const char* name, int isDoubles, int isOptional), {
89
+ const typeName = UTF8ToString(name);
90
+ const View = isDoubles ? Float64Array : Uint8Array;
91
+ const isLent = isDoubles
92
+ ? (value) => value instanceof Float64Array
93
+ : (value) => value instanceof Uint8Array || value instanceof Uint8ClampedArray || value instanceof Int8Array;
94
+ registerType(rawType, {
95
+ name: typeName,
96
+ toWireType(destructors, value) {
97
+ if (isOptional && (value === null || value === undefined)) return 0;
98
+ if (!isLent(value)) {
99
+ if (!Array.isArray(value) && !(ArrayBuffer.isView(value) && !(value instanceof DataView))) {
100
+ throwBindingError(`Cannot pass ${embindRepr(value)} as a ${typeName}`);
101
+ }
102
+ value = View.from(value);
103
+ }
104
+ const base = _malloc(8 + value.byteLength);
105
+ if (!base) throwBindingError(`Cannot copy ${value.byteLength} bytes into wasm memory`);
106
+ HEAPU32[base >> 2] = base + 8;
107
+ HEAPU32[(base >> 2) + 1] = value.length;
108
+ if (isDoubles) HEAPF64.set(value, (base + 8) >> 3);
109
+ else HEAPU8.set(value, base + 8);
110
+ if (destructors !== null) destructors.push(_free, base);
111
+ return base;
112
+ },
113
+ destructorFunction(ptr) {
114
+ _free(ptr);
115
+ },
116
+ });
117
+ });
118
+
119
+ EMSCRIPTEN_BINDINGS(crossbind_typed_args) {
120
+ crossbind_register_typed_arg(crossbind_tid_bytes_arg(), "Uint8Array", 0, 0);
121
+ crossbind_register_typed_arg(crossbind_tid_f64s_arg(), "Float64Array", 1, 0);
122
+ crossbind_register_typed_arg(crossbind_tid_bytes_opt_arg(), "Uint8Array | null", 0, 1);
123
+ crossbind_register_typed_arg(crossbind_tid_f64s_opt_arg(), "Float64Array | null", 1, 1);
124
+ }
125
+
78
126
  // Optional wire on wasm is an EM_VAL handle (None = the reserved null constant from
79
127
  // emscripten's own header: a missing value is null on every runtime, like C++ nullptr). Some-values become handles through Emval.toHandle; the registered
80
128
  // optional type's fromWireType does toValue + decref, so the refcount stays balanced.
@@ -118,6 +166,14 @@ int crossbind_emval_opt_bool(void* h, uint8_t* out) {
118
166
  *out = v.as<bool>() ? 1 : 0;
119
167
  return 1;
120
168
  }
169
+ // A 64-bit optional reads through the registered bigint type, so it takes what a plain 64-bit
170
+ // parameter takes: a BigInt or a safe-integer Number.
171
+ int crossbind_emval_opt_bits64(void* h, uint64_t* out, int isSigned) {
172
+ emscripten::val v = emscripten::val::take_ownership((emscripten::EM_VAL)h);
173
+ if (v.isUndefined() || v.isNull()) return 0;
174
+ *out = isSigned ? (uint64_t)v.as<int64_t>() : v.as<uint64_t>();
175
+ return 1;
176
+ }
121
177
  int crossbind_emval_opt_string(void* h, uint8_t** out) {
122
178
  emscripten::val v = emscripten::val::take_ownership((emscripten::EM_VAL)h);
123
179
  if (v.isUndefined() || v.isNull()) return 0;
package/crate/src/lib.rs CHANGED
@@ -418,6 +418,65 @@ option_wire! {
418
418
  String, crossbind_tid_optional_string, 'S';
419
419
  }
420
420
 
421
+ extern "C" {
422
+ fn crossbind_tid_optional_int64() -> *const c_void;
423
+ fn crossbind_tid_optional_uint64() -> *const c_void;
424
+ }
425
+ #[cfg(target_family = "wasm")]
426
+ extern "C" {
427
+ fn crossbind_emval_opt_bits64(handle: usize, out: *mut u64, signed: i32) -> i32;
428
+ }
429
+
430
+ mod sealed {
431
+ pub trait Sealed {}
432
+ impl Sealed for i64 {}
433
+ impl Sealed for u64 {}
434
+ }
435
+
436
+ #[doc(hidden)]
437
+ pub trait BigIntElement: sealed::Sealed + Copy + 'static {
438
+ const SIGNED: bool;
439
+ }
440
+ impl BigIntElement for i64 { const SIGNED: bool = true; }
441
+ impl BigIntElement for u64 { const SIGNED: bool = false; }
442
+
443
+ /// An `Option<i64>` / `Option<u64>` parameter: a BigInt or a safe-integer Number, null or undefined
444
+ /// for `None`. Generic, so it is compiled into the crate that uses it and a cargo archive built
445
+ /// before it existed still links next to one built after.
446
+ pub struct JsBigIntOptArg<T>(pub Option<T>);
447
+
448
+ impl<T: BigIntElement> WireType for JsBigIntOptArg<T> {
449
+ #[cfg(target_family = "wasm")]
450
+ type Wire = usize;
451
+ #[cfg(not(target_family = "wasm"))]
452
+ type Wire = *mut u8;
453
+ #[cfg(target_family = "wasm")]
454
+ const SIG: char = 'i';
455
+ #[cfg(not(target_family = "wasm"))]
456
+ const SIG: char = if T::SIGNED { 'J' } else { 'U' };
457
+ fn tid() -> *const c_void {
458
+ unsafe { if T::SIGNED { crossbind_tid_optional_int64() } else { crossbind_tid_optional_uint64() } }
459
+ }
460
+ #[cfg(target_family = "wasm")]
461
+ fn from_wire(w: usize) -> Self {
462
+ let mut bits: u64 = 0;
463
+ if unsafe { crossbind_emval_opt_bits64(w, &mut bits, T::SIGNED as i32) } == 0 { return JsBigIntOptArg(None); }
464
+ JsBigIntOptArg(Some(unsafe { core::mem::transmute_copy::<u64, T>(&bits) }))
465
+ }
466
+ #[cfg(not(target_family = "wasm"))]
467
+ fn from_wire(w: *mut u8) -> Self {
468
+ if w.is_null() { return JsBigIntOptArg(None); }
469
+ JsBigIntOptArg(Some(unsafe { core::ptr::read_unaligned(w as *const T) }))
470
+ }
471
+ fn to_wire(self) -> Self::Wire { unreachable!("an optional 64-bit integer is only a parameter") }
472
+ }
473
+
474
+ /// Registers the optional type a `JsBigIntOptArg<T>` parameter names (adapter-idempotent).
475
+ pub fn register_optional_bigint<T: BigIntElement>() {
476
+ let inner = unsafe { if T::SIGNED { crossbind_tid_int64() } else { crossbind_tid_uint64() } };
477
+ unsafe { crossbind_embind_register_optional(<JsBigIntOptArg<T> as WireType>::tid(), inner) };
478
+ }
479
+
421
480
  // wasm Some-path: hand the inner's in-memory repr to _emval_take_value. JS copies the value
422
481
  // synchronously, so stack temporaries suffice; the string wire is freed by embind itself.
423
482
  #[cfg(target_family = "wasm")]
@@ -879,6 +938,102 @@ impl WireType for JsF64s {
879
938
  impl ErrSentinel for JsBytes { fn err_sentinel() -> Self { JsBytes(Vec::new()) } }
880
939
  impl ErrSentinel for JsF64s { fn err_sentinel() -> Self { JsF64s(Vec::new()) } }
881
940
 
941
+ extern "C" {
942
+ fn crossbind_tid_bytes_arg() -> *const c_void;
943
+ fn crossbind_tid_f64s_arg() -> *const c_void;
944
+ }
945
+
946
+ // A typed array argument reaches the adapter as the JS view itself, and the adapter says where its
947
+ // elements are: in the view's own backing store on the native runtime, in a copy in wasm memory on
948
+ // the web. Either stays valid until the call returns.
949
+ #[repr(C)]
950
+ struct ViewWire {
951
+ data: *const c_void,
952
+ length: usize,
953
+ }
954
+
955
+ #[doc(hidden)]
956
+ pub trait ViewElement: Copy + 'static {
957
+ const NATIVE_SIG: char;
958
+ fn tid() -> *const c_void;
959
+ }
960
+
961
+ impl ViewElement for u8 {
962
+ const NATIVE_SIG: char = 'y';
963
+ fn tid() -> *const c_void { unsafe { crossbind_tid_bytes_arg() } }
964
+ }
965
+
966
+ impl ViewElement for f64 {
967
+ const NATIVE_SIG: char = 'z';
968
+ fn tid() -> *const c_void { unsafe { crossbind_tid_f64s_arg() } }
969
+ }
970
+
971
+ /// A byte or double parameter: `&[T]` reads the caller's view in place, `Vec<T>` copies it once.
972
+ pub struct JsViewArg<T> {
973
+ data: *const T,
974
+ length: usize,
975
+ }
976
+
977
+ pub type JsBytesArg = JsViewArg<u8>;
978
+ pub type JsF64sArg = JsViewArg<f64>;
979
+
980
+ impl<T: ViewElement> JsViewArg<T> {
981
+ pub fn as_slice(&self) -> &[T] {
982
+ if self.length == 0 { &[] } else { unsafe { std::slice::from_raw_parts(self.data, self.length) } }
983
+ }
984
+ pub fn to_vec(&self) -> Vec<T> {
985
+ self.as_slice().to_vec()
986
+ }
987
+ }
988
+
989
+ impl<T: ViewElement> WireType for JsViewArg<T> {
990
+ type Wire = usize;
991
+ #[cfg(target_family = "wasm")]
992
+ const SIG: char = 'p';
993
+ #[cfg(not(target_family = "wasm"))]
994
+ const SIG: char = T::NATIVE_SIG;
995
+ fn tid() -> *const c_void { T::tid() }
996
+ fn from_wire(w: usize) -> Self {
997
+ if w == 0 { return JsViewArg { data: core::ptr::null(), length: 0 }; }
998
+ let view = unsafe { &*(w as *const ViewWire) };
999
+ JsViewArg { data: view.data as *const T, length: view.length }
1000
+ }
1001
+ fn to_wire(self) -> usize { unreachable!("a typed array argument is never returned") }
1002
+ }
1003
+
1004
+ extern "C" {
1005
+ fn crossbind_tid_bytes_opt_arg() -> *const c_void;
1006
+ fn crossbind_tid_f64s_opt_arg() -> *const c_void;
1007
+ }
1008
+
1009
+ /// An `Option<&[T]>` parameter: null or undefined is `None`, a view is read as for `JsViewArg`.
1010
+ /// Generic like `JsBigIntOptArg`, for the same reason.
1011
+ pub struct JsViewOptArg<T>(Option<JsViewArg<T>>);
1012
+
1013
+ pub type JsBytesOptArg = JsViewOptArg<u8>;
1014
+ pub type JsF64sOptArg = JsViewOptArg<f64>;
1015
+
1016
+ impl<T: ViewElement> JsViewOptArg<T> {
1017
+ pub fn as_slice(&self) -> Option<&[T]> {
1018
+ self.0.as_ref().map(|view| view.as_slice())
1019
+ }
1020
+ }
1021
+
1022
+ impl<T: ViewElement> WireType for JsViewOptArg<T> {
1023
+ type Wire = usize;
1024
+ #[cfg(target_family = "wasm")]
1025
+ const SIG: char = 'p';
1026
+ #[cfg(not(target_family = "wasm"))]
1027
+ const SIG: char = if T::NATIVE_SIG == 'y' { 'Y' } else { 'Z' };
1028
+ fn tid() -> *const c_void {
1029
+ unsafe { if T::NATIVE_SIG == 'y' { crossbind_tid_bytes_opt_arg() } else { crossbind_tid_f64s_opt_arg() } }
1030
+ }
1031
+ fn from_wire(w: usize) -> Self {
1032
+ JsViewOptArg(if w == 0 { None } else { Some(JsViewArg::from_wire(w)) })
1033
+ }
1034
+ fn to_wire(self) -> usize { unreachable!("a typed array argument is never returned") }
1035
+ }
1036
+
882
1037
  impl Drop for JsValue {
883
1038
  fn drop(&mut self) {
884
1039
  unsafe { crossbind_v_unref(self.handle) };
@@ -1287,6 +1442,10 @@ arc_factories! {
1287
1442
  create_arc0 / arc_factory_invoker0 : ;
1288
1443
  create_arc1 / arc_factory_invoker1 : A0;
1289
1444
  create_arc2 / arc_factory_invoker2 : A0, A1;
1445
+ create_arc3 / arc_factory_invoker3 : A0, A1, A2;
1446
+ create_arc4 / arc_factory_invoker4 : A0, A1, A2, A3;
1447
+ create_arc5 / arc_factory_invoker5 : A0, A1, A2, A3, A4;
1448
+ create_arc6 / arc_factory_invoker6 : A0, A1, A2, A3, A4, A5;
1290
1449
  }
1291
1450
 
1292
1451
  // N-arity constructors: one monomorphized invoker + builder method per argument count.
@@ -1380,6 +1539,7 @@ constructors_ptr! {
1380
1539
  constructor_ptr4 / ctor_ptr_invoker4 : A0, A1, A2, A3;
1381
1540
  constructor_ptr5 / ctor_ptr_invoker5 : A0, A1, A2, A3, A4;
1382
1541
  constructor_ptr6 / ctor_ptr_invoker6 : A0, A1, A2, A3, A4, A5;
1542
+ constructor_ptr7 / ctor_ptr_invoker7 : A0, A1, A2, A3, A4, A5, A6;
1383
1543
  }
1384
1544
 
1385
1545
  // Raw-pointer factory variants: `Result<Self, E>` raises on Err, `Option<Self>` returns null
@@ -1522,3 +1682,45 @@ free_functions! {
1522
1682
  fn5 / free_invoker5 : A0, A1, A2, A3, A4;
1523
1683
  fn6 / free_invoker6 : A0, A1, A2, A3, A4, A5;
1524
1684
  }
1685
+
1686
+ // N-arity STATIC functions (`Class.name(..)` in JS): a free function registered on its class,
1687
+ // so it rides the free-function invokers. Generic only, so a cargo archive built before these
1688
+ // existed still links next to one built after.
1689
+ macro_rules! static_functions {
1690
+ ($( $method:ident / $invoker:ident : $($arg:ident),* ; )*) => {
1691
+ impl<T: 'static> ClassBuilder<T> {
1692
+ $(
1693
+ pub fn $method<$($arg: WireType,)* R: WireType>(self, name: &str, f: fn($($arg),*) -> R) -> Self {
1694
+ let mut sig = String::new();
1695
+ sig.push(R::SIG); sig.push('p');
1696
+ let mut args = vec![R::tid()];
1697
+ $( sig.push(<$arg as WireType>::SIG); args.push(<$arg as WireType>::tid()); )*
1698
+ let (argc, arg_ptr, name_ptr, sig_ptr);
1699
+ {
1700
+ let mut r = registry().lock().unwrap();
1701
+ argc = args.len() as u32;
1702
+ arg_ptr = r.argtypes(args);
1703
+ name_ptr = r.cstr(name);
1704
+ sig_ptr = r.cstr(&sig);
1705
+ }
1706
+ unsafe {
1707
+ crossbind_embind_register_class_class_function(
1708
+ self.cls, name_ptr, argc, arg_ptr, sig_ptr,
1709
+ $invoker::<$($arg,)* R> as *const () as usize, f as *const () as usize, false, false,
1710
+ );
1711
+ }
1712
+ self
1713
+ }
1714
+ )*
1715
+ }
1716
+ };
1717
+ }
1718
+ static_functions! {
1719
+ static_function0 / free_invoker0 : ;
1720
+ static_function1 / free_invoker1 : A0;
1721
+ static_function2 / free_invoker2 : A0, A1;
1722
+ static_function3 / free_invoker3 : A0, A1, A2;
1723
+ static_function4 / free_invoker4 : A0, A1, A2, A3;
1724
+ static_function5 / free_invoker5 : A0, A1, A2, A3, A4;
1725
+ static_function6 / free_invoker6 : A0, A1, A2, A3, A4, A5;
1726
+ }
@@ -33,6 +33,8 @@ CrossbindTid crossbind_tid_optional_int(void);
33
33
  CrossbindTid crossbind_tid_optional_double(void);
34
34
  CrossbindTid crossbind_tid_optional_bool(void);
35
35
  CrossbindTid crossbind_tid_optional_string(void);
36
+ CrossbindTid crossbind_tid_optional_int64(void);
37
+ CrossbindTid crossbind_tid_optional_uint64(void);
36
38
  void crossbind_embind_register_smart_ptr(
37
39
  CrossbindTid ptr_ty, CrossbindTid pointee_ty, const char* name, int sharing_policy,
38
40
  const char* gp_sig, CrossbindFn get_pointee, const char* ctor_sig, CrossbindFn constructor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossbind/core-embind-rust",
3
- "version": "2.0.0-beta.58",
3
+ "version": "2.0.0-beta.62",
4
4
  "description": "Rust producer for the crossbind embind registration protocol - bind Rust structs as classes reachable from JS on web (embind) and mobile (embind-jsi). Research/spike stage.",
5
5
  "homepage": "https://github.com/crossbind/crossbind/tree/main/core/embind-rust#readme",
6
6
  "repository": {
@@ -19,7 +19,6 @@
19
19
  "react-native"
20
20
  ],
21
21
  "scripts": {
22
- "e2e:prod": "node e2e/run.mjs",
23
- "clear": "rm -rf crate/target demo/target e2e/demo.mjs e2e/demo.wasm e2e/jsi-shape-check"
22
+ "clear": "rm -rf crate/target demo/target"
24
23
  }
25
24
  }