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