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