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

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
@@ -190,7 +190,7 @@ Generation rules (`crossbind/src/utils/rustBridgeGen.js`, run by `buildCargo` be
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
- | `-> Option<i32 / f64 / bool / String>` (method or free fn) | Some -> value, None -> **undefined** (typed `T \| undefined`) |
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
195
  | `other: &SomeClass` parameter | pass another bound instance (borrowed for the call) |
196
196
  | `export.bindings.vectors: [{ of: 'i32', name: '..' }]` in crossbind.config.mjs | `register_vector` |
@@ -229,7 +229,7 @@ scattered `Box::leak`.
229
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
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
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 |
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
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
234
  | Rust crate → real mobile archives (android/ios arm64) | ✅ compiles; iOS and Android linked+run |
235
235
 
package/adapters/jsi.cpp CHANGED
@@ -22,6 +22,7 @@
22
22
  // Compiled ONLY inside the RN native module (jsi + embind-jsi headers exist there via build_ios.js).
23
23
 
24
24
  #include <jsi/jsi.h>
25
+ #include <cmath>
25
26
  #include <cstdint>
26
27
  #include <cstdlib>
27
28
  #include <cstring>
@@ -55,6 +56,12 @@ const void* crossbind_tid_void() { return &typeid(void); }
55
56
  const void* crossbind_tid_std_string() { return &typeid(std::string); }
56
57
  const void* crossbind_tid_int64() { return &typeid(int64_t); }
57
58
  const void* crossbind_tid_uint64() { return &typeid(uint64_t); }
59
+ const void* crossbind_tid_int8() { return &typeid(int8_t); }
60
+ const void* crossbind_tid_int16() { return &typeid(int16_t); }
61
+ const void* crossbind_tid_uint8() { return &typeid(uint8_t); }
62
+ const void* crossbind_tid_uint16() { return &typeid(uint16_t); }
63
+ const void* crossbind_tid_uint32() { return &typeid(uint32_t); }
64
+ const void* crossbind_tid_float() { return &typeid(float); }
58
65
  // Adapter-local identities, NOT &typeid(std::optional<T>): linked C++ bindings may register
59
66
  // the real optional typeids themselves, and these types never interop with C++ signatures.
60
67
  const void* crossbind_tid_optional_int() { static const char t = 0; return &t; }
@@ -193,6 +200,63 @@ uint8_t* crossbind_v_cb_err_take() {
193
200
  static thread_local std::string g_pendingError;
194
201
  static thread_local bool g_hasPendingError = false;
195
202
 
203
+ static thread_local std::string g_pendingErrorCode;
204
+
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);
212
+ }
213
+
214
+ 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));
220
+ }
221
+
222
+ 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));
228
+ }
229
+
230
+ 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);
234
+ return number.isNumber() ? (unsigned)number.getNumber() : 0;
235
+ }
236
+
237
+ extern "C" void crossbind_v_bytes_from_js(size_t h, uint8_t* out, unsigned len) {
238
+ auto& rt = *jsRuntime;
239
+ auto array = crossbindHelper("__crossbind_v_to_array").call(rt, crossbindVHandle(h)).asObject(rt).asArray(rt);
240
+ for (unsigned i = 0; i < len; ++i) out[i] = (uint8_t)array.getValueAtIndex(rt, i).getNumber();
241
+ }
242
+
243
+ extern "C" void crossbind_v_f64s_from_js(size_t h, double* out, unsigned len) {
244
+ auto& rt = *jsRuntime;
245
+ auto array = crossbindHelper("__crossbind_v_to_array").call(rt, crossbindVHandle(h)).asObject(rt).asArray(rt);
246
+ for (unsigned i = 0; i < len; ++i) out[i] = array.getValueAtIndex(rt, i).getNumber();
247
+ }
248
+
249
+ extern "C" void crossbind_embind_raise_error(uint8_t* w);
250
+
251
+ extern "C" void crossbind_embind_raise_error_coded(uint8_t* w, uint8_t* c) {
252
+ crossbind_embind_raise_error(w);
253
+ if (!c) { g_pendingErrorCode.clear(); return; }
254
+ uint32_t len;
255
+ std::memcpy(&len, c, 4);
256
+ g_pendingErrorCode.assign((const char*)(c + 4), (size_t)len);
257
+ std::free(c);
258
+ }
259
+
196
260
  extern "C" void crossbind_embind_raise_error(uint8_t* w) {
197
261
  uint32_t len;
198
262
  std::memcpy(&len, w, 4);
@@ -238,7 +302,16 @@ static uint64_t readArg(jsi::Runtime& rt, const jsi::Value& v, char c) {
238
302
  }
239
303
  // i64/u64 ('j'/'u') arrive as JS BigInt (embind bigint types), Number accepted as a courtesy;
240
304
  // both directions are raw 64-bit slots, so the unsigned bit copy is sign-correct either way.
241
- if (c == 'j' || c == 'u') return v.isBigInt() ? v.getBigInt(rt).getUint64(rt) : (uint64_t)(int64_t)v.getNumber();
305
+ if (c == 'j' || c == 'u') {
306
+ if (v.isBigInt()) return v.getBigInt(rt).getUint64(rt);
307
+ // A Number is accepted only while it is an exact integer JS can represent; anything
308
+ // else would be truncated silently, and a 64-bit slot deserves the exact value.
309
+ double d = v.isNumber() ? v.getNumber() : 0;
310
+ if (!v.isNumber() || d != std::trunc(d) || std::fabs(d) > 9007199254740991.0) {
311
+ throw jsi::JSError(rt, "a 64-bit integer parameter takes a BigInt or a safe integer Number");
312
+ }
313
+ return (uint64_t)(int64_t)d;
314
+ }
242
315
  // Optional args ('I'/'D'/'B'/'S'): undefined/null -> 0 (None); otherwise a heap cell this
243
316
  // adapter owns and frees after dispatch ('S' carries a plain string wire).
244
317
  if (c == 'I' || c == 'D' || c == 'B' || c == 'S') {
@@ -250,24 +323,31 @@ static uint64_t readArg(jsi::Runtime& rt, const jsi::Value& v, char c) {
250
323
  }
251
324
  // Pointers (this/handle/value-object wire) arrive as BigInt, int/bool/enum as Number; accept
252
325
  // either kind defensively so a host that boxed a pointer as a Number still dispatches correctly.
253
- if (c == 'p') return v.isBigInt() ? v.getBigInt(rt).getUint64(rt) : (uint64_t)(int64_t)v.getNumber();
254
- return v.isNumber() ? (uint64_t)(int64_t)v.getNumber() : v.getBigInt(rt).getUint64(rt); // 'i'
326
+ if (c == 'p') return v.isBigInt() ? v.getBigInt(rt).getUint64(rt) : v.isNumber() ? (uint64_t)(int64_t)v.getNumber() : 0;
327
+ // 'i': ints and enums arrive as numbers, bools as booleans (the int wire carries 0/1). Reading
328
+ // a BigInt out of anything else is undefined behaviour (Hermes dies in cloneBigInt), so every
329
+ // other value is 0.
330
+ if (v.isBool()) return v.getBool() ? 1 : 0;
331
+ if (v.isNumber()) return (uint64_t)(int64_t)v.getNumber();
332
+ return v.isBigInt() ? v.getBigInt(rt).getUint64(rt) : 0;
255
333
  }
256
334
 
257
335
  // Bounded-dispatch: every wire kind is integer-class in the invoker's C signature (pointers and
258
336
  // ints natively; doubles as bit patterns in u64 - see WireType for f64), so uint64-slot casts are
259
337
  // ABI-correct on every native ABI. No FP registers, no libffi; the only bound is the arity cap.
260
338
  static jsi::Value callInvoker(jsi::Runtime& rt, const std::string& sig, void* invoker,
261
- const jsi::Value* a, uint64_t ctx, bool prepend) {
339
+ const jsi::Value* a, size_t count, uint64_t ctx, bool prepend) {
262
340
  const char ret = sig[0];
263
341
  const int total = (int)sig.size() - 1; // C args the raw invoker takes (sig[1..])
264
342
 
265
343
  // s[0..total-1] are the raw invoker's C args. When prepend, sig[1] is the baked target pointer
266
344
  // (embind sliced it off), so the jsi args a[0..] fill sig[2..]; otherwise a[0..] fill sig[1..].
267
- uint64_t s[6] = {0, 0, 0, 0, 0, 0};
345
+ // Widest call: a baked target + `this` + six declared arguments.
346
+ uint64_t s[8] = {0, 0, 0, 0, 0, 0, 0, 0};
268
347
  int base = 0;
269
348
  if (prepend) { s[0] = ctx; base = 1; }
270
- for (int j = base; j < total && j < 6; ++j) s[j] = readArg(rt, a[j - base], sig[1 + j]);
349
+ // 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;
271
351
 
272
352
  uint64_t r = 0;
273
353
  if (ret == 'v') {
@@ -278,7 +358,9 @@ static jsi::Value callInvoker(jsi::Runtime& rt, const std::string& sig, void* in
278
358
  case 3: ((void(*)(uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2]); break;
279
359
  case 4: ((void(*)(uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3]); break;
280
360
  case 5: ((void(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4]); break;
281
- default: ((void(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4], s[5]); break;
361
+ case 6: ((void(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4], s[5]); break;
362
+ case 7: ((void(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4], s[5], s[6]); break;
363
+ default: ((void(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]); break;
282
364
  }
283
365
  } else {
284
366
  switch (total) {
@@ -288,30 +370,43 @@ static jsi::Value callInvoker(jsi::Runtime& rt, const std::string& sig, void* in
288
370
  case 3: r = ((uint64_t(*)(uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2]); break;
289
371
  case 4: r = ((uint64_t(*)(uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3]); break;
290
372
  case 5: r = ((uint64_t(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4]); break;
291
- default: r = ((uint64_t(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4], s[5]); break;
373
+ case 6: r = ((uint64_t(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4], s[5]); break;
374
+ case 7: r = ((uint64_t(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4], s[5], s[6]); break;
375
+ default: r = ((uint64_t(*)(uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t))invoker)(s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]); break;
292
376
  }
293
377
  }
294
378
 
295
379
  // The consumer owns each incoming std::string arg buffer and optional arg cell; free them
296
380
  // now that the crate has copied.
297
- for (int j = base; j < total && j < 6; ++j) {
381
+ for (int j = base; j < total && j < 8; ++j) {
298
382
  const char cj = sig[1 + j];
299
383
  if (cj == 's' || cj == 'S' || cj == 'I' || cj == 'D' || cj == 'B') std::free((void*)(uintptr_t)s[j]);
300
384
  }
301
385
 
302
386
  // A shim raised (Result::Err): drop the sentinel return and surface a real JS exception.
387
+ // The error is BUILT IN JS, not thrown from here: Hermes re-wraps a C++ exception escaping a
388
+ // host function ("Exception in HostFunction: ..."), which loses both the plain message and the
389
+ // `code` property. The runtime's dyn-caller rethrows what this parks, so the object JS sees is
390
+ // a real Error with the binding's own message and code.
303
391
  if (g_hasPendingError) {
304
392
  g_hasPendingError = false;
305
393
  if (ret == 's' && r) std::free((void*)(uintptr_t)r);
306
- throw jsi::JSError(rt, std::move(g_pendingError));
394
+ std::string message = std::move(g_pendingError);
395
+ std::string code = std::move(g_pendingErrorCode);
396
+ g_pendingError.clear();
397
+ g_pendingErrorCode.clear();
398
+ crossbindHelper("__crossbind_park_error").call(
399
+ rt, jsi::String::createFromUtf8(rt, message),
400
+ code.empty() ? jsi::Value::undefined() : jsi::Value(jsi::String::createFromUtf8(rt, code)));
401
+ return jsi::Value::undefined();
307
402
  }
308
403
 
309
404
  if (ret == 'v') return jsi::Value::undefined();
310
405
  if (ret == 's') return wireStringToJsi(rt, r); // the crate returned a [u32 len][bytes] buffer
311
- // Optional returns ('I'/'D'/'B'/'S'): nullable heap cell - 0 is None (undefined), otherwise
406
+ // Optional returns ('I'/'D'/'B'/'S'): nullable heap cell - 0 is None (null), otherwise
312
407
  // the malloc'd inner value ('S' points at a plain string wire); freed here after reading.
313
408
  if (ret == 'I' || ret == 'D' || ret == 'B' || ret == 'S') {
314
- if (!r) return jsi::Value::undefined();
409
+ if (!r) return jsi::Value::null();
315
410
  if (ret == 'S') return wireStringToJsi(rt, r);
316
411
  uint8_t* cell = (uint8_t*)(uintptr_t)r;
317
412
  jsi::Value out = jsi::Value::undefined();
@@ -338,8 +433,8 @@ static jsi::Function wrapInvoker(const std::string& name, const char* sigC, void
338
433
  unsigned jsArgc = (unsigned)(sig.size() - 1 - (prepend ? 1 : 0)); // args embind actually passes
339
434
  return jsi::Function::createFromHostFunction(
340
435
  rt, jsi::PropNameID::forAscii(rt, dbg), jsArgc,
341
- [sig, invoker, ctx, prepend](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* a, size_t) -> jsi::Value {
342
- return callInvoker(rt, sig, invoker, a, ctx, prepend);
436
+ [sig, invoker, ctx, prepend](jsi::Runtime& rt, const jsi::Value&, const jsi::Value* a, size_t count) -> jsi::Value {
437
+ return callInvoker(rt, sig, invoker, a, count, ctx, prepend);
343
438
  });
344
439
  }
345
440
 
@@ -413,6 +508,22 @@ void crossbind_embind_register_enum_value(CrossbindTid enum_ty, const char* valu
413
508
  _embind_register_enum_value(enum_ty, value_name, value);
414
509
  }
415
510
 
511
+ void crossbind_embind_register_class_property(CrossbindTid cls, const char* name,
512
+ CrossbindTid getter_ty, const char* getter_sig, CrossbindFn getter, void* getter_ctx,
513
+ CrossbindTid setter_ty, const char* setter_sig, CrossbindFn setter, void* setter_ctx) {
514
+ // Not sliced: the fork's property accessors call `getter(getterContext, ptr)` and
515
+ // `setter(setterContext, ptr, value)`, so both contexts arrive as ordinary arguments
516
+ // (unlike a method invoker, where embind slices the target off).
517
+ auto get = wrapInvoker(std::string(name) + "_get", getter_sig, getter);
518
+ auto set = wrapInvoker(std::string(name) + "_set", setter_sig, setter);
519
+ _embind_register_class_property(cls, name, getter_ty, getter_sig, get, getter_ctx,
520
+ setter_ty, setter_sig, set, setter_ctx);
521
+ }
522
+ void crossbind_embind_register_constant(const char* name, CrossbindTid ty, double value) {
523
+ // The fork takes the constant as a jsi::Value; every kind we offer (int, float, bool) reads
524
+ // back through its own fromWireType from a plain number.
525
+ _embind_register_constant(name, ty, jsi::Value(value));
526
+ }
416
527
  void crossbind_embind_register_value_object(CrossbindTid struct_ty, const char* name,
417
528
  const char* ctor_sig, CrossbindFn ctor, const char* dtor_sig, CrossbindFn dtor) {
418
529
  auto ct = wrapInvoker(std::string(name) + "_ctor", ctor_sig, ctor);
package/adapters/web.cpp CHANGED
@@ -22,6 +22,21 @@ EM_JS(void, crossbind_embind_raise_error, (uint8_t* w), {
22
22
  throw new Error(msg);
23
23
  });
24
24
 
25
+ // The same escape with a machine-readable code on the Error, the way napi-rs carries its
26
+ // `error.code`: a null pointer means the producer had no code to give.
27
+ EM_JS(void, crossbind_embind_raise_error_coded, (uint8_t* w, uint8_t* c), {
28
+ const len = HEAPU32[w >> 2];
29
+ const msg = UTF8ToString(w + 4, len);
30
+ _free(w);
31
+ const error = new Error(msg);
32
+ if (c) {
33
+ const codeLen = HEAPU32[c >> 2];
34
+ error.code = UTF8ToString(c + 4, codeLen);
35
+ _free(c);
36
+ }
37
+ throw error;
38
+ });
39
+
25
40
  // Producer sig chars that emscripten's dynCall alphabet lacks: 's' (a jsi-marshalling hint;
26
41
  // the wasm string wire is a real pointer) -> 'p', and 'u' (unsigned 64 - signedness lives in
27
42
  // the registered typeid) -> 'j'. Translated copies are one-time registration data, leaked.
@@ -45,6 +60,12 @@ const void* crossbind_tid_void() { return &typeid(void); }
45
60
  const void* crossbind_tid_std_string() { return &typeid(std::string); }
46
61
  const void* crossbind_tid_int64() { return &typeid(int64_t); }
47
62
  const void* crossbind_tid_uint64() { return &typeid(uint64_t); }
63
+ const void* crossbind_tid_int8() { return &typeid(int8_t); }
64
+ const void* crossbind_tid_int16() { return &typeid(int16_t); }
65
+ const void* crossbind_tid_uint8() { return &typeid(uint8_t); }
66
+ const void* crossbind_tid_uint16() { return &typeid(uint16_t); }
67
+ const void* crossbind_tid_uint32() { return &typeid(uint32_t); }
68
+ const void* crossbind_tid_float() { return &typeid(float); }
48
69
  // Adapter-local identities, NOT &typeid(std::optional<T>): linked C++ bindings may register
49
70
  // the real optional typeids themselves, and these types never interop with C++ signatures.
50
71
  const void* crossbind_tid_optional_int() { static const char t = 0; return &t; }
@@ -54,8 +75,8 @@ const void* crossbind_tid_optional_string() { static const char t = 0; return &t
54
75
 
55
76
  }
56
77
 
57
- // Optional wire on wasm is an EM_VAL handle (None = the reserved undefined constant from
58
- // emscripten's own header). Some-values become handles through Emval.toHandle; the registered
78
+ // Optional wire on wasm is an EM_VAL handle (None = the reserved null constant from
79
+ // emscripten's own header: a missing value is null on every runtime, like C++ nullptr). Some-values become handles through Emval.toHandle; the registered
59
80
  // optional type's fromWireType does toValue + decref, so the refcount stays balanced.
60
81
  EM_JS(void*, crossbind_emval_from_number_js, (double v), { return Emval.toHandle(v); });
61
82
  EM_JS(void*, crossbind_emval_from_bool_js, (int v), { return Emval.toHandle(!!v); });
@@ -73,8 +94,8 @@ void* crossbind_emval_take_value(CrossbindTid inner, const void* argv) {
73
94
  if (inner == crossbind_tid_bool()) return crossbind_emval_from_bool_js((int)*(const uint8_t*)argv);
74
95
  return crossbind_emval_from_string_js(*(uint8_t* const*)argv); // std::string wire pointer
75
96
  }
76
- void* crossbind_emval_undefined() {
77
- return (void*)emscripten::internal::_EMVAL_UNDEFINED;
97
+ void* crossbind_emval_null() {
98
+ return (void*)emscripten::internal::_EMVAL_NULL;
78
99
  }
79
100
 
80
101
  // Optional PARAMETERS: embind hands the invoker an EM_VAL handle; these readers consume its
@@ -124,9 +145,20 @@ void* crossbind_emval_json_to_handle(uint8_t* w) {
124
145
  return (void*)h;
125
146
  }
126
147
 
148
+ // A Set or Map has no JSON form of its own, so the replacer writes it as the array or object the
149
+ // Rust side expects; everything else stringifies unchanged.
150
+ EM_JS(void*, crossbind_json_replacer, (), {
151
+ return Emval.toHandle(function (key, value) {
152
+ if (value instanceof Set) return Array.from(value);
153
+ if (value instanceof Map) return Object.fromEntries(value);
154
+ return value;
155
+ });
156
+ });
157
+
127
158
  uint8_t* crossbind_emval_handle_to_json(void* h) {
128
159
  emscripten::val v = emscripten::val::take_ownership((emscripten::EM_VAL)h);
129
- emscripten::val j = emscripten::val::global("JSON").call<emscripten::val>("stringify", v);
160
+ emscripten::val replacer = emscripten::val::take_ownership((emscripten::EM_VAL)crossbind_json_replacer());
161
+ emscripten::val j = emscripten::val::global("JSON").call<emscripten::val>("stringify", v, replacer);
130
162
  // JSON.stringify(undefined / a bare function) yields undefined: cross as null.
131
163
  std::string s = j.isUndefined() ? "null" : j.as<std::string>();
132
164
  uint8_t* w = (uint8_t*)malloc(4 + s.size());
@@ -147,6 +179,8 @@ void _embind_register_smart_ptr(CrossbindTid, CrossbindTid, const char*, emscrip
147
179
  void _embind_register_enum(CrossbindTid, const char*, size_t, bool, int);
148
180
  void _embind_register_enum_value(CrossbindTid, const char*, int);
149
181
  void _embind_register_value_object(CrossbindTid, const char*, const char*, CrossbindFn, const char*, CrossbindFn);
182
+ void _embind_register_constant(const char*, CrossbindTid, double);
183
+ void _embind_register_class_property(CrossbindTid, const char*, CrossbindTid, const char*, CrossbindFn, void*, CrossbindTid, const char*, CrossbindFn, void*);
150
184
  void _embind_register_value_object_field(CrossbindTid, const char*, CrossbindTid, const char*, CrossbindFn, void*, CrossbindTid, const char*, CrossbindFn, void*);
151
185
  void _embind_finalize_value_object(CrossbindTid);
152
186
 
@@ -180,6 +214,18 @@ void crossbind_embind_register_enum(CrossbindTid a, const char* b, size_t c, boo
180
214
  void crossbind_embind_register_enum_value(CrossbindTid a, const char* b, int c) {
181
215
  _embind_register_enum_value(a, b, c);
182
216
  }
217
+ // A constant is registered by value: embind carries the wire through a double, which holds every
218
+ // int, float and bool exactly (and a wasm32 pointer).
219
+ // A public field reads and writes as a JS property instead of a pair of calls.
220
+ void crossbind_embind_register_class_property(CrossbindTid cls, const char* name,
221
+ CrossbindTid getter_ty, const char* getter_sig, CrossbindFn getter, void* getter_ctx,
222
+ CrossbindTid setter_ty, const char* setter_sig, CrossbindFn setter, void* setter_ctx) {
223
+ _embind_register_class_property(cls, name, getter_ty, sigForWasm(getter_sig), getter, getter_ctx,
224
+ setter_ty, sigForWasm(setter_sig), setter, setter_ctx);
225
+ }
226
+ void crossbind_embind_register_constant(const char* name, CrossbindTid ty, double value) {
227
+ _embind_register_constant(name, ty, value);
228
+ }
183
229
  void crossbind_embind_register_value_object(CrossbindTid a, const char* b, const char* c, CrossbindFn d, const char* e, CrossbindFn f) {
184
230
  _embind_register_value_object(a, b, c, d, e, f);
185
231
  }
@@ -243,6 +289,27 @@ EM_JS(uint8_t*, crossbind_v_as_str, (void* h), {
243
289
  stringToUTF8(s, w + 4, len + 1);
244
290
  return w;
245
291
  });
292
+ // Typed arrays: bytes and doubles cross as the JS views themselves, copied once in each
293
+ // direction - a view into wasm memory would dangle the moment the heap grows.
294
+ EM_JS(void*, crossbind_v_bytes_to_js, (uint8_t* data, unsigned len), {
295
+ return Emval.toHandle(HEAPU8.slice(data, data + len));
296
+ });
297
+ EM_JS(void*, crossbind_v_f64s_to_js, (double* data, unsigned len), {
298
+ return Emval.toHandle(HEAPF64.slice(data >> 3, (data >> 3) + len));
299
+ });
300
+ EM_JS(unsigned, crossbind_v_typed_len, (void* h), {
301
+ const value = Emval.toValue(h);
302
+ return value && value.length !== undefined ? value.length : 0;
303
+ });
304
+ EM_JS(void, crossbind_v_bytes_from_js, (void* h, uint8_t* out, unsigned len), {
305
+ const value = Emval.toValue(h);
306
+ HEAPU8.set(value.subarray ? value.subarray(0, len) : Uint8Array.from(value).subarray(0, len), out);
307
+ });
308
+ EM_JS(void, crossbind_v_f64s_from_js, (void* h, double* out, unsigned len), {
309
+ const value = Emval.toValue(h);
310
+ HEAPF64.set(value.subarray ? value.subarray(0, len) : Float64Array.from(value).subarray(0, len), out >> 3);
311
+ });
312
+
246
313
  EM_JS(void*, crossbind_v_call, (void* f, unsigned argc, void* argv), {
247
314
  try {
248
315
  const fn = Emval.toValue(f);
package/crate/src/lib.rs CHANGED
@@ -36,6 +36,12 @@ extern "C" {
36
36
  );
37
37
  fn crossbind_embind_register_enum(enum_ty: *const c_void, name: *const c_char, size: usize, is_signed: bool, policy: i32);
38
38
  fn crossbind_embind_register_enum_value(enum_ty: *const c_void, name: *const c_char, value: i32);
39
+ fn crossbind_embind_register_constant(name: *const c_char, ty: *const c_void, value: f64);
40
+ fn crossbind_embind_register_class_property(
41
+ cls: *const c_void, name: *const c_char,
42
+ getter_ty: *const c_void, getter_sig: *const c_char, getter: usize, getter_ctx: *mut c_void,
43
+ setter_ty: *const c_void, setter_sig: *const c_char, setter: usize, setter_ctx: *mut c_void,
44
+ );
39
45
  fn crossbind_embind_register_value_object(
40
46
  struct_ty: *const c_void, name: *const c_char,
41
47
  ctor_sig: *const c_char, ctor: usize, dtor_sig: *const c_char, dtor: usize,
@@ -62,6 +68,12 @@ extern "C" {
62
68
  fn free(ptr: *mut u8);
63
69
  fn crossbind_tid_int64() -> *const c_void;
64
70
  fn crossbind_tid_uint64() -> *const c_void;
71
+ fn crossbind_tid_int8() -> *const c_void;
72
+ fn crossbind_tid_int16() -> *const c_void;
73
+ fn crossbind_tid_uint8() -> *const c_void;
74
+ fn crossbind_tid_uint16() -> *const c_void;
75
+ fn crossbind_tid_uint32() -> *const c_void;
76
+ fn crossbind_tid_float() -> *const c_void;
65
77
  fn crossbind_embind_register_optional(optional_ty: *const c_void, inner_ty: *const c_void);
66
78
  fn crossbind_tid_optional_int() -> *const c_void;
67
79
  fn crossbind_tid_optional_double() -> *const c_void;
@@ -71,6 +83,7 @@ extern "C" {
71
83
  // Adapter-owned error escape ([u32 len][bytes] wire): web throws a JS Error immediately,
72
84
  // native stores the message and throws after the invoker returns.
73
85
  fn crossbind_embind_raise_error(wire_msg: *mut u8);
86
+ fn crossbind_embind_raise_error_coded(msg: *mut u8, code: *mut u8);
74
87
  }
75
88
 
76
89
  // Optional wire helpers exist only on wasm (native optionals are nullable heap cells the jsi
@@ -78,7 +91,7 @@ extern "C" {
78
91
  #[cfg(target_family = "wasm")]
79
92
  extern "C" {
80
93
  fn crossbind_emval_take_value(inner_ty: *const c_void, argv: *const c_void) -> usize;
81
- fn crossbind_emval_undefined() -> usize;
94
+ fn crossbind_emval_null() -> usize;
82
95
  fn crossbind_emval_opt_i32(handle: usize, out: *mut i32) -> i32;
83
96
  fn crossbind_emval_opt_f64(handle: usize, out: *mut f64) -> i32;
84
97
  fn crossbind_emval_opt_bool(handle: usize, out: *mut u8) -> i32;
@@ -94,6 +107,8 @@ struct Registry {
94
107
  tids: Vec<Box<u8>>,
95
108
  argtypes: Vec<Box<[*const c_void]>>,
96
109
  class_tid: HashMap<TypeId, *const c_void>,
110
+ class_ptr_tid: HashMap<TypeId, *const c_void>,
111
+ owned_ptr_tid: HashMap<TypeId, *const c_void>,
97
112
  shared_ptr_tid: HashMap<TypeId, *const c_void>,
98
113
  }
99
114
  // Raw pointers are not Send/Sync; the arena is only ever touched under its Mutex.
@@ -103,6 +118,8 @@ fn registry() -> &'static Mutex<Registry> {
103
118
  static REG: OnceLock<Mutex<Registry>> = OnceLock::new();
104
119
  REG.get_or_init(|| Mutex::new(Registry {
105
120
  cstrings: Vec::new(), tids: Vec::new(), argtypes: Vec::new(), class_tid: HashMap::new(),
121
+ class_ptr_tid: HashMap::new(),
122
+ owned_ptr_tid: HashMap::new(),
106
123
  shared_ptr_tid: HashMap::new(),
107
124
  }))
108
125
  }
@@ -222,6 +239,98 @@ impl WireType for u64 {
222
239
  }
223
240
 
224
241
  // bool crosses as an int-sized wire (0/1), the way emscripten's ABI passes it.
242
+ // The narrower integers ride the int slot with their own type ids, so the host converter keeps
243
+ // each one's range check instead of silently wrapping a value the Rust type cannot hold.
244
+ macro_rules! int_wire {
245
+ ($( $ty:ty, $tid:ident ; )*) => {
246
+ $(
247
+ impl WireType for $ty {
248
+ type Wire = i32;
249
+ const SIG: char = 'i';
250
+ fn tid() -> *const c_void { unsafe { $tid() } }
251
+ fn from_wire(w: i32) -> $ty { w as $ty }
252
+ fn to_wire(self) -> i32 { self as i32 }
253
+ }
254
+ )*
255
+ };
256
+ }
257
+
258
+ int_wire! {
259
+ i8, crossbind_tid_int8;
260
+ i16, crossbind_tid_int16;
261
+ u8, crossbind_tid_uint8;
262
+ u16, crossbind_tid_uint16;
263
+ u32, crossbind_tid_uint32;
264
+ }
265
+
266
+ // f32 crosses through the double slot: JS has one number type, so the value widens on the way
267
+ // out and narrows on the way in - the Rust signature is what asks for single precision.
268
+ #[cfg(target_family = "wasm")]
269
+ impl WireType for f32 {
270
+ type Wire = f64;
271
+ const SIG: char = 'd';
272
+ fn tid() -> *const c_void { unsafe { crossbind_tid_float() } }
273
+ fn from_wire(w: f64) -> f32 { w as f32 }
274
+ fn to_wire(self) -> f64 { self as f64 }
275
+ }
276
+
277
+ #[cfg(not(target_family = "wasm"))]
278
+ impl WireType for f32 {
279
+ type Wire = u64;
280
+ const SIG: char = 'd';
281
+ fn tid() -> *const c_void { unsafe { crossbind_tid_float() } }
282
+ fn from_wire(w: u64) -> f32 { f64::from_bits(w) as f32 }
283
+ fn to_wire(self) -> u64 { (self as f64).to_bits() }
284
+ }
285
+
286
+ // usize/isize are 32-bit on wasm and 64-bit everywhere else, so each platform rides the slot
287
+ // that carries its full range: a number on wasm, a BigInt on native.
288
+ #[cfg(target_pointer_width = "32")]
289
+ impl WireType for usize {
290
+ type Wire = i32;
291
+ const SIG: char = 'i';
292
+ fn tid() -> *const c_void { unsafe { crossbind_tid_uint32() } }
293
+ fn from_wire(w: i32) -> usize { w as usize }
294
+ fn to_wire(self) -> i32 { self as i32 }
295
+ }
296
+
297
+ #[cfg(target_pointer_width = "32")]
298
+ impl WireType for isize {
299
+ type Wire = i32;
300
+ const SIG: char = 'i';
301
+ fn tid() -> *const c_void { unsafe { crossbind_tid_int() } }
302
+ fn from_wire(w: i32) -> isize { w as isize }
303
+ fn to_wire(self) -> i32 { self as i32 }
304
+ }
305
+
306
+ #[cfg(not(target_pointer_width = "32"))]
307
+ impl WireType for usize {
308
+ type Wire = u64;
309
+ const SIG: char = 'u';
310
+ fn tid() -> *const c_void { unsafe { crossbind_tid_uint64() } }
311
+ fn from_wire(w: u64) -> usize { w as usize }
312
+ fn to_wire(self) -> u64 { self as u64 }
313
+ }
314
+
315
+ #[cfg(not(target_pointer_width = "32"))]
316
+ impl WireType for isize {
317
+ type Wire = i64;
318
+ const SIG: char = 'j';
319
+ fn tid() -> *const c_void { unsafe { crossbind_tid_int64() } }
320
+ fn from_wire(w: i64) -> isize { w as isize }
321
+ fn to_wire(self) -> i64 { self as i64 }
322
+ }
323
+
324
+ // char crosses as a one-character string, the way JS spells a character; an incoming string
325
+ // yields its first character (an empty one is U+FFFD, the replacement character).
326
+ impl WireType for char {
327
+ type Wire = <String as WireType>::Wire;
328
+ const SIG: char = 's';
329
+ fn tid() -> *const c_void { <String as WireType>::tid() }
330
+ fn from_wire(w: Self::Wire) -> char { String::from_wire(w).chars().next().unwrap_or('\u{fffd}') }
331
+ fn to_wire(self) -> Self::Wire { self.to_string().to_wire() }
332
+ }
333
+
225
334
  impl WireType for bool {
226
335
  type Wire = i32;
227
336
  const SIG: char = 'i';
@@ -265,7 +374,7 @@ impl WireType for String {
265
374
  }
266
375
  }
267
376
 
268
- /// Option<T> both ways: on wasm the wire is an EM_VAL handle (None = undefined; incoming
377
+ /// Option<T> both ways: on wasm the wire is an EM_VAL handle (None = null; incoming
269
378
  /// undefined/null both read as None); on native it is a nullable heap cell (0 = None) the jsi
270
379
  /// adapter converts, tagged 'I'/'D'/'B'/'S'. Incoming cells stay adapter-owned (read, no free).
271
380
  macro_rules! option_wire {
@@ -279,7 +388,7 @@ macro_rules! option_wire {
279
388
  fn from_wire(w: usize) -> Self { <$inner as EmvalOptRead>::emval_opt_read(w) }
280
389
  fn to_wire(self) -> usize {
281
390
  match self {
282
- None => unsafe { crossbind_emval_undefined() },
391
+ None => unsafe { crossbind_emval_null() },
283
392
  Some(v) => v.emval_take(),
284
393
  }
285
394
  }
@@ -418,6 +527,29 @@ pub fn register_optional_f64() { unsafe { crossbind_embind_register_optional(cro
418
527
  pub fn register_optional_bool() { unsafe { crossbind_embind_register_optional(crossbind_tid_optional_bool(), crossbind_tid_bool()); } }
419
528
  pub fn register_optional_string() { unsafe { crossbind_embind_register_optional(crossbind_tid_optional_string(), crossbind_tid_std_string()); } }
420
529
 
530
+ /// A module-level constant: the value crosses once at registration, so JS reads a plain value
531
+ /// instead of calling a getter. Only the wire kinds a double carries exactly are offered.
532
+ pub trait ConstantValue: WireType {
533
+ fn as_constant(self) -> f64;
534
+ }
535
+ impl ConstantValue for i32 { fn as_constant(self) -> f64 { self as f64 } }
536
+ impl ConstantValue for f64 { fn as_constant(self) -> f64 { self } }
537
+ impl ConstantValue for bool { fn as_constant(self) -> f64 { if self { 1.0 } else { 0.0 } } }
538
+
539
+ /// A string constant: the double the ABI carries is the emval handle of a JS string, which the
540
+ /// emval type reads straight back. The handle is released on purpose - the constant owns it for
541
+ /// the life of the module.
542
+ pub fn constant_str(name: &str, value: &str) {
543
+ let handle = JsValue::from_str(value).release();
544
+ let name_ptr = { registry().lock().unwrap().cstr(name) };
545
+ unsafe { crossbind_embind_register_constant(name_ptr, crossbind_tid_emval(), handle as f64) };
546
+ }
547
+
548
+ pub fn constant<T: ConstantValue>(name: &str, value: T) {
549
+ let name_ptr = { registry().lock().unwrap().cstr(name) };
550
+ unsafe { crossbind_embind_register_constant(name_ptr, T::tid(), value.as_constant()) };
551
+ }
552
+
421
553
  /// Post-raise sentinel returns: only native ever observes them (the adapter throws right after
422
554
  /// the invoker returns); on web `crossbind_embind_raise_error` throws into JS and never comes back.
423
555
  pub trait ErrSentinel {
@@ -431,13 +563,119 @@ impl ErrSentinel for bool { fn err_sentinel() -> Self { false } }
431
563
  impl ErrSentinel for () { fn err_sentinel() -> Self {} }
432
564
  impl ErrSentinel for String { fn err_sentinel() -> Self { String::new() } }
433
565
  impl<T> ErrSentinel for *mut T { fn err_sentinel() -> Self { std::ptr::null_mut() } }
566
+ // A fallible optional (`Result<Option<T>, E>`) raises and then returns None.
567
+ impl<T> ErrSentinel for Option<T> { fn err_sentinel() -> Self { None } }
568
+ // The narrower integers and the pointer-wide ones share the zero sentinel.
569
+ impl ErrSentinel for i8 { fn err_sentinel() -> Self { 0 } }
570
+ impl ErrSentinel for i16 { fn err_sentinel() -> Self { 0 } }
571
+ impl ErrSentinel for u8 { fn err_sentinel() -> Self { 0 } }
572
+ impl ErrSentinel for u16 { fn err_sentinel() -> Self { 0 } }
573
+ impl ErrSentinel for u32 { fn err_sentinel() -> Self { 0 } }
574
+ impl ErrSentinel for f32 { fn err_sentinel() -> Self { 0.0 } }
575
+ impl ErrSentinel for usize { fn err_sentinel() -> Self { 0 } }
576
+ impl ErrSentinel for isize { fn err_sentinel() -> Self { 0 } }
577
+ impl ErrSentinel for char { fn err_sentinel() -> Self { '\u{fffd}' } }
434
578
 
435
579
  /// Routes a shim's `Err` to JS as an exception, then returns a dead sentinel value.
580
+ /// Runs a binding body so that a Rust panic reaches JS as an exception instead of tearing the
581
+ /// process down. The panic still means a bug: state is whatever the panic left behind, the same
582
+ /// caveat napi-rs documents for its own catch_unwind.
583
+ ///
584
+ /// Two runtimes, two mechanisms. Native targets unwind, so the panic is caught here. The wasm
585
+ /// target's std aborts instead of unwinding, so a panic hook raises the error from inside the
586
+ /// panic itself - the JS throw leaves the call before the abort can run.
587
+ pub fn guard<R: ErrSentinel>(body: impl FnOnce() -> R) -> R {
588
+ install_panic_hook();
589
+ #[cfg(target_family = "wasm")]
590
+ {
591
+ body()
592
+ }
593
+ #[cfg(not(target_family = "wasm"))]
594
+ {
595
+ match std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)) {
596
+ Ok(value) => value,
597
+ Err(payload) => raise_err(panic_text(&payload)),
598
+ }
599
+ }
600
+ }
601
+
602
+ fn install_panic_hook() {
603
+ static ONCE: std::sync::Once = std::sync::Once::new();
604
+ ONCE.call_once(|| {
605
+ let previous = std::panic::take_hook();
606
+ std::panic::set_hook(Box::new(move |info| {
607
+ let text = match info.payload().downcast_ref::<&str>() {
608
+ Some(text) => (*text).to_string(),
609
+ None => match info.payload().downcast_ref::<String>() {
610
+ Some(text) => text.clone(),
611
+ None => String::from("panicked"),
612
+ },
613
+ };
614
+ let text = match info.location() {
615
+ Some(at) => format!("{text} (panicked at {}:{})", at.file(), at.line()),
616
+ None => text,
617
+ };
618
+ // wasm: this throws into JS and the call never comes back, so the abort below the
619
+ // hook is skipped. Native: the adapter parks the message and throws once the
620
+ // invoker returns, so unwinding still reaches the catch in `guard`.
621
+ #[cfg(target_family = "wasm")]
622
+ unsafe { crossbind_embind_raise_error(text.to_wire()) };
623
+ #[cfg(not(target_family = "wasm"))]
624
+ {
625
+ let _ = &text;
626
+ previous(info);
627
+ }
628
+ }));
629
+ });
630
+ }
631
+
632
+ #[cfg(not(target_family = "wasm"))]
633
+ fn panic_text(payload: &Box<dyn std::any::Any + Send>) -> String {
634
+ if let Some(text) = payload.downcast_ref::<&str>() { return (*text).to_string(); }
635
+ if let Some(text) = payload.downcast_ref::<String>() { return text.clone(); }
636
+ String::from("panicked")
637
+ }
638
+
436
639
  pub fn raise_err<W: ErrSentinel>(msg: String) -> W {
437
640
  unsafe { crossbind_embind_raise_error(msg.to_wire()); }
438
641
  W::err_sentinel()
439
642
  }
440
643
 
644
+ /// The same escape carrying a machine-readable code, so JS can branch on `error.code` instead
645
+ /// of matching message text.
646
+ pub fn raise_err_coded<W: ErrSentinel>(msg: String, code: Option<String>) -> W {
647
+ let code_wire = match code {
648
+ Some(text) => text.to_wire(),
649
+ None => core::ptr::null_mut(),
650
+ };
651
+ unsafe { crossbind_embind_raise_error_coded(msg.to_wire(), code_wire); }
652
+ W::err_sentinel()
653
+ }
654
+
655
+ /// An error type that also implements `AsRef<str>` gives that string as the code, the way
656
+ /// napi-rs reads a code off its error enums. Everything else has no code: the two traits below
657
+ /// are picked apart by autoref, so no specialization feature is needed.
658
+ /// The fallback sits behind one more reference than the specialized impl, so method lookup finds
659
+ /// the coded one first when the bound holds and falls through to None when it does not.
660
+ pub trait CrossbindErrorCode { fn crossbind_code(&self) -> Option<String>; }
661
+ impl<E: AsRef<str>> CrossbindErrorCode for E {
662
+ fn crossbind_code(&self) -> Option<String> { Some(self.as_ref().to_string()) }
663
+ }
664
+ pub trait CrossbindErrorCodeFallback { fn crossbind_code(&self) -> Option<String>; }
665
+ impl<E> CrossbindErrorCodeFallback for &E {
666
+ fn crossbind_code(&self) -> Option<String> { None }
667
+ }
668
+
669
+ extern "C" fn property_getter<T: 'static, F: WireType>(ctx: usize, this: *mut T) -> <F as WireType>::Wire {
670
+ let getter: fn(&mut T) -> F = unsafe { core::mem::transmute(ctx) };
671
+ getter(unsafe { &mut *this }).to_wire()
672
+ }
673
+
674
+ extern "C" fn property_setter<T: 'static, F: WireType>(ctx: usize, this: *mut T, value: <F as WireType>::Wire) {
675
+ let setter: fn(&mut T, F) = unsafe { core::mem::transmute(ctx) };
676
+ setter(unsafe { &mut *this }, F::from_wire(value));
677
+ }
678
+
441
679
  extern "C" fn dtor_thunk<T>(p: *mut T) {
442
680
  if !p.is_null() {
443
681
  drop(unsafe { Box::from_raw(p) });
@@ -461,6 +699,20 @@ pub fn class_tid<T: 'static>() -> *const c_void {
461
699
  .expect("class parameter type must be registered before the binding that uses it")
462
700
  }
463
701
 
702
+ /// The registered smart-pointer typeid of an OWNED class (`.smart_ptr(..)`), for a method or
703
+ /// function that returns a fresh instance by value.
704
+ pub fn owned_ptr_tid<T: 'static>() -> *const c_void {
705
+ registry().lock().unwrap().owned_ptr_tid.get(&TypeId::of::<T>()).copied()
706
+ .expect("a class that returns itself by value must register its smart pointer first")
707
+ }
708
+
709
+ /// The registered POINTER typeid of a class: a pointer parameter takes null (a reference does
710
+ /// not), which is how `Option<&Class>` spells None.
711
+ pub fn class_ptr_tid<T: 'static>() -> *const c_void {
712
+ registry().lock().unwrap().class_ptr_tid.get(&TypeId::of::<T>()).copied()
713
+ .expect("class parameter type must be registered before the binding that uses it")
714
+ }
715
+
464
716
  /// The registered smart-pointer typeid of a SHARED class (`.smart_ptr_shared(..)`), for the
465
717
  /// generated `Arc<T>` return/parameter wrappers.
466
718
  pub fn shared_tid<T: 'static>() -> *const c_void {
@@ -503,6 +755,11 @@ extern "C" {
503
755
  fn crossbind_v_as_str(h: usize) -> *mut u8;
504
756
  fn crossbind_v_call(f: usize, argc: u32, argv: *const usize) -> usize;
505
757
  fn crossbind_v_cb_err_take() -> *mut u8;
758
+ fn crossbind_v_bytes_to_js(data: *const u8, len: u32) -> usize;
759
+ fn crossbind_v_f64s_to_js(data: *const f64, len: u32) -> usize;
760
+ fn crossbind_v_typed_len(h: usize) -> u32;
761
+ fn crossbind_v_bytes_from_js(h: usize, out: *mut u8, len: u32);
762
+ fn crossbind_v_f64s_from_js(h: usize, out: *mut f64, len: u32);
506
763
  }
507
764
 
508
765
  fn str_wire(s: &str) -> *mut u8 {
@@ -581,6 +838,47 @@ impl Clone for JsValue {
581
838
  }
582
839
  }
583
840
 
841
+ /// Typed arrays: a byte or double sequence crosses as the JS view of the same name, copied once
842
+ /// in each direction (a view into wasm memory would dangle when the heap grows).
843
+ pub struct JsBytes(pub Vec<u8>);
844
+
845
+ impl WireType for JsBytes {
846
+ type Wire = usize;
847
+ const SIG: char = 'i';
848
+ fn tid() -> *const c_void { unsafe { crossbind_tid_emval() } }
849
+ fn from_wire(w: usize) -> Self {
850
+ let len = unsafe { crossbind_v_typed_len(w) } as usize;
851
+ let mut data = vec![0u8; len];
852
+ unsafe { crossbind_v_bytes_from_js(w, data.as_mut_ptr(), len as u32) };
853
+ unsafe { crossbind_v_unref(w) };
854
+ JsBytes(data)
855
+ }
856
+ fn to_wire(self) -> usize {
857
+ unsafe { crossbind_v_bytes_to_js(self.0.as_ptr(), self.0.len() as u32) }
858
+ }
859
+ }
860
+
861
+ pub struct JsF64s(pub Vec<f64>);
862
+
863
+ impl WireType for JsF64s {
864
+ type Wire = usize;
865
+ const SIG: char = 'i';
866
+ fn tid() -> *const c_void { unsafe { crossbind_tid_emval() } }
867
+ fn from_wire(w: usize) -> Self {
868
+ let len = unsafe { crossbind_v_typed_len(w) } as usize;
869
+ let mut data = vec![0.0f64; len];
870
+ unsafe { crossbind_v_f64s_from_js(w, data.as_mut_ptr(), len as u32) };
871
+ unsafe { crossbind_v_unref(w) };
872
+ JsF64s(data)
873
+ }
874
+ fn to_wire(self) -> usize {
875
+ unsafe { crossbind_v_f64s_to_js(self.0.as_ptr(), self.0.len() as u32) }
876
+ }
877
+ }
878
+
879
+ impl ErrSentinel for JsBytes { fn err_sentinel() -> Self { JsBytes(Vec::new()) } }
880
+ impl ErrSentinel for JsF64s { fn err_sentinel() -> Self { JsF64s(Vec::new()) } }
881
+
584
882
  impl Drop for JsValue {
585
883
  fn drop(&mut self) {
586
884
  unsafe { crossbind_v_unref(self.handle) };
@@ -790,6 +1088,7 @@ pub fn class_<T: 'static>(name: &str) -> ClassBuilder<T> {
790
1088
  name_ptr = r.cstr(name);
791
1089
  dtor_sig = r.cstr("vp");
792
1090
  r.class_tid.insert(TypeId::of::<T>(), cls);
1091
+ r.class_ptr_tid.insert(TypeId::of::<T>(), ptr_ty);
793
1092
  }
794
1093
  unsafe {
795
1094
  crossbind_embind_register_class(
@@ -804,6 +1103,26 @@ pub fn class_<T: 'static>(name: &str) -> ClassBuilder<T> {
804
1103
  }
805
1104
 
806
1105
  impl<T: 'static> ClassBuilder<T> {
1106
+ /// A field that reads and writes as a JS property. The getter and setter are plain shim
1107
+ /// functions over the instance, so a private field can be exposed through accessors too.
1108
+ pub fn property<F: WireType>(self, name: &str, getter: fn(&mut T) -> F, setter: fn(&mut T, F)) -> Self {
1109
+ let (name_ptr, getter_sig, setter_sig);
1110
+ {
1111
+ let mut r = registry().lock().unwrap();
1112
+ name_ptr = r.cstr(name);
1113
+ getter_sig = r.cstr(&format!("{}pp", F::SIG));
1114
+ setter_sig = r.cstr(&format!("vpp{}", F::SIG));
1115
+ }
1116
+ unsafe {
1117
+ crossbind_embind_register_class_property(
1118
+ self.cls, name_ptr,
1119
+ F::tid(), getter_sig, property_getter::<T, F> as *const () as usize, getter as *mut c_void,
1120
+ F::tid(), setter_sig, property_setter::<T, F> as *const () as usize, setter as *mut c_void,
1121
+ );
1122
+ }
1123
+ self
1124
+ }
1125
+
807
1126
  /// Registers a smart-pointer type for this class so factory functions can return an
808
1127
  /// object that JS owns and frees on delete(). Call before `.createN`.
809
1128
  pub fn smart_ptr(mut self, name: &str) -> Self {
@@ -827,6 +1146,7 @@ impl<T: 'static> ClassBuilder<T> {
827
1146
  );
828
1147
  }
829
1148
  self.smart_ptr_ty = Some(ptr_ty);
1149
+ registry().lock().unwrap().owned_ptr_tid.insert(TypeId::of::<T>(), ptr_ty);
830
1150
  self
831
1151
  }
832
1152
 
@@ -918,6 +1238,10 @@ factories! {
918
1238
  create0 / factory_invoker0 : ;
919
1239
  create1 / factory_invoker1 : A0;
920
1240
  create2 / factory_invoker2 : A0, A1;
1241
+ create3 / factory_invoker3 : A0, A1, A2;
1242
+ create4 / factory_invoker4 : A0, A1, A2, A3;
1243
+ create5 / factory_invoker5 : A0, A1, A2, A3, A4;
1244
+ create6 / factory_invoker6 : A0, A1, A2, A3, A4, A5;
921
1245
  }
922
1246
 
923
1247
  // Arc-allocating factories for shared classes: identical registration shape, but the object
@@ -1006,6 +1330,9 @@ constructors! {
1006
1330
  constructor1 / ctor_invoker1 : A0;
1007
1331
  constructor2 / ctor_invoker2 : A0, A1;
1008
1332
  constructor3 / ctor_invoker3 : A0, A1, A2;
1333
+ constructor4 / ctor_invoker4 : A0, A1, A2, A3;
1334
+ constructor5 / ctor_invoker5 : A0, A1, A2, A3, A4;
1335
+ constructor6 / ctor_invoker6 : A0, A1, A2, A3, A4, A5;
1009
1336
  }
1010
1337
 
1011
1338
  // Raw-pointer ctor variants for fallible `new`: the shim boxes the value itself (or raises and
@@ -1050,6 +1377,9 @@ constructors_ptr! {
1050
1377
  constructor_ptr1 / ctor_ptr_invoker1 : A0;
1051
1378
  constructor_ptr2 / ctor_ptr_invoker2 : A0, A1;
1052
1379
  constructor_ptr3 / ctor_ptr_invoker3 : A0, A1, A2;
1380
+ constructor_ptr4 / ctor_ptr_invoker4 : A0, A1, A2, A3;
1381
+ constructor_ptr5 / ctor_ptr_invoker5 : A0, A1, A2, A3, A4;
1382
+ constructor_ptr6 / ctor_ptr_invoker6 : A0, A1, A2, A3, A4, A5;
1053
1383
  }
1054
1384
 
1055
1385
  // Raw-pointer factory variants: `Result<Self, E>` raises on Err, `Option<Self>` returns null
@@ -1095,6 +1425,10 @@ factories_ptr! {
1095
1425
  create_ptr0 / factory_ptr_invoker0 : ;
1096
1426
  create_ptr1 / factory_ptr_invoker1 : A0;
1097
1427
  create_ptr2 / factory_ptr_invoker2 : A0, A1;
1428
+ create_ptr3 / factory_ptr_invoker3 : A0, A1, A2;
1429
+ create_ptr4 / factory_ptr_invoker4 : A0, A1, A2, A3;
1430
+ create_ptr5 / factory_ptr_invoker5 : A0, A1, A2, A3, A4;
1431
+ create_ptr6 / factory_ptr_invoker6 : A0, A1, A2, A3, A4, A5;
1098
1432
  }
1099
1433
 
1100
1434
  // N-arity instance methods.
@@ -1141,6 +1475,8 @@ functions! {
1141
1475
  function2 / method_invoker2 : A0, A1;
1142
1476
  function3 / method_invoker3 : A0, A1, A2;
1143
1477
  function4 / method_invoker4 : A0, A1, A2, A3;
1478
+ function5 / method_invoker5 : A0, A1, A2, A3, A4;
1479
+ function6 / method_invoker6 : A0, A1, A2, A3, A4, A5;
1144
1480
  }
1145
1481
 
1146
1482
  // N-arity FREE functions (`Module.name(..)` in JS, no class). embind slices the target off
@@ -1183,4 +1519,6 @@ free_functions! {
1183
1519
  fn2 / free_invoker2 : A0, A1;
1184
1520
  fn3 / free_invoker3 : A0, A1, A2;
1185
1521
  fn4 / free_invoker4 : A0, A1, A2, A3;
1522
+ fn5 / free_invoker5 : A0, A1, A2, A3, A4;
1523
+ fn6 / free_invoker6 : A0, A1, A2, A3, A4, A5;
1186
1524
  }
@@ -40,6 +40,11 @@ void crossbind_embind_register_smart_ptr(
40
40
  void crossbind_embind_register_enum(
41
41
  CrossbindTid enum_ty, const char* name, size_t size, bool is_signed, int policy_value);
42
42
  void crossbind_embind_register_enum_value(CrossbindTid enum_ty, const char* value_name, int value);
43
+ void crossbind_embind_raise_error_coded(unsigned char* msg, unsigned char* code);
44
+ void crossbind_embind_register_constant(const char* name, const void* ty, double value);
45
+ void crossbind_embind_register_class_property(CrossbindTid cls, const char* name,
46
+ CrossbindTid getter_ty, const char* getter_sig, CrossbindFn getter, void* getter_ctx,
47
+ CrossbindTid setter_ty, const char* setter_sig, CrossbindFn setter, void* setter_ctx);
43
48
  void crossbind_embind_register_value_object(
44
49
  CrossbindTid struct_ty, const char* name,
45
50
  const char* ctor_sig, CrossbindFn ctor, const char* dtor_sig, CrossbindFn dtor);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossbind/core-embind-rust",
3
- "version": "2.0.0-beta.56",
3
+ "version": "2.0.0-beta.58",
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": {