crossbind 2.0.0-beta.60 → 2.0.0-beta.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/package.json +1 -1
- package/src/actions/buildDependencies.js +22 -7
- package/src/actions/buildJs.js +1 -1
- package/src/actions/buildLib.js +2 -0
- package/src/actions/buildNode.js +149 -0
- package/src/actions/buildWasm.js +43 -66
- package/src/actions/createInterface.js +101 -27
- package/src/actions/createLib.js +3 -2
- package/src/actions/getAllBridges.js +1 -1
- package/src/actions/getLinkInputs.js +63 -0
- package/src/actions/licenses.js +34 -0
- package/src/actions/run.js +97 -2
- package/src/assets/cmake/CMakeLists.txt +1 -1
- package/src/assets/cmake/dist.cmake +1 -1
- package/src/assets/js-runtime/adapters/worker-comlink.js +6 -2
- package/src/assets/toolchain-digests.json +26 -12
- package/src/bin.js +59 -13
- package/src/index.js +3 -1
- package/src/integration/getCrossbindScript.js +8 -10
- package/src/integration/getDependFilePath.js +9 -12
- package/src/state/index.js +16 -13
- package/src/state/loadConfig.js +3 -0
- package/src/utils/appRustCrates.js +2 -0
- package/src/utils/appSources.js +21 -0
- package/src/utils/cargoImport.js +44 -0
- package/src/utils/cargoTarget.js +2 -0
- package/src/utils/cppDts.js +77 -41
- package/src/utils/embindArgumentGuards.js +45 -0
- package/src/utils/embindRsFingerprint.js +26 -0
- package/src/utils/headerImports.js +45 -0
- package/src/utils/inputStamp.js +23 -0
- package/src/utils/linkLayout.js +8 -1
- package/src/utils/pullDockerImage.js +8 -5
- package/src/utils/resolveCompanionPackage.js +51 -0
- package/src/utils/resolveEmbindNapi.js +22 -0
- package/src/utils/resolveEmbindRust.js +17 -52
- package/src/utils/runCargo.js +4 -3
- package/src/utils/rustBridgeGen.js +895 -250
- package/src/utils/swigInterface.js +19 -4
- package/src/utils/systemKeys.js +8 -0
- package/src/utils/targets.js +56 -0
- package/src/utils/cppFieldBindings.js +0 -23
package/src/bin.js
CHANGED
|
@@ -9,12 +9,15 @@ import createBridgeFile from './actions/createInterface.js';
|
|
|
9
9
|
import createLib from './actions/createLib.js';
|
|
10
10
|
import buildWasm from './actions/buildWasm.js';
|
|
11
11
|
import buildWasiCommand from './actions/buildWasiCommand.js';
|
|
12
|
+
import buildNode, { publishNodeData } from './actions/buildNode.js';
|
|
12
13
|
import buildExternal from './actions/buildExternal.js';
|
|
13
14
|
import buildPackageTypes from './actions/buildTypes.js';
|
|
14
15
|
import buildLib from './actions/buildLib.js';
|
|
15
16
|
import buildDependencies from './actions/buildDependencies.js';
|
|
16
17
|
import runCrossbindApp from './actions/run.js';
|
|
17
|
-
import { getFilteredBuildTargets } from './actions/target.js';
|
|
18
|
+
import { getBuildTargets, getFilteredBuildTargets } from './actions/target.js';
|
|
19
|
+
import { OPT_IN_PLATFORMS } from './utils/targets.js';
|
|
20
|
+
import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from './utils/resolveEmbindNapi.js';
|
|
18
21
|
|
|
19
22
|
import writeJson from './utils/writeJson.js';
|
|
20
23
|
import flattenConfigForTable from './utils/flattenConfigForTable.js';
|
|
@@ -77,7 +80,7 @@ program.command('build')
|
|
|
77
80
|
targetParams.buildType = ['release'];
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
targetParams.platform = targetParams.platform || platforms;
|
|
83
|
+
targetParams.platform = targetParams.platform || platforms.filter((item) => !OPT_IN_PLATFORMS.includes(item));
|
|
81
84
|
targetParams.arch = targetParams.arch || archs;
|
|
82
85
|
targetParams.runtime = targetParams.runtime || runtimes;
|
|
83
86
|
targetParams.buildType = targetParams.buildType || buildTypes;
|
|
@@ -162,7 +165,7 @@ program.command('clean-deps')
|
|
|
162
165
|
});
|
|
163
166
|
|
|
164
167
|
// A container belongs to one image: the web container carries no NDK, and android runs a forced platform.
|
|
165
|
-
const IMAGE_ROLES = ['web', 'android'];
|
|
168
|
+
const IMAGE_ROLES = ['web', 'android', 'linux', 'windows'];
|
|
166
169
|
const dockerContainerName = (role) => {
|
|
167
170
|
if (!IMAGE_ROLES.includes(role)) {
|
|
168
171
|
console.error(`crossbind: unknown image "${role}" - expected one of ${IMAGE_ROLES.join(', ')}.`);
|
|
@@ -322,7 +325,8 @@ function run(programName, params) {
|
|
|
322
325
|
async function build(targetParams, rebuildOption) {
|
|
323
326
|
await buildDependencies({ targetParams, rebuildOption });
|
|
324
327
|
buildLib(targetParams);
|
|
325
|
-
createWasmJs(targetParams);
|
|
328
|
+
await createWasmJs(targetParams);
|
|
329
|
+
await createNodeAddons(targetParams);
|
|
326
330
|
await createWasiCommands(targetParams);
|
|
327
331
|
buildPackageTypes();
|
|
328
332
|
}
|
|
@@ -348,14 +352,7 @@ async function createWasiCommands(targetParams) {
|
|
|
348
352
|
}
|
|
349
353
|
}
|
|
350
354
|
|
|
351
|
-
|
|
352
|
-
if (state.config.export.bundle === false) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const targets = getFilteredBuildTargets(targetParams, { platform: 'wasm' });
|
|
356
|
-
if (targets.length === 0) {
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
355
|
+
function createBridges() {
|
|
359
356
|
let headers = [];
|
|
360
357
|
state.config.paths.header.forEach((headerPath) => {
|
|
361
358
|
headers.push(findFiles('**/*.h', { cwd: headerPath }));
|
|
@@ -367,12 +364,61 @@ async function createWasmJs(targetParams) {
|
|
|
367
364
|
const bridgePath = createBridgeFile(header);
|
|
368
365
|
bridges.push(bridgePath);
|
|
369
366
|
});
|
|
367
|
+
return bridges;
|
|
368
|
+
}
|
|
370
369
|
|
|
370
|
+
async function createNodeAddons(targetParams) {
|
|
371
|
+
if (state.config.export.bundle === false) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const targets = getBuildTargets(targetParams)
|
|
375
|
+
.filter((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm');
|
|
376
|
+
if (targets.length === 0) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// The bridges compile against the embind runtime, which speaks JSI.
|
|
380
|
+
const headerDirs = [
|
|
381
|
+
`${resolveEmbindJsiRoot()}/cpp/src`,
|
|
382
|
+
`${resolveEmbindNapiRoot()}/third_party/node-api-jsi/jsi`,
|
|
383
|
+
];
|
|
384
|
+
const opt = {
|
|
385
|
+
buildSource: false,
|
|
386
|
+
nativeGlob: [
|
|
387
|
+
`${state.config.paths.cli}/assets/cpp-runtime/commonBridges.cpp`,
|
|
388
|
+
...createBridges(),
|
|
389
|
+
],
|
|
390
|
+
headerDirs,
|
|
391
|
+
inputs: headerDirs.flatMap((dir) => findFiles('**/*.h', { cwd: dir })),
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
for (const target of targets) {
|
|
395
|
+
const distComplete = fs.existsSync(`${state.config.paths.output}/${target.addonName}`)
|
|
396
|
+
&& fs.existsSync(`${state.config.paths.output}/${target.jsName}`);
|
|
397
|
+
createLib(target, 'Bridge', opt);
|
|
398
|
+
const built = await buildNode(target, { force: !distComplete });
|
|
399
|
+
publishNodeData(target, { refresh: built });
|
|
400
|
+
if (!built) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
fs.mkdirSync(state.config.paths.output, { recursive: true });
|
|
404
|
+
fs.copyFileSync(`${state.config.paths.build}/${target.addonName}`, `${state.config.paths.output}/${target.addonName}`);
|
|
405
|
+
fs.copyFileSync(`${state.config.paths.build}/${target.jsName}`, `${state.config.paths.output}/${target.jsName}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function createWasmJs(targetParams) {
|
|
410
|
+
if (state.config.export.bundle === false) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const targets = getFilteredBuildTargets(targetParams, { platform: 'wasm' });
|
|
414
|
+
if (targets.length === 0) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
371
417
|
const opt = {
|
|
372
418
|
buildSource: false,
|
|
373
419
|
nativeGlob: [
|
|
374
420
|
`${state.config.paths.cli}/assets/cpp-runtime/commonBridges.cpp`,
|
|
375
|
-
...
|
|
421
|
+
...createBridges(),
|
|
376
422
|
],
|
|
377
423
|
};
|
|
378
424
|
|
package/src/index.js
CHANGED
|
@@ -15,5 +15,7 @@ export { default as getDependFilePath } from './integration/getDependFilePath.js
|
|
|
15
15
|
export { default as buildDependencies, getDependenciesStamp } from './actions/buildDependencies.js';
|
|
16
16
|
export { default as findFiles } from './utils/findFiles.js';
|
|
17
17
|
export { getContentHash, getFileHash } from './utils/hash.js';
|
|
18
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
collectInputFiles, collectRustBridgeFiles, collectRustSources, computeInputStamp,
|
|
20
|
+
} from './utils/inputStamp.js';
|
|
19
21
|
export { default as buildAppRustCrates } from './utils/appRustCrates.js';
|
|
@@ -4,6 +4,7 @@ import getData from '../actions/getData.js';
|
|
|
4
4
|
import loadJson from '../utils/loadJson.js';
|
|
5
5
|
import state from '../state/index.js';
|
|
6
6
|
import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
|
|
7
|
+
import { parseCargoMarkerName } from '../utils/cargoImport.js';
|
|
7
8
|
|
|
8
9
|
export default function getCrossbindScript(target, bridgePath) {
|
|
9
10
|
if (!bridgePath) {
|
|
@@ -25,25 +26,21 @@ export function getRustJsScript(target, rsFile) {
|
|
|
25
26
|
|
|
26
27
|
// Marker under <cache>/rust-crates/: a direct crate import - the model comes from the
|
|
27
28
|
// upstream crate's own source, and the bridge is synthesized against the cargo dependency.
|
|
28
|
-
const crateName = path.basename(rsReal, '.rs');
|
|
29
|
+
const { crateName, modulePath } = parseCargoMarkerName(path.basename(rsReal, '.rs'));
|
|
29
30
|
const cargoDeps = state.config.cargoDependencies ?? {};
|
|
30
31
|
if (Object.hasOwn(cargoDeps, crateName)
|
|
31
32
|
&& path.dirname(rsReal) === fs.realpathSync(path.join(state.config.paths.cache, 'rust-crates'))) {
|
|
32
|
-
const {
|
|
33
|
+
const { exports } = createCrateImportBridge({
|
|
33
34
|
crateName,
|
|
35
|
+
modulePath,
|
|
34
36
|
spec: cargoDeps[crateName],
|
|
35
37
|
cacheDir: state.config.paths.cache,
|
|
36
38
|
dtsMode: state.config.dts,
|
|
37
39
|
log: () => {},
|
|
38
40
|
});
|
|
39
|
-
// A crate
|
|
40
|
-
// name), so the proxy exports the clean name and reads the
|
|
41
|
-
return buildScript(target,
|
|
42
|
-
...model.classes.map((c) => c.name),
|
|
43
|
-
...model.enums.map((e) => e.name),
|
|
44
|
-
...model.freeFns.map((f) => f.jsName),
|
|
45
|
-
...(model.consts ?? []).map((c) => c.name),
|
|
46
|
-
].map((name) => ({ local: name, wire: `${namePrefix}${name}` })));
|
|
41
|
+
// A crate registers its public names under per-crate names (two crates may export the
|
|
42
|
+
// same name), so the proxy exports the clean name and reads the registered one off the module.
|
|
43
|
+
return buildScript(target, exports);
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
const pkg = state.config.allDependencies.find((d) => d.export?.type === 'cargo'
|
|
@@ -65,6 +62,7 @@ export function getRustJsScript(target, rsFile) {
|
|
|
65
62
|
}
|
|
66
63
|
const symbols = [
|
|
67
64
|
...model.classes.map((c) => c.name),
|
|
65
|
+
...(model.streams ?? []).map((s) => s.name),
|
|
68
66
|
...model.enums.map((e) => e.name),
|
|
69
67
|
...(model.freeFns ?? []).map((f) => f.jsName),
|
|
70
68
|
// `pub const`/`pub static` register as module constants, so the proxy exports them too.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import state from '../state/index.js';
|
|
4
|
+
import { parseCargoImport, writeCargoMarker } from '../utils/cargoImport.js';
|
|
4
5
|
|
|
5
6
|
const LAYOUT = { header: 'include', module: 'swig', source: '' };
|
|
6
7
|
|
|
@@ -76,21 +77,17 @@ function getFolderCandidates(pkg, alias) {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
export default function getDependFilePath(source, target) {
|
|
79
|
-
// `cargo:<crate
|
|
80
|
-
// generated marker .rs; the transformer turns it into the crate-import bridge
|
|
81
|
-
// file, no package). The scheme is unambiguous, so npm names never collide and
|
|
82
|
-
// undeclared crate is a hard, actionable error instead of a silent npm fallthrough.
|
|
80
|
+
// `cargo:<crate>[/<module>...]` imports (the node:/npm: convention for a non-npm store)
|
|
81
|
+
// resolve to a generated marker .rs; the transformer turns it into the crate-import bridge
|
|
82
|
+
// (no surface file, no package). The scheme is unambiguous, so npm names never collide and
|
|
83
|
+
// an undeclared crate is a hard, actionable error instead of a silent npm fallthrough.
|
|
83
84
|
if (source.startsWith('cargo:')) {
|
|
84
|
-
const
|
|
85
|
+
const cargoImport = parseCargoImport(source);
|
|
85
86
|
const cargoDeps = state.config.cargoDependencies ?? {};
|
|
86
|
-
if (!Object.hasOwn(cargoDeps, crateName)) {
|
|
87
|
-
throw new Error(`crossbind: '${source}' is not declared - add '${crateName}' to cargoDependencies in crossbind.config.`);
|
|
88
|
-
}
|
|
89
|
-
const marker = path.join(state.config.paths.cache, 'rust-crates', `${crateName}.rs`);
|
|
90
|
-
if (!fs.existsSync(marker)) {
|
|
91
|
-
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
92
|
-
fs.writeFileSync(marker, `// crossbind cargo crate import marker: ${crateName}\n`);
|
|
87
|
+
if (!Object.hasOwn(cargoDeps, cargoImport.crateName)) {
|
|
88
|
+
throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to cargoDependencies in crossbind.config.`);
|
|
93
89
|
}
|
|
90
|
+
const marker = writeCargoMarker(state.config.paths.cache, cargoImport);
|
|
94
91
|
existsCache.set(marker, true);
|
|
95
92
|
return marker;
|
|
96
93
|
}
|
package/src/state/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
1
|
import loadJson from '../utils/loadJson.js';
|
|
4
2
|
import writeJson from '../utils/writeJson.js';
|
|
5
|
-
import { TARGETS, targetPathOf } from '../utils/targets.js';
|
|
3
|
+
import { TARGETS, targetPathOf, nodeAddonNamesOf } from '../utils/targets.js';
|
|
4
|
+
import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
|
|
6
5
|
import loadConfig from './loadConfig.js';
|
|
7
6
|
|
|
8
7
|
const cacheDir = `${process.cwd()}/.crossbind`;
|
|
@@ -34,6 +33,8 @@ async function initProcessState() {
|
|
|
34
33
|
DOCKER_REGISTRY_MIRROR: 'CROSSBIND_REGISTRY_MIRROR',
|
|
35
34
|
DOCKER_IMAGE_WEB: 'CROSSBIND_IMAGE_WEB',
|
|
36
35
|
DOCKER_IMAGE_ANDROID: 'CROSSBIND_IMAGE_ANDROID',
|
|
36
|
+
DOCKER_IMAGE_LINUX: 'CROSSBIND_IMAGE_LINUX',
|
|
37
|
+
DOCKER_IMAGE_WINDOWS: 'CROSSBIND_IMAGE_WINDOWS',
|
|
37
38
|
}).forEach(([key, envKey]) => {
|
|
38
39
|
if (state.config.system[key] && !process.env[envKey]) {
|
|
39
40
|
process.env[envKey] = state.config.system[key];
|
|
@@ -54,21 +55,23 @@ async function initProcessState() {
|
|
|
54
55
|
// A wasi build is a single command module - no JS glue, no preload.
|
|
55
56
|
target.wasmName = `${state.config.general.name}-${target.path}.wasm`;
|
|
56
57
|
}
|
|
58
|
+
if (target.runtimeEnv === 'node' && target.platform !== 'wasm') {
|
|
59
|
+
Object.assign(target, nodeAddonNamesOf(target, state.config.general.name));
|
|
60
|
+
}
|
|
57
61
|
});
|
|
58
62
|
|
|
59
63
|
setAllDependecyPaths();
|
|
60
64
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
65
|
+
// Cargo-crate import markers must exist BEFORE any bundler starts: metro resolves against
|
|
66
|
+
// its startup file map, so a marker first created mid-resolution is invisible to that very
|
|
67
|
+
// build. Bare crates come from the config, module imports from the app's own sources.
|
|
64
68
|
const cargoDeps = state.config.cargoDependencies ?? {};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
69
|
+
const crateNames = Object.keys(cargoDeps);
|
|
70
|
+
const moduleImports = crateNames.length && state.config.paths.project
|
|
71
|
+
? findCargoModuleImportsIn(state.config.paths.project).filter((i) => Object.hasOwn(cargoDeps, i.crateName))
|
|
72
|
+
: [];
|
|
73
|
+
[...crateNames.map((crateName) => ({ crateName, modulePath: [] })), ...moduleImports]
|
|
74
|
+
.forEach((cargoImport) => writeCargoMarker(state.config.paths.cache, cargoImport));
|
|
72
75
|
|
|
73
76
|
if (state.config.build?.setState) {
|
|
74
77
|
state.config.build.setState(state);
|
package/src/state/loadConfig.js
CHANGED
|
@@ -126,6 +126,9 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
126
126
|
if (typeof newConfig.types !== 'boolean') {
|
|
127
127
|
throw new Error(`crossbind: "types" must be true or false (got '${newConfig.types}').`);
|
|
128
128
|
}
|
|
129
|
+
if (newConfig.targetSpecs.some((t) => Array.isArray(t?.specs?.cmake))) {
|
|
130
|
+
throw new Error('crossbind: "targetSpecs[].specs.cmake" must be an object such as { compileOptions: [\'-O3\'] } (got an array). CMake configure flags (-D...) belong in a recipe\'s getBuildParams.');
|
|
131
|
+
}
|
|
129
132
|
|
|
130
133
|
if (newConfig.paths.config && !newConfig.paths.project) {
|
|
131
134
|
newConfig.paths.project = getParentPath(newConfig.paths.config);
|
|
@@ -36,6 +36,8 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
|
|
|
36
36
|
// perfectly live bridge.
|
|
37
37
|
stale = !fs.existsSync(path.resolve(path.dirname(libRs), pathMod[1]));
|
|
38
38
|
} else if (cargoDependencies && name.startsWith('crate_')) {
|
|
39
|
+
// One bridge per crate; a `crate_<crate>__<module>` left by an older build would
|
|
40
|
+
// register the crate's types a second time.
|
|
39
41
|
stale = !Object.keys(cargoDependencies)
|
|
40
42
|
.some((c) => `crate_${c.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}` === name);
|
|
41
43
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte']);
|
|
5
|
+
// Native projects and build outputs: large, and never the app's JavaScript.
|
|
6
|
+
const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'ios', 'android', 'Pods', 'target']);
|
|
7
|
+
|
|
8
|
+
// The app's own JavaScript and TypeScript sources, whose import statements say what the app uses.
|
|
9
|
+
export default function appSourceFiles(projectDir) {
|
|
10
|
+
const files = [];
|
|
11
|
+
const walk = (dir) => {
|
|
12
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
13
|
+
if (entry.name.startsWith('.') || SKIPPED_DIRECTORIES.has(entry.name)) continue;
|
|
14
|
+
const full = path.join(dir, entry.name);
|
|
15
|
+
if (entry.isDirectory()) walk(full);
|
|
16
|
+
else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) files.push(full);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
walk(projectDir);
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import appSourceFiles from './appSources.js';
|
|
4
|
+
|
|
5
|
+
const PREFIX = 'cargo:';
|
|
6
|
+
const RUST_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7
|
+
const MODULE_IMPORT = /['"`]cargo:([A-Za-z0-9_-]+)((?:\/[A-Za-z_][A-Za-z0-9_]*)+)['"`]/g;
|
|
8
|
+
|
|
9
|
+
export function parseCargoImport(source) {
|
|
10
|
+
const [crateName, ...modulePath] = source.slice(PREFIX.length).split('/');
|
|
11
|
+
const bad = modulePath.find((segment) => !RUST_IDENT.test(segment));
|
|
12
|
+
if (bad !== undefined) {
|
|
13
|
+
throw new Error(`crossbind: '${source}' - '${bad}' is not a Rust module name; write the path as cargo:<crate>/<module>/<submodule>.`);
|
|
14
|
+
}
|
|
15
|
+
return { crateName, modulePath };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The marker's file name carries the module path after dots, which crate and module names cannot contain.
|
|
19
|
+
export function cargoMarkerName({ crateName, modulePath }) {
|
|
20
|
+
return [crateName, ...modulePath].join('.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function parseCargoMarkerName(name) {
|
|
24
|
+
const [crateName, ...modulePath] = name.split('.');
|
|
25
|
+
return { crateName, modulePath };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function writeCargoMarker(cacheDir, cargoImport) {
|
|
29
|
+
const marker = path.join(cacheDir, 'rust-crates', `${cargoMarkerName(cargoImport)}.rs`);
|
|
30
|
+
if (!fs.existsSync(marker)) {
|
|
31
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
32
|
+
fs.writeFileSync(marker, `// crossbind cargo crate import marker: ${[cargoImport.crateName, ...cargoImport.modulePath].join('/')}\n`);
|
|
33
|
+
}
|
|
34
|
+
return marker;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A specifier built at run time is not found.
|
|
38
|
+
export function findCargoModuleImports(text) {
|
|
39
|
+
return [...text.matchAll(MODULE_IMPORT)].map((m) => ({ crateName: m[1], modulePath: m[2].slice(1).split('/') }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function findCargoModuleImportsIn(projectDir) {
|
|
43
|
+
return appSourceFiles(projectDir).flatMap((file) => findCargoModuleImports(fs.readFileSync(file, 'utf8')));
|
|
44
|
+
}
|
package/src/utils/cargoTarget.js
CHANGED
|
@@ -9,6 +9,8 @@ export function cargoTripleFor(target) {
|
|
|
9
9
|
return target.arch === 'iphonesimulator' ? 'aarch64-apple-ios-sim' : 'aarch64-apple-ios';
|
|
10
10
|
case 'android':
|
|
11
11
|
return target.arch === 'x86_64' ? 'x86_64-linux-android' : 'aarch64-linux-android';
|
|
12
|
+
case 'darwin':
|
|
13
|
+
return target.arch === 'x64' ? 'x86_64-apple-darwin' : 'aarch64-apple-darwin';
|
|
12
14
|
default:
|
|
13
15
|
return null; // wasi (no rust wasip3 target yet) and anything else: unsupported
|
|
14
16
|
}
|
package/src/utils/cppDts.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { stripComments } from './swigInterface.js';
|
|
3
4
|
import writeIfChanged from './writeIfChanged.js';
|
|
4
5
|
|
|
5
6
|
// Best-effort .d.ts for `.h` imports, the C++ analog of the Rust emitDts: parse the
|
|
@@ -41,16 +42,7 @@ function tsType(raw, classNames, { isReturn = false } = {}) {
|
|
|
41
42
|
function parseArgs(rawArgs, classNames) {
|
|
42
43
|
const trimmed = rawArgs.trim();
|
|
43
44
|
if (trimmed === '' || trimmed === 'void') return [];
|
|
44
|
-
|
|
45
|
-
let depth = 0;
|
|
46
|
-
let current = '';
|
|
47
|
-
for (const ch of trimmed) {
|
|
48
|
-
if (ch === '<' || ch === '(') depth += 1;
|
|
49
|
-
if (ch === '>' || ch === ')') depth -= 1;
|
|
50
|
-
if (ch === ',' && depth === 0) { parts.push(current); current = ''; } else current += ch;
|
|
51
|
-
}
|
|
52
|
-
parts.push(current);
|
|
53
|
-
return parts.map((part, i) => {
|
|
45
|
+
return splitTopLevel(trimmed).map((part, i) => {
|
|
54
46
|
const noDefault = part.split('=')[0].trim();
|
|
55
47
|
// Declaration-style unnamed parameter: the whole token is a type.
|
|
56
48
|
const unnamed = tsType(noDefault, classNames);
|
|
@@ -81,36 +73,86 @@ function bodyStatements(body) {
|
|
|
81
73
|
return statements.map((s) => s.trim()).filter(Boolean);
|
|
82
74
|
}
|
|
83
75
|
|
|
76
|
+
// Splits at commas outside <>, (), [] and {}.
|
|
77
|
+
function splitTopLevel(text) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
let depth = 0;
|
|
80
|
+
let current = '';
|
|
81
|
+
for (const ch of text) {
|
|
82
|
+
if ('<([{'.includes(ch)) depth += 1;
|
|
83
|
+
if ('>)]}'.includes(ch)) depth -= 1;
|
|
84
|
+
if (ch === ',' && depth === 0) { parts.push(current); current = ''; } else current += ch;
|
|
85
|
+
}
|
|
86
|
+
return [...parts, current];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Value-semantics fields only: these are the types embind's .property can expose
|
|
90
|
+
// without ownership questions (and the worker clone can carry).
|
|
91
|
+
const FIELD_TYPES = new Set(['number', 'boolean', 'string']);
|
|
92
|
+
|
|
93
|
+
// `int x, *p, y = 2;` declares the int fields x and y and the pointer p. The types cover the value fields: pointers,
|
|
94
|
+
// arrays, bit-fields and references are left out.
|
|
95
|
+
const FIRST_DECLARATOR = /^([\s\S]*?)([*&\s]*)\b([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*|:\s*\w+)$/;
|
|
96
|
+
const NEXT_DECLARATOR = /^([*&\s]*)([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*|:\s*\w+)$/;
|
|
97
|
+
|
|
98
|
+
function valueField(typeText, classNames) {
|
|
99
|
+
const type = tsType(typeText.replace(/\b(?:const|volatile)\b/g, '').replace(/\s+/g, ' ').trim(), classNames);
|
|
100
|
+
return FIELD_TYPES.has(type) ? { type } : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function declaredFields(statement, classNames) {
|
|
104
|
+
const [first, ...rest] = splitTopLevel(statement).map((part) => part.split('=')[0].trim());
|
|
105
|
+
const head = first.match(FIRST_DECLARATOR);
|
|
106
|
+
if (!head || !head[1].trim()) return [];
|
|
107
|
+
const value = valueField(head[1], classNames);
|
|
108
|
+
const declarators = [head.slice(2), ...rest.map((part) => part.match(NEXT_DECLARATOR)?.slice(1) ?? null)];
|
|
109
|
+
return declarators.flatMap((declarator) => {
|
|
110
|
+
if (!value || !declarator || declarator[2] || /[*&]/.test(declarator[0])) return [];
|
|
111
|
+
return [{ name: declarator[1], ...value }];
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// `struct Name {`, `class Name : Base {` and `typedef struct [Tag] { ... } Alias, *Pointer;`, unions too.
|
|
116
|
+
// A class takes its tag, or its first typedef name when it has none, and keeps its other typedef names:
|
|
117
|
+
// SWIG registers a typedef'd struct under the typedef name.
|
|
118
|
+
const CLASS_HEAD = /\b(typedef\s+)?(class|struct|union)\b\s*([A-Za-z_]\w*)?\s*(?::[^{;]*)?\{/g;
|
|
119
|
+
|
|
120
|
+
function closingBrace(text, from) {
|
|
121
|
+
let depth = 1;
|
|
122
|
+
let i = from;
|
|
123
|
+
while (i < text.length && depth > 0) {
|
|
124
|
+
if (text[i] === '{') depth += 1;
|
|
125
|
+
if (text[i] === '}') depth -= 1;
|
|
126
|
+
i += 1;
|
|
127
|
+
}
|
|
128
|
+
return i;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function typedefNamesAfter(text, from) {
|
|
132
|
+
const tail = text.slice(from).match(/^([^;{}]*);/);
|
|
133
|
+
return tail ? tail[1].split(',').map((part) => part.trim()).filter((part) => /^[A-Za-z_]\w*$/.test(part)) : [];
|
|
134
|
+
}
|
|
135
|
+
|
|
84
136
|
export function parseCppSurface(source, log = console.log) {
|
|
85
|
-
const clean = source
|
|
86
|
-
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
87
|
-
.replace(/\/\/[^\n]*/g, ' ')
|
|
88
|
-
.replace(/^[ \t]*#[^\n]*$/gm, ' ');
|
|
137
|
+
const clean = stripComments(source).replace(/^[ \t]*#[^\n]*$/gm, ' ');
|
|
89
138
|
|
|
90
139
|
const classes = [];
|
|
91
140
|
const classNames = new Set();
|
|
92
|
-
const classRe = /\b(class|struct)\s+([A-Za-z_]\w*)\s*(?::[^{]*)?\{/g;
|
|
93
141
|
const found = [];
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
classNames.add(m[2]);
|
|
105
|
-
m = classRe.exec(clean);
|
|
142
|
+
for (const m of clean.matchAll(CLASS_HEAD)) {
|
|
143
|
+
const [head, typedefKeyword, kind, tag] = m;
|
|
144
|
+
const bodyStart = m.index + head.length;
|
|
145
|
+
const end = closingBrace(clean, bodyStart);
|
|
146
|
+
const names = typedefKeyword ? typedefNamesAfter(clean, end) : [];
|
|
147
|
+
const name = tag ?? names[0];
|
|
148
|
+
if (!name) continue;
|
|
149
|
+
found.push({ kind, name, aliases: names.filter((alias) => alias !== name), body: clean.slice(bodyStart, end - 1) });
|
|
150
|
+
classNames.add(name);
|
|
151
|
+
names.forEach((alias) => classNames.add(alias));
|
|
106
152
|
}
|
|
107
153
|
|
|
108
|
-
// Value-semantics fields only: these are the types embind's .property can expose
|
|
109
|
-
// without ownership questions (and the worker clone can carry).
|
|
110
|
-
const FIELD_TYPES = new Set(['number', 'boolean', 'string']);
|
|
111
|
-
|
|
112
154
|
for (const cls of found) {
|
|
113
|
-
let access = cls.kind === '
|
|
155
|
+
let access = cls.kind === 'class' ? 'private' : 'public';
|
|
114
156
|
let ctor = null;
|
|
115
157
|
const methods = [];
|
|
116
158
|
const fields = [];
|
|
@@ -122,14 +164,8 @@ export function parseCppSurface(source, log = console.log) {
|
|
|
122
164
|
|
|
123
165
|
const sig = statement.match(/^(static\s+)?(?:explicit\s+)?([\w:<>,\s*&]*?)\s*\b([A-Za-z_]\w*)\s*\(([\s\S]*)\)$/);
|
|
124
166
|
if (!sig) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
? decl.match(/^(.*?)\b([A-Za-z_]\w*)$/s)
|
|
128
|
-
: null;
|
|
129
|
-
if (field) {
|
|
130
|
-
const type = tsType(field[1], classNames);
|
|
131
|
-
// A reference member has no address a pointer to member could name, and a pointer member is memory.
|
|
132
|
-
if (type !== null && FIELD_TYPES.has(type) && !/[&*]/.test(field[1])) fields.push({ name: field[2], type });
|
|
167
|
+
if (!statement.includes('(') && !/^(static|using|typedef|friend|enum)\b/.test(statement)) {
|
|
168
|
+
fields.push(...declaredFields(statement, classNames));
|
|
133
169
|
}
|
|
134
170
|
continue;
|
|
135
171
|
}
|
|
@@ -142,7 +178,7 @@ export function parseCppSurface(source, log = console.log) {
|
|
|
142
178
|
if (ret === null) { log(`crossbind: dts: skipped ${cls.name}::${name} (unsupported return type '${retRaw.trim()}')`); continue; }
|
|
143
179
|
methods.push({ name, isStatic: Boolean(staticKw), args, ret });
|
|
144
180
|
}
|
|
145
|
-
classes.push({ name: cls.name, ctor, methods, fields });
|
|
181
|
+
classes.push({ name: cls.name, aliases: cls.aliases, ctor, methods, fields });
|
|
146
182
|
}
|
|
147
183
|
return { classes };
|
|
148
184
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// A release build of embind passes whatever JavaScript hands an integer or enum parameter straight to
|
|
2
|
+
// the wire, where anything that is not a number becomes 0: an enum member given to an `int` parameter,
|
|
3
|
+
// a Number given to an enum parameter, a one-letter string given to a `char`. The call then runs with 0
|
|
4
|
+
// and reports no error. These rewrites convert what has one meaning and throw a TypeError for the rest,
|
|
5
|
+
// as embind's assertion builds do.
|
|
6
|
+
|
|
7
|
+
const GUARDS = [
|
|
8
|
+
{
|
|
9
|
+
registration: '_embind_register_integer',
|
|
10
|
+
from: 'toWireType:(destructors,value)=>value,readValueFromPointer:integerReadValueFromPointer(',
|
|
11
|
+
to: 'toWireType:(destructors,value)=>{'
|
|
12
|
+
+ 'if(typeof value=="number"||typeof value=="boolean"||typeof value=="bigint")return value;'
|
|
13
|
+
+ 'if(typeof value=="string"&&size==1&&value.length==1)return value.charCodeAt(0);'
|
|
14
|
+
+ 'if(value!==null&&typeof value=="object"&&typeof value.value=="number")return value.value;'
|
|
15
|
+
+ 'throw new TypeError(name+" takes a number, got "+(typeof value=="string"?JSON.stringify(value):value===null?"null":typeof value))'
|
|
16
|
+
+ '},readValueFromPointer:integerReadValueFromPointer(',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
registration: '_embind_register_enum',
|
|
20
|
+
from: 'toWireType:(destructors,c)=>c.value,',
|
|
21
|
+
to: 'toWireType:(destructors,c)=>{'
|
|
22
|
+
+ 'if(typeof c=="number")return c;'
|
|
23
|
+
+ 'if(c!==null&&typeof c=="object"&&typeof c.value=="number")return c.value;'
|
|
24
|
+
+ 'throw new TypeError(name+" takes a member of the enum or its number, got "+(typeof c=="string"?JSON.stringify(c):c===null?"null":typeof c))'
|
|
25
|
+
+ '},',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// The release glue spells the bigint conversion `typeof value=="number"`, the debug glue `typeof value == 'number'`.
|
|
30
|
+
const BIGINT_FROM_NUMBER = /if\s*\(\s*typeof value\s*==\s*["']number["']\s*\)\s*\{\s*value\s*=\s*BigInt\(value\)\s*;?\s*\}/g;
|
|
31
|
+
const SAFE_BIGINT_FROM_NUMBER = 'if(typeof value=="number"){if(!Number.isSafeInteger(value))throw new TypeError("a 64-bit integer parameter takes a BigInt or a safe integer Number, got "+value);value=BigInt(value)}';
|
|
32
|
+
|
|
33
|
+
// Returns the rewritten glue and whether a bigint registration kept a conversion this rewrite does not recognise.
|
|
34
|
+
export function guardBigIntArguments(glue) {
|
|
35
|
+
const text = glue.replace(BIGINT_FROM_NUMBER, SAFE_BIGINT_FROM_NUMBER);
|
|
36
|
+
return { text, missed: text.includes('_embind_register_bigint') && !text.includes('Number.isSafeInteger(value)') };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Returns the rewritten glue and the registrations whose code no longer looks the way these rewrites expect.
|
|
40
|
+
export function guardEmbindArguments(glue) {
|
|
41
|
+
const missed = GUARDS.filter(({ registration, from, to }) => glue.includes(`${registration}=`) && !glue.includes(from) && !glue.includes(to))
|
|
42
|
+
.map(({ registration }) => registration);
|
|
43
|
+
const text = GUARDS.reduce((current, { from, to }) => current.split(from).join(to), glue);
|
|
44
|
+
return { text, missed };
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getContentHash, getFileHash } from './hash.js';
|
|
4
|
+
import { collectRustSources } from './inputStamp.js';
|
|
5
|
+
import resolveEmbindRustRoot from './resolveEmbindRust.js';
|
|
6
|
+
|
|
7
|
+
// Every cargo staticlib bundles the embind-rs it was built from. An archive from another one links stale glue, and next
|
|
8
|
+
// to an archive built after it every embind_rs symbol is defined twice, so a prebuilt records the one it came from.
|
|
9
|
+
export const EMBIND_RS_FINGERPRINT_FILE = 'crossbind-embind-rs.fingerprint';
|
|
10
|
+
|
|
11
|
+
// Paths count relative to the crate, so an archive built from the same embind-rs elsewhere matches.
|
|
12
|
+
export function getEmbindRsFingerprint() {
|
|
13
|
+
const crate = `${resolveEmbindRustRoot()}/crate`;
|
|
14
|
+
const files = [...collectRustSources([crate]), `${crate}/Cargo.toml`];
|
|
15
|
+
return getContentHash(files.map((file) => `${path.relative(crate, file)}:${getFileHash(file)}`).sort().join('\n'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isEmbindRsFingerprintStale(prebuiltDir, fingerprint) {
|
|
19
|
+
const file = `${prebuiltDir}/${EMBIND_RS_FINGERPRINT_FILE}`;
|
|
20
|
+
return !fs.existsSync(file) || fs.readFileSync(file, { encoding: 'utf8' }) !== fingerprint;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function writeEmbindRsFingerprint(prebuiltDir, fingerprint) {
|
|
24
|
+
fs.mkdirSync(prebuiltDir, { recursive: true });
|
|
25
|
+
fs.writeFileSync(`${prebuiltDir}/${EMBIND_RS_FINGERPRINT_FILE}`, fingerprint);
|
|
26
|
+
}
|