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