crossbind 2.0.0-beta.60 → 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.
Files changed (43) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/actions/buildDependencies.js +22 -7
  4. package/src/actions/buildJs.js +1 -1
  5. package/src/actions/buildLib.js +2 -0
  6. package/src/actions/buildNode.js +149 -0
  7. package/src/actions/buildWasm.js +43 -66
  8. package/src/actions/createInterface.js +101 -27
  9. package/src/actions/createLib.js +3 -2
  10. package/src/actions/getAllBridges.js +1 -1
  11. package/src/actions/getLinkInputs.js +63 -0
  12. package/src/actions/licenses.js +34 -0
  13. package/src/actions/run.js +97 -2
  14. package/src/assets/cmake/CMakeLists.txt +1 -1
  15. package/src/assets/cmake/dist.cmake +1 -1
  16. package/src/assets/js-runtime/adapters/worker-comlink.js +6 -2
  17. package/src/assets/toolchain-digests.json +26 -12
  18. package/src/bin.js +59 -13
  19. package/src/index.js +3 -1
  20. package/src/integration/getCrossbindScript.js +8 -10
  21. package/src/integration/getDependFilePath.js +9 -12
  22. package/src/state/index.js +16 -13
  23. package/src/state/loadConfig.js +3 -0
  24. package/src/utils/appRustCrates.js +2 -0
  25. package/src/utils/appSources.js +21 -0
  26. package/src/utils/cargoImport.js +44 -0
  27. package/src/utils/cargoTarget.js +2 -0
  28. package/src/utils/cppDts.js +77 -41
  29. package/src/utils/embindArgumentGuards.js +45 -0
  30. package/src/utils/embindRsFingerprint.js +26 -0
  31. package/src/utils/headerImports.js +45 -0
  32. package/src/utils/inputStamp.js +23 -0
  33. package/src/utils/linkLayout.js +8 -1
  34. package/src/utils/pullDockerImage.js +8 -5
  35. package/src/utils/resolveCompanionPackage.js +51 -0
  36. package/src/utils/resolveEmbindNapi.js +22 -0
  37. package/src/utils/resolveEmbindRust.js +17 -52
  38. package/src/utils/runCargo.js +4 -3
  39. package/src/utils/rustBridgeGen.js +895 -250
  40. package/src/utils/swigInterface.js +19 -4
  41. package/src/utils/systemKeys.js +8 -0
  42. package/src/utils/targets.js +56 -0
  43. package/src/utils/cppFieldBindings.js +0 -23
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
  import resolveEmbindRustRoot, { embindRustVersion } from './resolveEmbindRust.js';
5
5
  import runCargo, { toHostPath } from './runCargo.js';
6
6
  import writeIfChanged from './writeIfChanged.js';
7
+ import { cargoMarkerName, parseCargoMarkerName } from './cargoImport.js';
7
8
 
8
9
  // Generates the embind bridge for a cargo package as a COMPANION CRATE, the Rust analog of the
9
10
  // C++ .i.cpp bridges: the user's crate stays plain Rust (no embind-rs dependency, no macro line)
@@ -25,7 +26,9 @@ import writeIfChanged from './writeIfChanged.js';
25
26
  // i64 / u64 cross as JS BigInt, the narrower integers and f32 as numbers, char as a
26
27
  // one-character string; Result<T, E> in any return above throws in JS on Err
27
28
  // (E: Display); Option<Self> factories return JS null; Option<i32/f64/bool/String> works in
28
- // params (undefined/null -> None) and returns (None -> null); other Option shapes skip
29
+ // params (undefined/null -> None) and returns (None -> null), Option<i64/u64> and
30
+ // Option<&[u8]/&[f64]> in params only; other Option shapes skip; a NonZero integer param
31
+ // crosses as its integer (and Option<NonZero..> where that Option does), 0 throws
29
32
  // vectors come from crossbind.config.mjs: export.bindings.vectors = [{ of: 'i32', name: '..' }]
30
33
  // Anything outside this surface is skipped WITH a log line - never silently.
31
34
 
@@ -33,10 +36,31 @@ const PRIMITIVES = new Set(['i32', 'i64', 'u64', 'f64', 'bool', 'String', '()',
33
36
  'i8', 'i16', 'u8', 'u16', 'u32', 'f32', 'usize', 'isize', 'char']);
34
37
  const PARAM_ONLY = new Set(['&str', '&String']);
35
38
  const OPTION_INNERS = new Set(['i32', 'f64', 'bool', 'String']);
36
- const OPTION_PARAM_RE = /^Option<(i32|f64|bool|String|&str)>$/;
39
+ const OPTION_PARAM_RE = /^Option<(i32|f64|bool|String|&str|i64|u64)>$/;
37
40
  const OPTION_CLASS_REF_RE = /^Option<&(\w+)>$/;
38
- const OPTIONAL_REG = { i32: 'register_optional_i32', f64: 'register_optional_f64', bool: 'register_optional_bool', String: 'register_optional_string' };
41
+ const OPTIONAL_REG = {
42
+ i32: 'register_optional_i32', f64: 'register_optional_f64', bool: 'register_optional_bool', String: 'register_optional_string',
43
+ i64: 'register_optional_bigint::<i64>', u64: 'register_optional_bigint::<u64>',
44
+ };
45
+ // A 64-bit optional crosses as a BigInt or null, an optional typed array as the view or null.
46
+ const OPTION_BIGINT_RE = /^Option<(i64|u64)>$/;
47
+ const OPTION_VIEW_RE = /^Option<&\[(u8|f64)\]>$/;
48
+ // A NonZero parameter crosses as its integer, inside Option wherever that integer's Option does;
49
+ // the shim turns 0 into a JS error before the call.
50
+ const NON_ZERO = {
51
+ NonZeroU8: 'u8', NonZeroU16: 'u16', NonZeroU32: 'u32', NonZeroU64: 'u64', NonZeroUsize: 'usize',
52
+ NonZeroI8: 'i8', NonZeroI16: 'i16', NonZeroI32: 'i32', NonZeroI64: 'i64', NonZeroIsize: 'isize',
53
+ };
54
+ const nonZeroBase = (ty) => NON_ZERO[ty] ?? null;
55
+ const optionNonZero = (ty) => {
56
+ const inner = String(ty).match(/^Option<(NonZero\w+)>$/)?.[1];
57
+ const base = NON_ZERO[inner];
58
+ return base && OPTION_PARAM_RE.test(`Option<${base}>`) ? { inner, base } : null;
59
+ };
39
60
  const VECTOR_ITEM_TYPES = new Set(['i32', 'f64', 'bool']);
61
+ // The native adapter calls an invoker with at most 8 integer slots: a constructor spends one on the
62
+ // baked function, a method two (the function and `this`), so constructors go one argument further.
63
+ const MAX_CTOR_ARGS = 7;
40
64
  // serde_json::Value params/returns cross as a deep JSON copy (adapter-side codec, canonical
41
65
  // token 'Json'); the bare `Value` spelling counts only when the file imports serde_json.
42
66
  const JSON_TY = 'Json';
@@ -60,18 +84,26 @@ function closureShape(ty) {
60
84
  // structs that derive Serialize/Deserialize. The canonical token keeps the Rust spelling, so the
61
85
  // shim can name the exact type it converts to.
62
86
  const COLLECTION_RE = /^(?:Vec<.+>|\[.+;\s*\d+\]|&\[.+\]|\(.+,.*\)|(?:std::collections::)?(?:HashMap|BTreeMap|HashSet|BTreeSet)<.+>)$/;
63
- // `&[u8]` / `Vec<u8>` and `&[f64]` / `Vec<f64>` are typed arrays on the JS side.
87
+ // `&[u8]` / `Vec<u8>` and `&[f64]` / `Vec<f64>` are typed arrays on the JS side. A parameter
88
+ // takes the `arg` wire, which reads the view without an emval handle.
64
89
  const TYPED_ARRAYS = {
65
- 'Vec<u8>': { wrapper: 'JsBytes', owned: 'Vec<u8>', slice: false },
66
- '&[u8]': { wrapper: 'JsBytes', owned: 'Vec<u8>', slice: true },
67
- 'Vec<f64>': { wrapper: 'JsF64s', owned: 'Vec<f64>', slice: false },
68
- '&[f64]': { wrapper: 'JsF64s', owned: 'Vec<f64>', slice: true },
90
+ 'Vec<u8>': { wrapper: 'JsBytes', arg: 'JsBytesArg', owned: 'Vec<u8>', slice: false },
91
+ '&[u8]': { wrapper: 'JsBytes', arg: 'JsBytesArg', owned: 'Vec<u8>', slice: true },
92
+ 'Vec<f64>': { wrapper: 'JsF64s', arg: 'JsF64sArg', owned: 'Vec<f64>', slice: false },
93
+ '&[f64]': { wrapper: 'JsF64s', arg: 'JsF64sArg', owned: 'Vec<f64>', slice: true },
69
94
  };
70
95
  const typedArrayOf = (ty) => TYPED_ARRAYS[String(ty).replace(/\s+/g, '')] ?? null;
71
96
 
97
+ // serde carries fixed arrays of up to 32 items, and only a literal length says how long one is:
98
+ // any other array would leave the generated bridge uncompilable.
99
+ const SERDE_MAX_ARRAY = 32;
100
+ const arraysCarry = (text) => [...text.matchAll(/;\s*([^\]]*?)\s*\]/g)]
101
+ .every((m) => /^\d+$/.test(m[1]) && Number(m[1]) <= SERDE_MAX_ARRAY);
102
+
72
103
  const isCollection = (ty) => {
73
104
  if (typedArrayOf(ty)) return false;
74
105
  const text = String(ty).trim();
106
+ if (!arraysCarry(text)) return false;
75
107
  if (COLLECTION_RE.test(text)) return true;
76
108
  // An optional collection rides the same wire: JSON writes None as null.
77
109
  const inner = text.match(/^Option<(.+)>$/)?.[1];
@@ -116,6 +148,12 @@ const matchJsTok = (ty, ctx) => {
116
148
  return m[1];
117
149
  };
118
150
  const FN_SIG_RE = /^pub (?:const )?fn (\w+)\s*\(([^)]*)\)\s*(?:->\s*([\w:<>(),& ]+?))?\s*\{/;
151
+ // embind-rs registers constants of these types only (its ConstantValue impls and constant_str).
152
+ const CONSTANT_TYPES = new Set(['i32', 'f64', 'bool', '&str']);
153
+ // A Rust integer literal: decimal, hex, octal or binary, with `_` separators.
154
+ const intLiteral = (text) => (text.startsWith('-') ? -1 : 1) * Number(text.replace(/^-/, '').replaceAll('_', ''));
155
+ // `name: Type`; a `mut` binding stays inside the function and changes nothing that crosses.
156
+ const PARAM_RE = /^(?:mut\s+)?(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String|i64|u64|NonZero\w+|&\s*\w+|&\s*\[\s*(?:u8|f64)\s*\])\s*>|Vec\s*<[^;]+>|\[[^\]]+;\s*\d+\]|&\s*\[[^\]]+\]|\([^)]*,[^)]*\)|(?:std\s*::\s*collections\s*::\s*)?(?:HashMap|BTreeMap|HashSet|BTreeSet)\s*<[^>]+>|serde_json\s*::\s*Value|(?:std\s*::\s*sync\s*::\s*)?Arc\s*<\s*\w+\s*>|embind_rs\s*::\s*Js(?:Value|Function)|&\s*\w+|[\w()]+)$/;
119
157
 
120
158
  // The parameter list is read by counting parentheses, because a closure parameter carries its
121
159
  // own: `f: impl Fn(i32) -> i32` would end the list early for a regex.
@@ -252,12 +290,17 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
252
290
  }
253
291
 
254
292
  // Direct CRATE import (`import { X } from 'cargo:uuid'` with top-level `cargoDependencies`
255
- // declaring `uuid`): no surface file and no package - the bridge is generated from the
256
- // upstream crate's OWN multi-file source. cargo metadata (which fetches on first run) locates
257
- // the source and the resolved feature set; the bridge crate is a normal rlib under
258
- // rust-bridges/, so the app super-staticlib flow links it like any app-local surface.
293
+ // declaring `uuid`, or `cargo:<crate>/<module>` for one public module): no surface file and no
294
+ // package - the bridge is generated from the upstream crate's OWN multi-file source. cargo
295
+ // metadata (which fetches on first run) locates the source and the resolved feature set; the
296
+ // bridge crate is a normal rlib under rust-bridges/, so the app super-staticlib flow links it
297
+ // like any app-local surface. Every import of one crate shares that bridge, so an item reached
298
+ // through the root and through a module registers once and is the same JS class for both.
299
+ const crateSourceCache = new Map();
259
300
  const crateModelCache = new Map();
260
- export function createCrateImportBridge({ crateName, spec, cacheDir, dtsMode = 'sync', log = console.log }) {
301
+ export function createCrateImportBridge({ crateName, modulePath = [], spec, cacheDir, dtsMode = 'sync', log = console.log }) {
302
+ const importName = [crateName, ...modulePath].join('/');
303
+ const crateIdent = crateName.replaceAll('-', '_');
261
304
  const stem = `crate_${crateName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}`;
262
305
  const dir = `${cacheDir}/rust-bridges/${stem}`;
263
306
  const depLine = String(spec).trim().startsWith('{') ? `${crateName} = ${spec}` : `${crateName} = "${spec}"`;
@@ -295,49 +338,183 @@ export function createCrateImportBridge({ crateName, spec, cacheDir, dtsMode = '
295
338
  // cargo metadata needs a resolvable lib target before the real bridge exists.
296
339
  if (!fs.existsSync(`${dir}/src/lib.rs`)) writeIfChanged(`${dir}/src/lib.rs`, '// crossbind placeholder\n');
297
340
 
298
- const cacheKey = `${dir}|${depLine}`;
299
- let model = crateModelCache.get(cacheKey);
300
- if (!model) {
301
- let meta;
341
+ const { srcDir, features } = crateSource({ dir, depLine, crateName, importName });
342
+ const ownKey = modulePath.join('::');
343
+ const imports = [];
344
+ for (const importPath of crateImports(cacheDir, crateName, modulePath)) {
345
+ const key = importPath.join('::');
346
+ const cacheKey = `${srcDir}|${features.join(',')}|${key}`;
302
347
  try {
303
- const probe = runCargo(['metadata', '--format-version', '1', '--manifest-path', `${dir}/Cargo.toml`], {
304
- capture: true, maxBuffer: 128 * 1024 * 1024,
305
- });
306
- if (probe.status !== 0) throw new Error(String(probe.stderr ?? '').trim() || `exit code ${probe.status}`);
307
- meta = JSON.parse(probe.stdout);
348
+ if (!crateModelCache.has(cacheKey)) {
349
+ const model = parseCrateSurface({ srcDir, features, modulePath: importPath, log });
350
+ // Always audible (the transformer silences routine skip-noise): an empty surface
351
+ // means the import binds NOTHING - generic/re-export-style crates need an app-local .rs.
352
+ if (key === ownKey && !model.classes.length && !model.enums.length && !model.freeFns.length) {
353
+ const source = modulePath.length ? `module ${modulePath.join('::')}` : 'its lib.rs';
354
+ console.warn(`crossbind: crate import '${importName}' has no bindable surface (${source} defines no in-grammar types) - write an app-local surface .rs over it instead`);
355
+ }
356
+ crateModelCache.set(cacheKey, model);
357
+ }
358
+ imports.push({ modulePath: importPath, model: crateModelCache.get(cacheKey) });
308
359
  } catch (e) {
309
- throw new Error(`crossbind: crate import '${crateName}': cargo metadata failed (${e.message})`, { cause: e });
310
- }
311
- const pkg = meta.packages.find((p) => p.name === crateName);
312
- if (!pkg) throw new Error(`crossbind: crate import '${crateName}': crate not found in the cargo dependency graph`);
313
- const features = meta.resolve?.nodes?.find((n) => n.id === pkg.id)?.features ?? [];
314
- const srcDir = path.join(path.dirname(toHostPath(pkg.manifest_path)), 'src');
315
- model = parseCrateSurface({ srcDir, features, log });
316
- // Always audible (the transformer silences routine skip-noise): an empty surface means
317
- // the import will bind NOTHING - generic/re-export-style crates need an app-local .rs.
318
- if (!model.classes.length && !model.enums.length && !model.freeFns.length) {
319
- console.warn(`crossbind: crate import '${crateName}' has no bindable surface (its lib.rs defines no in-grammar types) - write an app-local surface .rs over it instead`);
360
+ if (key === ownKey) throw e;
361
+ log(`crossbind: crate import '${[crateName, ...importPath].join('/')}' left out of the ${crateName} bridge (${e.message})`);
320
362
  }
321
- crateModelCache.set(cacheKey, model);
322
363
  }
364
+ const shared = mergeCrateImports(crateIdent, crateName, imports, log);
323
365
 
324
366
  const jsonDeps = ['serde', 'serde_json'].filter((dep) => dep !== crateName).map((dep) => `${dep} = "1"`);
325
- writeIfChanged(`${dir}/Cargo.toml`, manifestWith(model.usesJson ? jsonDeps : []));
326
- // Two crates can export the same type name (semver::Version and uuid::Version both do), so a
327
- // crate import registers under `<crate>_<Name>` and the proxy module re-exports the clean name.
328
- const namePrefix = `${crateName.replaceAll('-', '_')}_`;
329
- const bridge = emitBridge(model, { userCrate: crateName.replaceAll('-', '_'), vectors: [], log, namePrefix });
330
- writeIfChanged(`${dir}/src/lib.rs`, bridge);
331
- // Editor types for the `cargo:<crate>` import: an ambient module the app's tsconfig
332
- // includes (e.g. "include": ["**/*", ".crossbind/rust-crates/types/**/*.d.ts"]). The scheme
333
- // keeps the module name unique, so npm packages/@types of the same name never clash.
334
- const dtsBody = emitDts(model, [], dtsMode).split('\n').map((l) => (l ? ` ${l}` : l)).join('\n');
335
- writeIfChanged(`${cacheDir}/rust-crates/types/${crateName}.d.ts`, `declare module 'cargo:${crateName}' {\n${dtsBody}\n}\n`);
367
+ writeIfChanged(`${dir}/Cargo.toml`, manifestWith(shared.model.usesJson ? jsonDeps : []));
368
+ writeIfChanged(`${dir}/src/lib.rs`, emitBridge(shared.model, {
369
+ userCrate: crateIdent, vectors: [], log, rustPaths: shared.rustPaths, wireNames: shared.wireNames,
370
+ }));
371
+ for (const { modulePath: importPath } of imports) {
372
+ writeImportDts({ cacheDir, crateName, modulePath: importPath, shared, dtsMode });
373
+ }
374
+ return {
375
+ bridgeDir: dir,
376
+ crateName: `${stem}_crossbind_app`,
377
+ model: imports.find((i) => i.modulePath.join('::') === ownKey).model,
378
+ exports: shared.exports.get(ownKey),
379
+ };
380
+ }
381
+
382
+ // cargo metadata locates the crate's source and the feature set it resolves to (fetching the crate
383
+ // on first use); both hold for every import of the crate.
384
+ function crateSource({ dir, depLine, crateName, importName }) {
385
+ const cacheKey = `${dir}|${depLine}`;
386
+ if (crateSourceCache.has(cacheKey)) return crateSourceCache.get(cacheKey);
387
+ let meta;
388
+ try {
389
+ const probe = runCargo(['metadata', '--format-version', '1', '--manifest-path', `${dir}/Cargo.toml`], {
390
+ capture: true, maxBuffer: 128 * 1024 * 1024,
391
+ });
392
+ if (probe.status !== 0) throw new Error(String(probe.stderr ?? '').trim() || `exit code ${probe.status}`);
393
+ meta = JSON.parse(probe.stdout);
394
+ } catch (e) {
395
+ throw new Error(`crossbind: crate import '${importName}': cargo metadata failed (${e.message})`, { cause: e });
396
+ }
397
+ const pkg = meta.packages.find((p) => p.name === crateName);
398
+ if (!pkg) throw new Error(`crossbind: crate import '${importName}': crate not found in the cargo dependency graph`);
399
+ const source = {
400
+ features: meta.resolve?.nodes?.find((n) => n.id === pkg.id)?.features ?? [],
401
+ srcDir: path.join(path.dirname(toHostPath(pkg.manifest_path)), 'src'),
402
+ };
403
+ crateSourceCache.set(cacheKey, source);
404
+ return source;
405
+ }
406
+
407
+ // The imports a crate's bridge serves: the root, the import at hand and every module import a
408
+ // marker names. Markers outlive a removed import, so the caller leaves out one that no longer
409
+ // resolves instead of failing the others.
410
+ function crateImports(cacheDir, crateName, modulePath) {
411
+ const dir = `${cacheDir}/rust-crates`;
412
+ const keys = new Set(['', modulePath.join('::')]);
413
+ for (const file of fs.existsSync(dir) ? fs.readdirSync(dir) : []) {
414
+ if (!file.endsWith('.rs')) continue;
415
+ const marker = parseCargoMarkerName(file.slice(0, -'.rs'.length));
416
+ if (marker.crateName === crateName) keys.add(marker.modulePath.join('::'));
417
+ }
418
+ return [...keys].sort().map((key) => (key ? key.split('::') : []));
419
+ }
420
+
421
+ const unionByName = (a, b) => [...a, ...b.filter((x) => !a.some((y) => y.name === x.name))];
422
+
423
+ // The same struct read through two imports: each may have bound members the other could not type
424
+ // (a return type only the other import collects), so the bridge takes both.
425
+ function mergeClass(a, b) {
336
426
  return {
337
- bridgeDir: dir, crateName: `${stem}_crossbind_app`, model, namePrefix,
427
+ ...a,
428
+ ctor: a.ctor ?? b.ctor,
429
+ factories: unionByName(a.factories, b.factories),
430
+ statics: unionByName(a.statics ?? [], b.statics ?? []),
431
+ methods: unionByName(a.methods, b.methods),
432
+ fields: unionByName(a.fields ?? [], b.fields ?? []),
433
+ hasDefault: a.hasDefault || b.hasDefault,
434
+ clonable: a.clonable || b.clonable,
435
+ hasDisplay: a.hasDisplay || b.hasDisplay,
436
+ shared: a.shared || b.shared,
338
437
  };
339
438
  }
340
439
 
440
+ // One model for every import of a crate. An item registers once, under a name taken from the
441
+ // module that defines it, so the root and a module import reach the same JS class; Rust names it
442
+ // by the path of the first import that reaches it (items of a private module are reachable only
443
+ // through their re-export). Each import keeps the list of names its JS module exports.
444
+ function mergeCrateImports(crateIdent, crateName, imports, log) {
445
+ const exported = ['classes', 'streams', 'enums', 'freeFns', 'consts'];
446
+ const lists = [...exported, 'valueObjects', 'newtypes', 'jsonTypes'];
447
+ const model = { usesJson: false, sharedOf: [], ...Object.fromEntries(lists.map((list) => [list, []])) };
448
+ const rustPaths = new Map();
449
+ const wireNames = new Map();
450
+ const homes = new Map();
451
+ const exports = new Map();
452
+ const names = new Map();
453
+ for (const { modulePath, model: part } of imports) {
454
+ const key = modulePath.join('::');
455
+ const access = [crateIdent, ...modulePath].join('::');
456
+ const own = [];
457
+ const itemNames = new Set();
458
+ for (const list of lists) {
459
+ for (const item of part[list]) {
460
+ const home = part.definedIn?.get(item.name) ?? key;
461
+ if (homes.has(item.name) && homes.get(item.name) !== home) {
462
+ log(`crossbind: rust bridge: cargo:${[crateName, ...modulePath].join('/')} ${item.name} skipped (another import of ${crateName} binds a different ${item.name})`);
463
+ continue;
464
+ }
465
+ const registered = list === 'freeFns' ? item.jsName : item.name;
466
+ if (!homes.has(item.name)) {
467
+ homes.set(item.name, home);
468
+ rustPaths.set(item.name, `${access}::${item.name}`);
469
+ wireNames.set(registered, `${[crateIdent, ...(home ? home.split('::') : [])].join('__')}_${registered}`);
470
+ model[list] = [...model[list], item];
471
+ } else if (list === 'classes') {
472
+ model.classes = model.classes.map((c) => (c.name === item.name ? mergeClass(c, item) : c));
473
+ }
474
+ itemNames.add(item.name);
475
+ if (exported.includes(list)) own.push({ local: registered, wire: wireNames.get(registered) });
476
+ }
477
+ }
478
+ model.usesJson ||= part.usesJson;
479
+ model.sharedOf = [...new Set([...model.sharedOf, ...part.sharedOf])];
480
+ exports.set(key, own);
481
+ names.set(key, itemNames);
482
+ }
483
+ return { model, rustPaths, wireNames, exports, names };
484
+ }
485
+
486
+ // Editor types for one `cargo:` import: an ambient module the app's tsconfig includes (e.g.
487
+ // "include": ["**/*", ".crossbind/rust-crates/types/**/*.d.ts"]); the scheme keeps the module
488
+ // name unique, so npm packages/@types of the same name never clash. An import declares its own
489
+ // items as the shared bridge binds them and imports a type it names from the import exporting it.
490
+ function writeImportDts({ cacheDir, crateName, modulePath, shared, dtsMode }) {
491
+ const key = modulePath.join('::');
492
+ const mine = shared.names.get(key);
493
+ const pick = (list) => list.filter((item) => mine.has(item.name));
494
+ const body = emitDts({
495
+ ...shared.model,
496
+ classes: pick(shared.model.classes),
497
+ streams: pick(shared.model.streams),
498
+ enums: pick(shared.model.enums),
499
+ freeFns: pick(shared.model.freeFns),
500
+ consts: pick(shared.model.consts),
501
+ valueObjects: pick(shared.model.valueObjects),
502
+ newtypes: pick(shared.model.newtypes),
503
+ jsonTypes: pick(shared.model.jsonTypes),
504
+ }, [], dtsMode);
505
+ const borrowed = new Map();
506
+ for (const type of [...shared.model.classes, ...shared.model.enums, ...shared.model.valueObjects]) {
507
+ if (mine.has(type.name) || !new RegExp(`\\b${type.name}\\b`).test(body)) continue;
508
+ const from = [...shared.names].find(([other, set]) => other !== key && set.has(type.name))?.[0] ?? '';
509
+ borrowed.set(from, [...(borrowed.get(from) ?? []), type.name]);
510
+ }
511
+ const imports = [...borrowed].map(([from, types]) => (
512
+ `import type { ${types.join(', ')} } from 'cargo:${[crateName, ...(from ? from.split('::') : [])].join('/')}';`));
513
+ const text = [...imports, body].join('\n').split('\n').map((l) => (l ? ` ${l}` : l)).join('\n');
514
+ writeIfChanged(`${cacheDir}/rust-crates/types/${cargoMarkerName({ crateName, modulePath })}.d.ts`,
515
+ `declare module 'cargo:${[crateName, ...modulePath].join('/')}' {\n${text}\n}\n`);
516
+ }
517
+
341
518
  // The embind-rs runtime crate ships inside @crossbind/core-embind-rust; the consumer (a plugin
342
519
  // or the package itself) declares that dependency and resolveEmbindRust finds it.
343
520
  function resolveEmbindRsDir() {
@@ -370,31 +547,60 @@ export function parseSurface(src, log) {
370
547
  return finalizeModel(acc, log);
371
548
  }
372
549
 
373
- // Parses an UPSTREAM crate for a direct crate import (`import .. from '<crate>'`): exported
374
- // types come from lib.rs; inherent impls and `impl Display` are collected across the crate's
375
- // `mod`-declared files, following the resolved feature set for cfg-gated modules.
376
- export function parseCrateSurface({ srcDir, features = [], log = console.log }) {
550
+ // Parses an UPSTREAM crate for a direct crate import (`import .. from 'cargo:<crate>'`):
551
+ // exported types come from lib.rs, or from the imported module (`cargo:<crate>/<module>`);
552
+ // inherent impls and `impl Display` are collected across the crate's `mod`-declared files,
553
+ // following the resolved feature set for cfg-gated modules.
554
+ export function parseCrateSurface({ srcDir, features = [], modulePath = [], log = console.log }) {
377
555
  const acc = newAcc();
378
556
  const enabled = new Set(features);
379
557
  const seen = new Set();
558
+ const imported = modulePath.join('::');
380
559
  const walk = (file, modPath) => {
381
- if (seen.has(file) || !fs.existsSync(file)) return;
560
+ if (!file || seen.has(file) || !fs.existsSync(file)) return;
382
561
  seen.add(file);
383
- // Root and glob-re-exported modules collect their pub types; other modules only
384
- // contribute impls (plus types individually re-exported by name).
385
- const collectTypes = modPath.length === 0 || acc.globModules.has(modPath.join('::'));
386
- const mods = scanSource(fs.readFileSync(file, 'utf8'), acc, { collectTypes, log });
387
- const childBase = file.endsWith('lib.rs') || file.endsWith('mod.rs') ? path.dirname(file) : file.slice(0, -3);
562
+ // The imported module and its glob-re-exported modules collect their pub types; other
563
+ // modules only contribute impls (plus types individually re-exported by name).
564
+ const here = modPath.join('::');
565
+ const collectTypes = here === imported || acc.globModules.has(here);
566
+ const mods = scanSource(fs.readFileSync(file, 'utf8'), acc, { collectTypes, modPath, enabled, log });
388
567
  for (const m of mods) {
389
- if (!cfgEnabled(m.cfg, enabled)) continue;
390
- walk(path.join(childBase, `${m.name}.rs`), [...modPath, m.name]);
391
- walk(path.join(childBase, m.name, 'mod.rs'), [...modPath, m.name]);
568
+ if (cfgEnabled(m.cfg, enabled)) walk(modFile(file, m.name), [...modPath, m.name]);
392
569
  }
393
570
  };
394
- walk(path.join(srcDir, 'lib.rs'), []);
571
+ const lib = path.join(srcDir, 'lib.rs');
572
+ // The imported module is read first, so its types are known wherever their impls live.
573
+ if (modulePath.length) walk(findModule(lib, modulePath, enabled, srcDir), modulePath);
574
+ walk(lib, []);
395
575
  return finalizeModel(acc, log);
396
576
  }
397
577
 
578
+ // `mod name;` lives in name.rs or name/mod.rs: beside lib.rs and mod.rs, else in the directory
579
+ // named after the declaring file.
580
+ function modFile(file, name) {
581
+ const base = file.endsWith('lib.rs') || file.endsWith('mod.rs') ? path.dirname(file) : file.slice(0, -3);
582
+ return [path.join(base, `${name}.rs`), path.join(base, name, 'mod.rs')].find((f) => fs.existsSync(f));
583
+ }
584
+
585
+ // Follows the `mod` declarations from lib.rs down to the imported module, which has to be public
586
+ // at every step (the bridge names it by path from outside the crate).
587
+ function findModule(lib, modulePath, enabled, srcDir) {
588
+ const where = modulePath.join('::');
589
+ let file = lib;
590
+ for (const name of modulePath) {
591
+ const decl = fs.existsSync(file)
592
+ ? scanSource(fs.readFileSync(file, 'utf8'), newAcc(), { collectTypes: false, log: () => {} })
593
+ .find((m) => m.name === name && cfgEnabled(m.cfg, enabled))
594
+ : undefined;
595
+ file = decl && modFile(file, name);
596
+ if (!file) {
597
+ throw new Error(`crossbind: rust bridge: module '${where}' not found in ${srcDir} - a module behind a cargo feature needs that feature in cargoDependencies, and inline mod { } blocks are not read`);
598
+ }
599
+ if (!decl.isPub) throw new Error(`crossbind: rust bridge: module '${where}' in ${srcDir} is private to the crate`);
600
+ }
601
+ return file;
602
+ }
603
+
398
604
  function newAcc() {
399
605
  return {
400
606
  enums: [], // { name, variants: [{ name, value }] }
@@ -406,41 +612,80 @@ function newAcc() {
406
612
  freeFns: [], // { name, jsName, args, ret, throws }
407
613
  displayNames: new Set(),
408
614
  wantedTypes: new Set(), // type names re-exported from lib.rs (`pub use ..::{X}`)
615
+ wantedFns: new Map(), // fn name -> module path it is re-exported from (`pub use ..::m::{f}`)
409
616
  globModules: new Set(), // module paths glob-re-exported from lib.rs (`pub use ..::m::*`)
617
+ traitImpls: new Map(), // type name -> traits it implements by hand (`impl Default for X`)
618
+ definedIn: new Map(), // item name -> path of the module that defines it
619
+ genericStructs: new Map(), // name -> { generics, definedIn } of `pub struct X<..>`
620
+ genericImpls: new Map(), // name -> [{ header, fns }] of `impl<..> X<..>` blocks
621
+ streamTraits: new Map(), // name -> { trait: 'Write' | 'Read', header } of its std::io impl
622
+ pending: [], // fn signatures in source order, typed once every type is known
623
+ structModules: new Map(), // struct name -> modules that declare one, pub or not
410
624
  };
411
625
  }
412
626
 
413
- // `#[cfg(..)]` on a `mod` line: null = ungated; no feature tokens = never enabled (so
414
- // cfg(test) / cfg(target_os = ..) modules are skipped).
627
+ // A name several modules declare (regex's `Regex` in both `regex::string` and `regex::bytes`)
628
+ // takes the impls of the module that defined the bound struct only.
629
+ function implOfBound(acc, item) {
630
+ return (acc.structModules.get(item.className)?.size ?? 0) <= 1 || acc.definedIn.get(item.className) === item.module;
631
+ }
632
+
633
+ // What a signature may lean on, as of the file it was read in.
634
+ function scanFlags(acc, collectTypes) {
635
+ return { allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse };
636
+ }
637
+
638
+ // The `#[cfg(..)]` predicate on an item (stacked ones combine as all(..)), or null when ungated.
415
639
  function cfgOf(attrs) {
416
- const cfg = attrs.find((a) => a.startsWith('#[cfg('));
417
- if (!cfg) return null;
418
- const features = [...cfg.matchAll(/feature\s*=\s*"([^"]+)"/g)].map((m) => m[1]);
419
- return { features, any: /cfg\(any/.test(cfg) };
640
+ const predicates = attrs.map((a) => a.match(/^#\[cfg\(([\s\S]*)\)\]$/)?.[1]).filter(Boolean);
641
+ if (!predicates.length) return null;
642
+ return predicates.length === 1 ? predicates[0] : `all(${predicates.join(', ')})`;
643
+ }
644
+
645
+ // A cfg predicate against the enabled cargo features: true, false, or null when it depends on
646
+ // the target (or on test/debug settings), which one generated bridge cannot know.
647
+ function cfgValue(predicate, enabled) {
648
+ const text = predicate.trim();
649
+ const feature = text.match(/^feature\s*=\s*"([^"]+)"$/);
650
+ if (feature) return enabled.has(feature[1]);
651
+ const call = text.match(/^(any|all|not)\s*\(([\s\S]*)\)$/);
652
+ if (!call) return null;
653
+ const values = splitTopLevel(call[2]).map((p) => p.trim()).filter(Boolean).map((p) => cfgValue(p, enabled));
654
+ if (call[1] === 'not') return values[0] === null ? null : !values[0];
655
+ if (call[1] === 'any') return values.includes(true) ? true : values.every((v) => v === false) ? false : null;
656
+ return values.includes(false) ? false : values.every((v) => v === true) ? true : null;
420
657
  }
421
658
 
659
+ // Only what every target compiles is bound: an item a target might lack would break its build.
422
660
  function cfgEnabled(cfg, enabled) {
423
- if (cfg === null) return true;
424
- if (cfg.features.length === 0) return false;
425
- return cfg.any ? cfg.features.some((f) => enabled.has(f)) : cfg.features.every((f) => enabled.has(f));
661
+ return cfg === null || cfgValue(cfg, enabled) === true;
426
662
  }
427
663
 
428
664
  // Scans ONE source file into the accumulator; returns the `mod name;` declarations found.
429
- // collectTypes: types and free fns register only from the crate root (lib.rs) - module files
430
- // contribute impls (methods, factories, Display) for already-known types.
431
- function scanSource(src, acc, { collectTypes, log }) {
665
+ // collectTypes: types and free fns register only from the imported root (lib.rs, or the module
666
+ // of a module import) - other files contribute impls (methods, factories, Display) for
667
+ // already-known types. modPath places the file in the crate, for relative `pub use` paths.
668
+ function scanSource(src, acc, { collectTypes, log, modPath = [], enabled = new Set() }) {
432
669
  const { enums, valueObjects, newtypes, jsonTypes, consts, classes, freeFns, displayNames, wantedTypes } = acc;
433
670
  acc.hasSerdeUse ||= /\buse\s+serde_json\b/.test(src);
434
671
  acc.hasArcUse ||= /\buse\s+std::sync::(?:Arc\b|\{[^}]*\bArc\b)/.test(src);
435
672
  acc.hasEmbindUse ||= /\buse\s+embind_rs::/.test(src);
673
+ const here = modPath.join('::');
436
674
  const mods = [];
437
- // Strip line comments; join multi-line `pub fn` signatures (to their brace) and multi-line
438
- // `pub use` re-export lists (to their semicolon).
439
- const rawLines = src.split('\n').map((l) => l.replace(/\/\/.*$/, ''));
675
+ const reexportedFrom = new Set();
676
+ // Strip line comments; join multi-line `pub fn` signatures (to their brace), multi-line
677
+ // `pub use` re-export lists (to their semicolon) and multi-line attributes (to their bracket).
678
+ const rawLines = src.split('\n').map(stripLineComment);
440
679
  const lines = [];
680
+ const unclosed = (line) => (line.match(/\[/g) ?? []).length > (line.match(/\]/g) ?? []).length;
441
681
  for (let i = 0; i < rawLines.length; i += 1) {
442
682
  let line = rawLines[i];
443
- if (/\bpub (?:const )?fn\b/.test(line)) {
683
+ if (/^\s*#!?\[/.test(line)) {
684
+ while (unclosed(line) && i + 1 < rawLines.length) {
685
+ i += 1;
686
+ line = `${line} ${rawLines[i].trim()}`;
687
+ }
688
+ } else if (/\bpub (?:const )?fn\b/.test(line)) {
444
689
  while (!line.includes('{') && !line.includes(';') && i + 1 < rawLines.length) {
445
690
  i += 1;
446
691
  line = `${line} ${rawLines[i].trim()}`;
@@ -460,28 +705,44 @@ function scanSource(src, acc, { collectTypes, log }) {
460
705
  if (t.startsWith('#[') || t.startsWith('#![')) { attrs.push(t); continue; }
461
706
  if (t === '') continue;
462
707
 
708
+ // An item behind a cfg this build leaves off, or one only some targets compile, is not
709
+ // there to bind. A `mod` keeps its cfg: the walk and the module lookup decide on it.
710
+ if (!cfgEnabled(cfgOf(attrs), enabled) && !/^(?:pub(?:\([^)]*\))?\s+)?mod\s/.test(t)) {
711
+ if (!(t.endsWith(';') && !t.includes('{'))) i = skipBlock(lines, i);
712
+ attrs = [];
713
+ continue;
714
+ }
715
+
463
716
  // Crate-root re-exports make module-defined types part of the surface: capitalized
464
717
  // leaves of `pub use ..::{X, Y};` are collected when the defining module is scanned.
465
718
  if (collectTypes && t.startsWith('pub use ')) {
466
719
  // Named re-exports mark module types as surface; glob re-exports mark their whole
467
720
  // module as root-like (its pub types are collected when the walk reaches it).
468
721
  const body = t.replace(/^pub use\s+/, '').replace(/;.*$/, '').trim();
722
+ // `crate::` paths start at the crate root; the others at the module being scanned.
723
+ const resolve = (p) => (p.startsWith('crate::') ? p.slice('crate::'.length) : [...modPath, p.replace(/^self::/, '')].join('::'));
469
724
  const items = [];
470
- const braced = body.match(/^(?:crate::|self::)?(?:([\w:]+)::)?\{(.*)\}$/);
725
+ const braced = body.match(/^((?:crate::|self::)?(?:[\w:]+::)?)\{(.*)\}$/);
471
726
  if (braced) {
472
- const prefix = braced[1] ? `${braced[1]}::` : '';
473
727
  braced[2].split(',').map((s) => s.trim()).filter(Boolean)
474
- .forEach((s) => items.push(prefix + s.replace(/^(?:crate::|self::)/, '')));
728
+ .forEach((s) => items.push(resolve(`${braced[1]}${s}`)));
475
729
  } else {
476
- items.push(body.replace(/^(?:crate::|self::)/, ''));
730
+ items.push(resolve(body));
477
731
  }
478
732
  for (const item of items) {
733
+ // The module an item comes from is walked even when its `mod` line is out of
734
+ // sight, declared inside a macro (argon2-rust declares its modules that way).
735
+ const rel = !here ? item : item.startsWith(`${here}::`) ? item.slice(here.length + 2) : '';
736
+ if (rel.includes('::')) reexportedFrom.add(rel.split('::')[0]);
479
737
  if (item.endsWith('::*')) {
480
738
  acc.globModules.add(item.slice(0, -3));
481
- } else {
482
- const leaf = (item.split('::').pop() ?? '').replace(/\s+as\s+\w+$/, '');
483
- if (/^[A-Z]\w*$/.test(leaf)) wantedTypes.add(leaf);
739
+ continue;
484
740
  }
741
+ // A renamed re-export exists here only under its new name.
742
+ if (/\s+as\s+\w+$/.test(item)) continue;
743
+ const leaf = item.split('::').pop() ?? '';
744
+ if (/^[A-Z]\w*$/.test(leaf)) wantedTypes.add(leaf);
745
+ else acc.wantedFns.set(leaf, item.split('::').slice(0, -1).join('::'));
485
746
  }
486
747
  attrs = [];
487
748
  continue;
@@ -489,32 +750,58 @@ function scanSource(src, acc, { collectTypes, log }) {
489
750
 
490
751
  let enumM = t.match(/^pub enum (\w+)/);
491
752
  if (enumM && !collectTypes && !wantedTypes.has(enumM[1])) enumM = null;
753
+ const anyStruct = t.match(/^(?:pub(?:\([^)]*\))?\s+)?struct (\w+)/);
754
+ if (anyStruct) acc.structModules.set(anyStruct[1], new Set([...(acc.structModules.get(anyStruct[1]) ?? []), here]));
492
755
  let structM = t.match(/^pub struct (\w+)/);
493
756
  if (structM && !collectTypes && !wantedTypes.has(structM[1])) structM = null;
757
+ const genericStructM = structM && t.match(/^pub struct \w+\s*<(.*)>/);
494
758
  const modM = t.match(/^(?:pub(?:\([^)]*\))?\s+)?mod (\w+)\s*([;{])/);
495
759
  // `pub const NAME: T = value;` (and `pub static`) - a value, not a getter, in JS.
496
760
  const constM = collectTypes ? t.match(/^pub (?:const|static) (\w+)\s*:\s*([\w()&' ]+?)\s*=\s*(.+?);$/) : null;
497
761
  const displayM = t.match(/^impl (?:[\w:]+::)?Display for (\w+)/);
498
762
  const traitImplM = !displayM && /^impl\b[^{]*\bfor\b/.test(t);
499
763
  const implM = displayM || traitImplM ? null : t.match(/^impl (\w+)\s*\{/);
500
- const freeFnM = collectTypes && !enumM && !structM && !modM && !displayM && !traitImplM && !implM
764
+ const otherImplM = !displayM && !traitImplM && !implM && /^(?:unsafe\s+)?impl\b/.test(t);
765
+ const fnSig = !enumM && !structM && !modM && !displayM && !traitImplM && !implM && !otherImplM
501
766
  ? matchFnSignature(t) : null;
767
+ // A free fn binds from the imported root, or from the module the root re-exports it from.
768
+ const freeFnM = fnSig && (collectTypes || acc.wantedFns.get(fnSig[1]) === here) ? fnSig : null;
502
769
 
503
770
  if (enumM) {
504
771
  const derive = attrs.find((a) => a.startsWith('#[derive')) ?? '';
505
772
 
506
773
  const variants = [];
507
774
  let idx = 0;
775
+ let variantAttrs = [];
776
+ // A variant only some targets compile shifts the implicit discriminants after it.
777
+ let shifted = false;
778
+ let targetDependent = false;
508
779
  for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1) {
509
- const v = lines[i].trim().match(/^(\w+)(?:\s*=\s*(-?\d+))?\s*,?$/);
510
- if (v) { variants.push({ name: v[1], value: v[2] !== undefined ? Number(v[2]) : idx }); idx = variants.at(-1).value + 1; }
780
+ const line = lines[i].trim();
781
+ if (line.startsWith('#[')) { variantAttrs.push(line); continue; }
782
+ const v = line.match(/^(\w+)(?:\s*=\s*(-?(?:0x[\da-fA-F_]+|0o[0-7_]+|0b[01_]+|\d[\d_]*)))?\s*,?$/);
783
+ const cfg = cfgOf(variantAttrs);
784
+ if (line) variantAttrs = [];
785
+ if (!v) continue;
786
+ const compiled = cfg === null || cfgValue(cfg, enabled);
787
+ if (v[2] === undefined && shifted) targetDependent = true;
788
+ if (compiled === null) { shifted = true; continue; }
789
+ if (!compiled) continue;
790
+ if (v[2] !== undefined) shifted = false;
791
+ variants.push({ name: v[1], value: v[2] !== undefined ? intLiteral(v[2]) : idx });
792
+ idx = variants.at(-1).value + 1;
511
793
  }
512
794
  const hasSerde = derive.includes('Serialize') && derive.includes('Deserialize');
513
- if (variants.length) enums.push({ name: enumM[1], variants });
795
+ if (targetDependent) log(`crossbind: rust bridge: enum ${enumM[1]} skipped (its discriminants depend on the target)`);
796
+ else if (variants.length) enums.push({ name: enumM[1], variants });
514
797
  // A variant that carries data cannot be an embind enum, but it can cross as a plain
515
798
  // JS value when the enum derives serde - in serde's own representation.
516
799
  else if (hasSerde) jsonTypes.push({ name: enumM[1] });
517
800
  else log(`crossbind: rust bridge: enum ${enumM[1]} skipped (a data enum crosses only when it derives Serialize and Deserialize)`);
801
+ } else if (genericStructM) {
802
+ // A generic struct binds only as a std::io stream (streamsOf decides).
803
+ acc.genericStructs.set(structM[1], { generics: genericStructM[1], definedIn: here });
804
+ if (!(t.endsWith(';') && !t.includes('{'))) i = skipBlock(lines, i);
518
805
  } else if (structM) {
519
806
  const isReprC = attrs.some((a) => /repr\(C\)/.test(a));
520
807
  const derive = attrs.find((a) => a.startsWith('#[derive')) ?? '';
@@ -525,13 +812,11 @@ function scanSource(src, acc, { collectTypes, log }) {
525
812
  newtypes.push({ name: newtypeM[1], inner: newtypeM[2] });
526
813
  } else if (isReprC) {
527
814
  const ok = derive.includes('Default') && derive.includes('Copy');
528
- const fields = [];
529
- for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1) {
530
- const f = lines[i].trim().match(/^pub (\w+)\s*:\s*([\w()]+)\s*,?$/);
531
- // A field may be another value object: embind nests them, and the wrapper is
532
- // repr(transparent) over the user struct, so the offset still lands right.
533
- if (f && (PRIMITIVES.has(f[2]) || valueObjects.some((v) => v.name === f[2]))) fields.push({ name: f[1], type: f[2] });
534
- }
815
+ const body = structFields(lines, i, enabled);
816
+ i = body.end;
817
+ // A field may be another value object: embind nests them, and the wrapper is
818
+ // repr(transparent) over the user struct, so the offset still lands right.
819
+ const fields = body.fields.filter((f) => PRIMITIVES.has(f.type) || valueObjects.some((v) => v.name === f.type));
535
820
  if (ok && fields.length) valueObjects.push({ name: structM[1], fields, serde: derive.includes('Serialize') && derive.includes('Deserialize') });
536
821
  else log(`crossbind: rust bridge: struct ${structM[1]} skipped (repr(C) needs derive(Default, Copy) and pub primitive or value-object fields)`);
537
822
  } else if (derive.includes('Serialize') && derive.includes('Deserialize') && !newtypeM) {
@@ -541,67 +826,222 @@ function scanSource(src, acc, { collectTypes, log }) {
541
826
  if (!jsonTypes.some((j) => j.name === structM[1])) jsonTypes.push({ name: structM[1] });
542
827
  if (t.endsWith('{')) { for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1); }
543
828
  } else {
544
- if (!classes.has(structM[1])) classes.set(structM[1], { name: structM[1], ctor: null, factories: [], methods: [], fields: [], hasDefault: derive.includes('Default') });
829
+ if (!classes.has(structM[1])) {
830
+ classes.set(structM[1], {
831
+ name: structM[1], ctor: null, factories: [], methods: [], statics: [], fields: [],
832
+ hasDefault: derive.includes('Default'), clonable: /\b(?:Clone|Copy)\b/.test(derive),
833
+ });
834
+ }
545
835
  const cls = classes.get(structM[1]);
546
836
  cls.serde = derive.includes('Serialize') && derive.includes('Deserialize');
547
837
  // Public fields read and write as JS properties, the way napi-rs exposes them.
548
838
  if (t.endsWith('{')) {
549
- for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1) {
550
- const f = lines[i].trim().match(/^pub (\w+)\s*:\s*([\w()]+)\s*,?$/);
551
- if (f) cls.fields.push({ name: f[1], type: f[2] });
552
- }
839
+ const body = structFields(lines, i, enabled);
840
+ i = body.end;
841
+ cls.fields.push(...body.fields);
553
842
  }
554
843
  }
555
844
  } else if (constM) {
556
845
  const constTy = normalizeStringSpelling(constM[2]);
557
- if (PRIMITIVES.has(constTy) || constTy === '&str') consts.push({ name: constM[1], ty: constTy, value: constM[3].trim() });
846
+ if (CONSTANT_TYPES.has(constTy)) consts.push({ name: constM[1], ty: constTy, value: constM[3].trim() });
558
847
  else log(`crossbind: rust bridge: const ${constM[1]} skipped (only i32, f64 and bool constants cross)`);
559
848
  } else if (modM) {
560
849
  // `mod x;` is followed for crate imports; inline `mod x { .. }` bodies are opaque.
561
- if (modM[2] === ';') mods.push({ name: modM[1], cfg: cfgOf(attrs) });
850
+ if (modM[2] === ';') mods.push({ name: modM[1], cfg: cfgOf(attrs), isPub: /^pub\s/.test(t) });
562
851
  else i = skipBlock(lines, i);
563
852
  } else if (displayM) {
564
853
  // `impl Display for X` -> a JS toString(); the block body itself is not parsed.
565
854
  displayNames.add(displayM[1]);
566
855
  i = skipBlock(lines, i);
567
856
  } else if (traitImplM) {
857
+ // `impl Default for X` and `impl Clone for X` count like the derives.
858
+ const byHand = t.match(/^impl\s+(?:[\w:]+::)?(Default|Clone|Copy)\s+for\s+(\w+)\b/);
859
+ if (byHand) acc.traitImpls.set(byHand[2], new Set([...(acc.traitImpls.get(byHand[2]) ?? []), byHand[1]]));
860
+ // `impl<W: Write> Write for X<W>` makes X a std::io stream (streamsOf).
861
+ const io = t.match(/^impl\s*<[^>]*>\s*(?:std::io::|io::)?(Write|Read)\s+for\s+(\w+)\s*</);
862
+ if (io) acc.streamTraits.set(io[2], { trait: io[1], header: implFns(lines, i, enabled).header });
568
863
  i = skipBlock(lines, i);
569
- } else if (implM && classes.has(implM[1])) {
570
- const cls = classes.get(implM[1]);
864
+ } else if (implM) {
865
+ // Typed in finalizeModel: a signature may name a struct declared further down, and the
866
+ // impl may come before its own struct. An impl of a type never bound drops there.
571
867
  let depth = 1;
868
+ let fnAttrs = [];
572
869
  for (i += 1; i < lines.length && depth > 0; i += 1) {
573
870
  const s = lines[i];
574
- const sig = matchFnSignature(s.trim());
575
- if (sig && depth === 1) parseFn(cls, sig, { enums, valueObjects, newtypes, jsonTypes, classes, allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse }, log);
871
+ const trimmed = s.trim();
872
+ if (depth === 1 && trimmed.startsWith('#[')) {
873
+ fnAttrs.push(trimmed);
874
+ } else if (depth === 1 && trimmed) {
875
+ const sig = matchFnSignature(trimmed);
876
+ if (sig && cfgEnabled(cfgOf(fnAttrs), enabled)) acc.pending.push({ className: implM[1], module: here, sig, flags: scanFlags(acc, collectTypes) });
877
+ fnAttrs = [];
878
+ }
576
879
  depth += (s.match(/\{/g) ?? []).length - (s.match(/\}/g) ?? []).length;
577
880
  }
578
881
  i -= 1;
579
- } else if (implM) {
580
- // Consume unknown-impl bodies so their fns are never misread as free functions.
581
- i = skipBlock(lines, i);
882
+ } else if (otherImplM) {
883
+ // `impl<W: Write> X<W>` feeds a std::io stream (streamsOf); no other generic impl is
884
+ // bound, and their fns are not free functions either.
885
+ const inherent = t.match(/^impl\s*<.*?>\s*(\w+)\s*<\s*\w+\s*>/);
886
+ if (inherent) {
887
+ const block = implFns(lines, i, enabled);
888
+ acc.genericImpls.set(inherent[1], [...(acc.genericImpls.get(inherent[1]) ?? []), block]);
889
+ i = block.end;
890
+ } else {
891
+ i = skipBlock(lines, i);
892
+ }
582
893
  } else if (freeFnM) {
583
- parseFreeFn(freeFns, freeFnM, { enums, valueObjects, newtypes, jsonTypes, classes, allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse }, log);
894
+ acc.pending.push({ free: freeFnM, here, flags: scanFlags(acc, collectTypes) });
895
+ }
896
+ // Where an item is defined names it in a crate bridge that several imports share.
897
+ const declared = enumM?.[1] ?? structM?.[1] ?? constM?.[1];
898
+ const bound = (list) => list.some((item) => item.name === declared);
899
+ if (declared && !acc.definedIn.has(declared)
900
+ && (classes.has(declared) || [enums, valueObjects, newtypes, jsonTypes, consts].some(bound))) {
901
+ acc.definedIn.set(declared, here);
584
902
  }
585
903
  attrs = [];
586
904
  }
905
+ for (const name of reexportedFrom) {
906
+ if (!mods.some((m) => m.name === name)) mods.push({ name, cfg: null, isPub: false });
907
+ }
587
908
  return mods;
588
909
  }
589
910
 
911
+ // The bounds a generic impl or struct puts on one type parameter (`W: Write + Send` and `where`
912
+ // clauses alike), without their std::io / marker paths.
913
+ function boundsOf(param, texts) {
914
+ const pattern = new RegExp(`\\b${param}\\s*:\\s*([^,{>]+)`, 'g');
915
+ return texts.flatMap((text) => [...text.matchAll(pattern)])
916
+ .flatMap((m) => m[1].split('+'))
917
+ .map((b) => b.trim().replace(/^(?:std::|core::)?(?:io::|marker::)/, ''))
918
+ .filter(Boolean);
919
+ }
920
+
921
+ // What crossbind's in-memory sink (writers) and source (readers) can stand in for.
922
+ const STREAM_BOUNDS = {
923
+ Write: new Set(['Write', 'Send', 'Sync', "'static"]),
924
+ Read: new Set(['Read', 'BufRead', 'Seek', 'Send', 'Sync', "'static"]),
925
+ };
926
+
927
+ // Rust's std::io streams - `X<W: Write>` or `X<R: Read>` with the matching trait impl - bind with
928
+ // crossbind's own byte sink or source in place of the type parameter, so JS hands over and gets
929
+ // back Uint8Array chunks. A stream is built by a `new` that takes the sink or source; a writer's
930
+ // consuming fn that returns its sink (`finish`) ends it.
931
+ function streamsOf(acc, log) {
932
+ const ctx = {
933
+ enums: acc.enums, valueObjects: acc.valueObjects, newtypes: acc.newtypes, jsonTypes: acc.jsonTypes,
934
+ classes: acc.classes, allowJson: false, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse,
935
+ };
936
+ const streams = [];
937
+ for (const [name, struct] of acc.genericStructs) {
938
+ const io = acc.streamTraits.get(name);
939
+ if (!io) continue;
940
+ const skip = (why) => log(`crossbind: rust bridge: ${name} skipped (${why})`);
941
+ const params = splitTopLevel(struct.generics).map((p) => p.trim()).filter(Boolean);
942
+ if (params.length !== 1 || params[0].startsWith("'")) { skip('a stream binds with one type parameter and no lifetimes'); continue; }
943
+ const param = params[0].match(/^\w+/)[0];
944
+ const impls = acc.genericImpls.get(name) ?? [];
945
+ const odd = boundsOf(param, [struct.generics, io.header, ...impls.map((impl) => impl.header)])
946
+ .find((b) => !STREAM_BOUNDS[io.trait].has(b));
947
+ if (odd) { skip(`its ${param}: ${odd} bound has no in-memory ${io.trait === 'Write' ? 'sink' : 'source'}`); continue; }
948
+
949
+ const methodsOf = { name, ctor: null, factories: [], methods: [], statics: [], fields: [], clonable: false };
950
+ const finishers = [];
951
+ let ctor = null;
952
+ for (const sig of impls.flatMap((impl) => impl.fns)) {
953
+ const [, fnName, rawParams, rawRet] = sig;
954
+ const fnParams = splitTopLevel(rawParams).map((p) => p.trim()).filter(Boolean);
955
+ const ret = analyzeReturn(rawRet ?? '()', ctx);
956
+ if (fnParams[0] === '&self' || fnParams[0] === '&mut self') {
957
+ parseFn(methodsOf, sig, ctx, log);
958
+ } else if (fnParams[0] === 'self' || fnParams[0] === 'mut self') {
959
+ const why = io.trait === 'Read'
960
+ ? `a reader is not consumed: its ${param} is the input passed to new`
961
+ : `a writer is consumed only by a fn that hands back its ${param}`;
962
+ if (io.trait === 'Write' && fnParams.length === 1 && ret.inner === param && !ret.optional) finishers.push({ name: fnName, throws: ret.throws });
963
+ else log(`crossbind: rust bridge: ${name}::${fnName} skipped (${why})`);
964
+ } else if (fnName === 'new') {
965
+ const at = fnParams.findIndex((p) => p.match(PARAM_RE)?.[2]?.replace(/\s+/g, '') === param);
966
+ const others = at < 0 ? null : parseParams(fnParams.filter((_, idx) => idx !== at), ctx, `${name}::new`, log);
967
+ const builds = ret.inner === 'Self' || ret.inner === name || ret.inner.startsWith(`${name}<`);
968
+ if (at < 0 || !others || !builds || ret.optional) { log(`crossbind: rust bridge: ${name}::new skipped (it has to take the ${param} and return the stream)`); continue; }
969
+ // A reader's source is the bytes JS passes in; a writer's sink is crossbind's own.
970
+ const args = io.trait === 'Read'
971
+ ? [...others.slice(0, at), { name: fnParams[at].match(PARAM_RE)[1], ty: '&[u8]' }, ...others.slice(at)]
972
+ : others;
973
+ if (args.length > MAX_CTOR_ARGS) { log(`crossbind: rust bridge: ${name}::new skipped (max ${MAX_CTOR_ARGS} args)`); continue; }
974
+ ctor = { args, at, throws: ret.throws };
975
+ } else {
976
+ log(`crossbind: rust bridge: ${name}::${fnName} skipped (a stream is built by its new)`);
977
+ }
978
+ }
979
+ if (!ctor) { skip(`a stream needs a new that takes its ${param}`); continue; }
980
+ acc.definedIn.set(name, struct.definedIn);
981
+ streams.push({
982
+ name,
983
+ role: io.trait === 'Write' ? 'writer' : 'reader',
984
+ ctor,
985
+ finishers,
986
+ methods: methodsOf.methods.filter((m) => !m.ownedClass && !m.borrowedSelf && !m.consumes),
987
+ });
988
+ }
989
+ return streams;
990
+ }
991
+
590
992
  function finalizeModel(acc, log) {
591
993
  const { enums, valueObjects, newtypes, jsonTypes, consts, classes, freeFns, displayNames } = acc;
994
+ for (const item of acc.pending) {
995
+ const ctx = { enums, valueObjects, newtypes, jsonTypes, classes, ...item.flags };
996
+ if (item.free) {
997
+ const known = freeFns.length;
998
+ parseFreeFn(freeFns, item.free, ctx, log);
999
+ if (freeFns.length > known && !acc.definedIn.has(item.free[1])) acc.definedIn.set(item.free[1], item.here);
1000
+ } else if (classes.has(item.className) && implOfBound(acc, item)) {
1001
+ parseFn(classes.get(item.className), item.sig, ctx, log);
1002
+ }
1003
+ }
1004
+ // `impl Default for X` and `impl Clone for X` count like the derives, wherever the impl sits.
1005
+ for (const [name, cls] of classes) {
1006
+ const byHand = acc.traitImpls?.get(name);
1007
+ cls.hasDefault ||= Boolean(byHand?.has('Default'));
1008
+ cls.clonable ||= Boolean(byHand?.has('Clone') || byHand?.has('Copy'));
1009
+ }
1010
+ const streams = streamsOf(acc, log);
1011
+ // A struct leaves its JS handle only as a copy, so consuming `self` and by-value parameters
1012
+ // need it to be Clone.
1013
+ const movesUncloned = (args) => args.find((p) => classes.has(p.ty) && !classes.get(p.ty).clonable);
1014
+ const keepMoves = (owner) => (f) => {
1015
+ const moved = movesUncloned(f.args);
1016
+ if (moved) log(`crossbind: rust bridge: ${owner}${f.name} skipped (a ${moved.ty} passed by value must be Clone or Copy)`);
1017
+ return !moved;
1018
+ };
1019
+ for (const cls of classes.values()) {
1020
+ cls.methods = cls.methods.filter((m) => {
1021
+ if (m.consumes && !cls.clonable) {
1022
+ log(`crossbind: rust bridge: ${cls.name}::${m.name} skipped (consuming self is not supported unless ${cls.name} is Clone or Copy)`);
1023
+ return false;
1024
+ }
1025
+ return keepMoves(`${cls.name}::`)(m);
1026
+ });
1027
+ cls.factories = cls.factories.filter(keepMoves(`${cls.name}::`));
1028
+ cls.statics = cls.statics.filter(keepMoves(`${cls.name}::`));
1029
+ if (cls.ctor && !keepMoves(`${cls.name}::`)({ name: 'new', args: cls.ctor.args })) cls.ctor = null;
1030
+ }
1031
+ freeFns.splice(0, freeFns.length, ...freeFns.filter(keepMoves('fn ')));
592
1032
  // A class with no exported surface is dropped (with a note), mirroring the C++ generator.
593
1033
  // A binding that borrows a dropped class would name a class that never registers and abort
594
1034
  // the module at init, so dropping a class drops those bindings too, until nothing changes.
595
1035
  const dropped = new Set();
596
1036
  const borrowsDropped = (args) => args.some((p) => {
597
1037
  const ty = String(p.ty);
598
- return (ty.startsWith('&') && dropped.has(ty.slice(1))) || dropped.has(ty.match(ARC_RE)?.[1]);
1038
+ return (ty.startsWith('&') && dropped.has(ty.slice(1))) || dropped.has(ty.match(ARC_RE)?.[1]) || dropped.has(ty);
599
1039
  });
600
1040
  let changed = true;
601
1041
  while (changed) {
602
1042
  changed = false;
603
1043
  for (const [name, cls] of classes) {
604
- if (!cls.ctor && !cls.factories.length && !cls.methods.length && !(cls.fields ?? []).length) {
1044
+ if (!cls.ctor && !cls.factories.length && !cls.methods.length && !cls.statics.length && !(cls.fields ?? []).length) {
605
1045
  classes.delete(name);
606
1046
  dropped.add(name);
607
1047
  changed = true;
@@ -625,9 +1065,19 @@ function finalizeModel(acc, log) {
625
1065
  for (const cls of classes.values()) {
626
1066
  cls.methods = cls.methods.filter(keepFn(`${cls.name}::`));
627
1067
  cls.factories = cls.factories.filter(keepFn(`${cls.name}::`));
1068
+ cls.statics = cls.statics.filter(keepFn(`${cls.name}::`));
628
1069
  if (cls.ctor && borrowsDropped(cls.ctor.args)) { cls.ctor = null; changed = true; log(`crossbind: rust bridge: ${cls.name}::new skipped (a parameter borrows a struct that is not registered)`); }
629
1070
  }
630
1071
  }
1072
+ // A stream method that names a dropped or uncloned struct goes alone; the stream goes when
1073
+ // its `new` does.
1074
+ const liveStreams = streams
1075
+ .map((s) => ({ ...s, methods: s.methods.filter((m) => !borrowsDropped(m.args) && !movesUncloned(m.args)) }))
1076
+ .filter((s) => {
1077
+ const lost = borrowsDropped(s.ctor.args) || movesUncloned(s.ctor.args);
1078
+ if (lost) log(`crossbind: rust bridge: ${s.name} skipped (its new takes a struct that is not registered or not Clone)`);
1079
+ return !lost;
1080
+ });
631
1081
  for (const cls of classes.values()) {
632
1082
  cls.fields = (cls.fields ?? []).filter((f) => {
633
1083
  const carries = PRIMITIVES.has(f.type) || enums.some((e) => e.name === f.type) || valueObjects.some((v) => v.name === f.type);
@@ -651,7 +1101,9 @@ function finalizeModel(acc, log) {
651
1101
  const usesJson = freeFns.some((f) => anyJson(f.args, f.ret))
652
1102
  || [...classes.values()].some((c) => (c.ctor && anyJson(c.ctor.args, '()'))
653
1103
  || c.factories.some((f) => anyJson(f.args, '()'))
654
- || c.methods.some((m) => anyJson(m.args, m.ret)));
1104
+ || c.methods.some((m) => anyJson(m.args, m.ret))
1105
+ || c.statics.some((f) => anyJson(f.args, f.ret)))
1106
+ || liveStreams.some((s) => anyJson(s.ctor.args, '()') || s.methods.some((m) => anyJson(m.args, m.ret)));
655
1107
 
656
1108
  // Shared (Arc) classes: collect every class named by an Arc<...> surface anywhere, then
657
1109
  // enforce the shapes whose Box paths would corrupt the Arc-based delete()/share machinery.
@@ -664,6 +1116,7 @@ function finalizeModel(acc, log) {
664
1116
  if (c.ctor) noteAll(c.ctor.args, '()');
665
1117
  c.factories.forEach((f) => noteAll(f.args, '()'));
666
1118
  c.methods.forEach((m) => noteAll(m.args, m.ret));
1119
+ c.statics.forEach((f) => noteAll(f.args, f.ret));
667
1120
  }
668
1121
  for (const c of classes.values()) {
669
1122
  if (!sharedOf.has(c.name)) continue;
@@ -680,10 +1133,74 @@ function finalizeModel(acc, log) {
680
1133
  throw new Error(`crossbind: rust bridge: ${c.name} is shared via Arc<...>, so every factory must return Arc<Self> ('${boxed.name}' returns Self)`);
681
1134
  }
682
1135
  }
683
- return { enums, valueObjects, newtypes, jsonTypes, consts, classes: [...classes.values()], freeFns, usesJson, sharedOf: [...sharedOf].sort() };
1136
+ return {
1137
+ enums, valueObjects, newtypes, jsonTypes, consts, classes: [...classes.values()], freeFns, usesJson,
1138
+ sharedOf: [...sharedOf].sort(), definedIn: acc.definedIn, streams: liveStreams,
1139
+ };
684
1140
  }
685
1141
 
686
1142
  // Consumes a brace-delimited block starting at line i; returns the closing line's index.
1143
+ // The impl block opened on line i: its header up to the brace (a `where` clause included), the
1144
+ // `pub fn` signatures a cfg keeps, and the index of its closing line.
1145
+ function implFns(lines, i, enabled) {
1146
+ let open = i;
1147
+ while (open < lines.length - 1 && !lines[open].includes('{')) open += 1;
1148
+ const header = lines.slice(i, open + 1).map((l) => l.trim()).join(' ');
1149
+ const fns = [];
1150
+ let attrs = [];
1151
+ let depth = (lines[open].match(/\{/g) ?? []).length - (lines[open].match(/\}/g) ?? []).length;
1152
+ let end = open + 1;
1153
+ for (; end < lines.length && depth > 0; end += 1) {
1154
+ const line = lines[end].trim();
1155
+ if (depth === 1 && line.startsWith('#[')) {
1156
+ attrs.push(line);
1157
+ } else if (depth === 1 && line) {
1158
+ const sig = matchFnSignature(line);
1159
+ if (sig && cfgEnabled(cfgOf(attrs), enabled)) fns.push(sig);
1160
+ attrs = [];
1161
+ }
1162
+ depth += (lines[end].match(/\{/g) ?? []).length - (lines[end].match(/\}/g) ?? []).length;
1163
+ }
1164
+ return { header, fns, end: end - 1 };
1165
+ }
1166
+
1167
+ // A line without its `//` comment. A `//` inside a string or char literal stays: a URL in an
1168
+ // attribute or a const is not a comment.
1169
+ function stripLineComment(line) {
1170
+ let inString = false;
1171
+ for (let i = 0; i < line.length; i += 1) {
1172
+ const c = line[i];
1173
+ if (inString) {
1174
+ if (c === '\\') i += 1;
1175
+ else if (c === '"') inString = false;
1176
+ } else if (c === '"') {
1177
+ inString = true;
1178
+ } else if (c === "'") {
1179
+ const char = line.slice(i).match(/^'(?:\\.[^']*|[^\\'])'/);
1180
+ if (char) i += char[0].length - 1;
1181
+ } else if (c === '/' && line[i + 1] === '/') {
1182
+ return line.slice(0, i);
1183
+ }
1184
+ }
1185
+ return line;
1186
+ }
1187
+
1188
+ // The `pub name: Type` fields of the struct body opened on line i, minus those a cfg leaves out,
1189
+ // and the index of its closing line.
1190
+ function structFields(lines, i, enabled) {
1191
+ const fields = [];
1192
+ let attrs = [];
1193
+ let end = i + 1;
1194
+ for (; end < lines.length && !/^\}/.test(lines[end].trim()); end += 1) {
1195
+ const line = lines[end].trim();
1196
+ if (line.startsWith('#[')) { attrs.push(line); continue; }
1197
+ const f = line.match(/^pub (\w+)\s*:\s*([\w()]+)\s*,?$/);
1198
+ if (f && cfgEnabled(cfgOf(attrs), enabled)) fields.push({ name: f[1], type: f[2] });
1199
+ if (line) attrs = [];
1200
+ }
1201
+ return { fields, end };
1202
+ }
1203
+
687
1204
  function skipBlock(lines, i) {
688
1205
  let depth = 0;
689
1206
  let started = false;
@@ -721,7 +1238,8 @@ function analyzeReturn(raw, ctx) {
721
1238
  // gets the values rather than a lazy iterator.
722
1239
  const iterator = raw.trim().match(/^impl\s+Iterator\s*<\s*Item\s*=\s*(.+?)\s*>$/);
723
1240
  if (iterator) return { inner: `Vec<${iterator[1].trim()}>`, throws: false, optional: false, collect: true };
724
- const r = normalizeStringSpelling(raw.trim());
1241
+ // `io::Result<T>` and other path-qualified Result aliases are Results too.
1242
+ const r = normalizeStringSpelling(raw.trim()).replace(/^(?:\w+::)+(?=Result\s*<)/, '');
725
1243
  const res = unwrapGeneric(r, 'Result');
726
1244
  // Only the Ok type crosses; the error type needs nothing but Display, so `Box<dyn Error>`
727
1245
  // and friends are read past here.
@@ -771,30 +1289,26 @@ function splitTopLevel(text) {
771
1289
  return parts;
772
1290
  }
773
1291
 
774
- function parseFn(cls, sig, ctx, log) {
775
- const { enums, valueObjects, classes } = ctx;
776
- const [, name, rawParams, rawRet] = sig;
777
- // A borrowed string is known in both directions: as a parameter it borrows, as a return it
778
- // comes back owned.
779
- const known = (ty) => PRIMITIVES.has(ty) || PARAM_ONLY.has(ty) || typedArrayOf(ty) || (isCollection(ty) && collectionCarries(ty, ctx))
780
- || enums.some((e) => e.name === ty) || valueObjects.some((v) => v.name === ty)
1292
+ // A borrowed string is known in both directions: as a parameter it borrows, as a return it
1293
+ // comes back owned.
1294
+ function isKnownType(ty, ctx) {
1295
+ return PRIMITIVES.has(ty) || PARAM_ONLY.has(ty) || typedArrayOf(ty) || (isCollection(ty) && collectionCarries(ty, ctx))
1296
+ || ctx.enums.some((e) => e.name === ty) || ctx.valueObjects.some((v) => v.name === ty)
781
1297
  || (ctx.newtypes ?? []).some((n) => n.name === ty)
782
1298
  || (ctx.jsonTypes ?? []).some((j) => j.name === ty)
783
1299
  || isOptionalRecord(ty, ctx);
1300
+ }
1301
+
1302
+ // The wire args of a binding, or null (with the reason logged) when a parameter cannot cross.
1303
+ // A struct passed by value is copied out of its JS handle; finalizeModel checks it is Clone.
1304
+ function parseParams(params, ctx, label, log) {
1305
+ const { classes } = ctx;
1306
+ const known = (ty) => isKnownType(ty, ctx);
784
1307
  // `&OtherClass` params: the referenced struct must already be declared (parsed) above.
785
1308
  const isClassRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty) && classes.has(ty.slice(1));
786
-
787
- const params = splitTopLevel(rawParams).map((p) => p.trim()).filter(Boolean);
788
- let selfKind = null;
789
- if (params[0] === '&mut self' || params[0] === '&self') selfKind = params.shift();
790
- else if (params[0] === 'self' || params[0] === 'mut self') {
791
- log(`crossbind: rust bridge: ${cls.name}::${name} skipped (consuming self is not supported)`);
792
- return;
793
- }
794
-
795
1309
  const args = [];
796
1310
  for (const p of params) {
797
- const m = p.match(/^(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String|&\s*\w+)\s*>|Vec\s*<[^;]+>|\[[^\]]+;\s*\d+\]|&\s*\[[^\]]+\]|\([^)]*,[^)]*\)|(?:std\s*::\s*collections\s*::\s*)?(?:HashMap|BTreeMap|HashSet|BTreeSet)\s*<[^>]+>|serde_json\s*::\s*Value|(?:std\s*::\s*sync\s*::\s*)?Arc\s*<\s*\w+\s*>|embind_rs\s*::\s*Js(?:Value|Function)|&\s*\w+|[\w()]+)$/);
1311
+ const m = p.match(PARAM_RE);
798
1312
  let ty = m?.[2].replace(/\s+/g, '');
799
1313
  if (ty) ty = normalizeStringSpelling(ty);
800
1314
  if (ty && !known(ty) && isJsonSpelling(ty, ctx)) ty = JSON_TY;
@@ -805,15 +1319,34 @@ function parseFn(cls, sig, ctx, log) {
805
1319
  const optClassRef = ty?.match(OPTION_CLASS_REF_RE)?.[1];
806
1320
  const closure = closureShape(p.slice(p.indexOf(':') + 1));
807
1321
  if (closure) { args.push({ name: `a${args.length}`, ty: `__closure${JSON.stringify(closure)}` }); continue; }
808
- if (!m || !(known(ty) || ty === JSON_TY || jsTokP || (arcParam && classes.has(arcParam)) || PARAM_ONLY.has(ty) || OPTION_PARAM_RE.test(ty) || isClassRef(ty)
809
- || (optClassRef && classes.has(optClassRef)))) {
1322
+ if (!m || !(known(ty) || ty === JSON_TY || jsTokP || (arcParam && classes.has(arcParam)) || PARAM_ONLY.has(ty) || OPTION_PARAM_RE.test(ty) || OPTION_VIEW_RE.test(ty) || nonZeroBase(ty) || optionNonZero(ty) || isClassRef(ty)
1323
+ || (optClassRef && classes.has(optClassRef)) || classes.has(ty))) {
810
1324
  log(isCollection(ty) && !collectionCarries(ty, ctx)
811
- ? `crossbind: rust bridge: ${cls.name}::${name} skipped (a struct inside '${ty}' must derive Serialize and Deserialize to cross)`
812
- : `crossbind: rust bridge: ${cls.name}::${name} skipped (unsupported parameter '${p}')`);
813
- return;
1325
+ ? `crossbind: rust bridge: ${label} skipped (a struct inside '${ty}' must derive Serialize and Deserialize to cross)`
1326
+ : `crossbind: rust bridge: ${label} skipped (unsupported parameter '${p}')`);
1327
+ return null;
814
1328
  }
815
1329
  args.push({ name: m[1], ty });
816
1330
  }
1331
+ return args;
1332
+ }
1333
+
1334
+ function parseFn(cls, sig, ctx, log) {
1335
+ const { classes } = ctx;
1336
+ const [, name, rawParams, rawRet] = sig;
1337
+ const known = (ty) => isKnownType(ty, ctx);
1338
+
1339
+ const params = splitTopLevel(rawParams).map((p) => p.trim()).filter(Boolean);
1340
+ let selfKind = null;
1341
+ if (params[0] === '&mut self' || params[0] === '&self') selfKind = params.shift();
1342
+ // A method that takes `self` runs on a copy, so the JS handle keeps its value (finalizeModel
1343
+ // checks the struct is Clone).
1344
+ else if (params[0] === 'self' || params[0] === 'mut self') { params.shift(); selfKind = 'self'; }
1345
+ const byRef = selfKind !== '&mut self';
1346
+ const consumes = selfKind === 'self';
1347
+
1348
+ const args = parseParams(params, ctx, `${cls.name}::${name}`, log);
1349
+ if (!args) return;
817
1350
  const returnInfo = analyzeReturn(rawRet ?? '()', ctx);
818
1351
  let { inner: ret, throws, optional } = returnInfo;
819
1352
  if (!known(ret) && isJsonSpelling(ret, ctx)) ret = JSON_TY;
@@ -828,13 +1361,15 @@ function parseFn(cls, sig, ctx, log) {
828
1361
  if (!selfKind) {
829
1362
  const arcSelf = ret === 'Arc<Self>' || ret === `Arc<${cls.name}>`;
830
1363
  if (!arcSelf && ret !== 'Self' && ret !== cls.name) {
831
- log(`crossbind: rust bridge: ${cls.name}::${name} skipped (associated fns must return Self, Result<Self, E> or Option<Self>)`);
1364
+ // Any other associated fn is a static function on the class: `Class.name(..)` in JS.
1365
+ const entry = callableEntry(name, args, rawRet, ctx, log, `${cls.name}::${name}`);
1366
+ if (entry) cls.statics.push(entry);
832
1367
  return;
833
1368
  }
834
1369
  if (name === 'new') {
835
1370
  if (arcSelf) { log(`crossbind: rust bridge: ${cls.name}::new skipped (Arc<Self> has no ctor shape - use a named factory like 'create')`); return; }
836
1371
  if (optional) { log(`crossbind: rust bridge: ${cls.name}::new skipped (Option<Self> has no ctor shape - use a named factory or Result<Self, E>)`); return; }
837
- if (args.length > 6) { log(`crossbind: rust bridge: ${cls.name}::new skipped (max 6 args)`); return; }
1372
+ if (args.length > MAX_CTOR_ARGS) { log(`crossbind: rust bridge: ${cls.name}::new skipped (max ${MAX_CTOR_ARGS} args)`); return; }
838
1373
  cls.ctor = { args, throws };
839
1374
  } else {
840
1375
  if (arcSelf && optional) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Option<Arc<Self>> is not supported in this wave)`); return; }
@@ -855,7 +1390,7 @@ function parseFn(cls, sig, ctx, log) {
855
1390
  return;
856
1391
  }
857
1392
  if (args.length > 6) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (max 6 args)`); return; }
858
- cls.methods.push({ name, args, ret: `Arc<${target}>`, byRef: selfKind === '&self', throws: false, optionalRet: false });
1393
+ cls.methods.push({ name, args, ret: `Arc<${target}>`, byRef, consumes, throws: false, optionalRet: false });
859
1394
  return;
860
1395
  }
861
1396
  if (optional && !OPTION_INNERS.has(ret)) {
@@ -868,7 +1403,7 @@ function parseFn(cls, sig, ctx, log) {
868
1403
  if (selfOwned || selfBorrowed) {
869
1404
  cls.methods.push({
870
1405
  name, args, ret: selfOwned ? (ret === 'Self' ? cls.name : ret) : cls.name,
871
- byRef: selfKind === '&self', throws, optionalRet: false,
1406
+ byRef, consumes, throws, optionalRet: false,
872
1407
  ownedClass: selfOwned ? (ret === 'Self' ? cls.name : ret) : null,
873
1408
  borrowedSelf: selfBorrowed,
874
1409
  });
@@ -879,43 +1414,22 @@ function parseFn(cls, sig, ctx, log) {
879
1414
  return;
880
1415
  }
881
1416
  if (args.length > 6) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (max 6 args)`); return; }
882
- cls.methods.push({ name, args, ret, byRef: selfKind === '&self', throws, optionalRet: optional, collect: returnInfo.collect });
1417
+ cls.methods.push({ name, args, ret, byRef, consumes, throws, optionalRet: optional, collect: returnInfo.collect });
883
1418
  }
884
1419
 
885
1420
  function parseFreeFn(freeFns, sig, ctx, log) {
886
- const { enums, valueObjects, classes } = ctx;
887
1421
  const [, name, rawParams, rawRet] = sig;
888
- // A borrowed string is known in both directions: as a parameter it borrows, as a return it
889
- // comes back owned.
890
- const known = (ty) => PRIMITIVES.has(ty) || PARAM_ONLY.has(ty) || typedArrayOf(ty) || (isCollection(ty) && collectionCarries(ty, ctx))
891
- || enums.some((e) => e.name === ty) || valueObjects.some((v) => v.name === ty)
892
- || (ctx.newtypes ?? []).some((n) => n.name === ty)
893
- || (ctx.jsonTypes ?? []).some((j) => j.name === ty)
894
- || isOptionalRecord(ty, ctx);
895
- const isClassRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty) && classes.has(ty.slice(1));
1422
+ const args = parseParams(splitTopLevel(rawParams).map((s) => s.trim()).filter(Boolean), ctx, `fn ${name}`, log);
1423
+ if (!args) return;
1424
+ const entry = callableEntry(name, args, rawRet, ctx, log, `fn ${name}`);
1425
+ if (entry) freeFns.push(entry);
1426
+ }
896
1427
 
897
- const args = [];
898
- for (const p of splitTopLevel(rawParams).map((s) => s.trim()).filter(Boolean)) {
899
- const m = p.match(/^(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String|&\s*\w+)\s*>|Vec\s*<[^;]+>|\[[^\]]+;\s*\d+\]|&\s*\[[^\]]+\]|\([^)]*,[^)]*\)|(?:std\s*::\s*collections\s*::\s*)?(?:HashMap|BTreeMap|HashSet|BTreeSet)\s*<[^>]+>|serde_json\s*::\s*Value|(?:std\s*::\s*sync\s*::\s*)?Arc\s*<\s*\w+\s*>|embind_rs\s*::\s*Js(?:Value|Function)|&\s*\w+|[\w()]+)$/);
900
- let ty = m?.[2].replace(/\s+/g, '');
901
- if (ty) ty = normalizeStringSpelling(ty);
902
- if (ty && !known(ty) && isJsonSpelling(ty, ctx)) ty = JSON_TY;
903
- if (ty) ty = normalizeArc(ty, ctx);
904
- const jsTokP = ty ? matchJsTok(ty, ctx) : null;
905
- if (jsTokP) ty = jsTokP;
906
- const arcParam = ty?.match(ARC_RE)?.[1];
907
- const optClassRef = ty?.match(OPTION_CLASS_REF_RE)?.[1];
908
- const closure = closureShape(p.slice(p.indexOf(':') + 1));
909
- if (closure) { args.push({ name: `a${args.length}`, ty: `__closure${JSON.stringify(closure)}` }); continue; }
910
- if (!m || !(known(ty) || ty === JSON_TY || jsTokP || (arcParam && classes.has(arcParam)) || PARAM_ONLY.has(ty) || OPTION_PARAM_RE.test(ty) || isClassRef(ty)
911
- || (optClassRef && classes.has(optClassRef)))) {
912
- log(isCollection(ty) && !collectionCarries(ty, ctx)
913
- ? `crossbind: rust bridge: fn ${name} skipped (a struct inside '${ty}' must derive Serialize and Deserialize to cross)`
914
- : `crossbind: rust bridge: fn ${name} skipped (unsupported parameter '${p}')`);
915
- return;
916
- }
917
- args.push({ name: m[1], ty });
918
- }
1428
+ // What a free function, or a static function on a class, hands back: null (with the reason
1429
+ // logged) when the return cannot cross.
1430
+ function callableEntry(name, args, rawRet, ctx, log, label) {
1431
+ const { classes } = ctx;
1432
+ const known = (ty) => isKnownType(ty, ctx);
919
1433
  const returnInfo = analyzeReturn(rawRet ?? '()', ctx);
920
1434
  let { inner: ret, throws, optional } = returnInfo;
921
1435
  if (!known(ret) && isJsonSpelling(ret, ctx)) ret = JSON_TY;
@@ -924,20 +1438,19 @@ function parseFreeFn(freeFns, sig, ctx, log) {
924
1438
  if (jsTokR) ret = jsTokR;
925
1439
  const retArc = ret.match(ARC_RE)?.[1];
926
1440
  if (retArc) {
927
- if (!classes.has(retArc)) { log(`crossbind: rust bridge: fn ${name} skipped (unsupported return '${ret}')`); return; }
928
- if (throws) { log(`crossbind: rust bridge: fn ${name} skipped (a fallible Arc return is not carried yet)`); return; }
1441
+ if (!classes.has(retArc)) { log(`crossbind: rust bridge: ${label} skipped (unsupported return '${ret}')`); return null; }
1442
+ if (throws) { log(`crossbind: rust bridge: ${label} skipped (a fallible Arc return is not carried yet)`); return null; }
929
1443
  }
930
- if (jsTokR && optional) { log(`crossbind: rust bridge: fn ${name} skipped (Option<${ret}> returns are not supported - return JsValue::null() instead)`); return; }
931
- if (optional && !OPTION_INNERS.has(ret) && !retArc) { log(`crossbind: rust bridge: fn ${name} skipped (Option<${ret}> return is not representable - inners: i32, f64, bool, String, Arc<Class>)`); return; }
932
- if (isCollection(ret) && !collectionCarries(ret, ctx)) { log(`crossbind: rust bridge: fn ${name} skipped (a struct inside '${ret}' must derive Serialize and Deserialize to cross)`); return; }
1444
+ if (jsTokR && optional) { log(`crossbind: rust bridge: ${label} skipped (Option<${ret}> returns are not supported - return JsValue::null() instead)`); return null; }
1445
+ if (optional && !OPTION_INNERS.has(ret) && !retArc) { log(`crossbind: rust bridge: ${label} skipped (Option<${ret}> return is not representable - inners: i32, f64, bool, String, Arc<Class>)`); return null; }
1446
+ if (isCollection(ret) && !collectionCarries(ret, ctx)) { log(`crossbind: rust bridge: ${label} skipped (a struct inside '${ret}' must derive Serialize and Deserialize to cross)`); return null; }
933
1447
  // A fresh instance handed back by value: JS owns it and frees it on delete().
934
1448
  if (classes.has(ret) && !(ctx.jsonTypes ?? []).some((j) => j.name === ret)) {
935
- freeFns.push({ name, jsName: camel(name), args, ret, throws, optionalRet: false, ownedClass: ret });
936
- return;
1449
+ return { name, jsName: camel(name), args, ret, throws, optionalRet: false, ownedClass: ret };
937
1450
  }
938
- if (!known(ret) && ret !== JSON_TY && !retArc && !jsTokR) { log(`crossbind: rust bridge: fn ${name} skipped (unsupported return '${ret}')`); return; }
939
- if (args.length > 6) { log(`crossbind: rust bridge: fn ${name} skipped (max 6 args)`); return; }
940
- freeFns.push({ name, jsName: camel(name), args, ret, throws, optionalRet: optional, collect: returnInfo.collect });
1451
+ if (!known(ret) && ret !== JSON_TY && !retArc && !jsTokR) { log(`crossbind: rust bridge: ${label} skipped (unsupported return '${ret}')`); return null; }
1452
+ if (args.length > 6) { log(`crossbind: rust bridge: ${label} skipped (max 6 args)`); return null; }
1453
+ return { name, jsName: camel(name), args, ret, throws, optionalRet: optional, collect: returnInfo.collect };
941
1454
  }
942
1455
 
943
1456
  // ---------------- emitter ----------------
@@ -945,13 +1458,16 @@ function parseFreeFn(freeFns, sig, ctx, log) {
945
1458
  const camel = (s) => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
946
1459
 
947
1460
  function emitBridge(model, {
948
- userCrate, vectors, log, prelude = '', namePrefix = '',
1461
+ userCrate, vectors, log, prelude = '', namePrefix = '', rustPaths = null, wireNames = null,
949
1462
  }) {
950
1463
  const U = userCrate;
1464
+ // A crate import's bridge names each item by the path it is reachable at (the root, or the
1465
+ // module it was imported from); every other bridge has one crate for everything.
1466
+ const rust = (name) => rustPaths?.get(name) ?? `${U}::${name}`;
951
1467
  // embind's registry is flat, so two crate imports that both export `Version` would collide.
952
- // A direct `cargo:` import registers its public names under a per-crate prefix; the generated
1468
+ // A direct `cargo:` import registers its public names under per-crate names; the generated
953
1469
  // proxy module maps them back to the clean names the import specifier already scopes.
954
- const pub = (name) => `${namePrefix}${name}`;
1470
+ const pub = (name) => wireNames?.get(name) ?? `${namePrefix}${name}`;
955
1471
  const isEnum = (ty) => model.enums.some((e) => e.name === ty);
956
1472
  const newtypeOf = (ty) => (model.newtypes ?? []).find((n) => n.name === ty);
957
1473
  // A serde record, a data enum, a value object inside an Option: all cross as plain JS values
@@ -971,6 +1487,8 @@ function emitBridge(model, {
971
1487
  // as the class pointer via a bridge-local Ref wrapper (borrowed unsafely at the call site).
972
1488
  // A slice is a collection, not a borrowed class.
973
1489
  const isRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty) && !isCollection(ty) && !typedArrayOf(ty);
1490
+ // A struct passed by value rides the `&Struct` pointer wire and is copied out of it.
1491
+ const isClassValue = (ty) => model.classes.some((c) => c.name === ty);
974
1492
  const arcInner = (ty) => String(ty).match(ARC_RE)?.[1];
975
1493
  // An optional Arc rides the nullable shared wrapper; every other optional keeps `Option<T>`.
976
1494
  // `&[T]` decodes into a Vec and is handed back as a slice; every other collection is owned.
@@ -994,7 +1512,7 @@ function emitBridge(model, {
994
1512
  || model.enums.some((e) => e.name === name)
995
1513
  || (model.newtypes ?? []).some((n) => n.name === name)
996
1514
  || (model.jsonTypes ?? []).some((j) => j.name === name)
997
- ? `${U}::${name}`
1515
+ ? `${rust(name)}`
998
1516
  : name));
999
1517
  const borrowSuffix = (ty) => (!fixedArrayRef(ty) && /^&\s*\[/.test(String(ty).trim()) ? '.as_slice()' : '');
1000
1518
  const borrowPrefix = (ty) => (fixedArrayRef(ty) ? '&' : '');
@@ -1047,7 +1565,7 @@ function emitBridge(model, {
1047
1565
  // (ownedTy qualifies user type names with the crate path)
1048
1566
  : optionStrParam(ty) ? `${expr}.as_deref()`
1049
1567
  : optionClassRef(ty) ? `${expr}.as_option()`
1050
- : newtypeOf(ty) ? `${U}::${ty}(${expr})`
1568
+ : newtypeOf(ty) ? `${rust(ty)}(${expr})`
1051
1569
  : PARAM_ONLY.has(ty) ? `&${expr}`
1052
1570
  : isRef(ty) ? `unsafe { &*${expr}.0 }`
1053
1571
  : ty === JSON_TY || arcInner(ty) || isEnum(ty) || isVo(ty) ? `${expr}.0` : expr);
@@ -1058,6 +1576,25 @@ function emitBridge(model, {
1058
1576
  : ty === JSON_TY ? `__CrossbindJson(${expr})`
1059
1577
  : arcInner(ty) ? `${arcInner(ty)}Shared(${expr})`
1060
1578
  : isEnum(ty) || isVo(ty) ? `${ty}W(${expr})` : expr);
1579
+ // A property keeps `wireTy` for both halves: embind registers one type for its getter and setter.
1580
+ const optionBigInt = (ty) => String(ty).match(OPTION_BIGINT_RE)?.[1];
1581
+ const optionView = (ty) => String(ty).match(OPTION_VIEW_RE)?.[1];
1582
+ const paramTy = (ty) => (nonZeroBase(ty) ? paramTy(nonZeroBase(ty))
1583
+ : optionNonZero(ty) ? paramTy(`Option<${optionNonZero(ty).base}>`)
1584
+ : typedArrayOf(ty) ? `embind_rs::${typedArrayOf(ty).arg}`
1585
+ : optionBigInt(ty) ? `embind_rs::JsBigIntOptArg<${optionBigInt(ty)}>`
1586
+ : optionView(ty) ? `embind_rs::JsViewOptArg<${optionView(ty)}>`
1587
+ : isClassValue(ty) ? `${ty}Ref` : wireTy(ty));
1588
+ // Every shim body runs inside embind_rs::guard, whose return type has an error sentinel, so a
1589
+ // zero can return the raised error before the call.
1590
+ const nonZeroFrom = (inner, value, some) => `match std::num::${inner}::new(${value}) { Some(n) => ${some}, None => return embind_rs::raise_err(String::from("a ${inner} cannot be 0")) }`;
1591
+ const unwrapParam = (ty, expr) => (nonZeroBase(ty) ? nonZeroFrom(ty, unwrapParam(nonZeroBase(ty), expr), 'n')
1592
+ : optionNonZero(ty) ? `match ${unwrapParam(`Option<${optionNonZero(ty).base}>`, expr)} { None => None, Some(v) => ${nonZeroFrom(optionNonZero(ty).inner, 'v', 'Some(n)')} }`
1593
+ : typedArrayOf(ty) ? `${expr}.${typedArrayOf(ty).slice ? 'as_slice()' : 'to_vec()'}`
1594
+ : optionBigInt(ty) ? `${expr}.0`
1595
+ : optionView(ty) ? `${expr}.as_slice()`
1596
+ : isClassValue(ty) ? `(unsafe { &*${expr}.0 }).clone()`
1597
+ : unwrap(ty, expr));
1061
1598
 
1062
1599
  const out = [];
1063
1600
  out.push('// Generated by crossbind rustBridgeGen - do not edit. The user crate stays plain Rust;');
@@ -1076,15 +1613,15 @@ function emitBridge(model, {
1076
1613
  // No derives on the wrapper: a plain fieldless enum need not be Clone or Copy, and the
1077
1614
  // wire only ever moves one (`to_wire` takes self, `from_wire` builds a fresh one).
1078
1615
  out.push('#[repr(transparent)]');
1079
- out.push(`pub struct ${e.name}W(pub ${U}::${e.name});`);
1616
+ out.push(`pub struct ${e.name}W(pub ${rust(e.name)});`);
1080
1617
  out.push(`impl WireType for ${e.name}W {`);
1081
1618
  out.push(' type Wire = i32;');
1082
1619
  out.push(" const SIG: char = 'i';");
1083
1620
  out.push(` fn tid() -> *const c_void { enum_tid::<${e.name}W>() }`);
1084
1621
  out.push(` fn from_wire(w: i32) -> ${e.name}W {`);
1085
1622
  out.push(' match w {');
1086
- for (const v of e.variants) out.push(` ${v.value} => ${e.name}W(${U}::${e.name}::${v.name}),`);
1087
- out.push(` _ => ${e.name}W(${U}::${e.name}::${e.variants[0].name}),`);
1623
+ for (const v of e.variants) out.push(` ${v.value} => ${e.name}W(${rust(e.name)}::${v.name}),`);
1624
+ out.push(` _ => ${e.name}W(${rust(e.name)}::${e.variants[0].name}),`);
1088
1625
  out.push(' }');
1089
1626
  out.push(' }');
1090
1627
  out.push(' fn to_wire(self) -> i32 { self.0 as i32 }');
@@ -1096,8 +1633,8 @@ function emitBridge(model, {
1096
1633
  for (const v of model.valueObjects) {
1097
1634
  out.push('#[derive(Clone, Copy)]');
1098
1635
  out.push('#[repr(transparent)]');
1099
- out.push(`pub struct ${v.name}W(pub ${U}::${v.name});`);
1100
- out.push(`impl Default for ${v.name}W { fn default() -> Self { ${v.name}W(${U}::${v.name}::default()) } }`);
1636
+ out.push(`pub struct ${v.name}W(pub ${rust(v.name)});`);
1637
+ out.push(`impl Default for ${v.name}W { fn default() -> Self { ${v.name}W(${rust(v.name)}::default()) } }`);
1101
1638
  out.push(`impl WireType for ${v.name}W {`);
1102
1639
  out.push(` type Wire = *mut ${v.name}W;`);
1103
1640
  out.push(" const SIG: char = 'p';");
@@ -1113,9 +1650,11 @@ function emitBridge(model, {
1113
1650
  // the referenced classes up front so the wrappers exist before the shims that use them.
1114
1651
  const classRefs = new Set();
1115
1652
  (model.classes ?? []).forEach((c) => c.methods.forEach((m) => { if (m.ownedClass || m.borrowedSelf) classRefs.add(m.ret); }));
1653
+ (model.classes ?? []).forEach((c) => (c.statics ?? []).forEach((f) => { if (f.ownedClass) classRefs.add(f.ownedClass); }));
1116
1654
  (model.freeFns ?? []).forEach((f) => { if (f.ownedClass) classRefs.add(f.ownedClass); });
1117
1655
  const scanRefs = (args) => args.forEach((p) => {
1118
1656
  if (isRef(p.ty)) classRefs.add(p.ty.slice(1));
1657
+ if (isClassValue(p.ty)) classRefs.add(p.ty);
1119
1658
  const optRef = optionClassRef(p.ty);
1120
1659
  if (optRef) classRefs.add(optRef);
1121
1660
  });
@@ -1123,40 +1662,45 @@ function emitBridge(model, {
1123
1662
  if (cls.ctor) scanRefs(cls.ctor.args);
1124
1663
  cls.factories.forEach((f) => scanRefs(f.args));
1125
1664
  cls.methods.forEach((m) => scanRefs(m.args));
1665
+ (cls.statics ?? []).forEach((f) => scanRefs(f.args));
1666
+ }
1667
+ for (const s of model.streams ?? []) {
1668
+ scanRefs(s.ctor.args);
1669
+ s.methods.forEach((m) => scanRefs(m.args));
1126
1670
  }
1127
1671
  (model.freeFns ?? []).forEach((f) => scanRefs(f.args));
1128
1672
  for (const name of [...classRefs].sort()) {
1129
1673
  out.push('#[derive(Clone, Copy)]');
1130
1674
  out.push('#[repr(transparent)]');
1131
- out.push(`pub struct ${name}Ref(pub *mut ${U}::${name});`);
1675
+ out.push(`pub struct ${name}Ref(pub *mut ${rust(name)});`);
1132
1676
  out.push(`impl WireType for ${name}Ref {`);
1133
1677
  out.push(' type Wire = *mut c_void;');
1134
1678
  out.push(" const SIG: char = 'p';");
1135
- out.push(` fn tid() -> *const c_void { embind_rs::class_tid::<${U}::${name}>() }`);
1136
- out.push(` fn from_wire(w: *mut c_void) -> ${name}Ref { ${name}Ref(w as *mut ${U}::${name}) }`);
1679
+ out.push(` fn tid() -> *const c_void { embind_rs::class_tid::<${rust(name)}>() }`);
1680
+ out.push(` fn from_wire(w: *mut c_void) -> ${name}Ref { ${name}Ref(w as *mut ${rust(name)}) }`);
1137
1681
  out.push(` fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }`);
1138
1682
  out.push('}');
1139
1683
  out.push('');
1140
1684
  // A fresh instance of the class: the wire is the smart pointer JS owns and frees.
1141
1685
  out.push('#[repr(transparent)]');
1142
- out.push(`pub struct ${name}Owned(pub *mut ${U}::${name});`);
1686
+ out.push(`pub struct ${name}Owned(pub *mut ${rust(name)});`);
1143
1687
  out.push(`impl WireType for ${name}Owned {`);
1144
1688
  out.push(' type Wire = *mut c_void;');
1145
1689
  out.push(" const SIG: char = 'p';");
1146
- out.push(` fn tid() -> *const c_void { embind_rs::owned_ptr_tid::<${U}::${name}>() }`);
1147
- out.push(` fn from_wire(w: *mut c_void) -> ${name}Owned { ${name}Owned(w as *mut ${U}::${name}) }`);
1690
+ out.push(` fn tid() -> *const c_void { embind_rs::owned_ptr_tid::<${rust(name)}>() }`);
1691
+ out.push(` fn from_wire(w: *mut c_void) -> ${name}Owned { ${name}Owned(w as *mut ${rust(name)}) }`);
1148
1692
  out.push(' fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }');
1149
1693
  out.push('}');
1150
1694
  out.push(`impl embind_rs::ErrSentinel for ${name}Owned { fn err_sentinel() -> Self { ${name}Owned(core::ptr::null_mut()) } }`);
1151
1695
  out.push('');
1152
1696
  // The same object handed back for chaining: a pointer, so JS does not own it twice.
1153
1697
  out.push('#[repr(transparent)]');
1154
- out.push(`pub struct ${name}RefOut(pub *mut ${U}::${name});`);
1698
+ out.push(`pub struct ${name}RefOut(pub *mut ${rust(name)});`);
1155
1699
  out.push(`impl WireType for ${name}RefOut {`);
1156
1700
  out.push(' type Wire = *mut c_void;');
1157
1701
  out.push(" const SIG: char = 'p';");
1158
- out.push(` fn tid() -> *const c_void { embind_rs::class_ptr_tid::<${U}::${name}>() }`);
1159
- out.push(` fn from_wire(w: *mut c_void) -> ${name}RefOut { ${name}RefOut(w as *mut ${U}::${name}) }`);
1702
+ out.push(` fn tid() -> *const c_void { embind_rs::class_ptr_tid::<${rust(name)}>() }`);
1703
+ out.push(` fn from_wire(w: *mut c_void) -> ${name}RefOut { ${name}RefOut(w as *mut ${rust(name)}) }`);
1160
1704
  out.push(' fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }');
1161
1705
  out.push('}');
1162
1706
  out.push(`impl embind_rs::ErrSentinel for ${name}RefOut { fn err_sentinel() -> Self { ${name}RefOut(core::ptr::null_mut()) } }`);
@@ -1164,17 +1708,17 @@ function emitBridge(model, {
1164
1708
  // `Option<&Class>` parameter: a null pointer is the None the caller wrote as null.
1165
1709
  out.push('#[derive(Clone, Copy)]');
1166
1710
  out.push('#[repr(transparent)]');
1167
- out.push(`pub struct ${name}RefOpt(pub *mut ${U}::${name});`);
1711
+ out.push(`pub struct ${name}RefOpt(pub *mut ${rust(name)});`);
1168
1712
  out.push(`impl ${name}RefOpt {`);
1169
- out.push(` pub fn as_option(&self) -> Option<&${U}::${name}> {`);
1713
+ out.push(` pub fn as_option(&self) -> Option<&${rust(name)}> {`);
1170
1714
  out.push(' if self.0.is_null() { None } else { Some(unsafe { &*self.0 }) }');
1171
1715
  out.push(' }');
1172
1716
  out.push('}');
1173
1717
  out.push(`impl WireType for ${name}RefOpt {`);
1174
1718
  out.push(' type Wire = *mut c_void;');
1175
1719
  out.push(" const SIG: char = 'p';");
1176
- out.push(` fn tid() -> *const c_void { embind_rs::class_ptr_tid::<${U}::${name}>() }`);
1177
- out.push(` fn from_wire(w: *mut c_void) -> ${name}RefOpt { ${name}RefOpt(w as *mut ${U}::${name}) }`);
1720
+ out.push(` fn tid() -> *const c_void { embind_rs::class_ptr_tid::<${rust(name)}>() }`);
1721
+ out.push(` fn from_wire(w: *mut c_void) -> ${name}RefOpt { ${name}RefOpt(w as *mut ${rust(name)}) }`);
1178
1722
  out.push(` fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }`);
1179
1723
  out.push('}');
1180
1724
  out.push('');
@@ -1250,15 +1794,15 @@ function emitBridge(model, {
1250
1794
  // pointer, one strong count per JS handle (given on to_wire, added on from_wire).
1251
1795
  for (const inner of model.sharedOf ?? []) {
1252
1796
  out.push('#[repr(transparent)]');
1253
- out.push(`pub struct ${inner}Shared(pub std::sync::Arc<${U}::${inner}>);`);
1797
+ out.push(`pub struct ${inner}Shared(pub std::sync::Arc<${rust(inner)}>);`);
1254
1798
  out.push(`impl WireType for ${inner}Shared {`);
1255
1799
  out.push(' type Wire = *mut c_void;');
1256
1800
  out.push(" const SIG: char = 'p';");
1257
- out.push(` fn tid() -> *const c_void { embind_rs::shared_tid::<${U}::${inner}>() }`);
1801
+ out.push(` fn tid() -> *const c_void { embind_rs::shared_tid::<${rust(inner)}>() }`);
1258
1802
  out.push(' fn from_wire(w: *mut c_void) -> Self {');
1259
1803
  out.push(' unsafe {');
1260
- out.push(` std::sync::Arc::increment_strong_count(w as *const ${U}::${inner});`);
1261
- out.push(` ${inner}Shared(std::sync::Arc::from_raw(w as *const ${U}::${inner}))`);
1804
+ out.push(` std::sync::Arc::increment_strong_count(w as *const ${rust(inner)});`);
1805
+ out.push(` ${inner}Shared(std::sync::Arc::from_raw(w as *const ${rust(inner)}))`);
1262
1806
  out.push(' }');
1263
1807
  out.push(' }');
1264
1808
  out.push(` fn to_wire(self) -> *mut c_void { std::sync::Arc::into_raw(self.0) as *mut c_void }`);
@@ -1267,16 +1811,16 @@ function emitBridge(model, {
1267
1811
  // `Option<Arc<T>>`: a null smart pointer is what embind reads back as JS null, so None
1268
1812
  // needs no optional type of its own.
1269
1813
  out.push('#[repr(transparent)]');
1270
- out.push(`pub struct ${inner}SharedOpt(pub Option<std::sync::Arc<${U}::${inner}>>);`);
1814
+ out.push(`pub struct ${inner}SharedOpt(pub Option<std::sync::Arc<${rust(inner)}>>);`);
1271
1815
  out.push(`impl WireType for ${inner}SharedOpt {`);
1272
1816
  out.push(' type Wire = *mut c_void;');
1273
1817
  out.push(" const SIG: char = 'p';");
1274
- out.push(` fn tid() -> *const c_void { embind_rs::shared_tid::<${U}::${inner}>() }`);
1818
+ out.push(` fn tid() -> *const c_void { embind_rs::shared_tid::<${rust(inner)}>() }`);
1275
1819
  out.push(' fn from_wire(w: *mut c_void) -> Self {');
1276
1820
  out.push(` if w.is_null() { return ${inner}SharedOpt(None); }`);
1277
1821
  out.push(' unsafe {');
1278
- out.push(` std::sync::Arc::increment_strong_count(w as *const ${U}::${inner});`);
1279
- out.push(` ${inner}SharedOpt(Some(std::sync::Arc::from_raw(w as *const ${U}::${inner})))`);
1822
+ out.push(` std::sync::Arc::increment_strong_count(w as *const ${rust(inner)});`);
1823
+ out.push(` ${inner}SharedOpt(Some(std::sync::Arc::from_raw(w as *const ${rust(inner)})))`);
1280
1824
  out.push(' }');
1281
1825
  out.push(' }');
1282
1826
  out.push(' fn to_wire(self) -> *mut c_void {');
@@ -1293,14 +1837,14 @@ function emitBridge(model, {
1293
1837
  const registrations = [];
1294
1838
  const usedOptionals = new Set();
1295
1839
  const noteOptionArgs = (args) => args.forEach((p) => {
1296
- const m = p.ty.match(OPTION_PARAM_RE);
1840
+ const m = (optionNonZero(p.ty) ? `Option<${optionNonZero(p.ty).base}>` : p.ty).match(OPTION_PARAM_RE);
1297
1841
  // `Option<&str>` rides the String optional; a class reference needs none (null is None).
1298
1842
  if (m) usedOptionals.add(m[1] === '&str' ? 'String' : m[1]);
1299
1843
  });
1300
1844
  for (const c of model.consts ?? []) {
1301
1845
  registrations.push(c.ty === '&str'
1302
- ? ` embind_rs::constant_str("${pub(c.name)}", ${U}::${c.name});`
1303
- : ` embind_rs::constant("${pub(c.name)}", ${U}::${c.name});`);
1846
+ ? ` embind_rs::constant_str("${pub(c.name)}", ${rust(c.name)});`
1847
+ : ` embind_rs::constant("${pub(c.name)}", ${rust(c.name)});`);
1304
1848
  }
1305
1849
  for (const e of model.enums) {
1306
1850
  registrations.push(` enum_::<${e.name}W>("${pub(e.name)}")${e.variants.map((v) => `.value("${v.name}", ${v.value})`).join('')};`);
@@ -1308,7 +1852,7 @@ function emitBridge(model, {
1308
1852
  for (const v of model.valueObjects) {
1309
1853
  const fields = v.fields.map((f) => {
1310
1854
  const fieldTy = model.valueObjects.some((o) => o.name === f.type) ? `${f.type}W` : f.type;
1311
- return `.field::<${fieldTy}>("${f.name}", core::mem::offset_of!(${U}::${v.name}, ${f.name}))`;
1855
+ return `.field::<${fieldTy}>("${f.name}", core::mem::offset_of!(${rust(v.name)}, ${f.name}))`;
1312
1856
  }).join('');
1313
1857
  registrations.push(` value_object_::<${v.name}W>("${pub(v.name)}")${fields}.finalize();`);
1314
1858
  }
@@ -1317,14 +1861,40 @@ function emitBridge(model, {
1317
1861
  registrations.push(` register_vector::<${vec.of}>("${pub(vec.name)}");`);
1318
1862
  }
1319
1863
 
1320
- for (const cls of model.classes) {
1321
- const C = `${U}::${cls.name}`;
1864
+ const RAISE = 'embind_rs::raise_err_coded(e.to_string(), { use embind_rs::{CrossbindErrorCode, CrossbindErrorCodeFallback}; (&e).crossbind_code() })';
1865
+ // A struct handed back by value: JS owns the new instance and frees it on delete().
1866
+ const ownedValue = (wrapper, call, throws) => (throws
1867
+ ? `match ${call} { Ok(v) => ${wrapper}(Box::into_raw(Box::new(v))), Err(e) => ${RAISE} }`
1868
+ : `${wrapper}(Box::into_raw(Box::new(${call})))`);
1869
+ // The return side of a free or static function: its wire type and the body producing it.
1870
+ const callable = (f, rawCall) => {
1871
+ const call = f.collect ? `(${rawCall}).collect::<Vec<_>>()` : rawCall;
1872
+ if (f.ownedClass) return { retTy: `${f.ownedClass}Owned`, body: ownedValue(`${f.ownedClass}Owned`, call, f.throws) };
1873
+ const retTy = f.optionalRet ? optionalWireTy(f.ret) : (f.ret === '()' ? '()' : wireTy(f.ret));
1874
+ if (f.optionalRet && !arcInner(f.ret)) usedOptionals.add(f.ret);
1875
+ const optArc = f.optionalRet && arcInner(f.ret);
1876
+ const body = f.throws
1877
+ ? `match ${call} { Ok(v) => ${wrap(f.ret, 'v')}, Err(e) => ${RAISE} }`
1878
+ : (f.ret === '()' ? call : optArc ? `${arcInner(f.ret)}SharedOpt(${call})` : wrap(f.ret, call));
1879
+ return { retTy, body };
1880
+ };
1881
+ // A class handed back by value needs its smart pointer before the binding that returns it
1882
+ // registers; registering every class first lets bindings name each other's classes in any
1883
+ // order (a builder and the struct it builds return each other).
1884
+ const ownedReturns = new Set([...model.classes.flatMap((c) => [...c.methods, ...(c.statics ?? [])]), ...(model.freeFns ?? [])]
1885
+ .map((f) => f.ownedClass).filter(Boolean));
1886
+ const classHeads = [];
1887
+ const classTails = [];
1888
+
1889
+ for (const [index, cls] of model.classes.entries()) {
1890
+ const C = `${rust(cls.name)}`;
1322
1891
  const shim = (fnName) => `__${cls.name.toLowerCase()}_${fnName}`;
1323
- const chain = [` class_::<${C}>("${pub(cls.name)}")`];
1324
-
1325
- const handsBackSelf = cls.methods.some((m) => m.ownedClass) || (model.freeFns ?? []).some((f) => f.ownedClass === cls.name);
1326
- if (cls.shared) chain.push(` .smart_ptr_shared("${pub(cls.name)}Shared")`);
1327
- else if (cls.factories.length || handsBackSelf) chain.push(` .smart_ptr("${pub(cls.name)}Ptr")`);
1892
+ const handle = `__class${index}`;
1893
+ const head = [` let ${handle} = class_::<${C}>("${pub(cls.name)}")`];
1894
+ if (cls.shared) head.push(` .smart_ptr_shared("${pub(cls.name)}Shared")`);
1895
+ else if (cls.factories.length || ownedReturns.has(cls.name)) head.push(` .smart_ptr("${pub(cls.name)}Ptr")`);
1896
+ classHeads.push(`${head.join('\n')};`);
1897
+ const chain = [` ${handle}`];
1328
1898
  // A struct that derives Default and declares no `new` still constructs from JS: the
1329
1899
  // derive is the author saying what a default instance is.
1330
1900
  if (!cls.ctor && !cls.shared && cls.hasDefault) {
@@ -1334,8 +1904,8 @@ function emitBridge(model, {
1334
1904
  if (cls.ctor) {
1335
1905
  const a = cls.ctor.args;
1336
1906
  noteOptionArgs(a);
1337
- const ps = a.map((p, i) => `a${i}: ${wireTy(p.ty)}`).join(', ');
1338
- const call = `${C}::new(${a.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
1907
+ const ps = a.map((p, i) => `a${i}: ${paramTy(p.ty)}`).join(', ');
1908
+ const call = `${C}::new(${a.map((p, i) => unwrapParam(p.ty, `a${i}`)).join(', ')})`;
1339
1909
  if (cls.ctor.throws) {
1340
1910
  out.push(`fn ${shim('new')}(${ps}) -> *mut ${C} { ${guarded('*mut', `match ${call} { Ok(v) => Box::into_raw(Box::new(v)), Err(e) => embind_rs::raise_err_coded(e.to_string(), { use embind_rs::{CrossbindErrorCode, CrossbindErrorCodeFallback}; (&e).crossbind_code() }) }`)} }`);
1341
1911
  chain.push(` .constructor_ptr${a.length}(${shim('new')})`);
@@ -1346,8 +1916,8 @@ function emitBridge(model, {
1346
1916
  }
1347
1917
  for (const f of cls.factories) {
1348
1918
  noteOptionArgs(f.args);
1349
- const ps = f.args.map((p, i) => `a${i}: ${wireTy(p.ty)}`).join(', ');
1350
- const call = `${C}::${f.name}(${f.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
1919
+ const ps = f.args.map((p, i) => `a${i}: ${paramTy(p.ty)}`).join(', ');
1920
+ const call = `${C}::${f.name}(${f.args.map((p, i) => unwrapParam(p.ty, `a${i}`)).join(', ')})`;
1351
1921
  if (f.shared) {
1352
1922
  if (f.throws) {
1353
1923
  out.push(`fn ${shim(f.name)}(${ps}) -> *mut ${C} { match ${call} { Ok(v) => std::sync::Arc::into_raw(v) as *mut ${C}, Err(e) => embind_rs::raise_err_coded(e.to_string(), { use embind_rs::{CrossbindErrorCode, CrossbindErrorCodeFallback}; (&e).crossbind_code() }) } }`);
@@ -1369,6 +1939,13 @@ function emitBridge(model, {
1369
1939
  chain.push(` .create_ptr${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
1370
1940
  }
1371
1941
  }
1942
+ for (const f of cls.statics ?? []) {
1943
+ noteOptionArgs(f.args);
1944
+ const ps = f.args.map((p, i) => `a${i}: ${paramTy(p.ty)}`).join(', ');
1945
+ const { retTy, body } = callable(f, `${C}::${f.name}(${f.args.map((p, i) => unwrapParam(p.ty, `a${i}`)).join(', ')})`);
1946
+ out.push(`fn ${shim(f.name)}(${ps}) -> ${retTy} { ${guarded(retTy, body)} }`);
1947
+ chain.push(` .static_function${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
1948
+ }
1372
1949
  for (const f of cls.fields ?? []) {
1373
1950
  out.push(`fn ${shim(`get_${f.name}`)}(t: &mut ${C}) -> ${wireTy(f.type)} { ${wrap(f.type, `t.${f.name}.clone()`)} }`);
1374
1951
  out.push(`fn ${shim(`set_${f.name}`)}(t: &mut ${C}, v: ${wireTy(f.type)}) { t.${f.name} = ${unwrap(f.type, 'v')}; }`);
@@ -1390,16 +1967,16 @@ function emitBridge(model, {
1390
1967
  for (const m of cls.methods) {
1391
1968
  if (accessorNames.has(m.name)) continue;
1392
1969
  noteOptionArgs(m.args);
1393
- const params = [`t: &mut ${C}`, ...m.args.map((p, i) => `a${i}: ${wireTy(p.ty)}`)].join(', ');
1394
- const callArgs = m.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ');
1395
- const recv = m.byRef ? '(&*t)' : 't';
1970
+ const params = [`t: &mut ${C}`, ...m.args.map((p, i) => `a${i}: ${paramTy(p.ty)}`)].join(', ');
1971
+ const callArgs = m.args.map((p, i) => unwrapParam(p.ty, `a${i}`)).join(', ');
1972
+ const recv = m.consumes ? '(&*t).clone()' : m.byRef ? '(&*t)' : 't';
1396
1973
  const rawCall = `${recv}.${m.name}(${callArgs})`;
1397
1974
  const call = m.collect ? `(${rawCall}).collect::<Vec<_>>()` : rawCall;
1398
1975
  if (m.ownedClass || m.borrowedSelf) {
1399
1976
  const wrapper = m.ownedClass ? `${m.ret}Owned` : `${m.ret}RefOut`;
1400
1977
  const value = m.ownedClass
1401
- ? `${wrapper}(Box::into_raw(Box::new(${call})))`
1402
- : `${wrapper}(${call} as *mut ${U}::${m.ret})`;
1978
+ ? ownedValue(wrapper, call, m.throws)
1979
+ : `${wrapper}(${call} as *mut ${rust(m.ret)})`;
1403
1980
  out.push(`fn ${shim(m.name)}(${params}) -> ${wrapper} { ${guarded(wrapper, value)} }`);
1404
1981
  chain.push(` .function${m.args.length}("${camel(m.name)}", ${shim(m.name)})`);
1405
1982
  classRefs.add(m.ret);
@@ -1418,27 +1995,71 @@ function emitBridge(model, {
1418
1995
  out.push(`fn ${shim('display_tostring')}(t: &mut ${C}) -> String { ${guarded('String', 'format!("{}", (&*t))')} }`);
1419
1996
  chain.push(` .function0("toString", ${shim('display_tostring')})`);
1420
1997
  }
1421
- registrations.push(`${chain.join('\n')};`);
1998
+ if (chain.length > 1) classTails.push(`${chain.join('\n')};`);
1422
1999
  }
2000
+ const streams = model.streams ?? [];
2001
+ if (streams.some((s) => s.role === 'writer')) {
2002
+ // The byte sink every writer stream writes into; each call hands JS what it has gathered.
2003
+ out.push('#[derive(Clone, Default)]');
2004
+ out.push('pub struct __CrossbindSink(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);');
2005
+ out.push('impl std::io::Write for __CrossbindSink {');
2006
+ out.push(' fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { self.0.lock().unwrap().extend_from_slice(buf); Ok(buf.len()) }');
2007
+ out.push(' fn flush(&mut self) -> std::io::Result<()> { Ok(()) }');
2008
+ out.push('}');
2009
+ out.push('impl __CrossbindSink { fn drain(&self) -> Vec<u8> { std::mem::take(&mut *self.0.lock().unwrap()) } }');
2010
+ out.push('');
2011
+ }
2012
+ for (const [index, s] of streams.entries()) {
2013
+ const S = `${s.name}CrossbindStream`;
2014
+ const shim = (fnName) => `__${s.name.toLowerCase()}_${fnName}`;
2015
+ const writer = s.role === 'writer';
2016
+ const finished = `embind_rs::raise_err(String::from("${s.name} is finished"))`;
2017
+ out.push(writer
2018
+ ? `pub struct ${S} { inner: Option<${rust(s.name)}<__CrossbindSink>>, sink: __CrossbindSink }`
2019
+ : `pub struct ${S} { inner: Option<${rust(s.name)}<std::io::Cursor<Vec<u8>>>> }`);
2020
+
2021
+ noteOptionArgs(s.ctor.args);
2022
+ const ps = s.ctor.args.map((p, i) => `a${i}: ${paramTy(p.ty)}`).join(', ');
2023
+ const passed = s.ctor.args.map((p, i) => (!writer && i === s.ctor.at ? `std::io::Cursor::new(a${i}.to_vec())` : unwrapParam(p.ty, `a${i}`)));
2024
+ const call = `${rust(s.name)}::new(${(writer ? [...passed.slice(0, s.ctor.at), 'sink.clone()', ...passed.slice(s.ctor.at)] : passed).join(', ')})`;
2025
+ const store = (value) => `Box::into_raw(Box::new(${S} { inner: Some(${value})${writer ? ', sink' : ''} }))`;
2026
+ const make = s.ctor.throws ? `match ${call} { Ok(v) => ${store('v')}, Err(e) => ${RAISE} }` : store(call);
2027
+ out.push(`fn ${shim('new')}(${ps}) -> *mut ${S} { ${guarded('*mut', writer ? `{ let sink = __CrossbindSink::default(); ${make} }` : make)} }`);
2028
+ const chain = [` __stream${index}`, ` .constructor_ptr${s.ctor.args.length}(${shim('new')})`];
2029
+
2030
+ if (writer) {
2031
+ out.push(`fn ${shim('write')}(t: &mut ${S}, a0: embind_rs::JsBytesArg) -> embind_rs::JsBytes { embind_rs::guard(|| match t.inner.as_mut() { None => ${finished}, Some(w) => match std::io::Write::write_all(w, a0.as_slice()) { Ok(()) => embind_rs::JsBytes(t.sink.drain()), Err(e) => ${RAISE} } }) }`);
2032
+ out.push(`fn ${shim('flush')}(t: &mut ${S}) -> embind_rs::JsBytes { embind_rs::guard(|| match t.inner.as_mut() { None => ${finished}, Some(w) => match std::io::Write::flush(w) { Ok(()) => embind_rs::JsBytes(t.sink.drain()), Err(e) => ${RAISE} } }) }`);
2033
+ chain.push(` .function1("write", ${shim('write')})`, ` .function0("flush", ${shim('flush')})`);
2034
+ for (const f of s.finishers) {
2035
+ const done = f.throws
2036
+ ? `match w.${f.name}() { Ok(_) => embind_rs::JsBytes(t.sink.drain()), Err(e) => ${RAISE} }`
2037
+ : `{ let _ = w.${f.name}(); embind_rs::JsBytes(t.sink.drain()) }`;
2038
+ out.push(`fn ${shim(f.name)}(t: &mut ${S}) -> embind_rs::JsBytes { embind_rs::guard(|| match t.inner.take() { None => ${finished}, Some(w) => ${done} }) }`);
2039
+ chain.push(` .function0("${camel(f.name)}", ${shim(f.name)})`);
2040
+ }
2041
+ } else {
2042
+ // read(max) takes up to max bytes, fewer only at the end, growing with what it gets.
2043
+ const readShim = (fnName, params, source) => `fn ${shim(fnName)}(t: &mut ${S}${params}) -> embind_rs::JsBytes { embind_rs::guard(|| match t.inner.as_mut() { None => ${finished}, Some(r) => { let mut out = Vec::new(); match std::io::Read::read_to_end(${source}, &mut out) { Ok(_) => embind_rs::JsBytes(out), Err(e) => ${RAISE} } } }) }`;
2044
+ out.push(readShim('read', ', a0: u32', '&mut std::io::Read::take(r, a0 as u64)'), readShim('read_all', '', 'r'));
2045
+ chain.push(` .function1("read", ${shim('read')})`, ` .function0("readAll", ${shim('read_all')})`);
2046
+ }
2047
+ for (const m of s.methods) {
2048
+ noteOptionArgs(m.args);
2049
+ const params = [`t: &mut ${S}`, ...m.args.map((p, i) => `a${i}: ${paramTy(p.ty)}`)].join(', ');
2050
+ const { retTy, body } = callable(m, `v.${m.name}(${m.args.map((p, i) => unwrapParam(p.ty, `a${i}`)).join(', ')})`);
2051
+ out.push(`fn ${shim(m.name)}(${params}) -> ${retTy} { ${guarded(retTy, `match t.inner.as_mut() { None => ${finished}, Some(v) => ${body} }`)} }`);
2052
+ chain.push(` .function${m.args.length}("${camel(m.name)}", ${shim(m.name)})`);
2053
+ }
2054
+ classHeads.push(` let __stream${index} = class_::<${S}>("${pub(s.name)}");`);
2055
+ classTails.push(`${chain.join('\n')};`);
2056
+ }
2057
+ registrations.push(...classHeads, ...classTails);
1423
2058
 
1424
2059
  for (const f of model.freeFns ?? []) {
1425
2060
  noteOptionArgs(f.args);
1426
- const ps = f.args.map((p, i) => `a${i}: ${wireTy(p.ty)}`).join(', ');
1427
- const rawCall = `${U}::${f.name}(${f.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
1428
- const call = f.collect ? `(${rawCall}).collect::<Vec<_>>()` : rawCall;
1429
- const retTy = f.optionalRet ? optionalWireTy(f.ret) : (f.ret === '()' ? '()' : wireTy(f.ret));
1430
- if (f.optionalRet && !arcInner(f.ret)) usedOptionals.add(f.ret);
1431
- if (f.ownedClass) {
1432
- const wrapper = `${f.ownedClass}Owned`;
1433
- out.push(`fn __free_${f.name}(${ps}) -> ${wrapper} { ${guarded(wrapper, `${wrapper}(Box::into_raw(Box::new(${call})))`)} }`);
1434
- registrations.push(` embind_rs::fn${f.args.length}("${pub(f.jsName)}", __free_${f.name});`);
1435
- classRefs.add(f.ownedClass);
1436
- continue;
1437
- }
1438
- const optArc = f.optionalRet && arcInner(f.ret);
1439
- const body = f.throws
1440
- ? `match ${call} { Ok(v) => ${wrap(f.ret, 'v')}, Err(e) => embind_rs::raise_err_coded(e.to_string(), { use embind_rs::{CrossbindErrorCode, CrossbindErrorCodeFallback}; (&e).crossbind_code() }) }`
1441
- : (f.ret === '()' ? call : optArc ? `${arcInner(f.ret)}SharedOpt(${call})` : wrap(f.ret, call));
2061
+ const ps = f.args.map((p, i) => `a${i}: ${paramTy(p.ty)}`).join(', ');
2062
+ const { retTy, body } = callable(f, `${rust(f.name)}(${f.args.map((p, i) => unwrapParam(p.ty, `a${i}`)).join(', ')})`);
1442
2063
  out.push(`fn __free_${f.name}(${ps}) -> ${retTy} { ${guarded(retTy, body)} }`);
1443
2064
  registrations.push(` embind_rs::fn${f.args.length}("${pub(f.jsName)}", __free_${f.name});`);
1444
2065
  }
@@ -1492,6 +2113,10 @@ export function emitDts(model, vectors, mode = 'sync') {
1492
2113
  const ts = (ty) => {
1493
2114
  const opt = ty.match(OPTION_PARAM_RE);
1494
2115
  if (opt) return `${TS_TYPES[opt[1]] ?? opt[1]} | null | undefined`;
2116
+ const optView = ty.match(OPTION_VIEW_RE);
2117
+ if (optView) return `${optView[1] === 'u8' ? 'Uint8Array' : 'Float64Array'} | null | undefined`;
2118
+ if (nonZeroBase(ty)) return TS_TYPES[nonZeroBase(ty)];
2119
+ if (optionNonZero(ty)) return `${TS_TYPES[optionNonZero(ty).base]} | null | undefined`;
1495
2120
  if (ty === JSON_TY) return 'JsonValue';
1496
2121
  const arc = ty.match(ARC_RE);
1497
2122
  if (arc) return arc[1]; // Arc<X> is transparent in JS: the shared instance itself
@@ -1536,6 +2161,9 @@ export function emitDts(model, vectors, mode = 'sync') {
1536
2161
  for (const f of cls.factories) {
1537
2162
  out.push(` static ${camel(f.name)}(${f.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${cls.name}${f.optional ? ' | null' : ''}`)};`);
1538
2163
  }
2164
+ for (const f of cls.statics ?? []) {
2165
+ out.push(` static ${camel(f.name)}(${f.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(f.ret)}${f.optionalRet ? ' | null' : ''}`)};`);
2166
+ }
1539
2167
  for (const m of cls.methods) {
1540
2168
  out.push(` ${camel(m.name)}(${m.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(m.ret)}${m.optionalRet ? ' | null' : ''}`)};`);
1541
2169
  }
@@ -1543,6 +2171,23 @@ export function emitDts(model, vectors, mode = 'sync') {
1543
2171
  out.push(` delete(): ${wrap('void')};`);
1544
2172
  out.push('}');
1545
2173
  }
2174
+ for (const s of model.streams ?? []) {
2175
+ out.push(`export declare class ${s.name} {`);
2176
+ out.push(` constructor(${s.ctor.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')});`);
2177
+ if (s.role === 'writer') {
2178
+ out.push(` write(data: Uint8Array): ${wrap('Uint8Array')};`);
2179
+ out.push(` flush(): ${wrap('Uint8Array')};`);
2180
+ for (const f of s.finishers) out.push(` ${camel(f.name)}(): ${wrap('Uint8Array')};`);
2181
+ } else {
2182
+ out.push(` read(max: number): ${wrap('Uint8Array')};`);
2183
+ out.push(` readAll(): ${wrap('Uint8Array')};`);
2184
+ }
2185
+ for (const m of s.methods) {
2186
+ out.push(` ${camel(m.name)}(${m.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(m.ret)}${m.optionalRet ? ' | null' : ''}`)};`);
2187
+ }
2188
+ out.push(` delete(): ${wrap('void')};`);
2189
+ out.push('}');
2190
+ }
1546
2191
  for (const f of model.freeFns ?? []) {
1547
2192
  out.push(`export declare function ${f.jsName}(${f.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(f.ret)}${f.optionalRet ? ' | null' : ''}`)};`);
1548
2193
  }