crossbind 0.0.1 → 2.0.0-beta.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/AGENTS.md +92 -0
  2. package/CHANGELOG.md +145 -0
  3. package/LICENSE +21 -0
  4. package/README.md +133 -0
  5. package/package.json +52 -4
  6. package/src/actions/buildBinTools.js +0 -0
  7. package/src/actions/buildCargo.js +88 -0
  8. package/src/actions/buildDependencies.js +157 -0
  9. package/src/actions/buildExternal.js +57 -0
  10. package/src/actions/buildJs.js +93 -0
  11. package/src/actions/buildLib.js +115 -0
  12. package/src/actions/buildTypes.js +33 -0
  13. package/src/actions/buildWasiCommand.js +101 -0
  14. package/src/actions/buildWasm.js +322 -0
  15. package/src/actions/createInterface.js +172 -0
  16. package/src/actions/createLib.js +249 -0
  17. package/src/actions/createXCFramework.js +82 -0
  18. package/src/actions/extensions.js +7 -0
  19. package/src/actions/getAllBridges.js +20 -0
  20. package/src/actions/getCmakeParameters.js +66 -0
  21. package/src/actions/getData.js +46 -0
  22. package/src/actions/getDependLibs.js +25 -0
  23. package/src/actions/isSourceNewer.js +35 -0
  24. package/src/actions/licenses.js +156 -0
  25. package/src/actions/run.js +305 -0
  26. package/src/actions/target.js +54 -0
  27. package/src/assets/cmake/CMakeLists.txt +61 -0
  28. package/src/assets/cmake/dist.cmake +57 -0
  29. package/src/assets/cmake/ios.toolchain.cmake +1176 -0
  30. package/src/assets/cpp-runtime/browser.cpp +12 -0
  31. package/src/assets/cpp-runtime/commonBridges.cpp +17 -0
  32. package/src/assets/cpp-runtime/crossbindEmptySource.cpp +9 -0
  33. package/src/assets/cpp-runtime/node.cpp +19 -0
  34. package/src/assets/js-runtime/adapters/exception-decode.js +79 -0
  35. package/src/assets/js-runtime/adapters/fs-browser.js +173 -0
  36. package/src/assets/js-runtime/adapters/fs-node.js +28 -0
  37. package/src/assets/js-runtime/adapters/path-fs.js +6 -0
  38. package/src/assets/js-runtime/adapters/path-url.js +8 -0
  39. package/src/assets/js-runtime/adapters/vector-coercion.js +209 -0
  40. package/src/assets/js-runtime/adapters/worker-comlink.js +309 -0
  41. package/src/assets/js-runtime/browser.js +16 -0
  42. package/src/assets/js-runtime/core.js +193 -0
  43. package/src/assets/js-runtime/edge.js +18 -0
  44. package/src/assets/js-runtime/node.js +20 -0
  45. package/src/assets/packaging/crossbind-package.podspec +17 -0
  46. package/src/assets/wasi-runtime/stubs.c +84 -0
  47. package/src/bin.js +416 -0
  48. package/src/index.js +19 -0
  49. package/src/integration/getCrossbindScript.js +180 -0
  50. package/src/integration/getDependFilePath.js +132 -0
  51. package/src/runtime/wasiRun.mjs +118 -0
  52. package/src/state/calculateDependencyParameters.js +97 -0
  53. package/src/state/index.js +128 -0
  54. package/src/state/loadConfig.js +215 -0
  55. package/src/utils/appRustCrates.js +106 -0
  56. package/src/utils/binTools.js +76 -0
  57. package/src/utils/bridgeAsyncGuard.js +37 -0
  58. package/src/utils/cargoTarget.js +15 -0
  59. package/src/utils/cppDts.js +201 -0
  60. package/src/utils/cppFieldBindings.js +23 -0
  61. package/src/utils/dependencyRebuild.js +99 -0
  62. package/src/utils/dirLock.js +71 -0
  63. package/src/utils/downloadAndExtractFile.js +102 -0
  64. package/src/utils/familyManifest.js +17 -0
  65. package/src/utils/findFiles.js +5 -0
  66. package/src/utils/fixPackageName.js +7 -0
  67. package/src/utils/flattenConfigForTable.js +75 -0
  68. package/src/utils/getAbsolutePath.js +14 -0
  69. package/src/utils/getCMakeListsFilePath.js +17 -0
  70. package/src/utils/getOsUserAndGroupId.js +11 -0
  71. package/src/utils/getParentPath.js +12 -0
  72. package/src/utils/hash.js +33 -0
  73. package/src/utils/inputStamp.js +28 -0
  74. package/src/utils/licenseReport.js +94 -0
  75. package/src/utils/linkLayout.js +34 -0
  76. package/src/utils/loadJs.js +31 -0
  77. package/src/utils/loadJson.js +16 -0
  78. package/src/utils/logger.js +110 -0
  79. package/src/utils/ociArtifact.js +124 -0
  80. package/src/utils/overrideDependency.js +99 -0
  81. package/src/utils/provenance.js +54 -0
  82. package/src/utils/pullDockerImage.js +105 -0
  83. package/src/utils/replaceBasePathForDocker.js +18 -0
  84. package/src/utils/resolveEmbindRust.js +66 -0
  85. package/src/utils/runCargo.js +228 -0
  86. package/src/utils/rustBridgeGen.js +1013 -0
  87. package/src/utils/rustMt.js +76 -0
  88. package/src/utils/rustSysroot.js +171 -0
  89. package/src/utils/rustSysrootPin.js +11 -0
  90. package/src/utils/systemKeys.js +29 -0
  91. package/src/utils/targets.js +109 -0
  92. package/src/utils/wasiToolchain.js +47 -0
  93. package/src/utils/writeIfChanged.js +8 -0
  94. package/src/utils/writeJson.js +9 -0
  95. package/vitest.config.js +40 -0
@@ -0,0 +1,1013 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import resolveEmbindRustRoot, { embindRustVersion } from './resolveEmbindRust.js';
5
+ import runCargo, { toHostPath } from './runCargo.js';
6
+ import writeIfChanged from './writeIfChanged.js';
7
+
8
+ // Generates the embind bridge for a cargo package as a COMPANION CRATE, the Rust analog of the
9
+ // C++ .i.cpp bridges: the user's crate stays plain Rust (no embind-rs dependency, no macro line)
10
+ // and crossbind emits <pkg>/.crossbind/bridge-crate/ which depends on the user crate by path and holds
11
+ // all registrations (newtype wrappers dodge the orphan rule; shim fns adapt &self and enum/value
12
+ // object types). buildCargo compiles the bridge crate; its staticlib bundles the user crate.
13
+ //
14
+ // v1 surface grammar (parsed from src/lib.rs, rustfmt-style):
15
+ // #[repr(i32)] pub enum E { A = 0, .. } -> enum
16
+ // #[repr(C)] #[derive(..Default..Copy..)] pub struct V -> value object (pub fields)
17
+ // pub struct C + inherent `impl C { pub fn .. }` -> class
18
+ // pub fn new(..) -> Self -> constructor (<=3 args)
19
+ // pub fn other(..) -> Self -> smart_ptr factory (<=2 args)
20
+ // pub fn m(&mut self / &self, ..) -> R -> method (<=4 args), JS name camelCase
21
+ // top-level pub fn f(..) -> R -> free function (<=4 args)
22
+ // impl Display for C -> toString() on the class
23
+ // params may also be &str / &String (String on the wire, borrowed at the call site)
24
+ // i64 / u64 cross as JS BigInt; Result<T, E> in any return above throws in JS on Err
25
+ // (E: Display); Option<Self> factories return JS null; Option<i32/f64/bool/String> works in
26
+ // params (undefined/null -> None) and returns (None -> undefined); other Option shapes skip
27
+ // vectors come from crossbind.config.mjs: export.bindings.vectors = [{ of: 'i32', name: '..' }]
28
+ // Anything outside this surface is skipped WITH a log line - never silently.
29
+
30
+ const PRIMITIVES = new Set(['i32', 'i64', 'u64', 'f64', 'bool', 'String', '()']);
31
+ const PARAM_ONLY = new Set(['&str', '&String']);
32
+ const OPTION_INNERS = new Set(['i32', 'f64', 'bool', 'String']);
33
+ const OPTION_PARAM_RE = /^Option<(i32|f64|bool|String)>$/;
34
+ const OPTIONAL_REG = { i32: 'register_optional_i32', f64: 'register_optional_f64', bool: 'register_optional_bool', String: 'register_optional_string' };
35
+ const VECTOR_ITEM_TYPES = new Set(['i32', 'f64', 'bool']);
36
+ // serde_json::Value params/returns cross as a deep JSON copy (adapter-side codec, canonical
37
+ // token 'Json'); the bare `Value` spelling counts only when the file imports serde_json.
38
+ const JSON_TY = 'Json';
39
+ const isJsonSpelling = (ty, ctx) => Boolean(ctx?.allowJson)
40
+ && (ty === 'serde_json::Value' || (Boolean(ctx.hasSerdeUse) && ty === 'Value'));
41
+ // Arc<Class> params/returns (shared ownership): canonical token 'Arc<X>'; the bare `Arc`
42
+ // spelling counts only when the file imports std::sync::Arc. Same app-surface gate as Json.
43
+ const ARC_RE = /^Arc<(\w+)>$/;
44
+ const normalizeArc = (ty, ctx) => {
45
+ if (!ctx?.allowJson) return ty;
46
+ const m = ty.match(/^(?:std::sync::)?Arc<(\w+)>$/);
47
+ if (!m) return ty;
48
+ if (!ty.startsWith('std::sync::') && !ctx.hasArcUse) return ty;
49
+ return `Arc<${m[1]}>`;
50
+ };
51
+ // Live JS handles (embind_rs::JsValue / JsFunction): bare spellings count only when the file
52
+ // imports from embind_rs - the one deliberate coupling of the E2 surface.
53
+ const JS_TOKS = new Set(['JsValue', 'JsFunction']);
54
+ // Returns the canonical token only when the spelling is ELIGIBLE (qualified, or bare with the
55
+ // embind_rs import present) - the token equals the bare name, so acceptance must key on this
56
+ // result, never on the raw string.
57
+ const matchJsTok = (ty, ctx) => {
58
+ if (!ctx?.allowJson) return null;
59
+ const m = String(ty).match(/^(?:embind_rs::)?(JsValue|JsFunction)$/);
60
+ if (!m) return null;
61
+ if (!String(ty).startsWith('embind_rs::') && !ctx.hasEmbindUse) return null;
62
+ return m[1];
63
+ };
64
+ const FN_SIG_RE = /^pub (?:const )?fn (\w+)\s*\(([^)]*)\)\s*(?:->\s*([\w:<>(),& ]+?))?\s*\{/;
65
+
66
+ export default function generateRustBridge({ crateDir, vectors = [], dtsFile = null, keepName = null, dtsMode = 'sync', log = console.log }) {
67
+ const libRsPath = `${crateDir}/src/lib.rs`;
68
+ if (!fs.existsSync(libRsPath)) throw new Error(`crossbind: rust bridge: ${libRsPath} not found`);
69
+ const rawCrateName = readCrateName(crateDir);
70
+ const userCrate = rawCrateName.replaceAll('-', '_');
71
+ const src = fs.readFileSync(libRsPath, 'utf8');
72
+
73
+ const model = parseSurface(src, log);
74
+ let bridge = emitBridge(model, { userCrate, vectors, log });
75
+ // Rust archives are linked LAZILY (never force_load/whole-archive: each staticlib bundles its
76
+ // own libstd, and fully loading two of them duplicates thousands of std symbols). Instead the
77
+ // consumer pins this keep symbol (-u/--undefined); codegen-units=1 puts it in the same object
78
+ // as the init-array constructor, so pulling it pulls the registrations - and only them.
79
+ if (keepName) bridge += `\n#[no_mangle]\npub extern "C" fn crossbind_keep_${keepName}() {}\n`;
80
+ if (dtsFile) writeIfChanged(dtsFile, emitDts(model, vectors, dtsMode));
81
+
82
+ const bridgeDir = `${crateDir}/.crossbind/bridge-crate`;
83
+ const embindRsDir = resolveEmbindRsDir();
84
+ const manifest = [
85
+ '# Generated by crossbind rustBridgeGen - do not edit.',
86
+ `# embind-rs from @crossbind/core-embind-rust ${embindRustVersion()}`,
87
+ '[package]',
88
+ `name = "${userCrate.replaceAll('_', '-')}-crossbind-bridge"`,
89
+ 'version = "0.0.0"',
90
+ 'edition = "2021"',
91
+ '',
92
+ '[lib]',
93
+ `name = "${userCrate}_crossbind_bridge"`,
94
+ 'crate-type = ["staticlib"]',
95
+ '',
96
+ '[dependencies]',
97
+ `${userCrate} = { package = "${rawCrateName}", path = "${relPath(bridgeDir, crateDir)}" }`,
98
+ `embind-rs = { path = "${relPath(bridgeDir, embindRsDir)}" }`,
99
+ ...(model.usesJson ? ['serde_json = "1"'] : []),
100
+ '',
101
+ '[profile.release]',
102
+ 'panic = "abort"',
103
+ '# One object per crate: the keep symbol and the init-array ctor must share an object.',
104
+ 'codegen-units = 1',
105
+ '',
106
+ '# Isolated on purpose: never join a surrounding workspace.',
107
+ '[workspace]',
108
+ '',
109
+ ].join('\n');
110
+
111
+ writeIfChanged(`${bridgeDir}/Cargo.toml`, manifest);
112
+ writeIfChanged(`${bridgeDir}/src/lib.rs`, bridge);
113
+ return { bridgeDir, crateName: `${userCrate}_crossbind_bridge` };
114
+ }
115
+
116
+ // App-local .rs files (the Rust analog of an app's own .h) get a SELF-CONTAINED synthesized
117
+ // crate: the user file is embedded via `#[path] mod user;` (no copy) and the bridge lives in the
118
+ // same crate, so registrations reference `user::Type` directly. The bundler transformer calls
119
+ // this on import (like createBridgeFile for C++); the native builds compile every crate under
120
+ // <project>/.crossbind/rust-bridges/ and link the staticlibs whole-archive.
121
+ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors = [], cargoDependencies = {}, dtsMode = 'sync', log = console.log }) {
122
+ const stem = path.basename(rsFile, '.rs').replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
123
+ const dir = `${cacheDir}/rust-bridges/${stem}`;
124
+ const model = parseSurface(fs.readFileSync(rsFile, 'utf8'), log);
125
+ const bridge = emitBridge(model, {
126
+ userCrate: 'user',
127
+ vectors,
128
+ log,
129
+ prelude: `#[path = "${relPath(`${dir}/src`, rsFile)}"]\nmod user;`,
130
+ });
131
+
132
+ const manifest = [
133
+ '# Generated by crossbind rustBridgeGen - do not edit.',
134
+ `# embind-rs from @crossbind/core-embind-rust ${embindRustVersion()}`,
135
+ '[package]',
136
+ `name = "${stem.replaceAll('_', '-')}-crossbind-app"`,
137
+ 'version = "0.0.0"',
138
+ 'edition = "2021"',
139
+ '',
140
+ '[lib]',
141
+ `name = "${stem}_crossbind_app"`,
142
+ '# rlib: bundled into the single app super-crate (one libstd for all app-local surfaces).',
143
+ 'crate-type = ["rlib"]',
144
+ '',
145
+ '[dependencies]',
146
+ `embind-rs = { path = "${relPath(dir, resolveEmbindRsDir())}" }`,
147
+ // The app config's top-level cargoDependencies, so an app-local surface can use
148
+ // upstream crates directly (values: a version string, or a verbatim `{ ... }` spec).
149
+ ...Object.entries(cargoDependencies).map(([name, spec]) => (
150
+ String(spec).trim().startsWith('{') ? `${name} = ${spec}` : `${name} = "${spec}"`
151
+ )),
152
+ ...(model.usesJson && !cargoDependencies.serde_json ? ['serde_json = "1"'] : []),
153
+ '',
154
+ '[profile.release]',
155
+ 'panic = "abort"',
156
+ 'codegen-units = 1',
157
+ '',
158
+ '# Isolated on purpose: never join a surrounding workspace.',
159
+ '[workspace]',
160
+ '',
161
+ ].join('\n');
162
+
163
+ writeIfChanged(`${dir}/Cargo.toml`, manifest);
164
+ writeIfChanged(`${dir}/src/lib.rs`, bridge);
165
+ // Relative `./x.rs` imports are typed by path resolution (ambient declarations only work
166
+ // for non-relative names like `cargo:x`), so the declaration mirrors the project-relative
167
+ // path under the language-neutral <cache>/types/ overlay (same home as .h declarations);
168
+ // @crossbind/typescript-config wires the rootDirs and keeps user folders free of generated files.
169
+ writeIfChanged(`${cacheDir}/types/${path.relative(projectPath, rsFile)}.d.ts`, emitDts(model, vectors, dtsMode));
170
+ return { bridgeDir: dir, crateName: `${stem}_crossbind_app`, model };
171
+ }
172
+
173
+ // Direct CRATE import (`import { X } from 'cargo:uuid'` with top-level `cargoDependencies`
174
+ // declaring `uuid`): no surface file and no package - the bridge is generated from the
175
+ // upstream crate's OWN multi-file source. cargo metadata (which fetches on first run) locates
176
+ // the source and the resolved feature set; the bridge crate is a normal rlib under
177
+ // rust-bridges/, so the app super-staticlib flow links it like any app-local surface.
178
+ const crateModelCache = new Map();
179
+ export function createCrateImportBridge({ crateName, spec, cacheDir, dtsMode = 'sync', log = console.log }) {
180
+ const stem = `crate_${crateName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}`;
181
+ const dir = `${cacheDir}/rust-bridges/${stem}`;
182
+ const depLine = String(spec).trim().startsWith('{') ? `${crateName} = ${spec}` : `${crateName} = "${spec}"`;
183
+ const manifest = [
184
+ '# Generated by crossbind rustBridgeGen - do not edit.',
185
+ `# embind-rs from @crossbind/core-embind-rust ${embindRustVersion()}`,
186
+ '[package]',
187
+ `name = "${stem.replaceAll('_', '-')}-crossbind-app"`,
188
+ 'version = "0.0.0"',
189
+ 'edition = "2021"',
190
+ '',
191
+ '[lib]',
192
+ `name = "${stem}_crossbind_app"`,
193
+ '# rlib: bundled into the single app super-crate (one libstd for all app-local surfaces).',
194
+ 'crate-type = ["rlib"]',
195
+ '',
196
+ '[dependencies]',
197
+ depLine,
198
+ `embind-rs = { path = "${relPath(dir, resolveEmbindRsDir())}" }`,
199
+ '',
200
+ '[profile.release]',
201
+ 'panic = "abort"',
202
+ 'codegen-units = 1',
203
+ '',
204
+ '# Isolated on purpose: never join a surrounding workspace.',
205
+ '[workspace]',
206
+ '',
207
+ ].join('\n');
208
+ writeIfChanged(`${dir}/Cargo.toml`, manifest);
209
+ // cargo metadata needs a resolvable lib target before the real bridge exists.
210
+ if (!fs.existsSync(`${dir}/src/lib.rs`)) writeIfChanged(`${dir}/src/lib.rs`, '// crossbind placeholder\n');
211
+
212
+ const cacheKey = `${dir}|${depLine}`;
213
+ let model = crateModelCache.get(cacheKey);
214
+ if (!model) {
215
+ let meta;
216
+ try {
217
+ const probe = runCargo(['metadata', '--format-version', '1', '--manifest-path', `${dir}/Cargo.toml`], {
218
+ capture: true, maxBuffer: 128 * 1024 * 1024,
219
+ });
220
+ if (probe.status !== 0) throw new Error(String(probe.stderr ?? '').trim() || `exit code ${probe.status}`);
221
+ meta = JSON.parse(probe.stdout);
222
+ } catch (e) {
223
+ throw new Error(`crossbind: crate import '${crateName}': cargo metadata failed (${e.message})`, { cause: e });
224
+ }
225
+ const pkg = meta.packages.find((p) => p.name === crateName);
226
+ if (!pkg) throw new Error(`crossbind: crate import '${crateName}': crate not found in the cargo dependency graph`);
227
+ const features = meta.resolve?.nodes?.find((n) => n.id === pkg.id)?.features ?? [];
228
+ const srcDir = path.join(path.dirname(toHostPath(pkg.manifest_path)), 'src');
229
+ model = parseCrateSurface({ srcDir, features, log });
230
+ // Always audible (the transformer silences routine skip-noise): an empty surface means
231
+ // the import will bind NOTHING - generic/re-export-style crates need an app-local .rs.
232
+ if (!model.classes.length && !model.enums.length && !model.freeFns.length) {
233
+ 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`);
234
+ }
235
+ crateModelCache.set(cacheKey, model);
236
+ }
237
+
238
+ const bridge = emitBridge(model, { userCrate: crateName.replaceAll('-', '_'), vectors: [], log });
239
+ writeIfChanged(`${dir}/src/lib.rs`, bridge);
240
+ // Editor types for the `cargo:<crate>` import: an ambient module the app's tsconfig
241
+ // includes (e.g. "include": ["**/*", ".crossbind/rust-crates/types/**/*.d.ts"]). The scheme
242
+ // keeps the module name unique, so npm packages/@types of the same name never clash.
243
+ const dtsBody = emitDts(model, [], dtsMode).split('\n').map((l) => (l ? ` ${l}` : l)).join('\n');
244
+ writeIfChanged(`${cacheDir}/rust-crates/types/${crateName}.d.ts`, `declare module 'cargo:${crateName}' {\n${dtsBody}\n}\n`);
245
+ return { bridgeDir: dir, crateName: `${stem}_crossbind_app`, model };
246
+ }
247
+
248
+ // The embind-rs runtime crate ships inside @crossbind/core-embind-rust; the consumer (a plugin
249
+ // or the package itself) declares that dependency and resolveEmbindRust finds it.
250
+ function resolveEmbindRsDir() {
251
+ return `${resolveEmbindRustRoot()}/crate`;
252
+ }
253
+
254
+ // Returns the RAW [package] name (dashes kept): cargo dependency lookups need it verbatim.
255
+ // Callers wanting the crate/lib identifier convert dashes to underscores themselves.
256
+ // Generated manifests address their siblings relatively. An absolute path bakes in the host
257
+ // checkout, which is wrong the moment the same tree is read from anywhere else - a container mount
258
+ // being the case that made this visible. Cargo resolves `path` against the manifest's directory
259
+ // and `#[path]` against the file carrying it, so a relative value is correct in both worlds.
260
+ export function relPath(fromDir, target) {
261
+ return path.relative(fromDir, target).split(path.sep).join('/') || '.';
262
+ }
263
+
264
+ export function readCrateName(crateDir) {
265
+ const toml = fs.readFileSync(`${crateDir}/Cargo.toml`, 'utf8');
266
+ const name = toml.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
267
+ if (!name) throw new Error(`crossbind: could not read [package] name from ${crateDir}/Cargo.toml`);
268
+ return name;
269
+ }
270
+
271
+
272
+ // ---------------- parser ----------------
273
+
274
+ export function parseSurface(src, log) {
275
+ const acc = newAcc();
276
+ scanSource(src, acc, { collectTypes: true, log });
277
+ return finalizeModel(acc, log);
278
+ }
279
+
280
+ // Parses an UPSTREAM crate for a direct crate import (`import .. from '<crate>'`): exported
281
+ // types come from lib.rs; inherent impls and `impl Display` are collected across the crate's
282
+ // `mod`-declared files, following the resolved feature set for cfg-gated modules.
283
+ export function parseCrateSurface({ srcDir, features = [], log = console.log }) {
284
+ const acc = newAcc();
285
+ const enabled = new Set(features);
286
+ const seen = new Set();
287
+ const walk = (file, modPath) => {
288
+ if (seen.has(file) || !fs.existsSync(file)) return;
289
+ seen.add(file);
290
+ // Root and glob-re-exported modules collect their pub types; other modules only
291
+ // contribute impls (plus types individually re-exported by name).
292
+ const collectTypes = modPath.length === 0 || acc.globModules.has(modPath.join('::'));
293
+ const mods = scanSource(fs.readFileSync(file, 'utf8'), acc, { collectTypes, log });
294
+ const childBase = file.endsWith('lib.rs') || file.endsWith('mod.rs') ? path.dirname(file) : file.slice(0, -3);
295
+ for (const m of mods) {
296
+ if (!cfgEnabled(m.cfg, enabled)) continue;
297
+ walk(path.join(childBase, `${m.name}.rs`), [...modPath, m.name]);
298
+ walk(path.join(childBase, m.name, 'mod.rs'), [...modPath, m.name]);
299
+ }
300
+ };
301
+ walk(path.join(srcDir, 'lib.rs'), []);
302
+ return finalizeModel(acc, log);
303
+ }
304
+
305
+ function newAcc() {
306
+ return {
307
+ enums: [], // { name, variants: [{ name, value }] }
308
+ valueObjects: [], // { name, fields: [{ name, type }] }
309
+ classes: new Map(), // name -> { name, ctor, factories: [], methods: [], hasDisplay }
310
+ freeFns: [], // { name, jsName, args, ret, throws }
311
+ displayNames: new Set(),
312
+ wantedTypes: new Set(), // type names re-exported from lib.rs (`pub use ..::{X}`)
313
+ globModules: new Set(), // module paths glob-re-exported from lib.rs (`pub use ..::m::*`)
314
+ };
315
+ }
316
+
317
+ // `#[cfg(..)]` on a `mod` line: null = ungated; no feature tokens = never enabled (so
318
+ // cfg(test) / cfg(target_os = ..) modules are skipped).
319
+ function cfgOf(attrs) {
320
+ const cfg = attrs.find((a) => a.startsWith('#[cfg('));
321
+ if (!cfg) return null;
322
+ const features = [...cfg.matchAll(/feature\s*=\s*"([^"]+)"/g)].map((m) => m[1]);
323
+ return { features, any: /cfg\(any/.test(cfg) };
324
+ }
325
+
326
+ function cfgEnabled(cfg, enabled) {
327
+ if (cfg === null) return true;
328
+ if (cfg.features.length === 0) return false;
329
+ return cfg.any ? cfg.features.some((f) => enabled.has(f)) : cfg.features.every((f) => enabled.has(f));
330
+ }
331
+
332
+ // Scans ONE source file into the accumulator; returns the `mod name;` declarations found.
333
+ // collectTypes: types and free fns register only from the crate root (lib.rs) - module files
334
+ // contribute impls (methods, factories, Display) for already-known types.
335
+ function scanSource(src, acc, { collectTypes, log }) {
336
+ const { enums, valueObjects, classes, freeFns, displayNames, wantedTypes } = acc;
337
+ acc.hasSerdeUse ||= /\buse\s+serde_json\b/.test(src);
338
+ acc.hasArcUse ||= /\buse\s+std::sync::(?:Arc\b|\{[^}]*\bArc\b)/.test(src);
339
+ acc.hasEmbindUse ||= /\buse\s+embind_rs::/.test(src);
340
+ const mods = [];
341
+ // Strip line comments; join multi-line `pub fn` signatures (to their brace) and multi-line
342
+ // `pub use` re-export lists (to their semicolon).
343
+ const rawLines = src.split('\n').map((l) => l.replace(/\/\/.*$/, ''));
344
+ const lines = [];
345
+ for (let i = 0; i < rawLines.length; i += 1) {
346
+ let line = rawLines[i];
347
+ if (/\bpub (?:const )?fn\b/.test(line)) {
348
+ while (!line.includes('{') && !line.includes(';') && i + 1 < rawLines.length) {
349
+ i += 1;
350
+ line = `${line} ${rawLines[i].trim()}`;
351
+ }
352
+ } else if (/^\s*pub use\b/.test(line)) {
353
+ while (!line.includes(';') && i + 1 < rawLines.length) {
354
+ i += 1;
355
+ line = `${line} ${rawLines[i].trim()}`;
356
+ }
357
+ }
358
+ lines.push(line);
359
+ }
360
+
361
+ let attrs = [];
362
+ for (let i = 0; i < lines.length; i += 1) {
363
+ const t = lines[i].trim();
364
+ if (t.startsWith('#[') || t.startsWith('#![')) { attrs.push(t); continue; }
365
+ if (t === '') continue;
366
+
367
+ // Crate-root re-exports make module-defined types part of the surface: capitalized
368
+ // leaves of `pub use ..::{X, Y};` are collected when the defining module is scanned.
369
+ if (collectTypes && t.startsWith('pub use ')) {
370
+ // Named re-exports mark module types as surface; glob re-exports mark their whole
371
+ // module as root-like (its pub types are collected when the walk reaches it).
372
+ const body = t.replace(/^pub use\s+/, '').replace(/;.*$/, '').trim();
373
+ const items = [];
374
+ const braced = body.match(/^(?:crate::|self::)?(?:([\w:]+)::)?\{(.*)\}$/);
375
+ if (braced) {
376
+ const prefix = braced[1] ? `${braced[1]}::` : '';
377
+ braced[2].split(',').map((s) => s.trim()).filter(Boolean)
378
+ .forEach((s) => items.push(prefix + s.replace(/^(?:crate::|self::)/, '')));
379
+ } else {
380
+ items.push(body.replace(/^(?:crate::|self::)/, ''));
381
+ }
382
+ for (const item of items) {
383
+ if (item.endsWith('::*')) {
384
+ acc.globModules.add(item.slice(0, -3));
385
+ } else {
386
+ const leaf = (item.split('::').pop() ?? '').replace(/\s+as\s+\w+$/, '');
387
+ if (/^[A-Z]\w*$/.test(leaf)) wantedTypes.add(leaf);
388
+ }
389
+ }
390
+ attrs = [];
391
+ continue;
392
+ }
393
+
394
+ let enumM = t.match(/^pub enum (\w+)/);
395
+ if (enumM && !collectTypes && !wantedTypes.has(enumM[1])) enumM = null;
396
+ let structM = t.match(/^pub struct (\w+)/);
397
+ if (structM && !collectTypes && !wantedTypes.has(structM[1])) structM = null;
398
+ const modM = t.match(/^(?:pub(?:\([^)]*\))?\s+)?mod (\w+)\s*([;{])/);
399
+ const displayM = t.match(/^impl (?:[\w:]+::)?Display for (\w+)/);
400
+ const traitImplM = !displayM && /^impl\b[^{]*\bfor\b/.test(t);
401
+ const implM = displayM || traitImplM ? null : t.match(/^impl (\w+)\s*\{/);
402
+ const freeFnM = collectTypes && !enumM && !structM && !modM && !displayM && !traitImplM && !implM
403
+ ? t.match(FN_SIG_RE) : null;
404
+
405
+ if (enumM) {
406
+ const isReprI32 = attrs.some((a) => /repr\(i32\)/.test(a));
407
+ const variants = [];
408
+ let idx = 0;
409
+ for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1) {
410
+ const v = lines[i].trim().match(/^(\w+)(?:\s*=\s*(-?\d+))?\s*,?$/);
411
+ if (v) { variants.push({ name: v[1], value: v[2] !== undefined ? Number(v[2]) : idx }); idx = variants.at(-1).value + 1; }
412
+ }
413
+ if (isReprI32 && variants.length) enums.push({ name: enumM[1], variants });
414
+ else log(`crossbind: rust bridge: enum ${enumM[1]} skipped (needs #[repr(i32)] and unit variants)`);
415
+ } else if (structM) {
416
+ const isReprC = attrs.some((a) => /repr\(C\)/.test(a));
417
+ const derive = attrs.find((a) => a.startsWith('#[derive')) ?? '';
418
+ if (isReprC) {
419
+ const ok = derive.includes('Default') && derive.includes('Copy');
420
+ const fields = [];
421
+ for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1) {
422
+ const f = lines[i].trim().match(/^pub (\w+)\s*:\s*([\w()]+)\s*,?$/);
423
+ if (f && PRIMITIVES.has(f[2])) fields.push({ name: f[1], type: f[2] });
424
+ }
425
+ if (ok && fields.length) valueObjects.push({ name: structM[1], fields });
426
+ else log(`crossbind: rust bridge: struct ${structM[1]} skipped (repr(C) needs derive(Default, Copy) and pub primitive fields)`);
427
+ } else {
428
+ if (!classes.has(structM[1])) classes.set(structM[1], { name: structM[1], ctor: null, factories: [], methods: [] });
429
+ if (t.endsWith('{')) { for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1); }
430
+ }
431
+ } else if (modM) {
432
+ // `mod x;` is followed for crate imports; inline `mod x { .. }` bodies are opaque.
433
+ if (modM[2] === ';') mods.push({ name: modM[1], cfg: cfgOf(attrs) });
434
+ else i = skipBlock(lines, i);
435
+ } else if (displayM) {
436
+ // `impl Display for X` -> a JS toString(); the block body itself is not parsed.
437
+ displayNames.add(displayM[1]);
438
+ i = skipBlock(lines, i);
439
+ } else if (traitImplM) {
440
+ i = skipBlock(lines, i);
441
+ } else if (implM && classes.has(implM[1])) {
442
+ const cls = classes.get(implM[1]);
443
+ let depth = 1;
444
+ for (i += 1; i < lines.length && depth > 0; i += 1) {
445
+ const s = lines[i];
446
+ const sig = s.trim().match(FN_SIG_RE);
447
+ if (sig && depth === 1) parseFn(cls, sig, { enums, valueObjects, classes, allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse }, log);
448
+ depth += (s.match(/\{/g) ?? []).length - (s.match(/\}/g) ?? []).length;
449
+ }
450
+ i -= 1;
451
+ } else if (implM) {
452
+ // Consume unknown-impl bodies so their fns are never misread as free functions.
453
+ i = skipBlock(lines, i);
454
+ } else if (freeFnM) {
455
+ parseFreeFn(freeFns, freeFnM, { enums, valueObjects, classes, allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse }, log);
456
+ }
457
+ attrs = [];
458
+ }
459
+ return mods;
460
+ }
461
+
462
+ function finalizeModel(acc, log) {
463
+ const { enums, valueObjects, classes, freeFns, displayNames } = acc;
464
+ // A class with no exported surface is dropped (with a note), mirroring the C++ generator.
465
+ for (const [name, cls] of classes) {
466
+ if (!cls.ctor && !cls.factories.length && !cls.methods.length) {
467
+ classes.delete(name);
468
+ log(`crossbind: rust bridge: struct ${name} has no exportable pub fns - not registered`);
469
+ }
470
+ }
471
+ for (const cls of classes.values()) {
472
+ const collides = cls.methods.some((m) => camel(m.name) === 'toString');
473
+ if (displayNames.has(cls.name) && collides) log(`crossbind: rust bridge: ${cls.name} Display->toString skipped (a toString method already exists)`);
474
+ cls.hasDisplay = displayNames.has(cls.name) && !collides;
475
+ }
476
+ const anyJson = (args, ret) => args.some((p) => p.ty === JSON_TY) || ret === JSON_TY;
477
+ const usesJson = freeFns.some((f) => anyJson(f.args, f.ret))
478
+ || [...classes.values()].some((c) => (c.ctor && anyJson(c.ctor.args, '()'))
479
+ || c.factories.some((f) => anyJson(f.args, '()'))
480
+ || c.methods.some((m) => anyJson(m.args, m.ret)));
481
+
482
+ // Shared (Arc) classes: collect every class named by an Arc<...> surface anywhere, then
483
+ // enforce the shapes whose Box paths would corrupt the Arc-based delete()/share machinery.
484
+ const sharedOf = new Set();
485
+ const noteArc = (ty) => { const m = String(ty).match(ARC_RE); if (m) sharedOf.add(m[1]); };
486
+ const noteAll = (args, ret) => { args.forEach((p) => noteArc(p.ty)); noteArc(ret); };
487
+ freeFns.forEach((f) => noteAll(f.args, f.ret));
488
+ for (const c of classes.values()) {
489
+ if (c.factories.some((f) => f.shared)) sharedOf.add(c.name);
490
+ if (c.ctor) noteAll(c.ctor.args, '()');
491
+ c.factories.forEach((f) => noteAll(f.args, '()'));
492
+ c.methods.forEach((m) => noteAll(m.args, m.ret));
493
+ }
494
+ for (const c of classes.values()) {
495
+ if (!sharedOf.has(c.name)) continue;
496
+ c.shared = true;
497
+ if (c.ctor) {
498
+ throw new Error(`crossbind: rust bridge: ${c.name} has Arc<...> surfaces, so a plain 'new' constructor cannot exist (its Box allocation would corrupt the shared delete()) - use a named factory returning Arc<Self>`);
499
+ }
500
+ const mut = c.methods.find((m) => !m.byRef);
501
+ if (mut) {
502
+ throw new Error(`crossbind: rust bridge: ${c.name} is shared via Arc<...>, so its methods must take &self ('${mut.name}' takes &mut self) - use interior mutability or drop the Arc surface`);
503
+ }
504
+ const boxed = c.factories.find((f) => !f.shared);
505
+ if (boxed) {
506
+ throw new Error(`crossbind: rust bridge: ${c.name} is shared via Arc<...>, so every factory must return Arc<Self> ('${boxed.name}' returns Self)`);
507
+ }
508
+ }
509
+ return { enums, valueObjects, classes: [...classes.values()], freeFns, usesJson, sharedOf: [...sharedOf].sort() };
510
+ }
511
+
512
+ // Consumes a brace-delimited block starting at line i; returns the closing line's index.
513
+ function skipBlock(lines, i) {
514
+ let depth = 0;
515
+ let started = false;
516
+ for (; i < lines.length; i += 1) {
517
+ const opens = (lines[i].match(/\{/g) ?? []).length;
518
+ depth += opens - ((lines[i].match(/\}/g) ?? []).length);
519
+ if (opens > 0) started = true;
520
+ if (started && depth <= 0) break;
521
+ }
522
+ return i;
523
+ }
524
+
525
+ // Splits `Result<T, E>` / `Option<T>` off a return type; `Result<T>` (an alias like
526
+ // anyhow/io::Result) also matches - the shim's Ok/Err arms work on any core Result underneath.
527
+ function analyzeReturn(raw) {
528
+ const r = raw.trim();
529
+ const res = r.match(/^Result\s*<\s*(.+?)\s*(?:,\s*([^<>]+?)\s*)?>$/);
530
+ if (res) return { inner: res[1], throws: true, optional: false };
531
+ const opt = r.match(/^Option\s*<\s*(.+?)\s*>$/);
532
+ if (opt) return { inner: opt[1], throws: false, optional: true };
533
+ return { inner: r, throws: false, optional: false };
534
+ }
535
+
536
+ function parseFn(cls, sig, ctx, log) {
537
+ const { enums, valueObjects, classes } = ctx;
538
+ const [, name, rawParams, rawRet] = sig;
539
+ const known = (ty) => PRIMITIVES.has(ty)
540
+ || enums.some((e) => e.name === ty) || valueObjects.some((v) => v.name === ty);
541
+ // `&OtherClass` params: the referenced struct must already be declared (parsed) above.
542
+ const isClassRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty) && classes.has(ty.slice(1));
543
+
544
+ const params = rawParams.split(',').map((p) => p.trim()).filter(Boolean);
545
+ let selfKind = null;
546
+ if (params[0] === '&mut self' || params[0] === '&self') selfKind = params.shift();
547
+ else if (params[0] === 'self' || params[0] === 'mut self') {
548
+ log(`crossbind: rust bridge: ${cls.name}::${name} skipped (consuming self is not supported)`);
549
+ return;
550
+ }
551
+
552
+ const args = [];
553
+ for (const p of params) {
554
+ const m = p.match(/^(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String)\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()]+)$/);
555
+ let ty = m?.[2].replace(/\s+/g, '');
556
+ if (ty && !known(ty) && isJsonSpelling(ty, ctx)) ty = JSON_TY;
557
+ if (ty) ty = normalizeArc(ty, ctx);
558
+ const jsTokP = ty ? matchJsTok(ty, ctx) : null;
559
+ if (jsTokP) ty = jsTokP;
560
+ const arcParam = ty?.match(ARC_RE)?.[1];
561
+ if (!m || !(known(ty) || ty === JSON_TY || jsTokP || (arcParam && classes.has(arcParam)) || PARAM_ONLY.has(ty) || OPTION_PARAM_RE.test(ty) || isClassRef(ty))) {
562
+ log(`crossbind: rust bridge: ${cls.name}::${name} skipped (unsupported parameter '${p}')`);
563
+ return;
564
+ }
565
+ args.push({ name: m[1], ty });
566
+ }
567
+ let { inner: ret, throws, optional } = analyzeReturn(rawRet ?? '()');
568
+ if (!known(ret) && isJsonSpelling(ret, ctx)) ret = JSON_TY;
569
+ ret = normalizeArc(ret.replace(/\s+/g, ''), ctx);
570
+ const jsTokR = matchJsTok(ret, ctx);
571
+ if (jsTokR) ret = jsTokR;
572
+ if (jsTokR && optional) {
573
+ log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Option<${ret}> returns are not supported - return JsValue::undefined() instead)`);
574
+ return;
575
+ }
576
+
577
+ if (!selfKind) {
578
+ const arcSelf = ret === 'Arc<Self>' || ret === `Arc<${cls.name}>`;
579
+ if (!arcSelf && ret !== 'Self' && ret !== cls.name) {
580
+ log(`crossbind: rust bridge: ${cls.name}::${name} skipped (associated fns must return Self, Result<Self, E> or Option<Self>)`);
581
+ return;
582
+ }
583
+ if (name === 'new') {
584
+ if (arcSelf) { log(`crossbind: rust bridge: ${cls.name}::new skipped (Arc<Self> has no ctor shape - use a named factory like 'create')`); return; }
585
+ 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; }
586
+ if (args.length > 3) { log(`crossbind: rust bridge: ${cls.name}::new skipped (max 3 args)`); return; }
587
+ cls.ctor = { args, throws };
588
+ } else {
589
+ if (arcSelf && optional) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Option<Arc<Self>> is not supported in this wave)`); return; }
590
+ if (args.length > 2) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (factories take max 2 args)`); return; }
591
+ cls.factories.push({ name, args, throws, optional, shared: arcSelf });
592
+ }
593
+ return;
594
+ }
595
+ const retArcInner = ret.match(ARC_RE)?.[1];
596
+ if (retArcInner) {
597
+ const target = retArcInner === 'Self' ? cls.name : retArcInner;
598
+ if (!classes.has(target)) {
599
+ log(`crossbind: rust bridge: ${cls.name}::${name} skipped (unsupported return '${ret}')`);
600
+ return;
601
+ }
602
+ if (optional || throws) {
603
+ log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Arc returns on methods support neither Option nor Result in this wave)`);
604
+ return;
605
+ }
606
+ if (args.length > 4) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (max 4 args)`); return; }
607
+ cls.methods.push({ name, args, ret: `Arc<${target}>`, byRef: selfKind === '&self', throws: false, optionalRet: false });
608
+ return;
609
+ }
610
+ if (optional && !OPTION_INNERS.has(ret)) {
611
+ log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Option<${ret}> return is not representable - inners: i32, f64, bool, String; or an Option<Self> factory)`);
612
+ return;
613
+ }
614
+ if (!known(ret) && ret !== JSON_TY && !jsTokR) {
615
+ log(`crossbind: rust bridge: ${cls.name}::${name} skipped (unsupported return '${ret}')`);
616
+ return;
617
+ }
618
+ if (args.length > 4) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (max 4 args)`); return; }
619
+ cls.methods.push({ name, args, ret, byRef: selfKind === '&self', throws, optionalRet: optional });
620
+ }
621
+
622
+ function parseFreeFn(freeFns, sig, ctx, log) {
623
+ const { enums, valueObjects, classes } = ctx;
624
+ const [, name, rawParams, rawRet] = sig;
625
+ const known = (ty) => PRIMITIVES.has(ty)
626
+ || enums.some((e) => e.name === ty) || valueObjects.some((v) => v.name === ty);
627
+ const isClassRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty) && classes.has(ty.slice(1));
628
+
629
+ const args = [];
630
+ for (const p of rawParams.split(',').map((s) => s.trim()).filter(Boolean)) {
631
+ const m = p.match(/^(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String)\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()]+)$/);
632
+ let ty = m?.[2].replace(/\s+/g, '');
633
+ if (ty && !known(ty) && isJsonSpelling(ty, ctx)) ty = JSON_TY;
634
+ if (ty) ty = normalizeArc(ty, ctx);
635
+ const jsTokP = ty ? matchJsTok(ty, ctx) : null;
636
+ if (jsTokP) ty = jsTokP;
637
+ const arcParam = ty?.match(ARC_RE)?.[1];
638
+ if (!m || !(known(ty) || ty === JSON_TY || jsTokP || (arcParam && classes.has(arcParam)) || PARAM_ONLY.has(ty) || OPTION_PARAM_RE.test(ty) || isClassRef(ty))) {
639
+ log(`crossbind: rust bridge: fn ${name} skipped (unsupported parameter '${p}')`);
640
+ return;
641
+ }
642
+ args.push({ name: m[1], ty });
643
+ }
644
+ let { inner: ret, throws, optional } = analyzeReturn(rawRet ?? '()');
645
+ if (!known(ret) && isJsonSpelling(ret, ctx)) ret = JSON_TY;
646
+ ret = normalizeArc(ret.replace(/\s+/g, ''), ctx);
647
+ const jsTokR = matchJsTok(ret, ctx);
648
+ if (jsTokR) ret = jsTokR;
649
+ const retArc = ret.match(ARC_RE)?.[1];
650
+ if (retArc) {
651
+ if (!classes.has(retArc)) { log(`crossbind: rust bridge: fn ${name} skipped (unsupported return '${ret}')`); return; }
652
+ if (optional || throws) { log(`crossbind: rust bridge: fn ${name} skipped (Arc returns support neither Option nor Result in this wave)`); return; }
653
+ }
654
+ if (jsTokR && optional) { log(`crossbind: rust bridge: fn ${name} skipped (Option<${ret}> returns are not supported - return JsValue::undefined() instead)`); return; }
655
+ if (optional && !OPTION_INNERS.has(ret)) { log(`crossbind: rust bridge: fn ${name} skipped (Option<${ret}> return is not representable - inners: i32, f64, bool, String)`); return; }
656
+ if (!known(ret) && ret !== JSON_TY && !retArc && !jsTokR) { log(`crossbind: rust bridge: fn ${name} skipped (unsupported return '${ret}')`); return; }
657
+ if (args.length > 4) { log(`crossbind: rust bridge: fn ${name} skipped (max 4 args)`); return; }
658
+ freeFns.push({ name, jsName: camel(name), args, ret, throws, optionalRet: optional });
659
+ }
660
+
661
+ // ---------------- emitter ----------------
662
+
663
+ const camel = (s) => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
664
+
665
+ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
666
+ const U = userCrate;
667
+ const isEnum = (ty) => model.enums.some((e) => e.name === ty);
668
+ const isVo = (ty) => model.valueObjects.some((v) => v.name === ty);
669
+ // Bridge-local wire type of a signature token, and the two directions of the shim adaptation.
670
+ // &str/&String params cross as String (borrowed at the call site); &OtherClass params cross
671
+ // as the class pointer via a bridge-local Ref wrapper (borrowed unsafely at the call site).
672
+ const isRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty);
673
+ const arcInner = (ty) => String(ty).match(ARC_RE)?.[1];
674
+ const wireTy = (ty) => (PARAM_ONLY.has(ty) ? 'String'
675
+ : isRef(ty) ? `${ty.slice(1)}Ref`
676
+ : ty === JSON_TY ? '__CrossbindJson'
677
+ : JS_TOKS.has(ty) ? `embind_rs::${ty}`
678
+ : arcInner(ty) ? `${arcInner(ty)}Shared`
679
+ : isEnum(ty) || isVo(ty) ? `${ty}W` : ty);
680
+ const unwrap = (ty, expr) => (PARAM_ONLY.has(ty) ? `&${expr}`
681
+ : isRef(ty) ? `unsafe { &*${expr}.0 }`
682
+ : ty === JSON_TY || arcInner(ty) || isEnum(ty) || isVo(ty) ? `${expr}.0` : expr);
683
+ const wrap = (ty, expr) => (ty === JSON_TY ? `__CrossbindJson(${expr})`
684
+ : arcInner(ty) ? `${arcInner(ty)}Shared(${expr})`
685
+ : isEnum(ty) || isVo(ty) ? `${ty}W(${expr})` : expr);
686
+
687
+ const out = [];
688
+ out.push('// Generated by crossbind rustBridgeGen - do not edit. The user crate stays plain Rust;');
689
+ out.push('// newtype wrappers (orphan rule) + shim fns adapt its pub surface to embind-rs.');
690
+ out.push('#![allow(clippy::all)]');
691
+ out.push('#![allow(unused_imports)]');
692
+ out.push('use embind_rs::{class_, enum_, enum_tid, register_vector, value_object_, value_object_tid, WireType};');
693
+ out.push('use std::ffi::c_void;');
694
+ out.push('');
695
+ if (prelude) {
696
+ out.push(prelude);
697
+ out.push('');
698
+ }
699
+
700
+ for (const e of model.enums) {
701
+ out.push('#[derive(Clone, Copy)]');
702
+ out.push('#[repr(transparent)]');
703
+ out.push(`pub struct ${e.name}W(pub ${U}::${e.name});`);
704
+ out.push(`impl WireType for ${e.name}W {`);
705
+ out.push(' type Wire = i32;');
706
+ out.push(" const SIG: char = 'i';");
707
+ out.push(` fn tid() -> *const c_void { enum_tid::<${e.name}W>() }`);
708
+ out.push(` fn from_wire(w: i32) -> ${e.name}W {`);
709
+ out.push(' match w {');
710
+ for (const v of e.variants) out.push(` ${v.value} => ${e.name}W(${U}::${e.name}::${v.name}),`);
711
+ out.push(` _ => ${e.name}W(${U}::${e.name}::${e.variants[0].name}),`);
712
+ out.push(' }');
713
+ out.push(' }');
714
+ out.push(' fn to_wire(self) -> i32 { self.0 as i32 }');
715
+ out.push('}');
716
+ out.push(`impl embind_rs::ErrSentinel for ${e.name}W { fn err_sentinel() -> Self { <${e.name}W as WireType>::from_wire(0) } }`);
717
+ out.push('');
718
+ }
719
+
720
+ for (const v of model.valueObjects) {
721
+ out.push('#[derive(Clone, Copy)]');
722
+ out.push('#[repr(transparent)]');
723
+ out.push(`pub struct ${v.name}W(pub ${U}::${v.name});`);
724
+ out.push(`impl Default for ${v.name}W { fn default() -> Self { ${v.name}W(${U}::${v.name}::default()) } }`);
725
+ out.push(`impl WireType for ${v.name}W {`);
726
+ out.push(` type Wire = *mut ${v.name}W;`);
727
+ out.push(" const SIG: char = 'p';");
728
+ out.push(` fn tid() -> *const c_void { value_object_tid::<${v.name}W>() }`);
729
+ out.push(` fn from_wire(w: *mut ${v.name}W) -> ${v.name}W { unsafe { *w } }`);
730
+ out.push(` fn to_wire(self) -> *mut ${v.name}W { Box::into_raw(Box::new(self)) }`);
731
+ out.push('}');
732
+ out.push(`impl embind_rs::ErrSentinel for ${v.name}W { fn err_sentinel() -> Self { Default::default() } }`);
733
+ out.push('');
734
+ }
735
+
736
+ // `&OtherClass` params ride the class pointer wire through per-class Ref wrappers; collect
737
+ // the referenced classes up front so the wrappers exist before the shims that use them.
738
+ const classRefs = new Set();
739
+ const scanRefs = (args) => args.forEach((p) => { if (isRef(p.ty)) classRefs.add(p.ty.slice(1)); });
740
+ for (const cls of model.classes) {
741
+ if (cls.ctor) scanRefs(cls.ctor.args);
742
+ cls.factories.forEach((f) => scanRefs(f.args));
743
+ cls.methods.forEach((m) => scanRefs(m.args));
744
+ }
745
+ (model.freeFns ?? []).forEach((f) => scanRefs(f.args));
746
+ for (const name of [...classRefs].sort()) {
747
+ out.push('#[derive(Clone, Copy)]');
748
+ out.push('#[repr(transparent)]');
749
+ out.push(`pub struct ${name}Ref(pub *mut ${U}::${name});`);
750
+ out.push(`impl WireType for ${name}Ref {`);
751
+ out.push(' type Wire = *mut c_void;');
752
+ out.push(" const SIG: char = 'p';");
753
+ out.push(` fn tid() -> *const c_void { embind_rs::class_tid::<${U}::${name}>() }`);
754
+ out.push(` fn from_wire(w: *mut c_void) -> ${name}Ref { ${name}Ref(w as *mut ${U}::${name}) }`);
755
+ out.push(` fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }`);
756
+ out.push('}');
757
+ out.push('');
758
+ }
759
+
760
+ if (model.usesJson) {
761
+ out.push('// serde_json::Value crosses as a deep JSON copy: the adapter converts handle <->');
762
+ out.push('// [u32 len][bytes] JSON text through the host JSON codec; serde maps text <-> Value here.');
763
+ out.push('#[repr(transparent)]');
764
+ out.push('pub struct __CrossbindJson(pub serde_json::Value);');
765
+ out.push('extern "C" {');
766
+ out.push(' fn crossbind_tid_emval() -> *const c_void;');
767
+ out.push(' fn crossbind_emval_json_to_handle(w: *mut u8) -> usize;');
768
+ out.push(' fn crossbind_emval_handle_to_json(h: usize) -> *mut u8;');
769
+ out.push(' fn malloc(n: usize) -> *mut u8;');
770
+ out.push(' fn free(p: *mut u8);');
771
+ out.push('}');
772
+ out.push('impl WireType for __CrossbindJson {');
773
+ out.push(' type Wire = usize;');
774
+ out.push(' // Handles are i32 table indexes on wasm but BigInt-marshalled 64-bit values on');
775
+ out.push(" // native (the jsi adapter's pointer slots), so the sig letter is per-family.");
776
+ out.push(' #[cfg(target_family = "wasm")]');
777
+ out.push(" const SIG: char = 'i';");
778
+ out.push(' #[cfg(not(target_family = "wasm"))]');
779
+ out.push(" const SIG: char = 'p';");
780
+ out.push(' fn tid() -> *const c_void { unsafe { crossbind_tid_emval() } }');
781
+ out.push(' fn from_wire(w: usize) -> Self {');
782
+ out.push(' unsafe {');
783
+ out.push(' let p = crossbind_emval_handle_to_json(w);');
784
+ out.push(' let len = *(p as *const u32) as usize;');
785
+ out.push(' let bytes = std::slice::from_raw_parts(p.add(4), len);');
786
+ out.push(' let v = serde_json::from_slice(bytes).unwrap_or(serde_json::Value::Null);');
787
+ out.push(' free(p);');
788
+ out.push(' __CrossbindJson(v)');
789
+ out.push(' }');
790
+ out.push(' }');
791
+ out.push(' fn to_wire(self) -> usize {');
792
+ out.push(' let s = self.0.to_string();');
793
+ out.push(' unsafe {');
794
+ out.push(' let bytes = s.as_bytes();');
795
+ out.push(' let base = malloc(4 + bytes.len());');
796
+ out.push(' *(base as *mut u32) = bytes.len() as u32;');
797
+ out.push(' std::ptr::copy_nonoverlapping(bytes.as_ptr(), base.add(4), bytes.len());');
798
+ out.push(' crossbind_emval_json_to_handle(base)');
799
+ out.push(' }');
800
+ out.push(' }');
801
+ out.push('}');
802
+ out.push('impl embind_rs::ErrSentinel for __CrossbindJson { fn err_sentinel() -> Self { __CrossbindJson(serde_json::Value::Null) } }');
803
+ out.push('');
804
+ }
805
+
806
+ // Arc<X> surfaces cross as the class's shared smart-pointer wire: the raw Arc::into_raw
807
+ // pointer, one strong count per JS handle (given on to_wire, added on from_wire).
808
+ for (const inner of model.sharedOf ?? []) {
809
+ out.push('#[repr(transparent)]');
810
+ out.push(`pub struct ${inner}Shared(pub std::sync::Arc<${U}::${inner}>);`);
811
+ out.push(`impl WireType for ${inner}Shared {`);
812
+ out.push(' type Wire = *mut c_void;');
813
+ out.push(" const SIG: char = 'p';");
814
+ out.push(` fn tid() -> *const c_void { embind_rs::shared_tid::<${U}::${inner}>() }`);
815
+ out.push(' fn from_wire(w: *mut c_void) -> Self {');
816
+ out.push(' unsafe {');
817
+ out.push(` std::sync::Arc::increment_strong_count(w as *const ${U}::${inner});`);
818
+ out.push(` ${inner}Shared(std::sync::Arc::from_raw(w as *const ${U}::${inner}))`);
819
+ out.push(' }');
820
+ out.push(' }');
821
+ out.push(` fn to_wire(self) -> *mut c_void { std::sync::Arc::into_raw(self.0) as *mut c_void }`);
822
+ out.push('}');
823
+ out.push('');
824
+ }
825
+
826
+ const registrations = [];
827
+ const usedOptionals = new Set();
828
+ const noteOptionArgs = (args) => args.forEach((p) => {
829
+ const m = p.ty.match(OPTION_PARAM_RE);
830
+ if (m) usedOptionals.add(m[1]);
831
+ });
832
+ for (const e of model.enums) {
833
+ registrations.push(` enum_::<${e.name}W>("${e.name}")${e.variants.map((v) => `.value("${v.name}", ${v.value})`).join('')};`);
834
+ }
835
+ for (const v of model.valueObjects) {
836
+ const fields = v.fields.map((f) => `.field::<${f.type}>("${f.name}", core::mem::offset_of!(${U}::${v.name}, ${f.name}))`).join('');
837
+ registrations.push(` value_object_::<${v.name}W>("${v.name}")${fields}.finalize();`);
838
+ }
839
+ for (const vec of vectors) {
840
+ if (!VECTOR_ITEM_TYPES.has(vec.of)) { log(`crossbind: rust bridge: vector of '${vec.of}' skipped (supported: i32, f64, bool)`); continue; }
841
+ registrations.push(` register_vector::<${vec.of}>("${vec.name}");`);
842
+ }
843
+
844
+ for (const cls of model.classes) {
845
+ const C = `${U}::${cls.name}`;
846
+ const shim = (fnName) => `__${cls.name.toLowerCase()}_${fnName}`;
847
+ const chain = [` class_::<${C}>("${cls.name}")`];
848
+
849
+ if (cls.shared) chain.push(` .smart_ptr_shared("${cls.name}Shared")`);
850
+ else if (cls.factories.length) chain.push(` .smart_ptr("${cls.name}Ptr")`);
851
+ if (cls.ctor) {
852
+ const a = cls.ctor.args;
853
+ noteOptionArgs(a);
854
+ const ps = a.map((p, i) => `a${i}: ${wireTy(p.ty)}`).join(', ');
855
+ const call = `${C}::new(${a.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
856
+ if (cls.ctor.throws) {
857
+ out.push(`fn ${shim('new')}(${ps}) -> *mut ${C} { match ${call} { Ok(v) => Box::into_raw(Box::new(v)), Err(e) => embind_rs::raise_err(e.to_string()) } }`);
858
+ chain.push(` .constructor_ptr${a.length}(${shim('new')})`);
859
+ } else {
860
+ out.push(`fn ${shim('new')}(${ps}) -> ${C} { ${call} }`);
861
+ chain.push(` .constructor${a.length}(${shim('new')})`);
862
+ }
863
+ }
864
+ for (const f of cls.factories) {
865
+ noteOptionArgs(f.args);
866
+ const ps = f.args.map((p, i) => `a${i}: ${wireTy(p.ty)}`).join(', ');
867
+ const call = `${C}::${f.name}(${f.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
868
+ if (f.shared) {
869
+ if (f.throws) {
870
+ 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(e.to_string()) } }`);
871
+ chain.push(` .create_ptr${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
872
+ } else {
873
+ out.push(`fn ${shim(f.name)}(${ps}) -> std::sync::Arc<${C}> { ${call} }`);
874
+ chain.push(` .create_arc${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
875
+ }
876
+ continue;
877
+ }
878
+ if (f.throws) {
879
+ out.push(`fn ${shim(f.name)}(${ps}) -> *mut ${C} { match ${call} { Ok(v) => Box::into_raw(Box::new(v)), Err(e) => embind_rs::raise_err(e.to_string()) } }`);
880
+ chain.push(` .create_ptr${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
881
+ } else if (f.optional) {
882
+ out.push(`fn ${shim(f.name)}(${ps}) -> *mut ${C} { match ${call} { Some(v) => Box::into_raw(Box::new(v)), None => core::ptr::null_mut() } }`);
883
+ chain.push(` .create_ptr${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
884
+ } else {
885
+ out.push(`fn ${shim(f.name)}(${ps}) -> ${C} { ${call} }`);
886
+ chain.push(` .create${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
887
+ }
888
+ }
889
+ for (const m of cls.methods) {
890
+ noteOptionArgs(m.args);
891
+ const params = [`t: &mut ${C}`, ...m.args.map((p, i) => `a${i}: ${wireTy(p.ty)}`)].join(', ');
892
+ const callArgs = m.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ');
893
+ const recv = m.byRef ? '(&*t)' : 't';
894
+ const call = `${recv}.${m.name}(${callArgs})`;
895
+ const body = m.throws
896
+ ? `match ${call} { Ok(v) => ${wrap(m.ret, 'v')}, Err(e) => embind_rs::raise_err(e.to_string()) }`
897
+ : (m.ret === '()' ? call : wrap(m.ret, call));
898
+ const retTy = m.optionalRet ? `Option<${m.ret}>` : (m.ret === '()' ? '()' : wireTy(m.ret));
899
+ if (m.optionalRet) usedOptionals.add(m.ret);
900
+ out.push(`fn ${shim(m.name)}(${params}) -> ${retTy} { ${body} }`);
901
+ chain.push(` .function${m.args.length}("${camel(m.name)}", ${shim(m.name)})`);
902
+ }
903
+ if (cls.hasDisplay) {
904
+ out.push(`fn ${shim('display_tostring')}(t: &mut ${C}) -> String { format!("{}", (&*t)) }`);
905
+ chain.push(` .function0("toString", ${shim('display_tostring')})`);
906
+ }
907
+ registrations.push(`${chain.join('\n')};`);
908
+ }
909
+
910
+ for (const f of model.freeFns ?? []) {
911
+ noteOptionArgs(f.args);
912
+ const ps = f.args.map((p, i) => `a${i}: ${wireTy(p.ty)}`).join(', ');
913
+ const call = `${U}::${f.name}(${f.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
914
+ const retTy = f.optionalRet ? `Option<${f.ret}>` : (f.ret === '()' ? '()' : wireTy(f.ret));
915
+ if (f.optionalRet) usedOptionals.add(f.ret);
916
+ const body = f.throws
917
+ ? `match ${call} { Ok(v) => ${wrap(f.ret, 'v')}, Err(e) => embind_rs::raise_err(e.to_string()) }`
918
+ : (f.ret === '()' ? call : wrap(f.ret, call));
919
+ out.push(`fn __free_${f.name}(${ps}) -> ${retTy} { ${body} }`);
920
+ registrations.push(` embind_rs::fn${f.args.length}("${f.jsName}", __free_${f.name});`);
921
+ }
922
+
923
+ // Optional inner types register once up front (the adapters dedupe across archives).
924
+ for (const inner of [...usedOptionals].sort()) {
925
+ registrations.unshift(` embind_rs::${OPTIONAL_REG[inner]}();`);
926
+ }
927
+
928
+ out.push('');
929
+ out.push('embind_rs::bindings! {');
930
+ out.push(registrations.join('\n'));
931
+ out.push('}');
932
+ out.push('');
933
+ return out.join('\n');
934
+ }
935
+
936
+ // ---------------- .d.ts emitter ----------------
937
+
938
+ const TS_TYPES = {
939
+ i32: 'number', i64: 'bigint', u64: 'bigint', f64: 'number', bool: 'boolean',
940
+ String: 'string', '&str': 'string', '&String': 'string', '()': 'void',
941
+ };
942
+
943
+ // Editor-facing types for the package import (`import { X } from '<pkg>'`): the metro/vite
944
+ // resolver serves the runtime proxy, this file serves TypeScript. Exports are typed post-init
945
+ // (null until any init() resolves, which binds every imported module - same as the C++ .h flow).
946
+ export function emitDts(model, vectors, mode = 'sync') {
947
+ const wrap = (t) => (mode === 'promise' ? `Promise<${t}>` : t);
948
+ const ts = (ty) => {
949
+ const opt = ty.match(OPTION_PARAM_RE);
950
+ if (opt) return `${TS_TYPES[opt[1]] ?? opt[1]} | null | undefined`;
951
+ if (ty === JSON_TY) return 'JsonValue';
952
+ const arc = ty.match(ARC_RE);
953
+ if (arc) return arc[1]; // Arc<X> is transparent in JS: the shared instance itself
954
+ if (ty === 'JsValue') return 'unknown';
955
+ if (ty === 'JsFunction') return '(...args: unknown[]) => unknown';
956
+ if (ty.startsWith('&')) return TS_TYPES[ty] ?? ty.slice(1); // &OtherClass param
957
+ return TS_TYPES[ty] ?? ty;
958
+ };
959
+ const out = ['// Generated by crossbind rustBridgeGen - do not edit. Values are usable after init().', ''];
960
+ if (model.usesJson) out.push('export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };', '');
961
+
962
+ for (const v of model.valueObjects) {
963
+ out.push(`export interface ${v.name} { ${v.fields.map((f) => `${f.name}: ${ts(f.type)};`).join(' ')} }`);
964
+ }
965
+ for (const e of model.enums) {
966
+ out.push(`export interface ${e.name} { readonly __crossbindEnum?: '${e.name}'; }`);
967
+ out.push(`export declare let ${e.name}: { ${e.variants.map((v) => `readonly ${v.name}: ${e.name};`).join(' ')} };`);
968
+ }
969
+ for (const cls of model.classes) {
970
+ out.push(`export declare class ${cls.name} {`);
971
+ if (cls.ctor) out.push(` constructor(${cls.ctor.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')});`);
972
+ else out.push(' private constructor();');
973
+ for (const f of cls.factories) {
974
+ out.push(` static ${camel(f.name)}(${f.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${cls.name}${f.optional ? ' | null' : ''}`)};`);
975
+ }
976
+ for (const m of cls.methods) {
977
+ out.push(` ${camel(m.name)}(${m.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(m.ret)}${m.optionalRet ? ' | undefined' : ''}`)};`);
978
+ }
979
+ if (cls.hasDisplay) out.push(` toString(): ${wrap('string')};`);
980
+ out.push(` delete(): ${wrap('void')};`);
981
+ out.push('}');
982
+ }
983
+ for (const f of model.freeFns ?? []) {
984
+ out.push(`export declare function ${f.jsName}(${f.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(f.ret)}${f.optionalRet ? ' | undefined' : ''}`)};`);
985
+ }
986
+ for (const vec of vectors) {
987
+ if (!VECTOR_ITEM_TYPES.has(vec.of)) continue;
988
+ out.push(`export declare class ${vec.name} {`);
989
+ out.push(' constructor();');
990
+ out.push(` push_back(value: ${ts(vec.of)}): ${wrap('void')};`);
991
+ out.push(` get(index: number): ${wrap(ts(vec.of))};`);
992
+ out.push(` size(): ${wrap('number')};`);
993
+ out.push(` delete(): ${wrap('void')};`);
994
+ out.push('}');
995
+ }
996
+ out.push('export declare let AllSymbols: Record<string, unknown>;');
997
+ out.push('export declare function initNative(config?: Record<string, unknown>): Promise<unknown>;');
998
+ out.push('');
999
+ return out.join('\n');
1000
+ }
1001
+
1002
+ // CLI: run inside a cargo package dir (reads its crossbind.config.mjs for crate path + vectors).
1003
+ const isCli = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
1004
+ if (isCli) {
1005
+ const config = (await import(pathToFileURL(`${process.cwd()}/crossbind.config.mjs`).href)).default;
1006
+ const crateDir = path.resolve(process.cwd(), config.export?.crate ?? 'crate');
1007
+ const { bridgeDir } = generateRustBridge({
1008
+ crateDir,
1009
+ vectors: config.export?.bindings?.vectors ?? [],
1010
+ dtsFile: `${process.cwd()}/dist/js/index.d.ts`,
1011
+ });
1012
+ console.log(bridgeDir);
1013
+ }