crossbind 2.0.0-beta.60 → 2.0.0-beta.63

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