crossbind 2.0.0-beta.56 → 2.0.0-beta.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +4 -4
- package/src/actions/buildWasm.js +21 -0
- package/src/actions/createInterface.js +156 -55
- package/src/actions/createLib.js +5 -3
- package/src/actions/getAllBridges.js +1 -1
- package/src/actions/run.js +12 -3
- package/src/assets/cmake/CMakeLists.txt +1 -1
- package/src/assets/js-runtime/adapters/worker-comlink.js +21 -2
- package/src/assets/toolchain-digests.json +12 -12
- package/src/integration/getCrossbindScript.js +14 -6
- package/src/utils/cppDts.js +7 -4
- package/src/utils/dependencyBridges.js +18 -0
- package/src/utils/downloadAndExtractFile.js +13 -1
- package/src/utils/pullDockerImage.js +10 -1
- package/src/utils/rustBridgeGen.js +668 -105
- package/src/utils/swigInterface.js +412 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<a href="#start-with-a-coding-agent">Coding agents</a>
|
|
22
22
|
· <a href="https://crossbind.dev/guide/">Guide</a>
|
|
23
23
|
· <a href="https://crossbind.dev/guide/quick-start/">Quick start</a>
|
|
24
|
-
· <a href="https://crossbind.dev/guide/
|
|
24
|
+
· <a href="https://crossbind.dev/guide/libraries/">Libraries</a>
|
|
25
25
|
· <a href="https://github.com/crossbind/crossbind/tree/main/examples">Examples</a>
|
|
26
26
|
· <a href="https://github.com/crossbind/crossbind/blob/main/CONTRIBUTING.md">Contributing</a>
|
|
27
27
|
</p>
|
|
@@ -275,7 +275,7 @@ export default {
|
|
|
275
275
|
|
|
276
276
|
The variant declares its transitive native dependency graph, so link order is
|
|
277
277
|
derived automatically. Install and register every platform variant your
|
|
278
|
-
project actually builds. See the [
|
|
278
|
+
project actually builds. See the [library guide](https://crossbind.dev/guide/libraries/)
|
|
279
279
|
and [`ports/README.md`](https://github.com/crossbind/crossbind/blob/main/ports/README.md)
|
|
280
280
|
for the package and license contracts.
|
|
281
281
|
|
|
@@ -345,4 +345,4 @@ pull-request process.
|
|
|
345
345
|
crossbind is available under the
|
|
346
346
|
[MIT License](https://github.com/crossbind/crossbind/blob/main/LICENSE).
|
|
347
347
|
|
|
348
|
-
Copyright © 2026 Buğra Sarı
|
|
348
|
+
Copyright © 2026 Buğra Sarı and crossbind contributors
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crossbind",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.60",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://crossbind.dev",
|
|
6
6
|
"repository": "https://github.com/crossbind/crossbind.git",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"test:coverage": "vitest run --coverage"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@vitest/coverage-v8": "^
|
|
23
|
-
"vitest": "^
|
|
22
|
+
"@vitest/coverage-v8": "^5.0.1",
|
|
23
|
+
"vitest": "^5.0.1"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"log-update": "^8.0.0",
|
|
34
34
|
"picocolors": "^1.1.1",
|
|
35
35
|
"replace": "^1.2.2",
|
|
36
|
-
"rollup": "^4.
|
|
36
|
+
"rollup": "^4.63.4",
|
|
37
37
|
"spdx-expression-parse": "^5.0.0",
|
|
38
38
|
"upath": "^3.0.8"
|
|
39
39
|
},
|
package/src/actions/buildWasm.js
CHANGED
|
@@ -13,6 +13,24 @@ import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
|
|
|
13
13
|
import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
14
14
|
import buildAppRustCrates from '../utils/appRustCrates.js';
|
|
15
15
|
|
|
16
|
+
// embind's bigint converter turns any Number into a BigInt, so 2^53+1 silently becomes 2^53.
|
|
17
|
+
// A 64-bit parameter takes a BigInt or a Number that is a safe integer instead; the jsi adapter
|
|
18
|
+
// enforces the same rule natively. Each runtimeEnv links its own glue, so each one calls this.
|
|
19
|
+
function guardBigIntArguments(target) {
|
|
20
|
+
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
21
|
+
replace({
|
|
22
|
+
regex: 'if\\s*\\(\\s*typeof value\\s*==\\s*"number"\\s*\\)\\s*\\{\\s*value\\s*=\\s*BigInt\\(value\\)\\s*;?\\s*\\}',
|
|
23
|
+
replacement: '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)}',
|
|
24
|
+
paths: [gluePath],
|
|
25
|
+
recursive: false,
|
|
26
|
+
silent: true,
|
|
27
|
+
});
|
|
28
|
+
const glue = fs.readFileSync(gluePath, 'utf8');
|
|
29
|
+
if (glue.includes('__embind_register_bigint') && !glue.includes('Number.isSafeInteger(value)')) {
|
|
30
|
+
logger.error('bigint safe-integer rewrite missed (emscripten glue format changed?): 64-bit Number arguments round silently');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
export default async function buildWasm(target, options = {}) {
|
|
17
35
|
const isProd = target.buildType === 'release';
|
|
18
36
|
const buildType = isProd ? 'Release' : 'Debug';
|
|
@@ -226,6 +244,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
226
244
|
recursive: false,
|
|
227
245
|
silent: true,
|
|
228
246
|
}); */
|
|
247
|
+
guardBigIntArguments(target);
|
|
229
248
|
await buildJs(target);
|
|
230
249
|
// fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
|
|
231
250
|
// fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
|
|
@@ -263,6 +282,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
263
282
|
const t1 = performance.now();
|
|
264
283
|
logger.doneStep(target, 'wasm');
|
|
265
284
|
logger.startStep(target, 'js');
|
|
285
|
+
guardBigIntArguments(target);
|
|
266
286
|
await buildJs(target);
|
|
267
287
|
logger.doneStep(target, 'js');
|
|
268
288
|
}
|
|
@@ -294,6 +314,7 @@ export default async function buildWasm(target, options = {}) {
|
|
|
294
314
|
], null, target);
|
|
295
315
|
logger.doneStep(target, 'wasm');
|
|
296
316
|
logger.startStep(target, 'js');
|
|
317
|
+
guardBigIntArguments(target);
|
|
297
318
|
await buildJs(target);
|
|
298
319
|
if (emccFlags.includes('FETCH')) {
|
|
299
320
|
fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
|
|
@@ -6,10 +6,19 @@ import { getFileHash } from '../utils/hash.js';
|
|
|
6
6
|
import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
|
|
7
7
|
import { writeHeaderDts, parseCppSurface } from '../utils/cppDts.js';
|
|
8
8
|
import { injectFieldBindings } from '../utils/cppFieldBindings.js';
|
|
9
|
+
import {
|
|
10
|
+
buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, indexTypeDefinitions, interfaceIncludes,
|
|
11
|
+
interfaceToRetryWithoutMacros, parseMacroDump, referencedTypeHeaders, selectSwigMacros,
|
|
12
|
+
} from '../utils/swigInterface.js';
|
|
9
13
|
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
10
|
-
import run from './run.js';
|
|
14
|
+
import run, { cxxPreprocessorFor } from './run.js';
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
// Part of every interface hash, so interfaces cached by an older generator are rebuilt.
|
|
17
|
+
const INTERFACE_FORMAT = 'swig-macros-1';
|
|
18
|
+
const predefinedMacros = new Map();
|
|
19
|
+
const typeDefinitions = new Map();
|
|
20
|
+
|
|
21
|
+
export default function createBridgeFile(headerOrModuleFilePath, target = state.targets.find((t) => t.platform === 'wasm'), { withDependencies = true } = {}) {
|
|
13
22
|
const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
|
|
14
23
|
if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
|
|
15
24
|
fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
|
|
@@ -17,10 +26,11 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
|
|
|
17
26
|
if (!fs.existsSync(`${state.config.paths.build}/bridge`)) {
|
|
18
27
|
fs.mkdirSync(`${state.config.paths.build}/bridge`, { recursive: true });
|
|
19
28
|
}
|
|
20
|
-
const interfaceFile = createInterfaceFile(interfaceFilePath, target);
|
|
21
29
|
// The header may live outside paths.native (e.g. a package-subpath import like
|
|
22
30
|
// '@scope/pkg/native/x.h'), so its own directory must reach swig's include list.
|
|
23
|
-
const
|
|
31
|
+
const sourceDir = upath.dirname(interfaceFilePath);
|
|
32
|
+
const interfaceFile = createInterfaceFile(interfaceFilePath, target, sourceDir);
|
|
33
|
+
const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, sourceDir, getFileHash(interfaceFilePath));
|
|
24
34
|
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
25
35
|
if (bridgeFile && !moduleRegex.test(interfaceFilePath)) {
|
|
26
36
|
// SWIG's -embind backend emits no member variables; inject .property lines for the
|
|
@@ -41,21 +51,43 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
|
|
|
41
51
|
dtsMode: state.config.dts,
|
|
42
52
|
});
|
|
43
53
|
}
|
|
54
|
+
if (bridgeFile && withDependencies && !moduleRegex.test(interfaceFilePath)) {
|
|
55
|
+
// A header's own bindings need the types its declarations use registered, which only their headers' bridges do.
|
|
56
|
+
const dependencyBridges = dependencyHeadersOf(interfaceFilePath, target).map((header) => {
|
|
57
|
+
try {
|
|
58
|
+
return createBridgeFile(header, target, { withDependencies: false });
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.warn(`crossbind: ${upath.basename(interfaceFilePath)} uses types from ${upath.basename(header)}, whose bindings failed (${e.message})`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}).filter(Boolean);
|
|
64
|
+
writeIfChanged(`${bridgeFile}.deps`, dependencyBridges.map((dependency) => `${dependency}\n`).join(''));
|
|
65
|
+
}
|
|
44
66
|
return bridgeFile;
|
|
45
67
|
}
|
|
46
68
|
|
|
47
|
-
function createInterfaceFile(headerOrModuleFilePath, target) {
|
|
69
|
+
function createInterfaceFile(headerOrModuleFilePath, target, sourceDir) {
|
|
48
70
|
if (!headerOrModuleFilePath) {
|
|
49
71
|
return null;
|
|
50
72
|
}
|
|
51
|
-
const
|
|
73
|
+
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
74
|
+
const isModule = moduleRegex.test(headerOrModuleFilePath);
|
|
75
|
+
const { includeRoot, headerPath } = isModule ? {} : includeLocation(headerOrModuleFilePath, target);
|
|
76
|
+
const packages = [state.config, ...state.config.allDependencies];
|
|
77
|
+
const prelude = isModule ? [] : findHeaderPrelude(headerOrModuleFilePath, packages, headerPath);
|
|
78
|
+
const ignored = isModule ? [] : findIgnoredDeclarations(headerOrModuleFilePath, packages, headerPath);
|
|
79
|
+
const completing = includeRoot ? findCompletingIncludes(headerOrModuleFilePath, includeRoot, headerPath) : [];
|
|
80
|
+
// A prelude or ignored-declaration change in the owning package, or a completed class moving to another header, must
|
|
81
|
+
// regenerate the interface as well.
|
|
82
|
+
const fileHash = [
|
|
83
|
+
INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...prelude,
|
|
84
|
+
...(ignored.length ? [`ignored:${ignored.join(',')}`] : []), ...(completing.length ? [`completing:${completing.join(',')}`] : []),
|
|
85
|
+
].join('\n');
|
|
52
86
|
const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
|
|
53
87
|
if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)) {
|
|
54
88
|
return cachedInterface;
|
|
55
89
|
}
|
|
56
90
|
|
|
57
|
-
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
58
|
-
const isModule = moduleRegex.test(headerOrModuleFilePath);
|
|
59
91
|
if (isModule) {
|
|
60
92
|
const newPath = `${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`;
|
|
61
93
|
fs.copyFileSync(headerOrModuleFilePath, newPath);
|
|
@@ -65,13 +97,6 @@ function createInterfaceFile(headerOrModuleFilePath, target) {
|
|
|
65
97
|
return newPath;
|
|
66
98
|
}
|
|
67
99
|
|
|
68
|
-
const headerPaths = (state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends || [])
|
|
69
|
-
.filter((d) => d.startsWith(state.config.paths.base));
|
|
70
|
-
|
|
71
|
-
const temp2 = headerPaths
|
|
72
|
-
.map((p) => headerOrModuleFilePath.match(new RegExp(`^${p}/.*?/include/(.*?)$`, 'i')))
|
|
73
|
-
.filter((p) => p && p.length === 2);
|
|
74
|
-
|
|
75
100
|
const temp = headerOrModuleFilePath.match(/^(.*)\..+?$/);
|
|
76
101
|
if (!temp || temp.length < 2) return null;
|
|
77
102
|
|
|
@@ -89,27 +114,14 @@ function createInterfaceFile(headerOrModuleFilePath, target) {
|
|
|
89
114
|
|
|
90
115
|
const fileName = filePathWithoutExt.split('/').at(-1);
|
|
91
116
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
%module ${fileName.toUpperCase()}
|
|
101
|
-
|
|
102
|
-
%{
|
|
103
|
-
#include "${headerPath}"
|
|
104
|
-
%}
|
|
105
|
-
|
|
106
|
-
%feature("shared_ptr");
|
|
107
|
-
%feature("polymorphic_shared_ptr");
|
|
108
|
-
|
|
109
|
-
%include "${headerPath}"
|
|
110
|
-
|
|
111
|
-
#endif
|
|
112
|
-
`;
|
|
117
|
+
const content = buildInterfaceContent({
|
|
118
|
+
moduleName: fileName.toUpperCase(),
|
|
119
|
+
headerPath,
|
|
120
|
+
prelude,
|
|
121
|
+
completing,
|
|
122
|
+
swigMacros: collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir),
|
|
123
|
+
ignored,
|
|
124
|
+
});
|
|
113
125
|
const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i`;
|
|
114
126
|
fs.writeFileSync(outputFilePath, content);
|
|
115
127
|
|
|
@@ -120,6 +132,83 @@ function createInterfaceFile(headerOrModuleFilePath, target) {
|
|
|
120
132
|
return outputFilePath;
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
// A header is included by its path under a project header directory or a dependency's include directory; one found only
|
|
136
|
+
// through its own directory has no include root to search.
|
|
137
|
+
function includeLocation(headerFile, target) {
|
|
138
|
+
const projectRoot = state.config.paths.header.find((path) => headerFile.startsWith(path));
|
|
139
|
+
if (projectRoot) return { includeRoot: projectRoot, headerPath: headerFile.substr(projectRoot.length + 1) };
|
|
140
|
+
const dependencyRoots = (state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends || [])
|
|
141
|
+
.filter((d) => d.startsWith(state.config.paths.base));
|
|
142
|
+
const match = dependencyRoots.map((p) => headerFile.match(new RegExp(`^(${p}/.*?/include)/(.*?)$`, 'i'))).find(Boolean);
|
|
143
|
+
return match ? { includeRoot: match[1], headerPath: match[2] } : { includeRoot: null, headerPath: headerFile.split('/').at(-1) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The headers under an include root are indexed once per build.
|
|
147
|
+
function definitionsUnder(includeRoot) {
|
|
148
|
+
if (!typeDefinitions.has(includeRoot)) {
|
|
149
|
+
const extensions = new RegExp(`\\.(${state.config.ext.header.join('|')})$`, 'i');
|
|
150
|
+
const files = fs.readdirSync(includeRoot, { recursive: true, withFileTypes: true })
|
|
151
|
+
.filter((entry) => entry.isFile() && extensions.test(entry.name))
|
|
152
|
+
.map((entry) => upath.relative(includeRoot, upath.join(entry.parentPath, entry.name)))
|
|
153
|
+
.sort()
|
|
154
|
+
.map((path) => ({ path, text: fs.readFileSync(upath.join(includeRoot, path), 'utf8') }));
|
|
155
|
+
typeDefinitions.set(includeRoot, indexTypeDefinitions(files));
|
|
156
|
+
}
|
|
157
|
+
return typeDefinitions.get(includeRoot);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function findCompletingIncludes(headerFile, includeRoot, headerPath) {
|
|
161
|
+
const headerText = fs.readFileSync(headerFile, 'utf8');
|
|
162
|
+
if (!headerText.includes('unique_ptr')) return [];
|
|
163
|
+
return completingIncludes({ headerText, headerPath, definitions: definitionsUnder(includeRoot) });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function dependencyHeadersOf(headerFile, target) {
|
|
167
|
+
const { includeRoot, headerPath } = includeLocation(headerFile, target);
|
|
168
|
+
if (!includeRoot) return [];
|
|
169
|
+
const headerText = fs.readFileSync(headerFile, 'utf8');
|
|
170
|
+
return referencedTypeHeaders({ headerText, headerPath, definitions: definitionsUnder(includeRoot) })
|
|
171
|
+
.map((header) => upath.join(includeRoot, header));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A header that does not preprocess on its own, or a host without the image's compiler, keeps the plain interface.
|
|
175
|
+
function collectSwigMacros(headerFile, includes, name, target, sourceDir) {
|
|
176
|
+
const interfaceDir = `${state.config.paths.build}/interface`;
|
|
177
|
+
try {
|
|
178
|
+
if (!predefinedMacros.has(target.path)) {
|
|
179
|
+
predefinedMacros.set(target.path, dumpMacros(`${interfaceDir}/predefined-${target.path}.macros.h`, [], [], target));
|
|
180
|
+
}
|
|
181
|
+
const macros = dumpMacros(`${interfaceDir}/${name}.macros.h`, includes, swigIncludePath(target, sourceDir), target);
|
|
182
|
+
return selectSwigMacros({ headerText: fs.readFileSync(headerFile, 'utf8'), macros, predefined: predefinedMacros.get(target.path) });
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.warn(`crossbind: SWIG reads ${upath.basename(headerFile)} without the macros of its includes (${e.message})`);
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function dumpMacros(outputFile, includes, includePath, target) {
|
|
190
|
+
run(cxxPreprocessorFor(target), [
|
|
191
|
+
'-x', 'c++', '-std=c++17', '-dM', '-E',
|
|
192
|
+
...includePath,
|
|
193
|
+
...includes.flatMap((header) => ['-include', header]),
|
|
194
|
+
'-o', outputFile,
|
|
195
|
+
'/dev/null',
|
|
196
|
+
], null, target);
|
|
197
|
+
return parseMacroDump(fs.readFileSync(outputFile, 'utf8'));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function swigIncludePath(target, sourceDir) {
|
|
201
|
+
const allHeaders = state.config.dependencyParameters.headerPathWithDepends.split(';');
|
|
202
|
+
const includePath = [
|
|
203
|
+
...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/include`),
|
|
204
|
+
...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/swig`),
|
|
205
|
+
...state.config.paths.header,
|
|
206
|
+
...allHeaders,
|
|
207
|
+
...(sourceDir ? [sourceDir] : []),
|
|
208
|
+
].filter((path) => !!path.toString()).map((path) => `-I${path}`);
|
|
209
|
+
return [...new Set(includePath)];
|
|
210
|
+
}
|
|
211
|
+
|
|
123
212
|
// Idempotent: wraps every emscripten::async() registration in the generated
|
|
124
213
|
// bridge behind #ifdef CROSSBIND_JSPI (see utils/bridgeAsyncGuard.js). Applied on
|
|
125
214
|
// the cached path too, so bridges generated by older versions get guarded the
|
|
@@ -132,40 +221,52 @@ function applyAsyncGuard(bridgeFilePath) {
|
|
|
132
221
|
}
|
|
133
222
|
}
|
|
134
223
|
|
|
135
|
-
|
|
224
|
+
// The interface text only names the header, so its hash alone kept a bridge across header edits: the
|
|
225
|
+
// header's own hash is part of the key.
|
|
226
|
+
function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null, sourceHash = '') {
|
|
136
227
|
if (!interfaceFilePath) {
|
|
137
228
|
return null;
|
|
138
229
|
}
|
|
139
230
|
|
|
140
|
-
const
|
|
231
|
+
const bridgeHash = () => `${getFileHash(interfaceFilePath)}\n${sourceHash}`;
|
|
232
|
+
const fileHash = bridgeHash();
|
|
141
233
|
const cachedBridge = state.cache.bridges[interfaceFilePath];
|
|
142
234
|
if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge)) {
|
|
143
235
|
applyAsyncGuard(cachedBridge);
|
|
144
236
|
return cachedBridge;
|
|
145
237
|
}
|
|
146
238
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/include`),
|
|
151
|
-
...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/swig`),
|
|
152
|
-
...state.config.paths.header,
|
|
153
|
-
...allHeaders,
|
|
154
|
-
...(sourceDir ? [sourceDir] : []),
|
|
155
|
-
].filter((path) => !!path.toString()).map((path) => `-I${path}`);
|
|
156
|
-
includePath = [...new Set(includePath)];
|
|
157
|
-
|
|
158
|
-
run('swig', [
|
|
239
|
+
const bridgeFilePath = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
|
|
240
|
+
// #error lines guard branches SWIG evaluates without the compiler's predefined macros, so they only warn.
|
|
241
|
+
const swig = () => run('swig', [
|
|
159
242
|
'-c++',
|
|
160
243
|
'-embind',
|
|
161
|
-
'-
|
|
162
|
-
|
|
244
|
+
'-cpperraswarn',
|
|
245
|
+
'-o', bridgeFilePath,
|
|
246
|
+
...swigIncludePath(target, sourceDir),
|
|
163
247
|
interfaceFilePath,
|
|
164
248
|
], null, target);
|
|
165
|
-
|
|
249
|
+
try {
|
|
250
|
+
swig();
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// Known macros can expose declarations SWIG cannot parse and used to skip; without them it parses as before.
|
|
253
|
+
const content = fs.readFileSync(interfaceFilePath, 'utf8');
|
|
254
|
+
const plain = interfaceToRetryWithoutMacros(content, e);
|
|
255
|
+
if (!plain) throw e;
|
|
256
|
+
console.warn(`crossbind: SWIG cannot parse ${upath.basename(interfaceFilePath)} with the macros of its includes; generating it without them`);
|
|
257
|
+
fs.writeFileSync(interfaceFilePath, plain);
|
|
258
|
+
try {
|
|
259
|
+
swig();
|
|
260
|
+
} catch (retryError) {
|
|
261
|
+
// The cached interface keeps its macros, so the next build tries them again.
|
|
262
|
+
fs.writeFileSync(interfaceFilePath, content);
|
|
263
|
+
throw retryError;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
applyAsyncGuard(bridgeFilePath);
|
|
166
267
|
|
|
167
|
-
state.cache.bridges[interfaceFilePath] =
|
|
168
|
-
state.cache.hashes[interfaceFilePath] =
|
|
268
|
+
state.cache.bridges[interfaceFilePath] = bridgeFilePath;
|
|
269
|
+
state.cache.hashes[interfaceFilePath] = bridgeHash();
|
|
169
270
|
saveCache();
|
|
170
271
|
|
|
171
272
|
return state.cache.bridges[interfaceFilePath];
|
package/src/actions/createLib.js
CHANGED
|
@@ -11,6 +11,7 @@ import state from '../state/index.js';
|
|
|
11
11
|
import logger from '../utils/logger.js';
|
|
12
12
|
import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
|
|
13
13
|
import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
|
|
14
|
+
import { withDependencyBridges } from '../utils/dependencyBridges.js';
|
|
14
15
|
|
|
15
16
|
const cpuCount = Math.max(1, os.cpus().length - 1);
|
|
16
17
|
const sharedPlatforms = ['android'];
|
|
@@ -31,9 +32,10 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
31
32
|
// wasi configure tool links need the runtime stubs; compiled below, referenced from LIBS.
|
|
32
33
|
const wasiStubObj = `${buildPath}/crossbind-wasi-stubs.o`;
|
|
33
34
|
|
|
35
|
+
const nativeGlob = options.nativeGlob ? withDependencyBridges(options.nativeGlob) : undefined;
|
|
34
36
|
// The lib dir is reused while the bridge list grows, so an early smaller nativeGlob build must not satisfy later sets: fingerprint the glob and miss on mismatch.
|
|
35
37
|
const fingerprintFile = `${libdir}/crossbind-nativeglob.fingerprint`;
|
|
36
|
-
const fingerprint =
|
|
38
|
+
const fingerprint = nativeGlob ? getFilesFingerprint(nativeGlob) : null;
|
|
37
39
|
const fingerprintChanged = fingerprint !== null
|
|
38
40
|
&& (!fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint);
|
|
39
41
|
|
|
@@ -70,7 +72,7 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
70
72
|
// Bridges guard _JSPI registrations behind CROSSBIND_JSPI (bridgeAsyncGuard); define it only when this target links with -sJSPI.
|
|
71
73
|
const isJspiTarget = target.platform === 'wasm' && configEmccFlags.includes('-sJSPI');
|
|
72
74
|
if (fileType === 'Bridge' && target.platform === 'wasm' && !isJspiTarget
|
|
73
|
-
&&
|
|
75
|
+
&& nativeGlob?.some((f) => fs.existsSync(f) && fs.readFileSync(f, { encoding: 'utf8' }).includes('emscripten::async()'))) {
|
|
74
76
|
logger.info(`[${target.path}] _JSPI bindings skipped: this target links without -sJSPI (add it to binary.emccFlags to enable them)`);
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -138,7 +140,7 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
138
140
|
buildEnv.params.push('-e', e);
|
|
139
141
|
});
|
|
140
142
|
} else {
|
|
141
|
-
buildParams = getCmakeParameters(target, options);
|
|
143
|
+
buildParams = getCmakeParameters(target, { ...options, nativeGlob });
|
|
142
144
|
|
|
143
145
|
triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
|
|
144
146
|
|
|
@@ -13,7 +13,7 @@ export default function getAllBridges(log = console.log) {
|
|
|
13
13
|
if (!fs.existsSync(sidecar)) return true;
|
|
14
14
|
const source = fs.readFileSync(sidecar, 'utf8').trim();
|
|
15
15
|
if (!source || fs.existsSync(source)) return true;
|
|
16
|
-
[bridge, `${bridge}.exports.json`, sidecar].forEach((f) => fs.rmSync(f, { force: true }));
|
|
16
|
+
[bridge, `${bridge}.exports.json`, `${bridge}.deps`, sidecar].forEach((f) => fs.rmSync(f, { force: true }));
|
|
17
17
|
log(`crossbind: pruned stale bridge '${path.basename(bridge)}' - its header is gone`);
|
|
18
18
|
return false;
|
|
19
19
|
});
|
package/src/actions/run.js
CHANGED
|
@@ -11,7 +11,7 @@ import { wasiCFlags, wasiCxxFlags, resolveWasiSdkPath, WASI_TARGET_TRIPLE } from
|
|
|
11
11
|
const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
|
|
12
12
|
const CROSSCOMPILER_ARM64 = 'aarch64-linux-android33';
|
|
13
13
|
const CROSSCOMPILER_x86_64 = 'x86_64-linux-android33';
|
|
14
|
-
const ANDROID_NDK = '/opt/android-sdk/ndk/
|
|
14
|
+
const ANDROID_NDK = '/opt/android-sdk/ndk/current';
|
|
15
15
|
const t = `${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin`;
|
|
16
16
|
const t2 = `${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64`;
|
|
17
17
|
|
|
@@ -62,6 +62,8 @@ const androidParamsX86_64 = [
|
|
|
62
62
|
`CFLAGS=--sysroot=${t2}/sysroot`,
|
|
63
63
|
];
|
|
64
64
|
|
|
65
|
+
// Xcode 27 refuses deployment targets below 15.0; React Native's own floor is 15.1.
|
|
66
|
+
const IOS_DEPLOYMENT_TARGET = '15.1';
|
|
65
67
|
const IOS_HOST_FLAGS = `-arch arm64 -isysroot ${iosSdkPath} -fembed-bitcode`;
|
|
66
68
|
const IOS_SIM_HOST_FLAGS = `-arch arm64 -isysroot ${iosSimSdkPath} -fembed-bitcode`;
|
|
67
69
|
const IOS_IPHONE_PARAMS = ['-e', `CFLAGS="${IOS_HOST_FLAGS}"`, '-e', `CXXFLAGS="${IOS_HOST_FLAGS}"`, '-e', `LDFLAGS="${IOS_HOST_FLAGS}"`];
|
|
@@ -102,6 +104,12 @@ const iosParams = [
|
|
|
102
104
|
'-e', `LDFLAGS="${IOS_HOST_FLAGS}"`,
|
|
103
105
|
]; */
|
|
104
106
|
|
|
107
|
+
// The C++ compiler of the image that runs SWIG for the target, so its -dM macro table matches that parse.
|
|
108
|
+
export function cxxPreprocessorFor(target) {
|
|
109
|
+
if (imageRoleFor(target) !== 'android') return 'em++';
|
|
110
|
+
return `${t}/${target.arch === 'x86_64' ? CROSSCOMPILER_x86_64 : CROSSCOMPILER_ARM64}-clang++`;
|
|
111
|
+
}
|
|
112
|
+
|
|
105
113
|
export default function run(program, params = [], platformPrefix = null, target = null, dockerOptions = {}) {
|
|
106
114
|
const buildPath = platformPrefix ? `${state.config.paths.build}/${platformPrefix}/${target.path}` : state.config.paths.build;
|
|
107
115
|
if (!fs.existsSync(buildPath)) {
|
|
@@ -188,7 +196,7 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
188
196
|
'Xcode',
|
|
189
197
|
'-DBUILD_SHARED_LIBS=OFF',
|
|
190
198
|
'-DFRAMEWORK=TRUE',
|
|
191
|
-
|
|
199
|
+
`-DCMAKE_OSX_DEPLOYMENT_TARGET=${IOS_DEPLOYMENT_TARGET}`,
|
|
192
200
|
'-DCMAKE_SYSTEM_NAME=iOS',
|
|
193
201
|
`-DMACOSX_FRAMEWORK_IDENTIFIER=dev.crossbind.${state.config.general.name}`,
|
|
194
202
|
`-DCMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER=dev.crossbind.${state.config.general.name}`,
|
|
@@ -209,11 +217,12 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
209
217
|
...dParams,
|
|
210
218
|
`-DCMAKE_TOOLCHAIN_FILE='${state.config.paths.cli}/assets/cmake/ios.toolchain.cmake'`,
|
|
211
219
|
`-DPLATFORM=${target.arch === 'iphoneos' ? 'OS64' : 'SIMULATORARM64'}`,
|
|
220
|
+
`-DDEPLOYMENT_TARGET=${IOS_DEPLOYMENT_TARGET}`,
|
|
212
221
|
'-DARCHS=arm64',
|
|
213
222
|
'-DENABLE_BITCODE=TRUE',
|
|
214
223
|
'-DBUILD_SHARED_LIBS=OFF',
|
|
215
224
|
'-DFRAMEWORK=TRUE',
|
|
216
|
-
|
|
225
|
+
`-DCMAKE_OSX_DEPLOYMENT_TARGET=${IOS_DEPLOYMENT_TARGET}`,
|
|
217
226
|
'-DCMAKE_SYSTEM_NAME=iOS',
|
|
218
227
|
`-DMACOSX_FRAMEWORK_IDENTIFIER=dev.crossbind.${state.config.general.name}`,
|
|
219
228
|
`-DCMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER=dev.crossbind.${state.config.general.name}`,
|
|
@@ -68,8 +68,27 @@ Comlink.transferHandlers.set('proxy', {
|
|
|
68
68
|
},
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
// 3. throw (
|
|
72
|
-
|
|
71
|
+
// 3. throw (extended): comlink's own handler ships only message/name/stack, which drops the
|
|
72
|
+
// properties a binding attaches to an error - `code` above all (Rust's error code, the napi
|
|
73
|
+
// contract). Own enumerable primitives ride along; deserialize assigns them back onto the Error.
|
|
74
|
+
Comlink.transferHandlers.set('throw', {
|
|
75
|
+
canHandle: _throwHandler.canHandle,
|
|
76
|
+
serialize(payload) {
|
|
77
|
+
const [serialized, transferables] = _throwHandler.serialize(payload);
|
|
78
|
+
const error = payload && payload.value;
|
|
79
|
+
if (serialized && serialized.isError && error instanceof Error) {
|
|
80
|
+
for (const key of Object.keys(error)) {
|
|
81
|
+
const value = error[key];
|
|
82
|
+
const kind = typeof value;
|
|
83
|
+
if (value === null || kind === 'string' || kind === 'number' || kind === 'boolean') {
|
|
84
|
+
serialized.value[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [serialized, transferables];
|
|
89
|
+
},
|
|
90
|
+
deserialize: _throwHandler.deserialize,
|
|
91
|
+
});
|
|
73
92
|
|
|
74
93
|
// 4. embindVector: convert embind vectors to arrays across worker boundary
|
|
75
94
|
Comlink.transferHandlers.set('embindVector', {
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
2
|
+
"version": "1.0.6",
|
|
3
3
|
"registry": "ghcr.io/crossbind",
|
|
4
4
|
"toolchains": {
|
|
5
5
|
"rust": "1.98.1"
|
|
6
6
|
},
|
|
7
7
|
"images": {
|
|
8
8
|
"rust-sysroot": {
|
|
9
|
-
"index": "sha256:
|
|
9
|
+
"index": "sha256:b82285c6c74f0f2f21bd4f3d07af8a005b16b9e8faaecbe672f6e9b139616eb7",
|
|
10
10
|
"platforms": {
|
|
11
|
-
"linux/arm64": "sha256:
|
|
12
|
-
"linux/amd64": "sha256:
|
|
11
|
+
"linux/arm64": "sha256:50f2bc41d084157bb2d63caa8a91486ea15d3aff248e0d84013a20c7b8cf0c57",
|
|
12
|
+
"linux/amd64": "sha256:65c63d0b3df31406c514a6ed9dbb5ba0940b27c94e2e89b3ceafedaee339bca8"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"base": {
|
|
16
|
-
"index": "sha256:
|
|
16
|
+
"index": "sha256:3a742a779a68e0180ca43232b9e28047e469462c575f1cdef8a3af3ba984263b",
|
|
17
17
|
"platforms": {
|
|
18
|
-
"linux/
|
|
19
|
-
"linux/
|
|
18
|
+
"linux/amd64": "sha256:ff2a42fe5cdff75a9ce89fde340dbde5655f639de2e1288baeb61f2e82d1d095",
|
|
19
|
+
"linux/arm64": "sha256:2571179e6597369544a887da197e280ad9d15075719c04113cc2fd243358f56d"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"web": {
|
|
23
|
-
"index": "sha256:
|
|
23
|
+
"index": "sha256:cd9065909b8e4aea584fbe6f6e0215773feaed97ab46e5ce265a487171f87757",
|
|
24
24
|
"platforms": {
|
|
25
|
-
"linux/arm64": "sha256:
|
|
26
|
-
"linux/amd64": "sha256:
|
|
25
|
+
"linux/arm64": "sha256:224665ea653e6ff6e483fc24b226fa68bf45173a2598439665d827780cbe5534",
|
|
26
|
+
"linux/amd64": "sha256:862b197c13dbdf163d480d35e8b17f96b23b447701cb8e79f0252773d7a48a42"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"android": {
|
|
30
|
-
"index": "sha256:
|
|
30
|
+
"index": "sha256:0d0b56a76cff5b16f52eba3a4d2b17bc888fb2ca502bfd0ae029bf837c6c354d",
|
|
31
31
|
"platforms": {
|
|
32
|
-
"linux/amd64": "sha256:
|
|
32
|
+
"linux/amd64": "sha256:77fb9007369c5c8003024aade231642afb798664f82aada1a8ac26b931a15fbf"
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -29,18 +29,21 @@ export function getRustJsScript(target, rsFile) {
|
|
|
29
29
|
const cargoDeps = state.config.cargoDependencies ?? {};
|
|
30
30
|
if (Object.hasOwn(cargoDeps, crateName)
|
|
31
31
|
&& path.dirname(rsReal) === fs.realpathSync(path.join(state.config.paths.cache, 'rust-crates'))) {
|
|
32
|
-
const { model } = createCrateImportBridge({
|
|
32
|
+
const { model, namePrefix } = createCrateImportBridge({
|
|
33
33
|
crateName,
|
|
34
34
|
spec: cargoDeps[crateName],
|
|
35
35
|
cacheDir: state.config.paths.cache,
|
|
36
36
|
dtsMode: state.config.dts,
|
|
37
37
|
log: () => {},
|
|
38
38
|
});
|
|
39
|
+
// A crate import registers its public names prefixed (two crates may export the same
|
|
40
|
+
// name), so the proxy exports the clean name and reads the prefixed one off the module.
|
|
39
41
|
return buildScript(target, [
|
|
40
42
|
...model.classes.map((c) => c.name),
|
|
41
43
|
...model.enums.map((e) => e.name),
|
|
42
44
|
...model.freeFns.map((f) => f.jsName),
|
|
43
|
-
|
|
45
|
+
...(model.consts ?? []).map((c) => c.name),
|
|
46
|
+
].map((name) => ({ local: name, wire: `${namePrefix}${name}` })));
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
const pkg = state.config.allDependencies.find((d) => d.export?.type === 'cargo'
|
|
@@ -64,6 +67,8 @@ export function getRustJsScript(target, rsFile) {
|
|
|
64
67
|
...model.classes.map((c) => c.name),
|
|
65
68
|
...model.enums.map((e) => e.name),
|
|
66
69
|
...(model.freeFns ?? []).map((f) => f.jsName),
|
|
70
|
+
// `pub const`/`pub static` register as module constants, so the proxy exports them too.
|
|
71
|
+
...(model.consts ?? []).map((c) => c.name),
|
|
67
72
|
...vectors.map((v) => v.name),
|
|
68
73
|
];
|
|
69
74
|
return buildScript(target, symbols);
|
|
@@ -79,8 +84,11 @@ function buildScript(target, symbols) {
|
|
|
79
84
|
let symbolExportDefineString = '';
|
|
80
85
|
let symbolExportAssignString = '';
|
|
81
86
|
if (symbols && Array.isArray(symbols)) {
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
// A symbol is either a plain name or { local, wire }: the second form exports `local`
|
|
88
|
+
// while reading the differently registered `wire` name off the module.
|
|
89
|
+
const pairs = symbols.map((s) => (typeof s === 'string' ? { local: s, wire: s } : s));
|
|
90
|
+
symbolExportDefineString = pairs.map((s) => `export let ${s.local} = null;`).join('\n');
|
|
91
|
+
symbolExportAssignString = pairs.map((s) => `${s.local} = m.${s.wire};`).join('\n');
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
return `
|
|
@@ -132,10 +140,10 @@ function buildScript(target, symbols) {
|
|
|
132
140
|
|
|
133
141
|
function getReactNativeScript(env) {
|
|
134
142
|
return `
|
|
135
|
-
import {
|
|
143
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
136
144
|
import Module from '@crossbind/core-embind-jsi';
|
|
137
145
|
|
|
138
|
-
const
|
|
146
|
+
const RNJsiLib = TurboModuleRegistry.get('RNJsiLib');
|
|
139
147
|
|
|
140
148
|
function setEnv() {
|
|
141
149
|
const env = JSON.parse('${env}');
|