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 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/packages/">Libraries</a>
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 [package guide](https://crossbind.dev/guide/packages/)
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.56",
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": "^4.1.10",
23
- "vitest": "^4.1.10"
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.62.4",
36
+ "rollup": "^4.63.4",
37
37
  "spdx-expression-parse": "^5.0.0",
38
38
  "upath": "^3.0.8"
39
39
  },
@@ -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
- export default function createBridgeFile(headerOrModuleFilePath, target = state.targets.find((t) => t.platform === 'wasm')) {
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 bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, upath.dirname(interfaceFilePath));
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 fileHash = getFileHash(headerOrModuleFilePath);
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
- let headerPath = state.config.paths.header.find((path) => headerOrModuleFilePath.startsWith(path));
93
- if (headerPath) headerPath = headerOrModuleFilePath.substr(headerPath.length + 1);
94
- else if (temp2 && temp2.length > 0) headerPath = temp2[0][1];
95
- else headerPath = headerOrModuleFilePath.split('/').at(-1);
96
-
97
- const content = `#ifndef _${fileName.toUpperCase()}_I
98
- #define _${fileName.toUpperCase()}_I
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
- function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null) {
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 fileHash = getFileHash(interfaceFilePath);
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 allHeaders = state.config.dependencyParameters.headerPathWithDepends.split(';');
148
-
149
- let includePath = [
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
- '-o', `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`,
162
- ...includePath,
244
+ '-cpperraswarn',
245
+ '-o', bridgeFilePath,
246
+ ...swigIncludePath(target, sourceDir),
163
247
  interfaceFilePath,
164
248
  ], null, target);
165
- applyAsyncGuard(`${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`);
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] = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
168
- state.cache.hashes[interfaceFilePath] = fileHash;
268
+ state.cache.bridges[interfaceFilePath] = bridgeFilePath;
269
+ state.cache.hashes[interfaceFilePath] = bridgeHash();
169
270
  saveCache();
170
271
 
171
272
  return state.cache.bridges[interfaceFilePath];
@@ -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 = options.nativeGlob ? getFilesFingerprint(options.nativeGlob) : null;
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
- && options.nativeGlob?.some((f) => fs.existsSync(f) && fs.readFileSync(f, { encoding: 'utf8' }).includes('emscripten::async()'))) {
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
  });
@@ -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/27.3.13750724';
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
- '-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0',
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
- '-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0',
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}`,
@@ -1,5 +1,5 @@
1
1
  cmake_minimum_required(VERSION 3.28)
2
- set(CMAKE_CXX_STANDARD 17)
2
+ set(CMAKE_CXX_STANDARD 20)
3
3
  project("${PROJECT_NAME}")
4
4
 
5
5
  option(BUILD_BRIDGE "Build Bridge" OFF)
@@ -68,8 +68,27 @@ Comlink.transferHandlers.set('proxy', {
68
68
  },
69
69
  });
70
70
 
71
- // 3. throw (restored)
72
- Comlink.transferHandlers.set('throw', _throwHandler);
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.3",
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:6a5db2ecf54398d837fdbbd8c1f2cdaafd14fd486ac3af4206d21bc9fe66e923",
9
+ "index": "sha256:b82285c6c74f0f2f21bd4f3d07af8a005b16b9e8faaecbe672f6e9b139616eb7",
10
10
  "platforms": {
11
- "linux/arm64": "sha256:b5dbd8b4f233f5d6c0cfe7554b95b412e0e3396d4d4847facf116c155251b0b7",
12
- "linux/amd64": "sha256:1989d0ab32d2372a2086efe50e33dbb861fa903e451b891f30004e0f172a1a9a"
11
+ "linux/arm64": "sha256:50f2bc41d084157bb2d63caa8a91486ea15d3aff248e0d84013a20c7b8cf0c57",
12
+ "linux/amd64": "sha256:65c63d0b3df31406c514a6ed9dbb5ba0940b27c94e2e89b3ceafedaee339bca8"
13
13
  }
14
14
  },
15
15
  "base": {
16
- "index": "sha256:c9b501dca53bc2e51dddd1af64ede75220792e66a47ed488ad6172b58bee1b37",
16
+ "index": "sha256:3a742a779a68e0180ca43232b9e28047e469462c575f1cdef8a3af3ba984263b",
17
17
  "platforms": {
18
- "linux/arm64": "sha256:edf01f0c4d7fc551e18775e76b2a6c2b9b54075b59f21f7635ccecaebcc6c282",
19
- "linux/amd64": "sha256:debea6ee164eeba49155ad59365313a9dd291491b3339491a780b46d1b5b96f4"
18
+ "linux/amd64": "sha256:ff2a42fe5cdff75a9ce89fde340dbde5655f639de2e1288baeb61f2e82d1d095",
19
+ "linux/arm64": "sha256:2571179e6597369544a887da197e280ad9d15075719c04113cc2fd243358f56d"
20
20
  }
21
21
  },
22
22
  "web": {
23
- "index": "sha256:633da840658f12f291c17c3305adf265c7d52307b3b61b84156d5f85f3121779",
23
+ "index": "sha256:cd9065909b8e4aea584fbe6f6e0215773feaed97ab46e5ce265a487171f87757",
24
24
  "platforms": {
25
- "linux/arm64": "sha256:a5fe9f3693d789f04490963afbe80fa71a8ff0d0cc6e442b76ca37492fbf3221",
26
- "linux/amd64": "sha256:65abff12143740d4557e9cbfd923b6fd6c5d01f25e96a1d27bbad5e083bb9f74"
25
+ "linux/arm64": "sha256:224665ea653e6ff6e483fc24b226fa68bf45173a2598439665d827780cbe5534",
26
+ "linux/amd64": "sha256:862b197c13dbdf163d480d35e8b17f96b23b447701cb8e79f0252773d7a48a42"
27
27
  }
28
28
  },
29
29
  "android": {
30
- "index": "sha256:1f415db57ccb57c8850ad4498d9fead35f5104099fd355c067208b2e53c25b23",
30
+ "index": "sha256:0d0b56a76cff5b16f52eba3a4d2b17bc888fb2ca502bfd0ae029bf837c6c354d",
31
31
  "platforms": {
32
- "linux/amd64": "sha256:38d7a10c233cfab13adb110383e386e2a649364a8285d6b3ad0696e1e343b58a"
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
- symbolExportDefineString = symbols.map((s) => `export let ${s} = null;`).join('\n');
83
- symbolExportAssignString = symbols.map((s) => `${s} = m.${s};`).join('\n');
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 { NativeModules } from 'react-native';
143
+ import { TurboModuleRegistry } from 'react-native';
136
144
  import Module from '@crossbind/core-embind-jsi';
137
145
 
138
- const { RNJsiLib } = NativeModules;
146
+ const RNJsiLib = TurboModuleRegistry.get('RNJsiLib');
139
147
 
140
148
  function setEnv() {
141
149
  const env = JSON.parse('${env}');