crossbind 2.0.0-beta.73 → 2.0.0-beta.75

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossbind",
3
- "version": "2.0.0-beta.73",
3
+ "version": "2.0.0-beta.75",
4
4
  "license": "MIT",
5
5
  "homepage": "https://crossbind.dev",
6
6
  "repository": {
@@ -27,6 +27,10 @@
27
27
  "types": "./src/app/index.d.ts",
28
28
  "default": "./src/app/index.js"
29
29
  },
30
+ "./licenses": {
31
+ "types": "./src/licenses/index.d.ts",
32
+ "default": "./src/licenses/index.js"
33
+ },
30
34
  "./*": "./*"
31
35
  },
32
36
  "devDependencies": {
@@ -34,7 +38,7 @@
34
38
  "vitest": "^5.0.1"
35
39
  },
36
40
  "dependencies": {
37
- "@crossbind/extension-conan": "2.0.0-beta.72",
41
+ "@crossbind/extension-conan": "2.0.0-beta.75",
38
42
  "@rollup/plugin-commonjs": "^29.0.3",
39
43
  "@rollup/plugin-json": "^6.1.0",
40
44
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -51,7 +55,7 @@
51
55
  "upath": "^3.0.8"
52
56
  },
53
57
  "optionalDependencies": {
54
- "@crossbind/bridge-tools": "2.0.0-beta.73"
58
+ "@crossbind/bridge-tools": "2.0.0-beta.75"
55
59
  },
56
60
  "keywords": [
57
61
  "webassembly",
@@ -15,9 +15,10 @@ import { guardBigIntArguments, guardEmbindArguments } from '../utils/embindArgum
15
15
  import { separateCallArguments } from '../utils/embindCallArguments.js';
16
16
  import { queueJspiCalls } from '../utils/embindJspiQueue.js';
17
17
  import { releaseOwnedHandles } from '../utils/embindObjectLifetime.js';
18
+ import { keepPointerOwners } from '../utils/embindPointerOwners.js';
18
19
  import { spawnPthreadsFromMainScript } from '../utils/pthreadMainScript.js';
19
20
 
20
- const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/embindObjectLifetime.js', '../utils/pthreadMainScript.js']
21
+ const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/embindObjectLifetime.js', '../utils/embindPointerOwners.js', '../utils/pthreadMainScript.js']
21
22
  .map((file) => fileURLToPath(new URL(file, import.meta.url)));
22
23
 
23
24
  // embind's bigint converter turns any Number into a BigInt, so 2^53+1 silently becomes 2^53.
@@ -76,6 +77,17 @@ function releaseOwnedObjects(target) {
76
77
  }
77
78
  }
78
79
 
80
+ // A pointer a member hands out outlived the object it points into once JavaScript freed that object
81
+ // (utils/embindPointerOwners.js); each runtimeEnv links its own glue, so each one calls this after its link.
82
+ function keepPointerOwnersAlive(target) {
83
+ const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
84
+ const { text, missed } = keepPointerOwners(fs.readFileSync(gluePath, 'utf8'));
85
+ fs.writeFileSync(gluePath, text);
86
+ if (missed) {
87
+ logger.error('embind pointer owner rewrite missed (emscripten glue format changed?): a pointer a method returns can outlive the object it points into');
88
+ }
89
+ }
90
+
79
91
  // A direct-mode mt init in a browser spawns its pthreads from "/undefined" (utils/pthreadMainScript.js).
80
92
  function pointPthreadSpawn(target) {
81
93
  const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
@@ -244,6 +256,7 @@ export default async function buildWasm(target, options = {}) {
244
256
  separateEmbindCalls(target);
245
257
  queueJspiEmbindCalls(target);
246
258
  releaseOwnedObjects(target);
259
+ keepPointerOwnersAlive(target);
247
260
  await buildJs(target);
248
261
  // fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
249
262
  // fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
@@ -286,6 +299,7 @@ export default async function buildWasm(target, options = {}) {
286
299
  separateEmbindCalls(target);
287
300
  queueJspiEmbindCalls(target);
288
301
  releaseOwnedObjects(target);
302
+ keepPointerOwnersAlive(target);
289
303
  await buildJs(target);
290
304
  logger.doneStep(target, 'js');
291
305
  }
@@ -323,6 +337,7 @@ export default async function buildWasm(target, options = {}) {
323
337
  separateEmbindCalls(target);
324
338
  queueJspiEmbindCalls(target);
325
339
  releaseOwnedObjects(target);
340
+ keepPointerOwnersAlive(target);
326
341
  await buildJs(target);
327
342
  if (emccFlags.includes('FETCH')) {
328
343
  fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
@@ -12,6 +12,8 @@ import {
12
12
  interfaceExtras, bridgeExtras, hasPointerRuntime, withPointerRuntimeAnchor, withoutBridgeExtras, POINTER_RUNTIME_ANCHOR,
13
13
  } from '../utils/bridgeExtras.js';
14
14
  import resolveNativeImport from '../integration/resolveNativeImport.js';
15
+ import getDependFilePath from '../integration/getDependFilePath.js';
16
+ import { boundHeaderSpecifiers } from '../utils/boundHeaders.js';
15
17
  import { writeHeaderDts, writeSourceImportDts } from '../utils/cppDts.js';
16
18
  import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
17
19
  import {
@@ -19,6 +21,7 @@ import {
19
21
  indexTypeDefinitions, inlineIncludes, interfaceIncludes, interfaceToRetryWithoutMacros, interfaceWithAllFunctions, parseMacroDump, referencedTypeHeaders,
20
22
  selectSwigMacros,
21
23
  } from '../utils/swigInterface.js';
24
+ import { readSwigTree, typeClosure } from '../utils/typeClosure.js';
22
25
  import writeIfChanged from '../utils/writeIfChanged.js';
23
26
  import { withDirLockSync } from '../utils/dirLock.js';
24
27
  import { sourceImportOfHeader } from '../utils/packageSourceImport.js';
@@ -29,12 +32,18 @@ import {
29
32
  } from '../utils/bridgeTools.js';
30
33
 
31
34
  // Part of every interface hash, so interfaces cached by an older generator are rebuilt.
32
- const INTERFACE_FORMAT = 'swig-macros-4';
35
+ const INTERFACE_FORMAT = 'swig-macros-5';
33
36
  // Part of every bridge hash: each bridge carries the SWIG fork's runtime and all bridges of a module must share it, so
34
37
  // bridges from a fork before field bindings and constants are rebuilt.
35
38
  const BRIDGE_FORMAT = 'swig-extras-2';
39
+ // Part of the key of SWIG's parse of a header, which the bindings plan reads.
40
+ const TREE_FORMAT = 'swig-xml-1';
36
41
  const predefinedMacros = new Map();
37
42
  const typeDefinitions = new Map();
43
+ const swigTrees = new Map();
44
+ const plans = new Map();
45
+ const importedHeaderSets = new Map();
46
+ const macroDumps = new Map();
38
47
 
39
48
  // Each process keeps the cache it loaded, so an interface or bridge another one has rebuilt since must not pass for
40
49
  // this one's: beside each file is the hash of what it was made from, blank while the file is written. A file without one
@@ -70,7 +79,11 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
70
79
  // '@scope/pkg/native/x.h'), so its own directory must reach swig's include list.
71
80
  const sourceDir = upath.dirname(interfaceFilePath);
72
81
  const allNames = wholeHeaders.some((file) => upath.resolve(file) === interfaceFilePath);
82
+ const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
83
+ const isModule = moduleRegex.test(interfaceFilePath);
73
84
  const names = bindingNames(interfaceFilePath, target, allNames);
85
+ const plan = isModule ? null : bindingPlan(target, wholeHeaders, interfaceFilePath, names);
86
+ names.classes = plan ? plan.classesOf(interfaceFilePath) : ALL_NAMES;
74
87
  if (withDependencies) warnUnimported(interfaceFilePath, names.functions);
75
88
  const interfaceFile = createInterfaceFile(interfaceFilePath, target, sourceDir, names);
76
89
  const { headerPath } = includeLocation(interfaceFilePath, target);
@@ -81,8 +94,8 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
81
94
  const bridgeSourceHash = names.named.length ? `${sourceHash}\nnamed:${names.named.join(',')}` : sourceHash;
82
95
  const extras = { named: names.named, headerText: () => fs.readFileSync(swigView ? upath.join(swigView.dir, headerPath) : interfaceFilePath, 'utf8') };
83
96
  const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, sourceDir, bridgeSourceHash, swigView, extras);
84
- const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
85
97
  if (bridgeFile) {
98
+ if (plan) warnUnboundClasses(bridgeFile, names.classes, plan);
86
99
  // Records which header this bridge came from, so directory-driven consumers
87
100
  // (getAllBridges) can prune bridges whose header no longer exists.
88
101
  writeIfChanged(`${bridgeFile}.source`, `${interfaceFilePath}\n`);
@@ -100,10 +113,10 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
100
113
  }
101
114
  writeSourceImportDts(dtsOptions);
102
115
  }
103
- if (bridgeFile && withDependencies && !moduleRegex.test(interfaceFilePath)) {
104
- // A header's own bindings need the types its declarations use registered, which only their headers' bridges do.
105
- // Such a bridge replaces the header's own, so a header bound whole keeps every constant here too.
106
- const dependencyBridges = dependencyHeadersOf(interfaceFilePath, target).map((header) => {
116
+ if (bridgeFile && withDependencies && !isModule) {
117
+ // A header's own bindings need the types they use registered, which only their headers' bridges do. Such a bridge
118
+ // replaces the header's own, so a header bound whole keeps every constant here too.
119
+ const dependencyBridges = plan.dependenciesOf(interfaceFilePath).map((header) => {
107
120
  try {
108
121
  return createBridgeFile(header, target, { withDependencies: false, wholeHeaders });
109
122
  } catch (e) {
@@ -145,10 +158,15 @@ function warnUnimported(header, functions) {
145
158
  console.warn(`crossbind: ${upath.basename(header)} binds none of its functions, since no source of the app imports a name from it. Import the functions you call by name, or the whole header with import * as.`);
146
159
  }
147
160
 
148
- function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { constants, functions, named = [] }) {
161
+ // `tree` writes the interface SWIG's parse for the bindings plan reads, beside the bridge's own and cached apart from it.
162
+ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, {
163
+ constants, functions, classes = ALL_NAMES, named = [],
164
+ }, { tree = false } = {}) {
149
165
  if (!headerOrModuleFilePath) {
150
166
  return null;
151
167
  }
168
+ const cacheKey = tree ? `${headerOrModuleFilePath}#tree` : headerOrModuleFilePath;
169
+ const outputName = (name) => (tree ? name.replace(/\.i$/, '.tree.i') : name);
152
170
  const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
153
171
  const isModule = moduleRegex.test(headerOrModuleFilePath);
154
172
  const { includeRoot, headerPath } = isModule ? {} : includeLocation(headerOrModuleFilePath, target);
@@ -170,28 +188,30 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
170
188
  ...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
171
189
  ...(constants.length ? [`constants:${constants === ALL_NAMES ? ALL_NAMES : constants.join(',')}`] : []),
172
190
  ...(functions === ALL_NAMES ? [] : [`functions:${functions.join(',')}`]),
191
+ ...(classes === ALL_NAMES ? [] : [`classes:${classes.join(',')}`]),
173
192
  ...(named.length ? [`named:${named.join(',')}`] : []),
193
+ ...(tree ? ['tree'] : []),
174
194
  ...packageInputsHash(),
175
195
  ].join('\n');
176
- const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
177
- if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
178
- && isMadeFrom(cachedInterface, fileHash) && !sharedInterface(headerOrModuleFilePath, cachedInterface)) {
196
+ const cachedInterface = state.cache.interfaces[cacheKey];
197
+ if (state.cache.hashes[cacheKey] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
198
+ && isMadeFrom(cachedInterface, fileHash) && !sharedInterface(cacheKey, cachedInterface)) {
179
199
  return cachedInterface;
180
200
  }
181
201
 
182
202
  if (isModule) {
183
- const newPath = `${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`;
184
- return recordInterface(headerOrModuleFilePath, newPath, fileHash, () => fs.copyFileSync(headerOrModuleFilePath, newPath));
203
+ const newPath = outputName(`${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`);
204
+ return recordInterface(cacheKey, newPath, fileHash, () => fs.copyFileSync(headerOrModuleFilePath, newPath));
185
205
  }
186
206
 
187
207
  if (!filePathWithoutExt) return null;
188
208
 
189
209
  if (fs.existsSync(interfaceFile)) {
190
- const newPath = `${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`;
191
- return recordInterface(headerOrModuleFilePath, newPath, fileHash, () => fs.copyFileSync(interfaceFile, newPath));
210
+ const newPath = outputName(`${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`);
211
+ return recordInterface(cacheKey, newPath, fileHash, () => fs.copyFileSync(interfaceFile, newPath));
192
212
  }
193
213
 
194
- const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1));
214
+ const fileName = interfaceName(headerOrModuleFilePath, filePathWithoutExt.split('/').at(-1), (name) => outputName(interfacePath(name)), cacheKey);
195
215
 
196
216
  const macros = collectSwigMacros(headerOrModuleFilePath, interfaceIncludes(headerPath, prelude), fileName, target, sourceDir, constants);
197
217
  const extras = interfaceExtras(named, macros.table, fs.readFileSync(headerOrModuleFilePath, 'utf8'));
@@ -205,10 +225,11 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
205
225
  constants,
206
226
  preamble,
207
227
  functions: functions === ALL_NAMES ? ALL_NAMES : [...functions, ...extras.functions],
228
+ classes,
208
229
  extras: extras.lines,
209
230
  });
210
- const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i`;
211
- return recordInterface(headerOrModuleFilePath, outputFilePath, fileHash, () => fs.writeFileSync(outputFilePath, content));
231
+ const outputFilePath = outputName(`${state.config.paths.build}/interface/${fileName}.i`);
232
+ return recordInterface(cacheKey, outputFilePath, fileHash, () => fs.writeFileSync(outputFilePath, content));
212
233
  }
213
234
 
214
235
  // The key is blank while the file is written, so a process that dies in between leaves no file passing for another hash.
@@ -230,9 +251,10 @@ const sharedInterface = (headerFile, interfaceFile) => Object.entries(state.cach
230
251
 
231
252
  // An interface is named after its header's file name. A second header with that name (tiffio.h and tiffio.hxx, or one
232
253
  // name in two directories) takes its extension, then a hash of its path, so neither replaces the other's bridge.
233
- function interfaceName(headerFile, base) {
254
+ // SWIG's parse for the bindings plan names its interface the same way, checked against the other parses'.
255
+ function interfaceName(headerFile, base, pathOf = interfacePath, identity = headerFile) {
234
256
  const candidates = [base, `${base}_${upath.extname(headerFile).slice(1)}`, `${base}_${getContentHash(headerFile).slice(0, 8)}`];
235
- return candidates.find((name) => !sharedInterface(headerFile, interfacePath(name))) ?? candidates.at(-1);
257
+ return candidates.find((name) => !sharedInterface(identity, pathOf(name))) ?? candidates.at(-1);
236
258
  }
237
259
 
238
260
  const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -263,14 +285,31 @@ function definitionsUnder(includeRoot) {
263
285
  files.forEach(({ path, text }) => {
264
286
  if (!nearest.has(text) || depth(path) < depth(nearest.get(text))) nearest.set(text, path);
265
287
  });
288
+ const originals = files.filter(({ path, text }) => nearest.get(text) === path);
266
289
  typeDefinitions.set(includeRoot, {
267
- definitions: indexTypeDefinitions(files.filter(({ path, text }) => nearest.get(text) === path)),
290
+ definitions: indexTypeDefinitions(originals),
291
+ aliases: indexAliases(originals),
268
292
  copyOf: new Map(files.map(({ path, text }) => [path, nearest.get(text)])),
269
293
  });
270
294
  }
271
295
  return typeDefinitions.get(includeRoot);
272
296
  }
273
297
 
298
+ // The names plain typedefs and alias declarations give (typedef T Name; typedef R (*Name)(...); using Name = T;), each
299
+ // mapped to the first header giving it, so the bindings plan follows a type through the header that names it.
300
+ const ALIAS = /\btypedef\b[^;{}]*?(?:\(\s*\*\s*([A-Za-z_]\w*)\s*\)[^;{}]*|\b([A-Za-z_]\w*)\s*(?:\[[^\]]*\]\s*)*);|\busing\s+([A-Za-z_]\w*)\s*=/g;
301
+
302
+ function indexAliases(files) {
303
+ const aliases = new Map();
304
+ for (const { path, text } of files) {
305
+ for (const [, pointer, plain, using] of text.matchAll(ALIAS)) {
306
+ const name = pointer ?? plain ?? using;
307
+ if (name && !aliases.has(name)) aliases.set(name, path);
308
+ }
309
+ }
310
+ return aliases;
311
+ }
312
+
274
313
  function lookupContext(includeRoot, headerPath, headerText) {
275
314
  const { definitions, copyOf } = definitionsUnder(includeRoot);
276
315
  return { headerText, headerPath: copyOf.get(headerPath) ?? headerPath, definitions };
@@ -290,6 +329,187 @@ function dependencyHeadersOf(headerFile, target) {
290
329
  .map((header) => upath.join(includeRoot, header));
291
330
  }
292
331
 
332
+ // The header under a header's include root that defines a type, or names one with a typedef or alias, for the plan.
333
+ function definingHeaderOf(headerFile, name, target) {
334
+ const { includeRoot } = includeLocation(headerFile, target);
335
+ if (!includeRoot) return null;
336
+ const { definitions, aliases } = definitionsUnder(includeRoot);
337
+ const found = definitions.get(name) ?? aliases.get(name) ?? aliases.get(name.split('::').at(-1));
338
+ return found ? upath.join(includeRoot, found) : null;
339
+ }
340
+
341
+ // SWIG's own parse of a header, which the bindings plan reads (see typeClosure.js): an interface with every function and
342
+ // class of the header, written beside the bridge's own, through SWIG's XML module, made again when what it reads changes.
343
+ function swigTreeOf(headerFile, target) {
344
+ const file = upath.resolve(headerFile);
345
+ const sourceDir = upath.dirname(file);
346
+ const { headerPath } = includeLocation(file, target);
347
+ const inlined = findSwigInlineIncludes(file, [state.config, ...state.config.allDependencies], headerPath);
348
+ const swigView = inlined.length ? writeSwigView(file, headerPath, inlined) : null;
349
+ const interfaceFile = createInterfaceFile(file, target, sourceDir, { constants: [], functions: ALL_NAMES, named: [] }, { tree: true });
350
+ if (!interfaceFile) return null;
351
+ const treeFile = interfaceFile.replace(/\.i$/, '.xml');
352
+ const treeHash = getContentHash([
353
+ TREE_FORMAT, getFileHash(interfaceFile), getFileHash(file), ...(swigView ? [`view:${swigView.hash}`] : []),
354
+ ].join('\n'));
355
+ // A parse that failed is kept too, so the plans of one process ask SWIG once for it.
356
+ const key = `${treeFile}\n${treeHash}`;
357
+ if (!swigTrees.has(key)) {
358
+ try {
359
+ if (!fs.existsSync(treeFile) || !fs.existsSync(keyOf(treeFile)) || fs.readFileSync(keyOf(treeFile), 'utf8') !== treeHash) {
360
+ fs.writeFileSync(keyOf(treeFile), '');
361
+ withMacroRetry(interfaceFile, () => runSwigFor([
362
+ '-c++', '-xml', '-cpperraswarn', '-o', treeFile,
363
+ ...(swigView ? [`-I${swigView.dir}`] : []),
364
+ ...swigIncludePath(target, sourceDir),
365
+ interfaceFile,
366
+ ], target));
367
+ fs.writeFileSync(keyOf(treeFile), treeHash);
368
+ }
369
+ swigTrees.set(key, readSwigTree(fs.readFileSync(treeFile, 'utf8')));
370
+ } catch (e) {
371
+ swigTrees.set(key, e);
372
+ }
373
+ }
374
+ const tree = swigTrees.get(key);
375
+ if (tree instanceof Error) throw tree;
376
+ return tree;
377
+ }
378
+
379
+ // A header by what it is under every path the build or a bundler reaches it through.
380
+ const headerKey = (file) => targetNeutral(realPath(file));
381
+
382
+ // Each header the app's sources import, with the names its bindings start from: ALL_NAMES for one bound whole. Every
383
+ // header a build binds asks, so the answer is kept until the app's imports change, or where a header counts as a
384
+ // dependency's does (a dependency built after the process loaded).
385
+ function importedHeaders(target) {
386
+ const projectDir = state.config.paths.project;
387
+ if (!projectDir || !fs.existsSync(projectDir)) return new Map();
388
+ const imports = findHeaderImportsIn(projectDir, state.config.ext.header);
389
+ const dependencyRoots = state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends ?? [];
390
+ const key = JSON.stringify([target.path, imports, state.config.paths.header, dependencyRoots]);
391
+ if (!importedHeaderSets.has(key)) importedHeaderSets.set(key, resolveImportedHeaders(imports, target));
392
+ return new Map(importedHeaderSets.get(key));
393
+ }
394
+
395
+ function resolveImportedHeaders(imports, target) {
396
+ const headers = new Map();
397
+ for (const { importer, specifier } of imports) {
398
+ const resolved = resolveNativeImport(specifier, importer, target);
399
+ if (!resolved || !fs.existsSync(resolved) || headers.has(headerKey(resolved))) continue;
400
+ const file = upath.resolve(resolved);
401
+ const names = bindingNames(file, target, false);
402
+ headers.set(headerKey(file), { file, start: names.functions === ALL_NAMES ? ALL_NAMES : names.constants });
403
+ }
404
+ return headers;
405
+ }
406
+
407
+ // Which classes each bridge registers, and which other bridges each header needs: the closures of what the app takes
408
+ // from every header it binds (see typeClosure.js). One bridge per header registers a class for every header using it,
409
+ // so its classes are the union of those closures. A header SWIG cannot parse keeps the dependencies found in its text,
410
+ // whose bridges bind every class, as all headers did before.
411
+ function bindingPlan(target, wholeHeaders, header, names) {
412
+ const headers = importedHeaders(target);
413
+ [...configBoundHeaders(target), ...wholeHeaders].forEach((file) => headers.set(headerKey(file), { file: upath.resolve(file), start: ALL_NAMES }));
414
+ // A header no source imports from reaches nothing, so it leaves the plan as it is.
415
+ const start = names.functions === ALL_NAMES ? ALL_NAMES : names.constants;
416
+ if (!headers.has(headerKey(header)) && (start === ALL_NAMES || start.length)) headers.set(headerKey(header), { file: header, start });
417
+ const key = JSON.stringify([target.path, ...[...headers].sort(([a], [b]) => (a < b ? -1 : 1)).map(([file, entry]) => [file, entry.start])]);
418
+ // A dev server binds again after an edit, which can move what a header's names reach.
419
+ const cached = plans.get(key);
420
+ if (cached?.stamps.every(([file, stamp]) => fileStamp(file) === stamp)) return cached.plan;
421
+ const made = makePlan(headers, target);
422
+ plans.set(key, made);
423
+ return made.plan;
424
+ }
425
+
426
+ const fileStamp = (file) => {
427
+ try {
428
+ const { mtimeMs, size } = fs.statSync(file);
429
+ return `${mtimeMs}:${size}`;
430
+ } catch (e) {
431
+ return 'missing';
432
+ }
433
+ };
434
+
435
+ // The dependency headers the config binds whole (export.bindings.headers) that this target's packages ship, which every
436
+ // caller binding a header has to see alike.
437
+ function configBoundHeaders(target) {
438
+ return boundHeaderSpecifiers(state.config).map((specifier) => {
439
+ try {
440
+ return getDependFilePath(specifier, target);
441
+ } catch (e) {
442
+ return null;
443
+ }
444
+ }).filter((file) => file && fs.existsSync(file));
445
+ }
446
+
447
+ function makePlan(headers, target) {
448
+ const closures = new Map();
449
+ const parsedNot = new Set();
450
+ const read = new Set();
451
+ for (const [key, { file, start }] of headers) {
452
+ try {
453
+ const reached = typeClosure(file, start, {
454
+ treeOf: (header) => {
455
+ read.add(header);
456
+ return swigTreeOf(header, target);
457
+ },
458
+ definingHeader: (name) => definingHeaderOf(file, name, target),
459
+ });
460
+ closures.set(key, [...reached].map(([name, { kind, header, bound }]) => ({ name, kind, header, bound, key: headerKey(header) })));
461
+ } catch (e) {
462
+ console.warn(`crossbind: SWIG cannot parse ${upath.basename(file)} for the classes its bindings need (${e.message}); the headers its types come from bind every class`);
463
+ closures.set(key, null);
464
+ dependencyHeadersOf(file, target).forEach((dependency) => parsedNot.add(headerKey(dependency)));
465
+ }
466
+ }
467
+ const plan = {
468
+ classesOf(file) {
469
+ const key = headerKey(file);
470
+ if (headers.get(key)?.start === ALL_NAMES || closures.get(key) === null || parsedNot.has(key)) return ALL_NAMES;
471
+ const classes = new Set();
472
+ for (const closure of closures.values()) {
473
+ closure?.forEach((type) => {
474
+ if (type.kind === 'class' && type.key === key) classes.add(type.name);
475
+ });
476
+ }
477
+ return [...classes].sort();
478
+ },
479
+ dependenciesOf(file) {
480
+ const key = headerKey(file);
481
+ const closure = closures.get(key);
482
+ if (closure === null) return dependencyHeadersOf(file, target);
483
+ const dependencies = new Map();
484
+ closure?.forEach((type) => {
485
+ if (type.key !== key && !dependencies.has(type.key)) dependencies.set(type.key, type.header);
486
+ });
487
+ return [...dependencies.values()];
488
+ },
489
+ // The name SWIG binds a class under, which a C struct takes from its typedef.
490
+ boundName(name) {
491
+ for (const closure of closures.values()) {
492
+ const type = closure?.find((reached) => reached.name === name && reached.bound);
493
+ if (type) return type.bound;
494
+ }
495
+ return name.split('::').at(-1);
496
+ },
497
+ };
498
+ // The headers the plan read, each as it was, so a later edit of one makes the plan again.
499
+ return { plan, stamps: [...read].map((file) => [file, fileStamp(file)]) };
500
+ }
501
+
502
+ // A class the plan asks for that SWIG left out of the bridge fails each binding that passes or returns it, when called.
503
+ function warnUnboundClasses(bridgeFile, classes, plan) {
504
+ const exportsFile = `${bridgeFile}.exports.json`;
505
+ if (classes === ALL_NAMES || !classes.length || !fs.existsSync(exportsFile)) return;
506
+ const exported = new Set(JSON.parse(fs.readFileSync(exportsFile, 'utf8')));
507
+ const missing = classes.filter((name) => !exported.has(plan.boundName(name)));
508
+ if (missing.length) {
509
+ console.warn(`crossbind: ${upath.basename(bridgeFile)} does not bind ${missing.join(', ')}, which the names the app imports use; a call passing or returning one fails`);
510
+ }
511
+ }
512
+
293
513
  // The proxy module's own exports, never names of the header.
294
514
  const PROXY_NAMES = new Set(['initNative', 'AllSymbols']);
295
515
  const C_IDENTIFIER = /^[A-Za-z_]\w*$/;
@@ -342,7 +562,11 @@ function collectSwigMacros(headerFile, includes, name, target, sourceDir, consta
342
562
  if (!predefinedMacros.has(target.path)) {
343
563
  predefinedMacros.set(target.path, dumpMacros(`${interfaceDir}/predefined-${target.path}.macros.h`, [], [], target));
344
564
  }
345
- const macros = dumpMacros(`${interfaceDir}/${name}.macros.h`, includes, swigIncludePath(target, sourceDir), target, [RELEASE_DEFINE]);
565
+ // The bridge's interface and SWIG's parse for the bindings plan read the same header with the same includes.
566
+ const includePath = swigIncludePath(target, sourceDir);
567
+ const dumpKey = JSON.stringify([name, includes, includePath, target.path, getFileHash(headerFile)]);
568
+ if (!macroDumps.has(dumpKey)) macroDumps.set(dumpKey, dumpMacros(`${interfaceDir}/${name}.macros.h`, includes, includePath, target, [RELEASE_DEFINE]));
569
+ const macros = macroDumps.get(dumpKey);
346
570
  return {
347
571
  lines: selectSwigMacros({ headerText: fs.readFileSync(headerFile, 'utf8'), macros, predefined: predefinedMacros.get(target.path), constants }),
348
572
  table: macros,
@@ -447,6 +671,26 @@ function addBridgeExtras(bridgeFilePath, interfaceFilePath, named, headerText, s
447
671
  fs.writeFileSync(warningsFile, [...readWarnings().filter((line) => !extras.answered.has(line)), ...extras.notes].map((line) => `${line}\n`).join(''));
448
672
  }
449
673
 
674
+ // Known macros can expose declarations SWIG cannot parse and used to skip; without them it parses as before.
675
+ function withMacroRetry(interfaceFilePath, swig) {
676
+ try {
677
+ swig();
678
+ } catch (e) {
679
+ const content = fs.readFileSync(interfaceFilePath, 'utf8');
680
+ const plain = interfaceToRetryWithoutMacros(content, e);
681
+ if (!plain) throw e;
682
+ console.warn(`crossbind: SWIG cannot parse ${upath.basename(interfaceFilePath)} with the macros of its includes; generating it without them`);
683
+ fs.writeFileSync(interfaceFilePath, plain);
684
+ try {
685
+ swig();
686
+ } catch (retryError) {
687
+ // The cached interface keeps its macros, so the next build tries them again.
688
+ fs.writeFileSync(interfaceFilePath, content);
689
+ throw retryError;
690
+ }
691
+ }
692
+ }
693
+
450
694
  // SWIG looks in its working directory and the interface's before its include path, and neither holds the header, so
451
695
  // it reads this copy, put first on that path: the includes its package lists are inlined there, while the compiler
452
696
  // still includes the real header.
@@ -488,23 +732,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
488
732
  ...swigIncludePath(target, sourceDir),
489
733
  interfaceFilePath,
490
734
  ], target);
491
- try {
492
- swig();
493
- } catch (e) {
494
- // Known macros can expose declarations SWIG cannot parse and used to skip; without them it parses as before.
495
- const content = fs.readFileSync(interfaceFilePath, 'utf8');
496
- const plain = interfaceToRetryWithoutMacros(content, e);
497
- if (!plain) throw e;
498
- console.warn(`crossbind: SWIG cannot parse ${upath.basename(interfaceFilePath)} with the macros of its includes; generating it without them`);
499
- fs.writeFileSync(interfaceFilePath, plain);
500
- try {
501
- swig();
502
- } catch (retryError) {
503
- // The cached interface keeps its macros, so the next build tries them again.
504
- fs.writeFileSync(interfaceFilePath, content);
505
- throw retryError;
506
- }
507
- }
735
+ withMacroRetry(interfaceFilePath, swig);
508
736
  applyAsyncGuard(bridgeFilePath);
509
737
  if (extras.named.length) addBridgeExtras(bridgeFilePath, interfaceFilePath, extras.named, extras.headerText(), swig);
510
738
  reportSkippedBindings(bridgeFilePath);
@@ -48,7 +48,10 @@ export default function createLib(target, fileType, options = {}) {
48
48
  // 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.
49
49
  const fingerprintFile = `${libdir}/crossbind-nativeglob.fingerprint`;
50
50
  // options.inputs lists what the sources compile against (a binding runtime's headers), so a change there rebuilds too.
51
- const fingerprint = nativeGlob ? getFilesFingerprint([...nativeGlob, ...(options.inputs ?? [])]) : null;
51
+ // A header edit can leave a bridge's text as it was, so the key beside each bridge, the hash of what it was made from, counts too.
52
+ const fingerprint = nativeGlob
53
+ ? getFilesFingerprint([...nativeGlob, ...nativeGlob.map((file) => `${file}.key`), ...(options.inputs ?? [])])
54
+ : null;
52
55
  const fingerprintChanged = fingerprint !== null
53
56
  && (!fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint);
54
57
 
@@ -61,7 +61,6 @@ export default function getCmakeParameters(target, options = {}) {
61
61
  `-DBUILD_TYPE=${buildType}`,
62
62
  `-DBUILD_${otherBuildType}_LIBS=OFF`,
63
63
  `-DCOMPILE_OPTIONS=${cmakeCompileOptions.join(';')}`,
64
- // '-DCMAKE_CXX_COMPILER_LAUNCHER=ccache',
65
64
  ...(options.buildSource !== false ? ['-DBUILD_SOURCE=TRUE'] : []),
66
65
  ]);
67
66