crossbind 2.0.0-beta.56 → 2.0.0-beta.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/actions/buildWasm.js +21 -0
- package/src/actions/createInterface.js +156 -55
- package/src/actions/createLib.js +5 -3
- package/src/actions/getAllBridges.js +1 -1
- package/src/actions/run.js +11 -2
- package/src/assets/js-runtime/adapters/worker-comlink.js +21 -2
- package/src/assets/toolchain-digests.json +12 -12
- package/src/integration/getCrossbindScript.js +12 -4
- package/src/utils/cppDts.js +7 -4
- package/src/utils/dependencyBridges.js +18 -0
- package/src/utils/downloadAndExtractFile.js +13 -1
- package/src/utils/pullDockerImage.js +10 -1
- package/src/utils/rustBridgeGen.js +668 -105
- package/src/utils/swigInterface.js +412 -0
|
@@ -21,21 +21,75 @@ import writeIfChanged from './writeIfChanged.js';
|
|
|
21
21
|
// top-level pub fn f(..) -> R -> free function (<=4 args)
|
|
22
22
|
// impl Display for C -> toString() on the class
|
|
23
23
|
// params may also be &str / &String (String on the wire, borrowed at the call site)
|
|
24
|
-
//
|
|
24
|
+
// a returned &str / &String comes back owned (a JS string);
|
|
25
|
+
// i64 / u64 cross as JS BigInt, the narrower integers and f32 as numbers, char as a
|
|
26
|
+
// one-character string; Result<T, E> in any return above throws in JS on Err
|
|
25
27
|
// (E: Display); Option<Self> factories return JS null; Option<i32/f64/bool/String> works in
|
|
26
|
-
// params (undefined/null -> None) and returns (None ->
|
|
28
|
+
// params (undefined/null -> None) and returns (None -> null); other Option shapes skip
|
|
27
29
|
// vectors come from crossbind.config.mjs: export.bindings.vectors = [{ of: 'i32', name: '..' }]
|
|
28
30
|
// Anything outside this surface is skipped WITH a log line - never silently.
|
|
29
31
|
|
|
30
|
-
const PRIMITIVES = new Set(['i32', 'i64', 'u64', 'f64', 'bool', 'String', '()'
|
|
32
|
+
const PRIMITIVES = new Set(['i32', 'i64', 'u64', 'f64', 'bool', 'String', '()',
|
|
33
|
+
'i8', 'i16', 'u8', 'u16', 'u32', 'f32', 'usize', 'isize', 'char']);
|
|
31
34
|
const PARAM_ONLY = new Set(['&str', '&String']);
|
|
32
35
|
const OPTION_INNERS = new Set(['i32', 'f64', 'bool', 'String']);
|
|
33
|
-
const OPTION_PARAM_RE = /^Option<(i32|f64|bool|String)>$/;
|
|
36
|
+
const OPTION_PARAM_RE = /^Option<(i32|f64|bool|String|&str)>$/;
|
|
37
|
+
const OPTION_CLASS_REF_RE = /^Option<&(\w+)>$/;
|
|
34
38
|
const OPTIONAL_REG = { i32: 'register_optional_i32', f64: 'register_optional_f64', bool: 'register_optional_bool', String: 'register_optional_string' };
|
|
35
39
|
const VECTOR_ITEM_TYPES = new Set(['i32', 'f64', 'bool']);
|
|
36
40
|
// serde_json::Value params/returns cross as a deep JSON copy (adapter-side codec, canonical
|
|
37
41
|
// token 'Json'); the bare `Value` spelling counts only when the file imports serde_json.
|
|
38
42
|
const JSON_TY = 'Json';
|
|
43
|
+
// A closure parameter (`impl Fn(..) -> R` or `Box<dyn Fn(..) -> R>`) takes a JS function: the
|
|
44
|
+
// shim wraps it so the Rust side calls back through the live handle.
|
|
45
|
+
const CLOSURE_RE = /^(?:impl\s+Fn|Box<\s*dyn\s+Fn)\s*\(([^)]*)\)\s*(?:->\s*([\w:<>&' ]+?)\s*)?(?:>\s*)?(?:\+[^>]*)?$/;
|
|
46
|
+
function closureShape(ty) {
|
|
47
|
+
const text = String(ty).replace(/\s+/g, ' ').trim();
|
|
48
|
+
const m = text.match(CLOSURE_RE);
|
|
49
|
+
if (!m) return null;
|
|
50
|
+
const boxed = text.startsWith('Box<');
|
|
51
|
+
const args = (m[1] ?? '').split(',').map((a) => a.trim()).filter(Boolean);
|
|
52
|
+
const ret = (m[2] ?? '()').replace(/>\s*$/, '').trim();
|
|
53
|
+
const CLOSURE_ARGS = new Set(['i32', 'f64', 'bool', 'String', '&str']);
|
|
54
|
+
if (!args.every((a) => CLOSURE_ARGS.has(a))) return null;
|
|
55
|
+
if (!['i32', 'f64', 'bool', 'String', '()'].includes(ret)) return null;
|
|
56
|
+
return { args, ret, boxed };
|
|
57
|
+
}
|
|
58
|
+
// Collections cross as plain JS arrays and objects. They ride the JSON wire, so the element type
|
|
59
|
+
// only has to be something serde can write: primitives, String, nested collections, and user
|
|
60
|
+
// structs that derive Serialize/Deserialize. The canonical token keeps the Rust spelling, so the
|
|
61
|
+
// shim can name the exact type it converts to.
|
|
62
|
+
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.
|
|
64
|
+
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 },
|
|
69
|
+
};
|
|
70
|
+
const typedArrayOf = (ty) => TYPED_ARRAYS[String(ty).replace(/\s+/g, '')] ?? null;
|
|
71
|
+
|
|
72
|
+
const isCollection = (ty) => {
|
|
73
|
+
if (typedArrayOf(ty)) return false;
|
|
74
|
+
const text = String(ty).trim();
|
|
75
|
+
if (COLLECTION_RE.test(text)) return true;
|
|
76
|
+
// An optional collection rides the same wire: JSON writes None as null.
|
|
77
|
+
const inner = text.match(/^Option<(.+)>$/)?.[1];
|
|
78
|
+
return Boolean(inner && COLLECTION_RE.test(inner.trim()));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// A user type inside a collection travels as JSON, so it has to derive Serialize/Deserialize.
|
|
82
|
+
// Types crossbind knows by itself (primitives, String) always do.
|
|
83
|
+
function collectionCarries(ty, ctx) {
|
|
84
|
+
const names = String(ty).match(/\b[A-Z]\w*\b/g) ?? [];
|
|
85
|
+
return names.every((name) => {
|
|
86
|
+
const vo = (ctx.valueObjects ?? []).find((v) => v.name === name);
|
|
87
|
+
if (vo) return Boolean(vo.serde);
|
|
88
|
+
const known = (ctx.classes?.has?.(name)) || (ctx.enums ?? []).some((e) => e.name === name)
|
|
89
|
+
|| (ctx.newtypes ?? []).some((n) => n.name === name);
|
|
90
|
+
return !known;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
39
93
|
const isJsonSpelling = (ty, ctx) => Boolean(ctx?.allowJson)
|
|
40
94
|
&& (ty === 'serde_json::Value' || (Boolean(ctx.hasSerdeUse) && ty === 'Value'));
|
|
41
95
|
// Arc<Class> params/returns (shared ownership): canonical token 'Arc<X>'; the bare `Arc`
|
|
@@ -63,6 +117,30 @@ const matchJsTok = (ty, ctx) => {
|
|
|
63
117
|
};
|
|
64
118
|
const FN_SIG_RE = /^pub (?:const )?fn (\w+)\s*\(([^)]*)\)\s*(?:->\s*([\w:<>(),& ]+?))?\s*\{/;
|
|
65
119
|
|
|
120
|
+
// The parameter list is read by counting parentheses, because a closure parameter carries its
|
|
121
|
+
// own: `f: impl Fn(i32) -> i32` would end the list early for a regex.
|
|
122
|
+
function matchFnSignature(line) {
|
|
123
|
+
const head = /^pub (?:const )?fn (\w+)\s*\(/.exec(line);
|
|
124
|
+
if (!head) return null;
|
|
125
|
+
let depth = 0;
|
|
126
|
+
let end = -1;
|
|
127
|
+
for (let i = head[0].length - 1; i < line.length; i += 1) {
|
|
128
|
+
if (line[i] === '(') depth += 1;
|
|
129
|
+
else if (line[i] === ')') {
|
|
130
|
+
depth -= 1;
|
|
131
|
+
if (depth === 0) { end = i; break; }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (end === -1) return null;
|
|
135
|
+
const params = line.slice(head[0].length, end);
|
|
136
|
+
// The body may start on the same line, so the return type ends at the opening brace.
|
|
137
|
+
const brace = line.indexOf('{', end);
|
|
138
|
+
if (brace === -1) return null;
|
|
139
|
+
const rest = line.slice(end + 1, brace).trim();
|
|
140
|
+
const ret = rest.startsWith('->') ? rest.slice(2).trim() : undefined;
|
|
141
|
+
return [line, head[1], params, ret];
|
|
142
|
+
}
|
|
143
|
+
|
|
66
144
|
export default function generateRustBridge({ crateDir, vectors = [], dtsFile = null, keepName = null, dtsMode = 'sync', log = console.log }) {
|
|
67
145
|
const libRsPath = `${crateDir}/src/lib.rs`;
|
|
68
146
|
if (!fs.existsSync(libRsPath)) throw new Error(`crossbind: rust bridge: ${libRsPath} not found`);
|
|
@@ -96,10 +174,11 @@ export default function generateRustBridge({ crateDir, vectors = [], dtsFile = n
|
|
|
96
174
|
'[dependencies]',
|
|
97
175
|
`${userCrate} = { package = "${rawCrateName}", path = "${relPath(bridgeDir, crateDir)}" }`,
|
|
98
176
|
`embind-rs = { path = "${relPath(bridgeDir, embindRsDir)}" }`,
|
|
99
|
-
...(model.usesJson ? ['serde_json = "1"'] : []),
|
|
177
|
+
...(model.usesJson ? ['serde = "1"', 'serde_json = "1"'] : []),
|
|
100
178
|
'',
|
|
101
179
|
'[profile.release]',
|
|
102
|
-
|
|
180
|
+
// Unwinding, not abort: the shims catch a panic and raise it as a JS exception.
|
|
181
|
+
'panic = "unwind"',
|
|
103
182
|
'# One object per crate: the keep symbol and the init-array ctor must share an object.',
|
|
104
183
|
'codegen-units = 1',
|
|
105
184
|
'',
|
|
@@ -149,10 +228,12 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
|
|
|
149
228
|
...Object.entries(cargoDependencies).map(([name, spec]) => (
|
|
150
229
|
String(spec).trim().startsWith('{') ? `${name} = ${spec}` : `${name} = "${spec}"`
|
|
151
230
|
)),
|
|
231
|
+
...(model.usesJson && !cargoDependencies.serde ? ['serde = "1"'] : []),
|
|
152
232
|
...(model.usesJson && !cargoDependencies.serde_json ? ['serde_json = "1"'] : []),
|
|
153
233
|
'',
|
|
154
234
|
'[profile.release]',
|
|
155
|
-
|
|
235
|
+
// Unwinding, not abort: the shims catch a panic and raise it as a JS exception.
|
|
236
|
+
'panic = "unwind"',
|
|
156
237
|
'codegen-units = 1',
|
|
157
238
|
'',
|
|
158
239
|
'# Isolated on purpose: never join a surrounding workspace.',
|
|
@@ -180,7 +261,10 @@ export function createCrateImportBridge({ crateName, spec, cacheDir, dtsMode = '
|
|
|
180
261
|
const stem = `crate_${crateName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}`;
|
|
181
262
|
const dir = `${cacheDir}/rust-bridges/${stem}`;
|
|
182
263
|
const depLine = String(spec).trim().startsWith('{') ? `${crateName} = ${spec}` : `${crateName} = "${spec}"`;
|
|
183
|
-
|
|
264
|
+
// The surface is only known after cargo metadata, which itself needs a manifest: the first
|
|
265
|
+
// write is the minimal one, the second adds the JSON wire's dependencies once the model says
|
|
266
|
+
// the bridge needs them.
|
|
267
|
+
const manifestWith = (extraDeps) => [
|
|
184
268
|
'# Generated by crossbind rustBridgeGen - do not edit.',
|
|
185
269
|
`# embind-rs from @crossbind/core-embind-rust ${embindRustVersion()}`,
|
|
186
270
|
'[package]',
|
|
@@ -196,16 +280,18 @@ export function createCrateImportBridge({ crateName, spec, cacheDir, dtsMode = '
|
|
|
196
280
|
'[dependencies]',
|
|
197
281
|
depLine,
|
|
198
282
|
`embind-rs = { path = "${relPath(dir, resolveEmbindRsDir())}" }`,
|
|
283
|
+
...extraDeps,
|
|
199
284
|
'',
|
|
200
285
|
'[profile.release]',
|
|
201
|
-
|
|
286
|
+
// Unwinding, not abort: the shims catch a panic and raise it as a JS exception.
|
|
287
|
+
'panic = "unwind"',
|
|
202
288
|
'codegen-units = 1',
|
|
203
289
|
'',
|
|
204
290
|
'# Isolated on purpose: never join a surrounding workspace.',
|
|
205
291
|
'[workspace]',
|
|
206
292
|
'',
|
|
207
293
|
].join('\n');
|
|
208
|
-
writeIfChanged(`${dir}/Cargo.toml`,
|
|
294
|
+
writeIfChanged(`${dir}/Cargo.toml`, manifestWith([]));
|
|
209
295
|
// cargo metadata needs a resolvable lib target before the real bridge exists.
|
|
210
296
|
if (!fs.existsSync(`${dir}/src/lib.rs`)) writeIfChanged(`${dir}/src/lib.rs`, '// crossbind placeholder\n');
|
|
211
297
|
|
|
@@ -235,14 +321,21 @@ export function createCrateImportBridge({ crateName, spec, cacheDir, dtsMode = '
|
|
|
235
321
|
crateModelCache.set(cacheKey, model);
|
|
236
322
|
}
|
|
237
323
|
|
|
238
|
-
const
|
|
324
|
+
const jsonDeps = ['serde', 'serde_json'].filter((dep) => dep !== crateName).map((dep) => `${dep} = "1"`);
|
|
325
|
+
writeIfChanged(`${dir}/Cargo.toml`, manifestWith(model.usesJson ? jsonDeps : []));
|
|
326
|
+
// Two crates can export the same type name (semver::Version and uuid::Version both do), so a
|
|
327
|
+
// crate import registers under `<crate>_<Name>` and the proxy module re-exports the clean name.
|
|
328
|
+
const namePrefix = `${crateName.replaceAll('-', '_')}_`;
|
|
329
|
+
const bridge = emitBridge(model, { userCrate: crateName.replaceAll('-', '_'), vectors: [], log, namePrefix });
|
|
239
330
|
writeIfChanged(`${dir}/src/lib.rs`, bridge);
|
|
240
331
|
// Editor types for the `cargo:<crate>` import: an ambient module the app's tsconfig
|
|
241
332
|
// includes (e.g. "include": ["**/*", ".crossbind/rust-crates/types/**/*.d.ts"]). The scheme
|
|
242
333
|
// keeps the module name unique, so npm packages/@types of the same name never clash.
|
|
243
334
|
const dtsBody = emitDts(model, [], dtsMode).split('\n').map((l) => (l ? ` ${l}` : l)).join('\n');
|
|
244
335
|
writeIfChanged(`${cacheDir}/rust-crates/types/${crateName}.d.ts`, `declare module 'cargo:${crateName}' {\n${dtsBody}\n}\n`);
|
|
245
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
bridgeDir: dir, crateName: `${stem}_crossbind_app`, model, namePrefix,
|
|
338
|
+
};
|
|
246
339
|
}
|
|
247
340
|
|
|
248
341
|
// The embind-rs runtime crate ships inside @crossbind/core-embind-rust; the consumer (a plugin
|
|
@@ -306,6 +399,9 @@ function newAcc() {
|
|
|
306
399
|
return {
|
|
307
400
|
enums: [], // { name, variants: [{ name, value }] }
|
|
308
401
|
valueObjects: [], // { name, fields: [{ name, type }] }
|
|
402
|
+
newtypes: [], // { name, inner } - crosses as the inner value
|
|
403
|
+
jsonTypes: [], // { name } - data enums that cross as a plain JS value
|
|
404
|
+
consts: [], // { name, ty, value } - registered as a module constant
|
|
309
405
|
classes: new Map(), // name -> { name, ctor, factories: [], methods: [], hasDisplay }
|
|
310
406
|
freeFns: [], // { name, jsName, args, ret, throws }
|
|
311
407
|
displayNames: new Set(),
|
|
@@ -333,7 +429,7 @@ function cfgEnabled(cfg, enabled) {
|
|
|
333
429
|
// collectTypes: types and free fns register only from the crate root (lib.rs) - module files
|
|
334
430
|
// contribute impls (methods, factories, Display) for already-known types.
|
|
335
431
|
function scanSource(src, acc, { collectTypes, log }) {
|
|
336
|
-
const { enums, valueObjects, classes, freeFns, displayNames, wantedTypes } = acc;
|
|
432
|
+
const { enums, valueObjects, newtypes, jsonTypes, consts, classes, freeFns, displayNames, wantedTypes } = acc;
|
|
337
433
|
acc.hasSerdeUse ||= /\buse\s+serde_json\b/.test(src);
|
|
338
434
|
acc.hasArcUse ||= /\buse\s+std::sync::(?:Arc\b|\{[^}]*\bArc\b)/.test(src);
|
|
339
435
|
acc.hasEmbindUse ||= /\buse\s+embind_rs::/.test(src);
|
|
@@ -396,38 +492,70 @@ function scanSource(src, acc, { collectTypes, log }) {
|
|
|
396
492
|
let structM = t.match(/^pub struct (\w+)/);
|
|
397
493
|
if (structM && !collectTypes && !wantedTypes.has(structM[1])) structM = null;
|
|
398
494
|
const modM = t.match(/^(?:pub(?:\([^)]*\))?\s+)?mod (\w+)\s*([;{])/);
|
|
495
|
+
// `pub const NAME: T = value;` (and `pub static`) - a value, not a getter, in JS.
|
|
496
|
+
const constM = collectTypes ? t.match(/^pub (?:const|static) (\w+)\s*:\s*([\w()&' ]+?)\s*=\s*(.+?);$/) : null;
|
|
399
497
|
const displayM = t.match(/^impl (?:[\w:]+::)?Display for (\w+)/);
|
|
400
498
|
const traitImplM = !displayM && /^impl\b[^{]*\bfor\b/.test(t);
|
|
401
499
|
const implM = displayM || traitImplM ? null : t.match(/^impl (\w+)\s*\{/);
|
|
402
500
|
const freeFnM = collectTypes && !enumM && !structM && !modM && !displayM && !traitImplM && !implM
|
|
403
|
-
? t
|
|
501
|
+
? matchFnSignature(t) : null;
|
|
404
502
|
|
|
405
503
|
if (enumM) {
|
|
406
|
-
const
|
|
504
|
+
const derive = attrs.find((a) => a.startsWith('#[derive')) ?? '';
|
|
505
|
+
|
|
407
506
|
const variants = [];
|
|
408
507
|
let idx = 0;
|
|
409
508
|
for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1) {
|
|
410
509
|
const v = lines[i].trim().match(/^(\w+)(?:\s*=\s*(-?\d+))?\s*,?$/);
|
|
411
510
|
if (v) { variants.push({ name: v[1], value: v[2] !== undefined ? Number(v[2]) : idx }); idx = variants.at(-1).value + 1; }
|
|
412
511
|
}
|
|
413
|
-
|
|
414
|
-
|
|
512
|
+
const hasSerde = derive.includes('Serialize') && derive.includes('Deserialize');
|
|
513
|
+
if (variants.length) enums.push({ name: enumM[1], variants });
|
|
514
|
+
// A variant that carries data cannot be an embind enum, but it can cross as a plain
|
|
515
|
+
// JS value when the enum derives serde - in serde's own representation.
|
|
516
|
+
else if (hasSerde) jsonTypes.push({ name: enumM[1] });
|
|
517
|
+
else log(`crossbind: rust bridge: enum ${enumM[1]} skipped (a data enum crosses only when it derives Serialize and Deserialize)`);
|
|
415
518
|
} else if (structM) {
|
|
416
519
|
const isReprC = attrs.some((a) => /repr\(C\)/.test(a));
|
|
417
520
|
const derive = attrs.find((a) => a.startsWith('#[derive')) ?? '';
|
|
418
|
-
|
|
521
|
+
// A single-field tuple struct over a primitive is a newtype: it crosses as the inner
|
|
522
|
+
// value, the way napi-rs spells `#[napi(transparent)]`.
|
|
523
|
+
const newtypeM = t.match(/^pub struct (\w+)\s*\(\s*pub ([\w()]+)\s*,?\s*\)\s*;/);
|
|
524
|
+
if (newtypeM && PRIMITIVES.has(newtypeM[2])) {
|
|
525
|
+
newtypes.push({ name: newtypeM[1], inner: newtypeM[2] });
|
|
526
|
+
} else if (isReprC) {
|
|
419
527
|
const ok = derive.includes('Default') && derive.includes('Copy');
|
|
420
528
|
const fields = [];
|
|
421
529
|
for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1) {
|
|
422
530
|
const f = lines[i].trim().match(/^pub (\w+)\s*:\s*([\w()]+)\s*,?$/);
|
|
423
|
-
|
|
531
|
+
// A field may be another value object: embind nests them, and the wrapper is
|
|
532
|
+
// repr(transparent) over the user struct, so the offset still lands right.
|
|
533
|
+
if (f && (PRIMITIVES.has(f[2]) || valueObjects.some((v) => v.name === f[2]))) fields.push({ name: f[1], type: f[2] });
|
|
424
534
|
}
|
|
425
|
-
if (ok && fields.length) valueObjects.push({ name: structM[1], fields });
|
|
426
|
-
else log(`crossbind: rust bridge: struct ${structM[1]} skipped (repr(C) needs derive(Default, Copy) and pub primitive fields)`);
|
|
427
|
-
} else {
|
|
428
|
-
|
|
535
|
+
if (ok && fields.length) valueObjects.push({ name: structM[1], fields, serde: derive.includes('Serialize') && derive.includes('Deserialize') });
|
|
536
|
+
else log(`crossbind: rust bridge: struct ${structM[1]} skipped (repr(C) needs derive(Default, Copy) and pub primitive or value-object fields)`);
|
|
537
|
+
} else if (derive.includes('Serialize') && derive.includes('Deserialize') && !newtypeM) {
|
|
538
|
+
// Deriving serde says "this is data": it crosses as a plain JS object, and its
|
|
539
|
+
// methods (if any) stay native, the way napi-rs separates #[napi(object)] from
|
|
540
|
+
// #[napi].
|
|
541
|
+
if (!jsonTypes.some((j) => j.name === structM[1])) jsonTypes.push({ name: structM[1] });
|
|
429
542
|
if (t.endsWith('{')) { for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1); }
|
|
543
|
+
} else {
|
|
544
|
+
if (!classes.has(structM[1])) classes.set(structM[1], { name: structM[1], ctor: null, factories: [], methods: [], fields: [], hasDefault: derive.includes('Default') });
|
|
545
|
+
const cls = classes.get(structM[1]);
|
|
546
|
+
cls.serde = derive.includes('Serialize') && derive.includes('Deserialize');
|
|
547
|
+
// Public fields read and write as JS properties, the way napi-rs exposes them.
|
|
548
|
+
if (t.endsWith('{')) {
|
|
549
|
+
for (i += 1; i < lines.length && !/^\}/.test(lines[i].trim()); i += 1) {
|
|
550
|
+
const f = lines[i].trim().match(/^pub (\w+)\s*:\s*([\w()]+)\s*,?$/);
|
|
551
|
+
if (f) cls.fields.push({ name: f[1], type: f[2] });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
430
554
|
}
|
|
555
|
+
} else if (constM) {
|
|
556
|
+
const constTy = normalizeStringSpelling(constM[2]);
|
|
557
|
+
if (PRIMITIVES.has(constTy) || constTy === '&str') consts.push({ name: constM[1], ty: constTy, value: constM[3].trim() });
|
|
558
|
+
else log(`crossbind: rust bridge: const ${constM[1]} skipped (only i32, f64 and bool constants cross)`);
|
|
431
559
|
} else if (modM) {
|
|
432
560
|
// `mod x;` is followed for crate imports; inline `mod x { .. }` bodies are opaque.
|
|
433
561
|
if (modM[2] === ';') mods.push({ name: modM[1], cfg: cfgOf(attrs) });
|
|
@@ -443,8 +571,8 @@ function scanSource(src, acc, { collectTypes, log }) {
|
|
|
443
571
|
let depth = 1;
|
|
444
572
|
for (i += 1; i < lines.length && depth > 0; i += 1) {
|
|
445
573
|
const s = lines[i];
|
|
446
|
-
const sig = s.trim()
|
|
447
|
-
if (sig && depth === 1) parseFn(cls, sig, { enums, valueObjects, classes, allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse }, log);
|
|
574
|
+
const sig = matchFnSignature(s.trim());
|
|
575
|
+
if (sig && depth === 1) parseFn(cls, sig, { enums, valueObjects, newtypes, jsonTypes, classes, allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse }, log);
|
|
448
576
|
depth += (s.match(/\{/g) ?? []).length - (s.match(/\}/g) ?? []).length;
|
|
449
577
|
}
|
|
450
578
|
i -= 1;
|
|
@@ -452,7 +580,7 @@ function scanSource(src, acc, { collectTypes, log }) {
|
|
|
452
580
|
// Consume unknown-impl bodies so their fns are never misread as free functions.
|
|
453
581
|
i = skipBlock(lines, i);
|
|
454
582
|
} else if (freeFnM) {
|
|
455
|
-
parseFreeFn(freeFns, freeFnM, { enums, valueObjects, classes, allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse }, log);
|
|
583
|
+
parseFreeFn(freeFns, freeFnM, { enums, valueObjects, newtypes, jsonTypes, classes, allowJson: collectTypes, hasSerdeUse: acc.hasSerdeUse, hasArcUse: acc.hasArcUse, hasEmbindUse: acc.hasEmbindUse }, log);
|
|
456
584
|
}
|
|
457
585
|
attrs = [];
|
|
458
586
|
}
|
|
@@ -460,20 +588,66 @@ function scanSource(src, acc, { collectTypes, log }) {
|
|
|
460
588
|
}
|
|
461
589
|
|
|
462
590
|
function finalizeModel(acc, log) {
|
|
463
|
-
const { enums, valueObjects, classes, freeFns, displayNames } = acc;
|
|
591
|
+
const { enums, valueObjects, newtypes, jsonTypes, consts, classes, freeFns, displayNames } = acc;
|
|
464
592
|
// A class with no exported surface is dropped (with a note), mirroring the C++ generator.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
593
|
+
// A binding that borrows a dropped class would name a class that never registers and abort
|
|
594
|
+
// the module at init, so dropping a class drops those bindings too, until nothing changes.
|
|
595
|
+
const dropped = new Set();
|
|
596
|
+
const borrowsDropped = (args) => args.some((p) => {
|
|
597
|
+
const ty = String(p.ty);
|
|
598
|
+
return (ty.startsWith('&') && dropped.has(ty.slice(1))) || dropped.has(ty.match(ARC_RE)?.[1]);
|
|
599
|
+
});
|
|
600
|
+
let changed = true;
|
|
601
|
+
while (changed) {
|
|
602
|
+
changed = false;
|
|
603
|
+
for (const [name, cls] of classes) {
|
|
604
|
+
if (!cls.ctor && !cls.factories.length && !cls.methods.length && !(cls.fields ?? []).length) {
|
|
605
|
+
classes.delete(name);
|
|
606
|
+
dropped.add(name);
|
|
607
|
+
changed = true;
|
|
608
|
+
log(`crossbind: rust bridge: struct ${name} has no exportable pub fns - not registered`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const keepFn = (owner) => (f) => {
|
|
612
|
+
// A function that hands back a class the model dropped would name a type that never
|
|
613
|
+
// registers, so it goes with it.
|
|
614
|
+
if (f.ownedClass && dropped.has(f.ownedClass)) {
|
|
615
|
+
changed = true;
|
|
616
|
+
log(`crossbind: rust bridge: ${owner}${f.name} skipped (it returns a struct that is not registered)`);
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
if (!borrowsDropped(f.args)) return true;
|
|
620
|
+
changed = true;
|
|
621
|
+
log(`crossbind: rust bridge: ${owner}${f.name} skipped (a parameter borrows a struct that is not registered)`);
|
|
622
|
+
return false;
|
|
623
|
+
};
|
|
624
|
+
freeFns.splice(0, freeFns.length, ...freeFns.filter(keepFn('fn ')));
|
|
625
|
+
for (const cls of classes.values()) {
|
|
626
|
+
cls.methods = cls.methods.filter(keepFn(`${cls.name}::`));
|
|
627
|
+
cls.factories = cls.factories.filter(keepFn(`${cls.name}::`));
|
|
628
|
+
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)`); }
|
|
469
629
|
}
|
|
470
630
|
}
|
|
631
|
+
for (const cls of classes.values()) {
|
|
632
|
+
cls.fields = (cls.fields ?? []).filter((f) => {
|
|
633
|
+
const carries = PRIMITIVES.has(f.type) || enums.some((e) => e.name === f.type) || valueObjects.some((v) => v.name === f.type);
|
|
634
|
+
if (!carries) log(`crossbind: rust bridge: ${cls.name}.${f.name} skipped (a property carries primitives, enums and value objects)`);
|
|
635
|
+
return carries;
|
|
636
|
+
});
|
|
637
|
+
}
|
|
471
638
|
for (const cls of classes.values()) {
|
|
472
639
|
const collides = cls.methods.some((m) => camel(m.name) === 'toString');
|
|
473
640
|
if (displayNames.has(cls.name) && collides) log(`crossbind: rust bridge: ${cls.name} Display->toString skipped (a toString method already exists)`);
|
|
474
641
|
cls.hasDisplay = displayNames.has(cls.name) && !collides;
|
|
475
642
|
}
|
|
476
|
-
const
|
|
643
|
+
const isJsonRecordName = (ty) => {
|
|
644
|
+
const text = String(ty).trim();
|
|
645
|
+
const inner = text.match(/^Option<(.+)>$/)?.[1]?.trim() ?? text;
|
|
646
|
+
return jsonTypes.some((j) => j.name === inner)
|
|
647
|
+
|| (text.startsWith('Option<') && valueObjects.some((v) => v.name === inner));
|
|
648
|
+
};
|
|
649
|
+
const anyJson = (args, ret) => args.some((p) => p.ty === JSON_TY || isCollection(p.ty) || isJsonRecordName(p.ty))
|
|
650
|
+
|| ret === JSON_TY || isCollection(ret) || isJsonRecordName(ret);
|
|
477
651
|
const usesJson = freeFns.some((f) => anyJson(f.args, f.ret))
|
|
478
652
|
|| [...classes.values()].some((c) => (c.ctor && anyJson(c.ctor.args, '()'))
|
|
479
653
|
|| c.factories.some((f) => anyJson(f.args, '()'))
|
|
@@ -506,7 +680,7 @@ function finalizeModel(acc, log) {
|
|
|
506
680
|
throw new Error(`crossbind: rust bridge: ${c.name} is shared via Arc<...>, so every factory must return Arc<Self> ('${boxed.name}' returns Self)`);
|
|
507
681
|
}
|
|
508
682
|
}
|
|
509
|
-
return { enums, valueObjects, classes: [...classes.values()], freeFns, usesJson, sharedOf: [...sharedOf].sort() };
|
|
683
|
+
return { enums, valueObjects, newtypes, jsonTypes, consts, classes: [...classes.values()], freeFns, usesJson, sharedOf: [...sharedOf].sort() };
|
|
510
684
|
}
|
|
511
685
|
|
|
512
686
|
// Consumes a brace-delimited block starting at line i; returns the closing line's index.
|
|
@@ -524,24 +698,93 @@ function skipBlock(lines, i) {
|
|
|
524
698
|
|
|
525
699
|
// Splits `Result<T, E>` / `Option<T>` off a return type; `Result<T>` (an alias like
|
|
526
700
|
// anyhow/io::Result) also matches - the shim's Ok/Err arms work on any core Result underneath.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
701
|
+
// `&'static str` and `Cow<'_, str>` are the same JS string as `&str`.
|
|
702
|
+
function normalizeStringSpelling(ty) {
|
|
703
|
+
const text = String(ty).trim();
|
|
704
|
+
|
|
705
|
+
if (/^&\s*'\w+\s+str$/.test(text)) return '&str';
|
|
706
|
+
// A Cow return becomes an owned String on the way out, like any borrowed string.
|
|
707
|
+
if (/^(?:std::borrow::)?Cow<\s*(?:'\w+\s*,\s*)?str\s*>$/.test(text)) return '&str';
|
|
708
|
+
return text;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// `Option<Record>` and `Option<ValueObject>` ride the JSON wire whole: null is the None.
|
|
712
|
+
function isOptionalRecord(ty, ctx) {
|
|
713
|
+
const inner = String(ty).trim().match(/^Option<(.+)>$/)?.[1]?.trim();
|
|
714
|
+
if (!inner || !ctx) return false;
|
|
715
|
+
return (ctx.jsonTypes ?? []).some((j) => j.name === inner)
|
|
716
|
+
|| (ctx.valueObjects ?? []).some((v) => v.name === inner);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function analyzeReturn(raw, ctx) {
|
|
720
|
+
// `impl Iterator<Item = T>` is read as the sequence it produces: the shim collects it, so JS
|
|
721
|
+
// gets the values rather than a lazy iterator.
|
|
722
|
+
const iterator = raw.trim().match(/^impl\s+Iterator\s*<\s*Item\s*=\s*(.+?)\s*>$/);
|
|
723
|
+
if (iterator) return { inner: `Vec<${iterator[1].trim()}>`, throws: false, optional: false, collect: true };
|
|
724
|
+
const r = normalizeStringSpelling(raw.trim());
|
|
725
|
+
const res = unwrapGeneric(r, 'Result');
|
|
726
|
+
// Only the Ok type crosses; the error type needs nothing but Display, so `Box<dyn Error>`
|
|
727
|
+
// and friends are read past here.
|
|
728
|
+
if (res !== null) {
|
|
729
|
+
const ok = splitTopLevel(res)[0].trim();
|
|
730
|
+
if (isCollection(ok)) return { inner: ok, throws: true, optional: false };
|
|
731
|
+
const inner = unwrapGeneric(ok, 'Option');
|
|
732
|
+
return inner !== null
|
|
733
|
+
? { inner: inner.trim(), throws: true, optional: true }
|
|
734
|
+
: { inner: ok, throws: true, optional: false };
|
|
735
|
+
}
|
|
736
|
+
if (isCollection(r) || isOptionalRecord(r, ctx)) return { inner: r, throws: false, optional: false };
|
|
737
|
+
const opt = unwrapGeneric(r, 'Option');
|
|
738
|
+
if (opt !== null) return { inner: opt.trim(), throws: false, optional: true };
|
|
533
739
|
return { inner: r, throws: false, optional: false };
|
|
534
740
|
}
|
|
535
741
|
|
|
742
|
+
// `Name<...>` -> the text between the outermost angle brackets, or null when the type is not
|
|
743
|
+
// that generic. Counting depth is what lets a nested generic (`Result<i32, Box<dyn Error>>`)
|
|
744
|
+
// keep its own commas.
|
|
745
|
+
function unwrapGeneric(ty, name) {
|
|
746
|
+
const head = new RegExp(`^${name}\\s*<`).exec(ty);
|
|
747
|
+
if (!head || !ty.endsWith('>')) return null;
|
|
748
|
+
let depth = 0;
|
|
749
|
+
for (let i = head[0].length - 1; i < ty.length; i += 1) {
|
|
750
|
+
if (ty[i] === '<') depth += 1;
|
|
751
|
+
else if (ty[i] === '>') {
|
|
752
|
+
depth -= 1;
|
|
753
|
+
if (depth === 0) return i === ty.length - 1 ? ty.slice(head[0].length, i) : null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function splitTopLevel(text) {
|
|
760
|
+
const parts = [];
|
|
761
|
+
let depth = 0;
|
|
762
|
+
let start = 0;
|
|
763
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
764
|
+
// `->` is an arrow, not a closing angle bracket: a closure parameter has one.
|
|
765
|
+
const isArrow = text[i] === '>' && text[i - 1] === '-';
|
|
766
|
+
if (text[i] === '<' || text[i] === '(' || text[i] === '[') depth += 1;
|
|
767
|
+
else if (!isArrow && (text[i] === '>' || text[i] === ')' || text[i] === ']')) depth -= 1;
|
|
768
|
+
else if (text[i] === ',' && depth === 0) { parts.push(text.slice(start, i)); start = i + 1; }
|
|
769
|
+
}
|
|
770
|
+
parts.push(text.slice(start));
|
|
771
|
+
return parts;
|
|
772
|
+
}
|
|
773
|
+
|
|
536
774
|
function parseFn(cls, sig, ctx, log) {
|
|
537
775
|
const { enums, valueObjects, classes } = ctx;
|
|
538
776
|
const [, name, rawParams, rawRet] = sig;
|
|
539
|
-
|
|
540
|
-
|
|
777
|
+
// A borrowed string is known in both directions: as a parameter it borrows, as a return it
|
|
778
|
+
// comes back owned.
|
|
779
|
+
const known = (ty) => PRIMITIVES.has(ty) || PARAM_ONLY.has(ty) || typedArrayOf(ty) || (isCollection(ty) && collectionCarries(ty, ctx))
|
|
780
|
+
|| enums.some((e) => e.name === ty) || valueObjects.some((v) => v.name === ty)
|
|
781
|
+
|| (ctx.newtypes ?? []).some((n) => n.name === ty)
|
|
782
|
+
|| (ctx.jsonTypes ?? []).some((j) => j.name === ty)
|
|
783
|
+
|| isOptionalRecord(ty, ctx);
|
|
541
784
|
// `&OtherClass` params: the referenced struct must already be declared (parsed) above.
|
|
542
785
|
const isClassRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty) && classes.has(ty.slice(1));
|
|
543
786
|
|
|
544
|
-
const params = rawParams
|
|
787
|
+
const params = splitTopLevel(rawParams).map((p) => p.trim()).filter(Boolean);
|
|
545
788
|
let selfKind = null;
|
|
546
789
|
if (params[0] === '&mut self' || params[0] === '&self') selfKind = params.shift();
|
|
547
790
|
else if (params[0] === 'self' || params[0] === 'mut self') {
|
|
@@ -551,26 +794,34 @@ function parseFn(cls, sig, ctx, log) {
|
|
|
551
794
|
|
|
552
795
|
const args = [];
|
|
553
796
|
for (const p of params) {
|
|
554
|
-
const m = p.match(/^(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String)\s*>|serde_json\s*::\s*Value|(?:std\s*::\s*sync\s*::\s*)?Arc\s*<\s*\w+\s*>|embind_rs\s*::\s*Js(?:Value|Function)|&\s*\w+|[\w()]+)$/);
|
|
797
|
+
const m = p.match(/^(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String|&\s*\w+)\s*>|Vec\s*<[^;]+>|\[[^\]]+;\s*\d+\]|&\s*\[[^\]]+\]|\([^)]*,[^)]*\)|(?:std\s*::\s*collections\s*::\s*)?(?:HashMap|BTreeMap|HashSet|BTreeSet)\s*<[^>]+>|serde_json\s*::\s*Value|(?:std\s*::\s*sync\s*::\s*)?Arc\s*<\s*\w+\s*>|embind_rs\s*::\s*Js(?:Value|Function)|&\s*\w+|[\w()]+)$/);
|
|
555
798
|
let ty = m?.[2].replace(/\s+/g, '');
|
|
799
|
+
if (ty) ty = normalizeStringSpelling(ty);
|
|
556
800
|
if (ty && !known(ty) && isJsonSpelling(ty, ctx)) ty = JSON_TY;
|
|
557
801
|
if (ty) ty = normalizeArc(ty, ctx);
|
|
558
802
|
const jsTokP = ty ? matchJsTok(ty, ctx) : null;
|
|
559
803
|
if (jsTokP) ty = jsTokP;
|
|
560
804
|
const arcParam = ty?.match(ARC_RE)?.[1];
|
|
561
|
-
|
|
562
|
-
|
|
805
|
+
const optClassRef = ty?.match(OPTION_CLASS_REF_RE)?.[1];
|
|
806
|
+
const closure = closureShape(p.slice(p.indexOf(':') + 1));
|
|
807
|
+
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)))) {
|
|
810
|
+
log(isCollection(ty) && !collectionCarries(ty, ctx)
|
|
811
|
+
? `crossbind: rust bridge: ${cls.name}::${name} skipped (a struct inside '${ty}' must derive Serialize and Deserialize to cross)`
|
|
812
|
+
: `crossbind: rust bridge: ${cls.name}::${name} skipped (unsupported parameter '${p}')`);
|
|
563
813
|
return;
|
|
564
814
|
}
|
|
565
815
|
args.push({ name: m[1], ty });
|
|
566
816
|
}
|
|
567
|
-
|
|
817
|
+
const returnInfo = analyzeReturn(rawRet ?? '()', ctx);
|
|
818
|
+
let { inner: ret, throws, optional } = returnInfo;
|
|
568
819
|
if (!known(ret) && isJsonSpelling(ret, ctx)) ret = JSON_TY;
|
|
569
820
|
ret = normalizeArc(ret.replace(/\s+/g, ''), ctx);
|
|
570
821
|
const jsTokR = matchJsTok(ret, ctx);
|
|
571
822
|
if (jsTokR) ret = jsTokR;
|
|
572
823
|
if (jsTokR && optional) {
|
|
573
|
-
log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Option<${ret}> returns are not supported - return JsValue::
|
|
824
|
+
log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Option<${ret}> returns are not supported - return JsValue::null() instead)`);
|
|
574
825
|
return;
|
|
575
826
|
}
|
|
576
827
|
|
|
@@ -583,11 +834,11 @@ function parseFn(cls, sig, ctx, log) {
|
|
|
583
834
|
if (name === 'new') {
|
|
584
835
|
if (arcSelf) { log(`crossbind: rust bridge: ${cls.name}::new skipped (Arc<Self> has no ctor shape - use a named factory like 'create')`); return; }
|
|
585
836
|
if (optional) { log(`crossbind: rust bridge: ${cls.name}::new skipped (Option<Self> has no ctor shape - use a named factory or Result<Self, E>)`); return; }
|
|
586
|
-
if (args.length >
|
|
837
|
+
if (args.length > 6) { log(`crossbind: rust bridge: ${cls.name}::new skipped (max 6 args)`); return; }
|
|
587
838
|
cls.ctor = { args, throws };
|
|
588
839
|
} else {
|
|
589
840
|
if (arcSelf && optional) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Option<Arc<Self>> is not supported in this wave)`); return; }
|
|
590
|
-
if (args.length >
|
|
841
|
+
if (args.length > 6) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (factories take max 6 args)`); return; }
|
|
591
842
|
cls.factories.push({ name, args, throws, optional, shared: arcSelf });
|
|
592
843
|
}
|
|
593
844
|
return;
|
|
@@ -599,11 +850,11 @@ function parseFn(cls, sig, ctx, log) {
|
|
|
599
850
|
log(`crossbind: rust bridge: ${cls.name}::${name} skipped (unsupported return '${ret}')`);
|
|
600
851
|
return;
|
|
601
852
|
}
|
|
602
|
-
if (
|
|
603
|
-
log(`crossbind: rust bridge: ${cls.name}::${name} skipped (
|
|
853
|
+
if (throws) {
|
|
854
|
+
log(`crossbind: rust bridge: ${cls.name}::${name} skipped (a fallible Arc return is not carried yet)`);
|
|
604
855
|
return;
|
|
605
856
|
}
|
|
606
|
-
if (args.length >
|
|
857
|
+
if (args.length > 6) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (max 6 args)`); return; }
|
|
607
858
|
cls.methods.push({ name, args, ret: `Arc<${target}>`, byRef: selfKind === '&self', throws: false, optionalRet: false });
|
|
608
859
|
return;
|
|
609
860
|
}
|
|
@@ -611,37 +862,62 @@ function parseFn(cls, sig, ctx, log) {
|
|
|
611
862
|
log(`crossbind: rust bridge: ${cls.name}::${name} skipped (Option<${ret}> return is not representable - inners: i32, f64, bool, String; or an Option<Self> factory)`);
|
|
612
863
|
return;
|
|
613
864
|
}
|
|
865
|
+
const selfOwned = ret === 'Self' || ret === cls.name || classes.has(ret);
|
|
866
|
+
const borrowedSelf = ret.replace(/\s+/g, '').match(/^&(?:mut)?(Self|\w+)$/)?.[1];
|
|
867
|
+
const selfBorrowed = borrowedSelf === 'Self' || borrowedSelf === cls.name;
|
|
868
|
+
if (selfOwned || selfBorrowed) {
|
|
869
|
+
cls.methods.push({
|
|
870
|
+
name, args, ret: selfOwned ? (ret === 'Self' ? cls.name : ret) : cls.name,
|
|
871
|
+
byRef: selfKind === '&self', throws, optionalRet: false,
|
|
872
|
+
ownedClass: selfOwned ? (ret === 'Self' ? cls.name : ret) : null,
|
|
873
|
+
borrowedSelf: selfBorrowed,
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
614
877
|
if (!known(ret) && ret !== JSON_TY && !jsTokR) {
|
|
615
878
|
log(`crossbind: rust bridge: ${cls.name}::${name} skipped (unsupported return '${ret}')`);
|
|
616
879
|
return;
|
|
617
880
|
}
|
|
618
|
-
if (args.length >
|
|
619
|
-
cls.methods.push({ name, args, ret, byRef: selfKind === '&self', throws, optionalRet: optional });
|
|
881
|
+
if (args.length > 6) { log(`crossbind: rust bridge: ${cls.name}::${name} skipped (max 6 args)`); return; }
|
|
882
|
+
cls.methods.push({ name, args, ret, byRef: selfKind === '&self', throws, optionalRet: optional, collect: returnInfo.collect });
|
|
620
883
|
}
|
|
621
884
|
|
|
622
885
|
function parseFreeFn(freeFns, sig, ctx, log) {
|
|
623
886
|
const { enums, valueObjects, classes } = ctx;
|
|
624
887
|
const [, name, rawParams, rawRet] = sig;
|
|
625
|
-
|
|
626
|
-
|
|
888
|
+
// A borrowed string is known in both directions: as a parameter it borrows, as a return it
|
|
889
|
+
// comes back owned.
|
|
890
|
+
const known = (ty) => PRIMITIVES.has(ty) || PARAM_ONLY.has(ty) || typedArrayOf(ty) || (isCollection(ty) && collectionCarries(ty, ctx))
|
|
891
|
+
|| enums.some((e) => e.name === ty) || valueObjects.some((v) => v.name === ty)
|
|
892
|
+
|| (ctx.newtypes ?? []).some((n) => n.name === ty)
|
|
893
|
+
|| (ctx.jsonTypes ?? []).some((j) => j.name === ty)
|
|
894
|
+
|| isOptionalRecord(ty, ctx);
|
|
627
895
|
const isClassRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty) && classes.has(ty.slice(1));
|
|
628
896
|
|
|
629
897
|
const args = [];
|
|
630
|
-
for (const p of rawParams
|
|
631
|
-
const m = p.match(/^(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String)\s*>|serde_json\s*::\s*Value|(?:std\s*::\s*sync\s*::\s*)?Arc\s*<\s*\w+\s*>|embind_rs\s*::\s*Js(?:Value|Function)|&\s*\w+|[\w()]+)$/);
|
|
898
|
+
for (const p of splitTopLevel(rawParams).map((s) => s.trim()).filter(Boolean)) {
|
|
899
|
+
const m = p.match(/^(\w+)\s*:\s*(&\s*(?:str|String)|Option\s*<\s*(?:i32|f64|bool|String|&\s*\w+)\s*>|Vec\s*<[^;]+>|\[[^\]]+;\s*\d+\]|&\s*\[[^\]]+\]|\([^)]*,[^)]*\)|(?:std\s*::\s*collections\s*::\s*)?(?:HashMap|BTreeMap|HashSet|BTreeSet)\s*<[^>]+>|serde_json\s*::\s*Value|(?:std\s*::\s*sync\s*::\s*)?Arc\s*<\s*\w+\s*>|embind_rs\s*::\s*Js(?:Value|Function)|&\s*\w+|[\w()]+)$/);
|
|
632
900
|
let ty = m?.[2].replace(/\s+/g, '');
|
|
901
|
+
if (ty) ty = normalizeStringSpelling(ty);
|
|
633
902
|
if (ty && !known(ty) && isJsonSpelling(ty, ctx)) ty = JSON_TY;
|
|
634
903
|
if (ty) ty = normalizeArc(ty, ctx);
|
|
635
904
|
const jsTokP = ty ? matchJsTok(ty, ctx) : null;
|
|
636
905
|
if (jsTokP) ty = jsTokP;
|
|
637
906
|
const arcParam = ty?.match(ARC_RE)?.[1];
|
|
638
|
-
|
|
639
|
-
|
|
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}')`);
|
|
640
915
|
return;
|
|
641
916
|
}
|
|
642
917
|
args.push({ name: m[1], ty });
|
|
643
918
|
}
|
|
644
|
-
|
|
919
|
+
const returnInfo = analyzeReturn(rawRet ?? '()', ctx);
|
|
920
|
+
let { inner: ret, throws, optional } = returnInfo;
|
|
645
921
|
if (!known(ret) && isJsonSpelling(ret, ctx)) ret = JSON_TY;
|
|
646
922
|
ret = normalizeArc(ret.replace(/\s+/g, ''), ctx);
|
|
647
923
|
const jsTokR = matchJsTok(ret, ctx);
|
|
@@ -649,38 +925,137 @@ function parseFreeFn(freeFns, sig, ctx, log) {
|
|
|
649
925
|
const retArc = ret.match(ARC_RE)?.[1];
|
|
650
926
|
if (retArc) {
|
|
651
927
|
if (!classes.has(retArc)) { log(`crossbind: rust bridge: fn ${name} skipped (unsupported return '${ret}')`); return; }
|
|
652
|
-
if (
|
|
928
|
+
if (throws) { log(`crossbind: rust bridge: fn ${name} skipped (a fallible Arc return is not carried yet)`); return; }
|
|
929
|
+
}
|
|
930
|
+
if (jsTokR && optional) { log(`crossbind: rust bridge: fn ${name} skipped (Option<${ret}> returns are not supported - return JsValue::null() instead)`); return; }
|
|
931
|
+
if (optional && !OPTION_INNERS.has(ret) && !retArc) { log(`crossbind: rust bridge: fn ${name} skipped (Option<${ret}> return is not representable - inners: i32, f64, bool, String, Arc<Class>)`); return; }
|
|
932
|
+
if (isCollection(ret) && !collectionCarries(ret, ctx)) { log(`crossbind: rust bridge: fn ${name} skipped (a struct inside '${ret}' must derive Serialize and Deserialize to cross)`); return; }
|
|
933
|
+
// A fresh instance handed back by value: JS owns it and frees it on delete().
|
|
934
|
+
if (classes.has(ret) && !(ctx.jsonTypes ?? []).some((j) => j.name === ret)) {
|
|
935
|
+
freeFns.push({ name, jsName: camel(name), args, ret, throws, optionalRet: false, ownedClass: ret });
|
|
936
|
+
return;
|
|
653
937
|
}
|
|
654
|
-
if (jsTokR && optional) { log(`crossbind: rust bridge: fn ${name} skipped (Option<${ret}> returns are not supported - return JsValue::undefined() instead)`); return; }
|
|
655
|
-
if (optional && !OPTION_INNERS.has(ret)) { log(`crossbind: rust bridge: fn ${name} skipped (Option<${ret}> return is not representable - inners: i32, f64, bool, String)`); return; }
|
|
656
938
|
if (!known(ret) && ret !== JSON_TY && !retArc && !jsTokR) { log(`crossbind: rust bridge: fn ${name} skipped (unsupported return '${ret}')`); return; }
|
|
657
|
-
if (args.length >
|
|
658
|
-
freeFns.push({ name, jsName: camel(name), args, ret, throws, optionalRet: optional });
|
|
939
|
+
if (args.length > 6) { log(`crossbind: rust bridge: fn ${name} skipped (max 6 args)`); return; }
|
|
940
|
+
freeFns.push({ name, jsName: camel(name), args, ret, throws, optionalRet: optional, collect: returnInfo.collect });
|
|
659
941
|
}
|
|
660
942
|
|
|
661
943
|
// ---------------- emitter ----------------
|
|
662
944
|
|
|
663
945
|
const camel = (s) => s.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
664
946
|
|
|
665
|
-
function emitBridge(model, {
|
|
947
|
+
function emitBridge(model, {
|
|
948
|
+
userCrate, vectors, log, prelude = '', namePrefix = '',
|
|
949
|
+
}) {
|
|
666
950
|
const U = userCrate;
|
|
951
|
+
// embind's registry is flat, so two crate imports that both export `Version` would collide.
|
|
952
|
+
// A direct `cargo:` import registers its public names under a per-crate prefix; the generated
|
|
953
|
+
// proxy module maps them back to the clean names the import specifier already scopes.
|
|
954
|
+
const pub = (name) => `${namePrefix}${name}`;
|
|
667
955
|
const isEnum = (ty) => model.enums.some((e) => e.name === ty);
|
|
956
|
+
const newtypeOf = (ty) => (model.newtypes ?? []).find((n) => n.name === ty);
|
|
957
|
+
// A serde record, a data enum, a value object inside an Option: all cross as plain JS values
|
|
958
|
+
// through the JSON wire.
|
|
959
|
+
const jsonRecord = (ty) => {
|
|
960
|
+
const text = String(ty).trim();
|
|
961
|
+
const inner = text.match(/^Option<(.+)>$/)?.[1]?.trim() ?? text;
|
|
962
|
+
return (model.jsonTypes ?? []).some((j) => j.name === inner)
|
|
963
|
+
|| (text.startsWith('Option<') && model.valueObjects.some((v) => v.name === inner));
|
|
964
|
+
};
|
|
965
|
+
// A panic inside a binding becomes a JS exception; an Arc wrapper has no sentinel to return
|
|
966
|
+
// afterwards, so those bodies run unguarded (a panic there still aborts).
|
|
967
|
+
const guarded = (retTy, body) => (retTy.endsWith('Shared') ? body : `embind_rs::guard(|| ${body})`);
|
|
668
968
|
const isVo = (ty) => model.valueObjects.some((v) => v.name === ty);
|
|
669
969
|
// Bridge-local wire type of a signature token, and the two directions of the shim adaptation.
|
|
670
970
|
// &str/&String params cross as String (borrowed at the call site); &OtherClass params cross
|
|
671
971
|
// as the class pointer via a bridge-local Ref wrapper (borrowed unsafely at the call site).
|
|
672
|
-
|
|
972
|
+
// A slice is a collection, not a borrowed class.
|
|
973
|
+
const isRef = (ty) => ty.startsWith('&') && !PARAM_ONLY.has(ty) && !isCollection(ty) && !typedArrayOf(ty);
|
|
673
974
|
const arcInner = (ty) => String(ty).match(ARC_RE)?.[1];
|
|
674
|
-
|
|
975
|
+
// An optional Arc rides the nullable shared wrapper; every other optional keeps `Option<T>`.
|
|
976
|
+
// `&[T]` decodes into a Vec and is handed back as a slice; every other collection is owned.
|
|
977
|
+
// `&[T; N]` keeps its fixed length (serde reads a JS array of exactly N items); `&[T]`
|
|
978
|
+
// decodes into a Vec.
|
|
979
|
+
const fixedArrayRef = (ty) => String(ty).trim().match(/^&\s*\[\s*(.+?)\s*;\s*(\d+)\s*\]$/);
|
|
980
|
+
const ownedTy = (ty) => {
|
|
981
|
+
const text = String(ty).trim();
|
|
982
|
+
const fixed = fixedArrayRef(text);
|
|
983
|
+
if (fixed) return qualifyUserTypes(`[${fixed[1]}; ${fixed[2]}]`);
|
|
984
|
+
const slice = text.match(/^&\s*\[(.+)\]$/);
|
|
985
|
+
return qualifyUserTypes(slice ? `Vec<${slice[1]}>` : text);
|
|
986
|
+
};
|
|
987
|
+
// Element and value types declared by the user crate need its path; std and primitive
|
|
988
|
+
// spellings stay as they are.
|
|
989
|
+
const STD_COLLECTIONS = { HashMap: 'std::collections::HashMap', BTreeMap: 'std::collections::BTreeMap', HashSet: 'std::collections::HashSet', BTreeSet: 'std::collections::BTreeSet' };
|
|
990
|
+
const qualifyUserTypes = (text) => String(text).replace(/\b([A-Z]\w*)\b/g, (name) => (
|
|
991
|
+
STD_COLLECTIONS[name] ? STD_COLLECTIONS[name] :
|
|
992
|
+
model.classes.some((c) => c.name === name)
|
|
993
|
+
|| model.valueObjects.some((v) => v.name === name)
|
|
994
|
+
|| model.enums.some((e) => e.name === name)
|
|
995
|
+
|| (model.newtypes ?? []).some((n) => n.name === name)
|
|
996
|
+
|| (model.jsonTypes ?? []).some((j) => j.name === name)
|
|
997
|
+
? `${U}::${name}`
|
|
998
|
+
: name));
|
|
999
|
+
const borrowSuffix = (ty) => (!fixedArrayRef(ty) && /^&\s*\[/.test(String(ty).trim()) ? '.as_slice()' : '');
|
|
1000
|
+
const borrowPrefix = (ty) => (fixedArrayRef(ty) ? '&' : '');
|
|
1001
|
+
// `__closure{...}` carries the shape the parser read off `impl Fn(..)`.
|
|
1002
|
+
const closureOf = (ty) => {
|
|
1003
|
+
const text = String(ty);
|
|
1004
|
+
return text.startsWith('__closure') ? JSON.parse(text.slice('__closure'.length)) : null;
|
|
1005
|
+
};
|
|
1006
|
+
const jsValueOf = (ty, expr) => (ty === 'i32' || ty === 'f64' ? `embind_rs::JsValue::from_f64(${expr} as f64)`
|
|
1007
|
+
: ty === 'bool' ? `embind_rs::JsValue::from_bool(${expr})`
|
|
1008
|
+
: `embind_rs::JsValue::from_str(&${expr})`);
|
|
1009
|
+
const fromJsValue = (ty, expr) => (ty === 'i32' ? `${expr}.as_f64().unwrap_or(0.0) as i32`
|
|
1010
|
+
: ty === 'f64' ? `${expr}.as_f64().unwrap_or(0.0)`
|
|
1011
|
+
: ty === 'bool' ? `${expr}.as_bool().unwrap_or(false)`
|
|
1012
|
+
: ty === 'String' ? `${expr}.as_string().unwrap_or_default()`
|
|
1013
|
+
: '()');
|
|
1014
|
+
const closureExpr = (shape, handle) => {
|
|
1015
|
+
const params = shape.args.map((ty, i) => `p${i}: ${ty}`).join(', ');
|
|
1016
|
+
const call = `${handle}.call${shape.args.length}(${shape.args.map((ty, i) => `&${jsValueOf(ty, `p${i}`)}`).join(', ')})`;
|
|
1017
|
+
const body = shape.ret === '()'
|
|
1018
|
+
? `match ${call} { Ok(_) => (), Err(e) => embind_rs::raise_err(e) }`
|
|
1019
|
+
: `match ${call} { Ok(v) => ${fromJsValue(shape.ret, 'v')}, Err(e) => embind_rs::raise_err(e) }`;
|
|
1020
|
+
const closure = `move |${params}| -> ${shape.ret} { ${body} }`;
|
|
1021
|
+
return shape.boxed ? `Box::new(${closure})` : closure;
|
|
1022
|
+
};
|
|
1023
|
+
const optionStrParam = (ty) => ty.replace(/\s+/g, '') === 'Option<&str>';
|
|
1024
|
+
// Only a bound class counts: `Option<&str>` is a string, not a class reference.
|
|
1025
|
+
const optionClassRef = (ty) => {
|
|
1026
|
+
const name = String(ty).replace(/\s+/g, '').match(/^Option<&(\w+)>$/)?.[1];
|
|
1027
|
+
return name && model.classes.some((c) => c.name === name) ? name : null;
|
|
1028
|
+
};
|
|
1029
|
+
const optionalWireTy = (ty) => (arcInner(ty) ? `${arcInner(ty)}SharedOpt` : `Option<${ty}>`);
|
|
1030
|
+
const wireTy = (ty) => (typedArrayOf(ty) ? `embind_rs::${typedArrayOf(ty).wrapper}`
|
|
1031
|
+
: closureOf(ty) ? 'embind_rs::JsFunction'
|
|
1032
|
+
: isCollection(ty) || jsonRecord(ty) ? '__CrossbindJson'
|
|
1033
|
+
: optionStrParam(ty) ? 'Option<String>'
|
|
1034
|
+
: optionClassRef(ty) ? `${optionClassRef(ty)}RefOpt`
|
|
1035
|
+
: newtypeOf(ty) ? newtypeOf(ty).inner
|
|
1036
|
+
: PARAM_ONLY.has(ty) ? 'String'
|
|
675
1037
|
: isRef(ty) ? `${ty.slice(1)}Ref`
|
|
676
1038
|
: ty === JSON_TY ? '__CrossbindJson'
|
|
677
1039
|
: JS_TOKS.has(ty) ? `embind_rs::${ty}`
|
|
678
1040
|
: arcInner(ty) ? `${arcInner(ty)}Shared`
|
|
679
1041
|
: isEnum(ty) || isVo(ty) ? `${ty}W` : ty);
|
|
680
|
-
|
|
1042
|
+
// A collection parameter is decoded from the JSON value; a shape the Rust type cannot hold
|
|
1043
|
+
// raises instead of silently defaulting.
|
|
1044
|
+
const unwrap = (ty, expr) => (typedArrayOf(ty) ? `${expr}.0${typedArrayOf(ty).slice ? '.as_slice()' : ''}`
|
|
1045
|
+
: closureOf(ty) ? closureExpr(closureOf(ty), expr)
|
|
1046
|
+
: isCollection(ty) || jsonRecord(ty) ? `${borrowPrefix(ty)}__crossbind_from_json::<${ownedTy(ty)}>(${expr}.0)${borrowSuffix(ty)}`
|
|
1047
|
+
// (ownedTy qualifies user type names with the crate path)
|
|
1048
|
+
: optionStrParam(ty) ? `${expr}.as_deref()`
|
|
1049
|
+
: optionClassRef(ty) ? `${expr}.as_option()`
|
|
1050
|
+
: newtypeOf(ty) ? `${U}::${ty}(${expr})`
|
|
1051
|
+
: PARAM_ONLY.has(ty) ? `&${expr}`
|
|
681
1052
|
: isRef(ty) ? `unsafe { &*${expr}.0 }`
|
|
682
1053
|
: ty === JSON_TY || arcInner(ty) || isEnum(ty) || isVo(ty) ? `${expr}.0` : expr);
|
|
683
|
-
const wrap = (ty, expr) => (ty
|
|
1054
|
+
const wrap = (ty, expr) => (typedArrayOf(ty) ? `embind_rs::${typedArrayOf(ty).wrapper}(${expr})`
|
|
1055
|
+
: isCollection(ty) || jsonRecord(ty) ? `__CrossbindJson(__crossbind_to_json(&(${expr})))`
|
|
1056
|
+
: newtypeOf(ty) ? `(${expr}).0`
|
|
1057
|
+
: PARAM_ONLY.has(ty) ? `String::from(${expr})`
|
|
1058
|
+
: ty === JSON_TY ? `__CrossbindJson(${expr})`
|
|
684
1059
|
: arcInner(ty) ? `${arcInner(ty)}Shared(${expr})`
|
|
685
1060
|
: isEnum(ty) || isVo(ty) ? `${ty}W(${expr})` : expr);
|
|
686
1061
|
|
|
@@ -698,7 +1073,8 @@ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
|
|
|
698
1073
|
}
|
|
699
1074
|
|
|
700
1075
|
for (const e of model.enums) {
|
|
701
|
-
|
|
1076
|
+
// No derives on the wrapper: a plain fieldless enum need not be Clone or Copy, and the
|
|
1077
|
+
// wire only ever moves one (`to_wire` takes self, `from_wire` builds a fresh one).
|
|
702
1078
|
out.push('#[repr(transparent)]');
|
|
703
1079
|
out.push(`pub struct ${e.name}W(pub ${U}::${e.name});`);
|
|
704
1080
|
out.push(`impl WireType for ${e.name}W {`);
|
|
@@ -736,7 +1112,13 @@ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
|
|
|
736
1112
|
// `&OtherClass` params ride the class pointer wire through per-class Ref wrappers; collect
|
|
737
1113
|
// the referenced classes up front so the wrappers exist before the shims that use them.
|
|
738
1114
|
const classRefs = new Set();
|
|
739
|
-
|
|
1115
|
+
(model.classes ?? []).forEach((c) => c.methods.forEach((m) => { if (m.ownedClass || m.borrowedSelf) classRefs.add(m.ret); }));
|
|
1116
|
+
(model.freeFns ?? []).forEach((f) => { if (f.ownedClass) classRefs.add(f.ownedClass); });
|
|
1117
|
+
const scanRefs = (args) => args.forEach((p) => {
|
|
1118
|
+
if (isRef(p.ty)) classRefs.add(p.ty.slice(1));
|
|
1119
|
+
const optRef = optionClassRef(p.ty);
|
|
1120
|
+
if (optRef) classRefs.add(optRef);
|
|
1121
|
+
});
|
|
740
1122
|
for (const cls of model.classes) {
|
|
741
1123
|
if (cls.ctor) scanRefs(cls.ctor.args);
|
|
742
1124
|
cls.factories.forEach((f) => scanRefs(f.args));
|
|
@@ -755,9 +1137,70 @@ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
|
|
|
755
1137
|
out.push(` fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }`);
|
|
756
1138
|
out.push('}');
|
|
757
1139
|
out.push('');
|
|
1140
|
+
// A fresh instance of the class: the wire is the smart pointer JS owns and frees.
|
|
1141
|
+
out.push('#[repr(transparent)]');
|
|
1142
|
+
out.push(`pub struct ${name}Owned(pub *mut ${U}::${name});`);
|
|
1143
|
+
out.push(`impl WireType for ${name}Owned {`);
|
|
1144
|
+
out.push(' type Wire = *mut c_void;');
|
|
1145
|
+
out.push(" const SIG: char = 'p';");
|
|
1146
|
+
out.push(` fn tid() -> *const c_void { embind_rs::owned_ptr_tid::<${U}::${name}>() }`);
|
|
1147
|
+
out.push(` fn from_wire(w: *mut c_void) -> ${name}Owned { ${name}Owned(w as *mut ${U}::${name}) }`);
|
|
1148
|
+
out.push(' fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }');
|
|
1149
|
+
out.push('}');
|
|
1150
|
+
out.push(`impl embind_rs::ErrSentinel for ${name}Owned { fn err_sentinel() -> Self { ${name}Owned(core::ptr::null_mut()) } }`);
|
|
1151
|
+
out.push('');
|
|
1152
|
+
// The same object handed back for chaining: a pointer, so JS does not own it twice.
|
|
1153
|
+
out.push('#[repr(transparent)]');
|
|
1154
|
+
out.push(`pub struct ${name}RefOut(pub *mut ${U}::${name});`);
|
|
1155
|
+
out.push(`impl WireType for ${name}RefOut {`);
|
|
1156
|
+
out.push(' type Wire = *mut c_void;');
|
|
1157
|
+
out.push(" const SIG: char = 'p';");
|
|
1158
|
+
out.push(` fn tid() -> *const c_void { embind_rs::class_ptr_tid::<${U}::${name}>() }`);
|
|
1159
|
+
out.push(` fn from_wire(w: *mut c_void) -> ${name}RefOut { ${name}RefOut(w as *mut ${U}::${name}) }`);
|
|
1160
|
+
out.push(' fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }');
|
|
1161
|
+
out.push('}');
|
|
1162
|
+
out.push(`impl embind_rs::ErrSentinel for ${name}RefOut { fn err_sentinel() -> Self { ${name}RefOut(core::ptr::null_mut()) } }`);
|
|
1163
|
+
out.push('');
|
|
1164
|
+
// `Option<&Class>` parameter: a null pointer is the None the caller wrote as null.
|
|
1165
|
+
out.push('#[derive(Clone, Copy)]');
|
|
1166
|
+
out.push('#[repr(transparent)]');
|
|
1167
|
+
out.push(`pub struct ${name}RefOpt(pub *mut ${U}::${name});`);
|
|
1168
|
+
out.push(`impl ${name}RefOpt {`);
|
|
1169
|
+
out.push(` pub fn as_option(&self) -> Option<&${U}::${name}> {`);
|
|
1170
|
+
out.push(' if self.0.is_null() { None } else { Some(unsafe { &*self.0 }) }');
|
|
1171
|
+
out.push(' }');
|
|
1172
|
+
out.push('}');
|
|
1173
|
+
out.push(`impl WireType for ${name}RefOpt {`);
|
|
1174
|
+
out.push(' type Wire = *mut c_void;');
|
|
1175
|
+
out.push(" const SIG: char = 'p';");
|
|
1176
|
+
out.push(` fn tid() -> *const c_void { embind_rs::class_ptr_tid::<${U}::${name}>() }`);
|
|
1177
|
+
out.push(` fn from_wire(w: *mut c_void) -> ${name}RefOpt { ${name}RefOpt(w as *mut ${U}::${name}) }`);
|
|
1178
|
+
out.push(` fn to_wire(self) -> *mut c_void { self.0 as *mut c_void }`);
|
|
1179
|
+
out.push('}');
|
|
1180
|
+
out.push('');
|
|
758
1181
|
}
|
|
759
1182
|
|
|
760
1183
|
if (model.usesJson) {
|
|
1184
|
+
out.push('// A collection crosses as a JS array or object through the same JSON wire: decoding a');
|
|
1185
|
+
out.push('// shape the Rust type cannot hold raises in JS instead of quietly yielding a default.');
|
|
1186
|
+
out.push('fn __crossbind_from_json<T: serde::de::DeserializeOwned>(value: serde_json::Value) -> T {');
|
|
1187
|
+
out.push(' match serde_json::from_value(value) {');
|
|
1188
|
+
out.push(' Ok(parsed) => parsed,');
|
|
1189
|
+
out.push(' Err(e) => {');
|
|
1190
|
+
out.push(' // Raise first: on wasm that throws into JS right here, so a bad argument');
|
|
1191
|
+
out.push(' // costs one error instead of the panic that would poison the instance.');
|
|
1192
|
+
out.push(' let _: () = embind_rs::raise_err(format!("cannot read this value as {}: {e}", std::any::type_name::<T>()));');
|
|
1193
|
+
out.push(' // Native only, where raising parks the error and returns: no value of T');
|
|
1194
|
+
out.push(' // can be fabricated, so unwind into the shim guard instead.');
|
|
1195
|
+
out.push(' panic!("cannot read this value as {}", std::any::type_name::<T>())');
|
|
1196
|
+
out.push(' }');
|
|
1197
|
+
out.push(' }');
|
|
1198
|
+
out.push('}');
|
|
1199
|
+
out.push('');
|
|
1200
|
+
out.push('fn __crossbind_to_json<T: serde::Serialize + ?Sized>(value: &T) -> serde_json::Value {');
|
|
1201
|
+
out.push(' serde_json::to_value(value).unwrap_or(serde_json::Value::Null)');
|
|
1202
|
+
out.push('}');
|
|
1203
|
+
out.push('');
|
|
761
1204
|
out.push('// serde_json::Value crosses as a deep JSON copy: the adapter converts handle <->');
|
|
762
1205
|
out.push('// [u32 len][bytes] JSON text through the host JSON codec; serde maps text <-> Value here.');
|
|
763
1206
|
out.push('#[repr(transparent)]');
|
|
@@ -821,44 +1264,84 @@ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
|
|
|
821
1264
|
out.push(` fn to_wire(self) -> *mut c_void { std::sync::Arc::into_raw(self.0) as *mut c_void }`);
|
|
822
1265
|
out.push('}');
|
|
823
1266
|
out.push('');
|
|
1267
|
+
// `Option<Arc<T>>`: a null smart pointer is what embind reads back as JS null, so None
|
|
1268
|
+
// needs no optional type of its own.
|
|
1269
|
+
out.push('#[repr(transparent)]');
|
|
1270
|
+
out.push(`pub struct ${inner}SharedOpt(pub Option<std::sync::Arc<${U}::${inner}>>);`);
|
|
1271
|
+
out.push(`impl WireType for ${inner}SharedOpt {`);
|
|
1272
|
+
out.push(' type Wire = *mut c_void;');
|
|
1273
|
+
out.push(" const SIG: char = 'p';");
|
|
1274
|
+
out.push(` fn tid() -> *const c_void { embind_rs::shared_tid::<${U}::${inner}>() }`);
|
|
1275
|
+
out.push(' fn from_wire(w: *mut c_void) -> Self {');
|
|
1276
|
+
out.push(` if w.is_null() { return ${inner}SharedOpt(None); }`);
|
|
1277
|
+
out.push(' unsafe {');
|
|
1278
|
+
out.push(` std::sync::Arc::increment_strong_count(w as *const ${U}::${inner});`);
|
|
1279
|
+
out.push(` ${inner}SharedOpt(Some(std::sync::Arc::from_raw(w as *const ${U}::${inner})))`);
|
|
1280
|
+
out.push(' }');
|
|
1281
|
+
out.push(' }');
|
|
1282
|
+
out.push(' fn to_wire(self) -> *mut c_void {');
|
|
1283
|
+
out.push(' match self.0 {');
|
|
1284
|
+
out.push(' Some(value) => std::sync::Arc::into_raw(value) as *mut c_void,');
|
|
1285
|
+
out.push(' None => core::ptr::null_mut(),');
|
|
1286
|
+
out.push(' }');
|
|
1287
|
+
out.push(' }');
|
|
1288
|
+
out.push('}');
|
|
1289
|
+
out.push(`impl embind_rs::ErrSentinel for ${inner}SharedOpt { fn err_sentinel() -> Self { ${inner}SharedOpt(None) } }`);
|
|
1290
|
+
out.push('');
|
|
824
1291
|
}
|
|
825
1292
|
|
|
826
1293
|
const registrations = [];
|
|
827
1294
|
const usedOptionals = new Set();
|
|
828
1295
|
const noteOptionArgs = (args) => args.forEach((p) => {
|
|
829
1296
|
const m = p.ty.match(OPTION_PARAM_RE);
|
|
830
|
-
|
|
1297
|
+
// `Option<&str>` rides the String optional; a class reference needs none (null is None).
|
|
1298
|
+
if (m) usedOptionals.add(m[1] === '&str' ? 'String' : m[1]);
|
|
831
1299
|
});
|
|
1300
|
+
for (const c of model.consts ?? []) {
|
|
1301
|
+
registrations.push(c.ty === '&str'
|
|
1302
|
+
? ` embind_rs::constant_str("${pub(c.name)}", ${U}::${c.name});`
|
|
1303
|
+
: ` embind_rs::constant("${pub(c.name)}", ${U}::${c.name});`);
|
|
1304
|
+
}
|
|
832
1305
|
for (const e of model.enums) {
|
|
833
|
-
registrations.push(` enum_::<${e.name}W>("${e.name}")${e.variants.map((v) => `.value("${v.name}", ${v.value})`).join('')};`);
|
|
1306
|
+
registrations.push(` enum_::<${e.name}W>("${pub(e.name)}")${e.variants.map((v) => `.value("${v.name}", ${v.value})`).join('')};`);
|
|
834
1307
|
}
|
|
835
1308
|
for (const v of model.valueObjects) {
|
|
836
|
-
const fields = v.fields.map((f) =>
|
|
837
|
-
|
|
1309
|
+
const fields = v.fields.map((f) => {
|
|
1310
|
+
const fieldTy = model.valueObjects.some((o) => o.name === f.type) ? `${f.type}W` : f.type;
|
|
1311
|
+
return `.field::<${fieldTy}>("${f.name}", core::mem::offset_of!(${U}::${v.name}, ${f.name}))`;
|
|
1312
|
+
}).join('');
|
|
1313
|
+
registrations.push(` value_object_::<${v.name}W>("${pub(v.name)}")${fields}.finalize();`);
|
|
838
1314
|
}
|
|
839
1315
|
for (const vec of vectors) {
|
|
840
1316
|
if (!VECTOR_ITEM_TYPES.has(vec.of)) { log(`crossbind: rust bridge: vector of '${vec.of}' skipped (supported: i32, f64, bool)`); continue; }
|
|
841
|
-
registrations.push(` register_vector::<${vec.of}>("${vec.name}");`);
|
|
1317
|
+
registrations.push(` register_vector::<${vec.of}>("${pub(vec.name)}");`);
|
|
842
1318
|
}
|
|
843
1319
|
|
|
844
1320
|
for (const cls of model.classes) {
|
|
845
1321
|
const C = `${U}::${cls.name}`;
|
|
846
1322
|
const shim = (fnName) => `__${cls.name.toLowerCase()}_${fnName}`;
|
|
847
|
-
const chain = [` class_::<${C}>("${cls.name}")`];
|
|
1323
|
+
const chain = [` class_::<${C}>("${pub(cls.name)}")`];
|
|
848
1324
|
|
|
849
|
-
|
|
850
|
-
|
|
1325
|
+
const handsBackSelf = cls.methods.some((m) => m.ownedClass) || (model.freeFns ?? []).some((f) => f.ownedClass === cls.name);
|
|
1326
|
+
if (cls.shared) chain.push(` .smart_ptr_shared("${pub(cls.name)}Shared")`);
|
|
1327
|
+
else if (cls.factories.length || handsBackSelf) chain.push(` .smart_ptr("${pub(cls.name)}Ptr")`);
|
|
1328
|
+
// A struct that derives Default and declares no `new` still constructs from JS: the
|
|
1329
|
+
// derive is the author saying what a default instance is.
|
|
1330
|
+
if (!cls.ctor && !cls.shared && cls.hasDefault) {
|
|
1331
|
+
out.push(`fn ${shim('default')}() -> *mut ${C} { ${guarded('*mut', `Box::into_raw(Box::new(<${C} as Default>::default()))`)} }`);
|
|
1332
|
+
chain.push(` .constructor_ptr0(${shim('default')})`);
|
|
1333
|
+
}
|
|
851
1334
|
if (cls.ctor) {
|
|
852
1335
|
const a = cls.ctor.args;
|
|
853
1336
|
noteOptionArgs(a);
|
|
854
1337
|
const ps = a.map((p, i) => `a${i}: ${wireTy(p.ty)}`).join(', ');
|
|
855
1338
|
const call = `${C}::new(${a.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
|
|
856
1339
|
if (cls.ctor.throws) {
|
|
857
|
-
out.push(`fn ${shim('new')}(${ps}) -> *mut ${C} { match ${call} { Ok(v) => Box::into_raw(Box::new(v)), Err(e) => embind_rs::
|
|
1340
|
+
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() }) }`)} }`);
|
|
858
1341
|
chain.push(` .constructor_ptr${a.length}(${shim('new')})`);
|
|
859
1342
|
} else {
|
|
860
|
-
out.push(`fn ${shim('new')}(${ps}) -> ${C} { ${call} }`);
|
|
861
|
-
chain.push(` .
|
|
1343
|
+
out.push(`fn ${shim('new')}(${ps}) -> *mut ${C} { ${guarded('*mut', `Box::into_raw(Box::new(${call}))`)} }`);
|
|
1344
|
+
chain.push(` .constructor_ptr${a.length}(${shim('new')})`);
|
|
862
1345
|
}
|
|
863
1346
|
}
|
|
864
1347
|
for (const f of cls.factories) {
|
|
@@ -867,7 +1350,7 @@ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
|
|
|
867
1350
|
const call = `${C}::${f.name}(${f.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
|
|
868
1351
|
if (f.shared) {
|
|
869
1352
|
if (f.throws) {
|
|
870
|
-
out.push(`fn ${shim(f.name)}(${ps}) -> *mut ${C} { match ${call} { Ok(v) => std::sync::Arc::into_raw(v) as *mut ${C}, Err(e) => embind_rs::
|
|
1353
|
+
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() }) } }`);
|
|
871
1354
|
chain.push(` .create_ptr${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
|
|
872
1355
|
} else {
|
|
873
1356
|
out.push(`fn ${shim(f.name)}(${ps}) -> std::sync::Arc<${C}> { ${call} }`);
|
|
@@ -876,32 +1359,63 @@ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
|
|
|
876
1359
|
continue;
|
|
877
1360
|
}
|
|
878
1361
|
if (f.throws) {
|
|
879
|
-
out.push(`fn ${shim(f.name)}(${ps}) -> *mut ${C} { match ${call} { Ok(v) => Box::into_raw(Box::new(v)), Err(e) => embind_rs::
|
|
1362
|
+
out.push(`fn ${shim(f.name)}(${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() }) }`)} }`);
|
|
880
1363
|
chain.push(` .create_ptr${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
|
|
881
1364
|
} else if (f.optional) {
|
|
882
|
-
out.push(`fn ${shim(f.name)}(${ps}) -> *mut ${C} { match ${call} { Some(v) => Box::into_raw(Box::new(v)), None => core::ptr::null_mut() } }`);
|
|
1365
|
+
out.push(`fn ${shim(f.name)}(${ps}) -> *mut ${C} { ${guarded('*mut', `match ${call} { Some(v) => Box::into_raw(Box::new(v)), None => core::ptr::null_mut() }`)} }`);
|
|
883
1366
|
chain.push(` .create_ptr${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
|
|
884
1367
|
} else {
|
|
885
|
-
out.push(`fn ${shim(f.name)}(${ps}) -> ${C} { ${call} }`);
|
|
886
|
-
chain.push(` .
|
|
1368
|
+
out.push(`fn ${shim(f.name)}(${ps}) -> *mut ${C} { ${guarded('*mut', `Box::into_raw(Box::new(${call}))`)} }`);
|
|
1369
|
+
chain.push(` .create_ptr${f.args.length}("${camel(f.name)}", ${shim(f.name)})`);
|
|
887
1370
|
}
|
|
888
1371
|
}
|
|
1372
|
+
for (const f of cls.fields ?? []) {
|
|
1373
|
+
out.push(`fn ${shim(`get_${f.name}`)}(t: &mut ${C}) -> ${wireTy(f.type)} { ${wrap(f.type, `t.${f.name}.clone()`)} }`);
|
|
1374
|
+
out.push(`fn ${shim(`set_${f.name}`)}(t: &mut ${C}, v: ${wireTy(f.type)}) { t.${f.name} = ${unwrap(f.type, 'v')}; }`);
|
|
1375
|
+
chain.push(` .property("${camel(f.name)}", ${shim(`get_${f.name}`)}, ${shim(`set_${f.name}`)})`);
|
|
1376
|
+
}
|
|
1377
|
+
// `foo()` next to `set_foo(v)` reads as one JS property, the pair napi-rs spells with
|
|
1378
|
+
// #[napi(getter)] and #[napi(setter)].
|
|
1379
|
+
// A setter returns nothing: `set_point(p) -> f64` is a method that happens to be named
|
|
1380
|
+
// that way, not the write half of a property.
|
|
1381
|
+
const accessorPairs = cls.methods.filter((m) => m.args.length === 0 && m.ret !== '()'
|
|
1382
|
+
&& cls.methods.some((other) => other.name === `set_${m.name}` && other.args.length === 1
|
|
1383
|
+
&& other.args[0].ty === m.ret && other.ret === '()'));
|
|
1384
|
+
const accessorNames = new Set(accessorPairs.flatMap((m) => [m.name, `set_${m.name}`]));
|
|
1385
|
+
for (const m of accessorPairs) {
|
|
1386
|
+
out.push(`fn ${shim(`prop_get_${m.name}`)}(t: &mut ${C}) -> ${wireTy(m.ret)} { ${wrap(m.ret, `(&*t).${m.name}()`)} }`);
|
|
1387
|
+
out.push(`fn ${shim(`prop_set_${m.name}`)}(t: &mut ${C}, v: ${wireTy(m.ret)}) { t.set_${m.name}(${unwrap(m.ret, 'v')}); }`);
|
|
1388
|
+
chain.push(` .property("${camel(m.name)}", ${shim(`prop_get_${m.name}`)}, ${shim(`prop_set_${m.name}`)})`);
|
|
1389
|
+
}
|
|
889
1390
|
for (const m of cls.methods) {
|
|
1391
|
+
if (accessorNames.has(m.name)) continue;
|
|
890
1392
|
noteOptionArgs(m.args);
|
|
891
1393
|
const params = [`t: &mut ${C}`, ...m.args.map((p, i) => `a${i}: ${wireTy(p.ty)}`)].join(', ');
|
|
892
1394
|
const callArgs = m.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ');
|
|
893
1395
|
const recv = m.byRef ? '(&*t)' : 't';
|
|
894
|
-
const
|
|
1396
|
+
const rawCall = `${recv}.${m.name}(${callArgs})`;
|
|
1397
|
+
const call = m.collect ? `(${rawCall}).collect::<Vec<_>>()` : rawCall;
|
|
1398
|
+
if (m.ownedClass || m.borrowedSelf) {
|
|
1399
|
+
const wrapper = m.ownedClass ? `${m.ret}Owned` : `${m.ret}RefOut`;
|
|
1400
|
+
const value = m.ownedClass
|
|
1401
|
+
? `${wrapper}(Box::into_raw(Box::new(${call})))`
|
|
1402
|
+
: `${wrapper}(${call} as *mut ${U}::${m.ret})`;
|
|
1403
|
+
out.push(`fn ${shim(m.name)}(${params}) -> ${wrapper} { ${guarded(wrapper, value)} }`);
|
|
1404
|
+
chain.push(` .function${m.args.length}("${camel(m.name)}", ${shim(m.name)})`);
|
|
1405
|
+
classRefs.add(m.ret);
|
|
1406
|
+
continue;
|
|
1407
|
+
}
|
|
1408
|
+
const optArc = m.optionalRet && arcInner(m.ret);
|
|
895
1409
|
const body = m.throws
|
|
896
|
-
? `match ${call} { Ok(v) => ${wrap(m.ret, 'v')}, Err(e) => embind_rs::
|
|
897
|
-
: (m.ret === '()' ? call : wrap(m.ret, call));
|
|
898
|
-
const retTy = m.optionalRet ?
|
|
899
|
-
if (m.optionalRet) usedOptionals.add(m.ret);
|
|
900
|
-
out.push(`fn ${shim(m.name)}(${params}) -> ${retTy} { ${body} }`);
|
|
1410
|
+
? `match ${call} { Ok(v) => ${wrap(m.ret, 'v')}, Err(e) => embind_rs::raise_err_coded(e.to_string(), { use embind_rs::{CrossbindErrorCode, CrossbindErrorCodeFallback}; (&e).crossbind_code() }) }`
|
|
1411
|
+
: (m.ret === '()' ? call : optArc ? `${arcInner(m.ret)}SharedOpt(${call})` : wrap(m.ret, call));
|
|
1412
|
+
const retTy = m.optionalRet ? optionalWireTy(m.ret) : (m.ret === '()' ? '()' : wireTy(m.ret));
|
|
1413
|
+
if (m.optionalRet && !arcInner(m.ret)) usedOptionals.add(m.ret);
|
|
1414
|
+
out.push(`fn ${shim(m.name)}(${params}) -> ${retTy} { ${guarded(retTy, body)} }`);
|
|
901
1415
|
chain.push(` .function${m.args.length}("${camel(m.name)}", ${shim(m.name)})`);
|
|
902
1416
|
}
|
|
903
1417
|
if (cls.hasDisplay) {
|
|
904
|
-
out.push(`fn ${shim('display_tostring')}(t: &mut ${C}) -> String { format!("{}", (&*t)) }`);
|
|
1418
|
+
out.push(`fn ${shim('display_tostring')}(t: &mut ${C}) -> String { ${guarded('String', 'format!("{}", (&*t))')} }`);
|
|
905
1419
|
chain.push(` .function0("toString", ${shim('display_tostring')})`);
|
|
906
1420
|
}
|
|
907
1421
|
registrations.push(`${chain.join('\n')};`);
|
|
@@ -910,14 +1424,23 @@ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
|
|
|
910
1424
|
for (const f of model.freeFns ?? []) {
|
|
911
1425
|
noteOptionArgs(f.args);
|
|
912
1426
|
const ps = f.args.map((p, i) => `a${i}: ${wireTy(p.ty)}`).join(', ');
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
1427
|
+
const rawCall = `${U}::${f.name}(${f.args.map((p, i) => unwrap(p.ty, `a${i}`)).join(', ')})`;
|
|
1428
|
+
const call = f.collect ? `(${rawCall}).collect::<Vec<_>>()` : rawCall;
|
|
1429
|
+
const retTy = f.optionalRet ? optionalWireTy(f.ret) : (f.ret === '()' ? '()' : wireTy(f.ret));
|
|
1430
|
+
if (f.optionalRet && !arcInner(f.ret)) usedOptionals.add(f.ret);
|
|
1431
|
+
if (f.ownedClass) {
|
|
1432
|
+
const wrapper = `${f.ownedClass}Owned`;
|
|
1433
|
+
out.push(`fn __free_${f.name}(${ps}) -> ${wrapper} { ${guarded(wrapper, `${wrapper}(Box::into_raw(Box::new(${call})))`)} }`);
|
|
1434
|
+
registrations.push(` embind_rs::fn${f.args.length}("${pub(f.jsName)}", __free_${f.name});`);
|
|
1435
|
+
classRefs.add(f.ownedClass);
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
const optArc = f.optionalRet && arcInner(f.ret);
|
|
916
1439
|
const body = f.throws
|
|
917
|
-
? `match ${call} { Ok(v) => ${wrap(f.ret, 'v')}, Err(e) => embind_rs::
|
|
918
|
-
: (f.ret === '()' ? call : wrap(f.ret, call));
|
|
919
|
-
out.push(`fn __free_${f.name}(${ps}) -> ${retTy} { ${body} }`);
|
|
920
|
-
registrations.push(` embind_rs::fn${f.args.length}("${f.jsName}", __free_${f.name});`);
|
|
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));
|
|
1442
|
+
out.push(`fn __free_${f.name}(${ps}) -> ${retTy} { ${guarded(retTy, body)} }`);
|
|
1443
|
+
registrations.push(` embind_rs::fn${f.args.length}("${pub(f.jsName)}", __free_${f.name});`);
|
|
921
1444
|
}
|
|
922
1445
|
|
|
923
1446
|
// Optional inner types register once up front (the adapters dedupe across archives).
|
|
@@ -935,9 +1458,30 @@ function emitBridge(model, { userCrate, vectors, log, prelude = '' }) {
|
|
|
935
1458
|
|
|
936
1459
|
// ---------------- .d.ts emitter ----------------
|
|
937
1460
|
|
|
1461
|
+
// A collection's TypeScript shape: an array for the sequence kinds, a Record for the maps, a
|
|
1462
|
+
// tuple for a tuple. Anything the mapping does not recognise stays `unknown`, which is honest.
|
|
1463
|
+
function tsCollection(ty) {
|
|
1464
|
+
const text = String(ty).trim();
|
|
1465
|
+
const map = text.match(/^(?:std::collections::)?(?:HashMap|BTreeMap)<\s*([^,]+),\s*(.+)>$/);
|
|
1466
|
+
if (map) return `Record<${tsInner(map[1])}, ${tsInner(map[2])}>`;
|
|
1467
|
+
const vec = text.match(/^Vec<(.+)>$/) || text.match(/^&?\s*\[(.+);\s*\d+\]$/) || text.match(/^&\s*\[(.+)\]$/);
|
|
1468
|
+
if (vec) return `${tsInner(vec[1])}[]`;
|
|
1469
|
+
const tuple = text.match(/^\((.+)\)$/);
|
|
1470
|
+
if (tuple) return `[${tuple[1].split(',').map((part) => tsInner(part)).join(', ')}]`;
|
|
1471
|
+
return 'unknown';
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function tsInner(ty) {
|
|
1475
|
+
const text = String(ty).trim();
|
|
1476
|
+
return TS_TYPES[text] ?? (isCollection(text) ? tsCollection(text) : 'unknown');
|
|
1477
|
+
}
|
|
1478
|
+
|
|
938
1479
|
const TS_TYPES = {
|
|
939
1480
|
i32: 'number', i64: 'bigint', u64: 'bigint', f64: 'number', bool: 'boolean',
|
|
940
1481
|
String: 'string', '&str': 'string', '&String': 'string', '()': 'void',
|
|
1482
|
+
i8: 'number', i16: 'number', u8: 'number', u16: 'number', u32: 'number', f32: 'number',
|
|
1483
|
+
// usize/isize carry a pointer-wide range: a number on wasm (32-bit), a BigInt on native.
|
|
1484
|
+
usize: 'number | bigint', isize: 'number | bigint', char: 'string',
|
|
941
1485
|
};
|
|
942
1486
|
|
|
943
1487
|
// Editor-facing types for the package import (`import { X } from '<pkg>'`): the metro/vite
|
|
@@ -953,6 +1497,20 @@ export function emitDts(model, vectors, mode = 'sync') {
|
|
|
953
1497
|
if (arc) return arc[1]; // Arc<X> is transparent in JS: the shared instance itself
|
|
954
1498
|
if (ty === 'JsValue') return 'unknown';
|
|
955
1499
|
if (ty === 'JsFunction') return '(...args: unknown[]) => unknown';
|
|
1500
|
+
const shape = String(ty).startsWith('__closure') ? JSON.parse(String(ty).slice('__closure'.length)) : null;
|
|
1501
|
+
if (shape) {
|
|
1502
|
+
const args = shape.args.map((a, i) => `a${i}: ${TS_TYPES[a] ?? 'unknown'}`).join(', ');
|
|
1503
|
+
return `(${args}) => ${shape.ret === '()' ? 'void' : TS_TYPES[shape.ret] ?? 'unknown'}`;
|
|
1504
|
+
}
|
|
1505
|
+
const typed = typedArrayOf(ty);
|
|
1506
|
+
if (typed) return typed.wrapper === 'JsBytes' ? 'Uint8Array' : 'Float64Array';
|
|
1507
|
+
if (isCollection(ty)) return tsCollection(ty);
|
|
1508
|
+
const optStr = ty.replace(/\s+/g, '') === 'Option<&str>';
|
|
1509
|
+
if (optStr) return 'string | null | undefined';
|
|
1510
|
+
const optRef = ty.replace(/\s+/g, '').match(/^Option<&(\w+)>$/)?.[1];
|
|
1511
|
+
if (optRef) return `${optRef} | null | undefined`;
|
|
1512
|
+
const nt = (model.newtypes ?? []).find((n) => n.name === ty);
|
|
1513
|
+
if (nt) return TS_TYPES[nt.inner] ?? nt.inner;
|
|
956
1514
|
if (ty.startsWith('&')) return TS_TYPES[ty] ?? ty.slice(1); // &OtherClass param
|
|
957
1515
|
return TS_TYPES[ty] ?? ty;
|
|
958
1516
|
};
|
|
@@ -962,26 +1520,31 @@ export function emitDts(model, vectors, mode = 'sync') {
|
|
|
962
1520
|
for (const v of model.valueObjects) {
|
|
963
1521
|
out.push(`export interface ${v.name} { ${v.fields.map((f) => `${f.name}: ${ts(f.type)};`).join(' ')} }`);
|
|
964
1522
|
}
|
|
1523
|
+
for (const c of model.consts ?? []) {
|
|
1524
|
+
out.push(`export declare const ${c.name}: ${TS_TYPES[c.ty] ?? c.ty};`);
|
|
1525
|
+
}
|
|
965
1526
|
for (const e of model.enums) {
|
|
966
1527
|
out.push(`export interface ${e.name} { readonly __crossbindEnum?: '${e.name}'; }`);
|
|
967
1528
|
out.push(`export declare let ${e.name}: { ${e.variants.map((v) => `readonly ${v.name}: ${e.name};`).join(' ')} };`);
|
|
968
1529
|
}
|
|
969
1530
|
for (const cls of model.classes) {
|
|
970
1531
|
out.push(`export declare class ${cls.name} {`);
|
|
1532
|
+
for (const f of cls.fields ?? []) out.push(` ${camel(f.name)}: ${ts(f.type)};`);
|
|
971
1533
|
if (cls.ctor) out.push(` constructor(${cls.ctor.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')});`);
|
|
1534
|
+
else if (!cls.shared && cls.hasDefault) out.push(' constructor();');
|
|
972
1535
|
else out.push(' private constructor();');
|
|
973
1536
|
for (const f of cls.factories) {
|
|
974
1537
|
out.push(` static ${camel(f.name)}(${f.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${cls.name}${f.optional ? ' | null' : ''}`)};`);
|
|
975
1538
|
}
|
|
976
1539
|
for (const m of cls.methods) {
|
|
977
|
-
out.push(` ${camel(m.name)}(${m.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(m.ret)}${m.optionalRet ? ' |
|
|
1540
|
+
out.push(` ${camel(m.name)}(${m.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(m.ret)}${m.optionalRet ? ' | null' : ''}`)};`);
|
|
978
1541
|
}
|
|
979
1542
|
if (cls.hasDisplay) out.push(` toString(): ${wrap('string')};`);
|
|
980
1543
|
out.push(` delete(): ${wrap('void')};`);
|
|
981
1544
|
out.push('}');
|
|
982
1545
|
}
|
|
983
1546
|
for (const f of model.freeFns ?? []) {
|
|
984
|
-
out.push(`export declare function ${f.jsName}(${f.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(f.ret)}${f.optionalRet ? ' |
|
|
1547
|
+
out.push(`export declare function ${f.jsName}(${f.args.map((p) => `${p.name}: ${ts(p.ty)}`).join(', ')}): ${wrap(`${ts(f.ret)}${f.optionalRet ? ' | null' : ''}`)};`);
|
|
985
1548
|
}
|
|
986
1549
|
for (const vec of vectors) {
|
|
987
1550
|
if (!VECTOR_ITEM_TYPES.has(vec.of)) continue;
|