crossbind 2.0.0-beta.74 → 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.74",
3
+ "version": "2.0.0-beta.75",
4
4
  "license": "MIT",
5
5
  "homepage": "https://crossbind.dev",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  "vitest": "^5.0.1"
39
39
  },
40
40
  "dependencies": {
41
- "@crossbind/extension-conan": "2.0.0-beta.74",
41
+ "@crossbind/extension-conan": "2.0.0-beta.75",
42
42
  "@rollup/plugin-commonjs": "^29.0.3",
43
43
  "@rollup/plugin-json": "^6.1.0",
44
44
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -55,7 +55,7 @@
55
55
  "upath": "^3.0.8"
56
56
  },
57
57
  "optionalDependencies": {
58
- "@crossbind/bridge-tools": "2.0.0-beta.74"
58
+ "@crossbind/bridge-tools": "2.0.0-beta.75"
59
59
  },
60
60
  "keywords": [
61
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);
@@ -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
 
@@ -26,6 +26,31 @@ export const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
26
26
  // Containers share the memory of Docker's VM, and one compile at full -j can take most of a desktop VM
27
27
  // (GDAL's bindings peak at 7.5 of 7.7 GiB), so the exclusive steps of parallel builds take turns.
28
28
  const DOCKER_COMPILE_LOCK = path.join(os.homedir(), '.crossbind', 'docker-compile.lock');
29
+ // With CROSSBIND_SCCACHE set, C and C++ compiles go through sccache, which its SCCACHE_* variables configure, by CMake's
30
+ // launchers. A wasm build compiles without it: sccache 0.19 recognizes neither em++ nor emscripten's clang, which has
31
+ // no default target, and cannot cache the -Xclang flags emcc passes that clang.
32
+ const SCCACHE_LAUNCHERS = ['CMAKE_C_COMPILER_LAUNCHER', 'CMAKE_CXX_COMPILER_LAUNCHER'];
33
+ // sccache's key leaves out the target llvm-mingw's compilers take from their own names, so an x64 and an arm64 compile of
34
+ // one source would share it; the buster, which sccache hashes into every key, names the target.
35
+ const SCCACHE_BUSTER = 'SCCACHE_C_CUSTOM_CACHE_BUSTER';
36
+ const sccacheBuster = (target) => [process.env[SCCACHE_BUSTER], target?.platform, target?.arch].filter(Boolean).join('-');
37
+ const SCCACHE_SETTING =/^(SCCACHE_\w+|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN)$/;
38
+ const SCCACHE_IN_IMAGE = 'command -v sccache >/dev/null || { echo "crossbind: CROSSBIND_SCCACHE needs toolchain images that carry sccache, '
39
+ + 'image family 1.0.21 or later." >&2; exit 1; }; ';
40
+ // A docker run's sccache server ends with its container, so the command stops it first, which waits for the cache writes
41
+ // it holds; a DOCKER_EXEC container's server serves the commands after it.
42
+ const SCCACHE_IN_CONTAINER = {
43
+ DOCKER_RUN: `${SCCACHE_IN_IMAGE}"$0" "$@"; status=$?; sccache --stop-server >/dev/null 2>&1; exit $status`,
44
+ DOCKER_EXEC: `${SCCACHE_IN_IMAGE}exec "$0" "$@"`,
45
+ };
46
+ const isSccacheOn = () => /^(1|true)$/i.test(process.env.CROSSBIND_SCCACHE ?? '');
47
+ const sccacheSettings = () => Object.keys(process.env).filter((key) => SCCACHE_SETTING.test(key)).sort();
48
+ // Where a container finds a local sccache directory: a docker run mounts it here, a long-lived container sees the project.
49
+ const SCCACHE_CONTAINER_DIR = '/tmp/crossbind/sccache';
50
+ const isInside = (parent, child) => {
51
+ const relative = path.relative(parent, child);
52
+ return !relative.startsWith('..') && !path.isAbsolute(relative);
53
+ };
29
54
  const envParams = (env) => Object.entries(env).flatMap(([variable, value]) => ['-e', `${variable}=${value}`]);
30
55
  const CROSSCOMPILER_ARM64 = `aarch64-linux-android${ANDROID_API_LEVEL}`;
31
56
  const CROSSCOMPILER_x86_64 = `x86_64-linux-android${ANDROID_API_LEVEL}`;
@@ -139,6 +164,15 @@ export default function run(program, params = [], platformPrefix = null, target
139
164
  ? { runner: 'LOCAL' }
140
165
  : runnerFor(role, state.config.system);
141
166
  const isDocker = runner === 'DOCKER_RUN' || runner === 'DOCKER_EXEC';
167
+ const isSccache = isSccacheOn() && target?.platform !== 'wasm';
168
+ if (isSccache && runner === 'REMOTE') {
169
+ throw new Error('crossbind: CROSSBIND_SCCACHE works with the DOCKER_RUN, DOCKER_EXEC and LOCAL runners; a remote runner would need your '
170
+ + 'cache\'s keys. Unset it for a remote build.');
171
+ }
172
+ const sccacheDir = isSccache && isDocker ? process.env.SCCACHE_DIR : undefined;
173
+ if (sccacheDir && runner === 'DOCKER_EXEC' && !isInside(state.config.paths.base, sccacheDir)) {
174
+ throw new Error(`crossbind: a DOCKER_EXEC container reaches the project alone, so SCCACHE_DIR (${sccacheDir}) must sit inside ${state.config.paths.base}.`);
175
+ }
142
176
  const wasiHostSdk = target?.platform === 'wasi' ? resolveWasiSdkPath(state.config.system) : null;
143
177
  if (isDocker) {
144
178
  // Google ships the linux NDK for x86_64 only, so the android index carries no arm64 leaf;
@@ -212,10 +246,10 @@ export default function run(program, params = [], platformPrefix = null, target
212
246
  if (dProgram === 'cmake') {
213
247
  platformParams = [];
214
248
  if (dParams[0] !== '--build' && dParams[0] !== '--install') {
249
+ // CMake's own iOS platform with Makefiles: the Xcode generator spent 40-55 s configuring even a
250
+ // small project. Not the app host's toolchain file, whose searches find the Mac's own packages.
215
251
  dParams = [
216
252
  ...dParams,
217
- '-G',
218
- 'Xcode',
219
253
  '-DBUILD_SHARED_LIBS=OFF',
220
254
  '-DFRAMEWORK=TRUE',
221
255
  `-DCMAKE_OSX_DEPLOYMENT_TARGET=${IOS_DEPLOYMENT_TARGET}`,
@@ -227,6 +261,11 @@ export default function run(program, params = [], platformPrefix = null, target
227
261
  '-DCMAKE_SYSTEM_PROCESSOR=arm64',
228
262
  `-DCMAKE_C_FLAGS=${target.arch === 'iphoneos' ? '-fembed-bitcode' : '-fembed-bitcode-marker'}`,
229
263
  `-DCMAKE_CXX_FLAGS=${target.arch === 'iphoneos' ? '-fembed-bitcode' : '-fembed-bitcode-marker'}`,
264
+ // The objects carry bitcode as before; a link, as of a configure check or a recipe's tests,
265
+ // takes none, since Xcode's linker reads -fembed-bitcode-marker's mode as a file.
266
+ '-DCMAKE_EXE_LINKER_FLAGS=-fembed-bitcode=off',
267
+ '-DCMAKE_SHARED_LINKER_FLAGS=-fembed-bitcode=off',
268
+ '-DCMAKE_MODULE_LINKER_FLAGS=-fembed-bitcode=off',
230
269
  "-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY='iPhone Developer'",
231
270
  `-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=${state.config.system.XCODE_DEVELOPMENT_TEAM}`,
232
271
  ];
@@ -355,6 +394,18 @@ export default function run(program, params = [], platformPrefix = null, target
355
394
  const containerOptions = { cwd: buildPath, stdio: dockerOptions.console ? 'inherit' : ['ignore', 'pipe', 'pipe'], maxBuffer: EXEC_MAX_BUFFER };
356
395
  let fileExecParams;
357
396
  if (runner === 'LOCAL') {
397
+ if (isSccache) {
398
+ // By its full path: a macOS build narrows the PATH below.
399
+ const sccache = findExecutable('sccache', process.env.PATH ?? '');
400
+ if (!sccache) throw new Error('crossbind: CROSSBIND_SCCACHE needs sccache on the PATH: install it with `brew install sccache`, or unset CROSSBIND_SCCACHE.');
401
+ SCCACHE_LAUNCHERS.forEach((key) => {
402
+ env[key] = sccache;
403
+ });
404
+ sccacheSettings().forEach((key) => {
405
+ env[key] = process.env[key];
406
+ });
407
+ env[SCCACHE_BUSTER] = sccacheBuster(target);
408
+ }
358
409
  env.PATH = `/opt/homebrew/bin:${env.PATH}`;
359
410
  // A package's *-config script on the PATH (nc-config, sfcgal-config, ...) would compile that
360
411
  // package into the archives. cmake is the one host tool a darwin build takes from the PATH.
@@ -392,6 +443,11 @@ export default function run(program, params = [], platformPrefix = null, target
392
443
  if (runner === 'DOCKER_RUN') {
393
444
  imageOrContainer = getDockerImage(role);
394
445
  runnerParams = ['run', '--rm', ...DOCKER_RUN_SECURITY_ARGS, '-v', `${state.config.paths.base}:/tmp/crossbind/live`];
446
+ if (sccacheDir) {
447
+ // Made here, as docker would make a missing one owned by root.
448
+ fs.mkdirSync(sccacheDir, { recursive: true });
449
+ runnerParams.push('-v', `${sccacheDir}:${SCCACHE_CONTAINER_DIR}`);
450
+ }
395
451
  if (target?.platform === 'android') {
396
452
  // Google ships the linux NDK for x86_64 only; use the amd64 leaf ref (classic store holds one platform per digest).
397
453
  pullDockerImage(role, 'linux/amd64');
@@ -403,6 +459,16 @@ export default function run(program, params = [], platformPrefix = null, target
403
459
  runnerParams = ['exec'];
404
460
  }
405
461
 
462
+ // The settings go by name, docker taking their values from its own environment, so no key sits in the arguments.
463
+ const insideDir = sccacheDir && (runner === 'DOCKER_RUN' ? SCCACHE_CONTAINER_DIR : replaceBasePathForDocker(sccacheDir));
464
+ const sccache = isSccache ? [
465
+ ...SCCACHE_LAUNCHERS.flatMap((key) => ['-e', `${key}=sccache`]),
466
+ ...sccacheSettings().filter((key) => !(sccacheDir && key === 'SCCACHE_DIR') && key !== SCCACHE_BUSTER).flatMap((key) => ['-e', key]),
467
+ '-e',
468
+ `${SCCACHE_BUSTER}=${sccacheBuster(target)}`,
469
+ ...(sccacheDir ? ['-e', `SCCACHE_DIR=${insideDir}`] : []),
470
+ ] : [];
471
+ const command = isSccache ? ['sh', '-c', SCCACHE_IN_CONTAINER[runner], dProgram, ...dParams] : [dProgram, ...dParams];
406
472
  const args = [
407
473
  ...runnerParams,
408
474
  '--user',
@@ -410,10 +476,9 @@ export default function run(program, params = [], platformPrefix = null, target
410
476
  '--workdir',
411
477
  replaceBasePathForDocker(dockerOptions.workdir || buildPath),
412
478
  ...replaceBasePathForDocker(dockerEnv),
413
- // '-e', replaceBasePathForDocker(`CCACHE_DIR=${state.config.paths.build}/ccache`),
479
+ ...sccache,
414
480
  imageOrContainer,
415
- replaceBasePathForDocker(dProgram),
416
- ...replaceBasePathForDocker(dParams),
481
+ ...replaceBasePathForDocker(command),
417
482
  ];
418
483
  fileExecParams = ['docker', args, options];
419
484
  }
@@ -45,8 +45,19 @@ const ownsObject = (obj) => {
45
45
  // frees nothing.
46
46
  const moduleObjects = new WeakSet();
47
47
 
48
+ // A pointer a member handed out holds a clone of the handle of the object it points into
49
+ // (utils/embindPointerOwners.js), which goes once no handle shares the pointer's count.
50
+ const releaseOwner = (obj) => {
51
+ const count = obj?.$$?.count;
52
+ if (!count?.owner || count.value > (obj.isDeleted() ? 0 : 1)) return;
53
+ const { owner } = count;
54
+ count.owner = undefined;
55
+ if (!owner.isDeleted()) owner.delete();
56
+ };
57
+
48
58
  const freeIfOwned = (obj) => {
49
59
  if (ownsObject(obj) && !moduleObjects.has(obj) && !obj.isDeleted()) obj.delete();
60
+ releaseOwner(obj);
50
61
  };
51
62
 
52
63
  // The worker side of a handle the main thread no longer reaches: its id goes, and the object with the last id.
@@ -1,55 +1,55 @@
1
1
  {
2
- "version": "1.0.20",
2
+ "version": "1.0.21",
3
3
  "registry": "ghcr.io/crossbind",
4
4
  "toolchains": {
5
5
  "rust": "1.99.1"
6
6
  },
7
7
  "images": {
8
8
  "rust-sysroot": {
9
- "index": "sha256:3f62871ccbebbdca53b798fa1b21173bee848ada7744f5108df66a290d3cca78",
9
+ "index": "sha256:de0e2969f74bb458d134f796d04f60ab2b2342ddc4f7d336a2ac945973c628a0",
10
10
  "platforms": {
11
- "linux/arm64": "sha256:ec7c2bba3add2385099b71520c5d68540e8dba691f57a42c0e9143ea5b8981df",
12
- "linux/amd64": "sha256:dfad6e04d3168f5e7375aba271d0e615cc6658793527503ce3070efbd4ac5f91"
11
+ "linux/amd64": "sha256:2b4b338f185a44dfffd28eece9a79eedf7cef27fdd613ac4428bded4b394e773",
12
+ "linux/arm64": "sha256:8f0f72be0386b0a823db7520e23b1eb424da9daf64a364dffa983738d0b07035"
13
13
  }
14
14
  },
15
15
  "base": {
16
- "index": "sha256:a5c7ea17f3e13c13d3c9f53307cce95d037bbb6b58b08034cfa2cb8112a63407",
16
+ "index": "sha256:8894f7f3deb8e3980cf138fea3552e3375d6250d08aba5b3828909bf55d796fc",
17
17
  "platforms": {
18
- "linux/amd64": "sha256:a0934c868ccd5a09de14d193ae33b091ecfddf3c13548595e918c0b053e41dd6",
19
- "linux/arm64": "sha256:14d4bcea7537972052cd6cee314a5c08f1cd68eebd3a7461ff7ca594994c49eb"
18
+ "linux/arm64": "sha256:9e8c9c6495d4c0e743c2a9b471b3e76fe8117507b44e7eb469009cd241473262",
19
+ "linux/amd64": "sha256:4a8305251eb48835bc13c9640303f6e9dfdb14be8ccb28d57ed808175d4dc8e9"
20
20
  }
21
21
  },
22
22
  "web": {
23
- "index": "sha256:00a19f61fa46315175be11f20cea051311c218ab6e99bd1bca70236415a69af9",
23
+ "index": "sha256:5497576cba5a945249f9ff76f8cc40b43b0a26c1d0ce9dd7874ed842550b96c2",
24
24
  "platforms": {
25
- "linux/arm64": "sha256:6e75223e8c696d93f5ab57e286425b4b94c4b1dcb3cf291e63c2820636dfa2a6",
26
- "linux/amd64": "sha256:fe4e84f435838a35f2d60774be0050c39f2ecf1b8ed6cca23530b8c32f5c6211"
25
+ "linux/arm64": "sha256:a661e57cd9a748b8053a3b315474b5e42f39eb691ef2834f9b57dc2f0cb3db17",
26
+ "linux/amd64": "sha256:82d2163287aba2d77af0db0517ee4a96e69e34e2d54d45b4ef6f39151a466494"
27
27
  }
28
28
  },
29
29
  "android": {
30
- "index": "sha256:df5bf439ab81afbe28a166dcd3c0ec281aea11d4265b1fc24225cc686f643ce1",
30
+ "index": "sha256:f54939cf8d648db75178d9a8cb1456453459b03d46fca7f2fa483b979677aa00",
31
31
  "platforms": {
32
- "linux/amd64": "sha256:d2f5460a998aa1695b04d0d1bc231af4d77dd78931ca0f2b02adec51a61f0c02"
32
+ "linux/amd64": "sha256:1030809494407742bd71675d1c8d2967b08a42285ccb47ddf81fa52e8d41c2c6"
33
33
  }
34
34
  },
35
35
  "android-app": {
36
- "index": "sha256:d72766f41a7d0343fdf36a425a9c5e0ea7dda8ac6f3b2697fe49d79a21e5018b",
36
+ "index": "sha256:9be66dedaaf5f36e5795d080eaa43a523b79246c014719783c15e522d6d6f6c4",
37
37
  "platforms": {
38
- "linux/amd64": "sha256:8e62eea8685cd28276dfdd28b34aa75b3a0a677b40a93ea3f28f7f3ef1565425"
38
+ "linux/amd64": "sha256:1f7963b928513a1892c39e706515a0929abd4b6f72946c2943d476be2741725e"
39
39
  }
40
40
  },
41
41
  "linux": {
42
- "index": "sha256:a4828cf2821cc2313f5ddf5bc761073c90616fbef09daa8b8d93e7fa3050497a",
42
+ "index": "sha256:4cdbe97aef06a84f82bf1f6e2f5badc354638c4f15364033ff1059247caa28ed",
43
43
  "platforms": {
44
- "linux/arm64": "sha256:501a878b581295d3946945dfa9bc98a0aa3de5d4c6bc006c7224756cccbf1e35",
45
- "linux/amd64": "sha256:ee5a4f7ee23fdda7e50d4130d783a76d7c44d079ae34b4a326565510eea761dc"
44
+ "linux/arm64": "sha256:b074c80b96455cfa8cfc1349bc1b77ce3286bff36d095831416c4e1111c69cf0",
45
+ "linux/amd64": "sha256:ca2194dd604dadfdb73f0bbe3b2e5a65ab6330bae6f9b8079b1c8ca9c9e2a11a"
46
46
  }
47
47
  },
48
48
  "windows": {
49
- "index": "sha256:13fc02ab67d1465cbe75715d7de6a87c5319477eba2c0459c914bb5b698df766",
49
+ "index": "sha256:89dea8be3a1415c87854c2bccfb2cee95ec1d2f775b1b4db6260882d01d02a46",
50
50
  "platforms": {
51
- "linux/amd64": "sha256:78883c59167af69c5b9433ef6cdfa35aab3833501bb85d830d95f24079a6c8d0",
52
- "linux/arm64": "sha256:f27f09f8e661261c770976580d9861d4ce290eb1ac3030e6198f3c0a4987bd8a"
51
+ "linux/amd64": "sha256:454b9c339628d033d43f64422a12be9445193f167183fbeca71b4ae8af128673",
52
+ "linux/arm64": "sha256:9d823e18683cedfcc94ff6fb3c637c74929b4ee5601f72b3b87a4cedc7cd2d9e"
53
53
  }
54
54
  }
55
55
  }
@@ -1,3 +1,25 @@
1
+ // A const char* followed by an integer may be a byte span, with embedded NULs or no terminator.
2
+ // Preserve its address; JavaScript can read exactly the reported length with readBuffer. The argument keeps its const for
3
+ // callbackArgument, which takes it by reference: a callback's const std::vector<std::string> & binds to no other.
4
+ const CALLBACK_ARGUMENT_AT = `template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
5
+ using Arg = std::remove_reference_t<decltype(std::get<I>(values))>;
6
+ using A = std::remove_cv_t<Arg>;
7
+ if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
8
+ using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
9
+ if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
10
+ else return callbackArgument<Arg>(std::get<I>(values));
11
+ } else return callbackArgument<Arg>(std::get<I>(values));
12
+ }`;
13
+ // The form a bridge cached before the argument kept its const still holds.
14
+ const CALLBACK_ARGUMENT_AT_WITHOUT_CONST = `template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
15
+ using A = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I>(values))>>;
16
+ if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
17
+ using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
18
+ if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
19
+ else return callbackArgument<A>(std::get<I>(values));
20
+ } else return callbackArgument<A>(std::get<I>(values));
21
+ }`;
22
+
1
23
  // Adapt the runtime emitted by the pinned SWIG fork. Kept in the generator so cache rebuilds
2
24
  // produce the same runtime in every bridge, including builds on a remote runner.
3
25
  export default function fixBridgeRuntime(text) {
@@ -22,16 +44,7 @@ template<typename T> constexpr bool isPassedByAddress =`);
22
44
  }
23
45
  if (!text.includes('auto callbackArgumentAt(')) {
24
46
  text = '#include <tuple>\n' + text;
25
- text = text.replace('#ifndef __EMSCRIPTEN__\nstruct RuntimeCallbacks {', `// A const char* followed by an integer may be a byte span, with embedded NULs or no terminator.
26
- // Preserve its address; JavaScript can read exactly the reported length with readBuffer.
27
- template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
28
- using A = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I>(values))>>;
29
- if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
30
- using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
31
- if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
32
- else return callbackArgument<A>(std::get<I>(values));
33
- } else return callbackArgument<A>(std::get<I>(values));
34
- }
47
+ text = text.replace('#ifndef __EMSCRIPTEN__\nstruct RuntimeCallbacks {', `${CALLBACK_ARGUMENT_AT}
35
48
  #ifndef __EMSCRIPTEN__
36
49
  struct RuntimeCallbacks {`);
37
50
  text = text.replaceAll('template<typename R, typename... A> R invokeCallback(size_t slot, A... args) {',
@@ -44,5 +57,5 @@ struct RuntimeCallbacks {`);
44
57
  }
45
58
  template<typename F> struct Callback {`);
46
59
  }
47
- return text;
60
+ return text.replace(CALLBACK_ARGUMENT_AT_WITHOUT_CONST, CALLBACK_ARGUMENT_AT);
48
61
  }
@@ -0,0 +1,42 @@
1
+ // A pointer a member function or property hands out can point into the object it was called on, as a getter of a
2
+ // member behind a unique_ptr does, or name that object, as a fluent call does. JavaScript frees the object once
3
+ // nothing reaches its handle (utils/embindObjectLifetime.js), and the pointer then reads freed memory. The handle of
4
+ // such a pointer holds a clone of the object's handle, on the count the pointer's clones share, so the object goes
5
+ // with the last of them. Only a raw pointer, a NativePointer and an emscripten::val can borrow: a copy and a smart
6
+ // pointer hold what they point at. Static methods and free functions have no object to hold.
7
+
8
+ const HELPERS = 'function crossbindMayBorrow(type){if(!type)return false;if(type.name==="emscripten::val")return true;'
9
+ + 'var cls=type.registeredClass;return!!cls&&!type.isReference&&(!type.isSmartPointer||cls.name==="NativePointer")}'
10
+ + 'function crossbindKeepOwner(value,owner){var $$=value&&value.$$;var held=owner&&owner.$$;'
11
+ + 'if(!$$||!$$.count||$$.count.owner||!held||!held.ptr||held.preservePointerOnDelete||$$.count===held.count)return value;'
12
+ + 'if($$.smartPtr?$$.ptrType.registeredClass.name==="NativePointer":!$$.count.owned)$$.count.owner=owner.clone();return value}'
13
+ + 'function crossbindKeepsOwner(fn,type){if(!crossbindMayBorrow(type))return fn;'
14
+ + 'var kept=function(...args){var owner=this;var value=fn.apply(owner,args);'
15
+ + 'return value instanceof Promise?value.then(resolved=>crossbindKeepOwner(resolved,owner)):crossbindKeepOwner(value,owner)};'
16
+ + 'Object.defineProperty(kept,"name",{value:fn.name});return kept}';
17
+
18
+ // The release glue spells each on one line, the debug glue with spaces and quoted property names. A release glue
19
+ // names embind's registrations by the minified keys of the wasm imports, so a glue that registers class methods is
20
+ // known by the pure virtual check of the method registration, and one that registers properties by the getter's
21
+ // `this` check.
22
+ const CRAFT = /function craftInvokerFunction\s*\(/;
23
+ const MEMBER = /(var memberFunction\s*=\s*)(craftInvokerFunction\(\s*humanName,\s*argTypes,\s*classType,\s*rawInvoker,\s*context(?:,\s*isAsync)?\s*\))(\s*;)/g;
24
+ const GETTER = /return (getterReturnType(?:\.fromWireType|\[['"]fromWireType['"]\])\(\s*getter\(\s*getterContext,\s*ptr\s*\)\s*\))/g;
25
+ const METHODS = /pureVirtualFunctions\.push\(\s*methodName\s*\)/;
26
+ const PROPERTIES = /humanName\s*\+\s*['"] getter['"]/;
27
+
28
+ const count = (text, pattern) => text.match(pattern)?.length ?? 0;
29
+
30
+ // Returns the rewritten glue and whether it binds class methods or properties this rewrite does not recognise.
31
+ export function keepPointerOwners(glue) {
32
+ const hasMethods = METHODS.test(glue);
33
+ const hasProperties = PROPERTIES.test(glue);
34
+ if (!hasMethods && !hasProperties) return { text: glue, missed: false };
35
+ const recognised = CRAFT.test(glue) && (!hasMethods || count(glue, MEMBER) === 1) && (!hasProperties || count(glue, GETTER) === 1);
36
+ if (!recognised) return { text: glue, missed: true };
37
+ const text = glue
38
+ .replace(CRAFT, (craft) => `${HELPERS}${craft}`)
39
+ .replace(MEMBER, (_, assign, call, end) => `${assign}crossbindKeepsOwner(${call},argTypes[0])${end}`)
40
+ .replace(GETTER, (_, read) => `return crossbindKeepOwner(${read},this)`);
41
+ return { text, missed: false };
42
+ }
@@ -65,7 +65,7 @@ export function interfaceIncludes(headerPath, prelude = []) {
65
65
  // wraps just the imported header. The ignored declarations follow the macros, so the retry without macros keeps them.
66
66
  export function buildInterfaceContent({
67
67
  moduleName, headerPath, prelude = [], completing = [], swigMacros = [], ignored = [], constants = [], preamble = [], functions = ALL_NAMES,
68
- extras = [],
68
+ classes = ALL_NAMES, extras = [],
69
69
  }) {
70
70
  const includes = [...new Set([...interfaceIncludes(headerPath, prelude), ...completing])]
71
71
  .map((header) => `#include "${header}"`)
@@ -89,7 +89,7 @@ ${extraBlock}%{
89
89
  ${includes}
90
90
  %}
91
91
 
92
- ${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}%feature("shared_ptr");
92
+ ${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}${classRequests(classes)}%feature("shared_ptr");
93
93
  %feature("polymorphic_shared_ptr");
94
94
 
95
95
  ${macros}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
@@ -104,17 +104,32 @@ function constantRequests(constants) {
104
104
  return constants.length ? `${constants.map((name) => `%feature("embind:constant") ${name};`).join('\n')}\n\n` : '';
105
105
  }
106
106
 
107
- // SWIG ignores every global function, then wraps the listed names again: class members, enums and constants stay. The
108
- // package's own ignores come after, since SWIG applies the later of two renames of a name.
107
+ // SWIG ignores every global function, then wraps the listed names again: class members, enums and constants stay. A
108
+ // function declared through a function typedef (OpenSSL's `OSSL_provider_init_fn OSSL_provider_init;`) reads as a
109
+ // variable, so global variables are ignored too, as rule 10 binds a mutable one only when imported. The package's own
110
+ // ignores come after, since SWIG applies the later of two renames of a name.
109
111
  function functionRequests(functions) {
110
112
  if (functions === ALL_NAMES) return '';
111
113
  const names = [...new Set(functions)].sort();
112
- return `%rename($ignore, %$isfunction, %$isglobal) "";\n${names.map((name) => `%rename("%s") ${name};\n`).join('')}\n`;
114
+ return `%rename($ignore, %$isfunction, %$isglobal) "";\n%rename($ignore, %$isvariable, %$isglobal) "";\n${names.map((name) => `%rename("%s") ${name};\n`).join('')}\n`;
113
115
  }
114
116
 
115
- // Editor declarations need the whole header even when the compiled bridge selects functions by import.
117
+ // SWIG ignores every class, then wraps the classes the app's imports reach (see typeClosure.js) again. A class outside
118
+ // any namespace is wrapped as a class only, so a C function sharing its name (struct stat and stat()) stays out; SWIG
119
+ // matches a namespace's class by its full name only without that condition.
120
+ function classRequests(classes) {
121
+ if (classes === ALL_NAMES) return '';
122
+ const lines = [...new Set(classes)].sort()
123
+ .map((name) => (name.includes('::') ? `%rename("%s") ${name};\n` : `%rename("%s", %$isclass) ${name};\n`));
124
+ return `%rename($ignore, %$isclass) "";\n${lines.join('')}\n`;
125
+ }
126
+
127
+ // Editor declarations and SWIG's parse for the bindings plan need the whole header even when the compiled bridge
128
+ // selects functions and classes by import.
116
129
  export function interfaceWithAllFunctions(content) {
117
- return content.replace(/%rename\(\$ignore, %\$isfunction, %\$isglobal\) "";\n(?:%rename\("%s"\) [A-Za-z_]\w*;\n)*\n/g, '');
130
+ return content
131
+ .replace(/%rename\(\$ignore, %\$isfunction, %\$isglobal\) "";\n(?:%rename\(\$ignore, %\$isvariable, %\$isglobal\) "";\n)?(?:%rename\("%s"\) [A-Za-z_]\w*;\n)*\n/g, '')
132
+ .replace(/%rename\(\$ignore, %\$isclass\) "";\n(?:%rename\("%s"(?:, %\$isclass)?\) [A-Za-z_][\w:]*;\n)*\n/g, '');
118
133
  }
119
134
 
120
135
  export function withoutSwigMacros(content) {
@@ -0,0 +1,170 @@
1
+ import { ALL_NAMES } from './headerImports.js';
2
+
3
+ // SWIG's own parse of a header (swig -xml), read for the classes a bridge has to register. embind registers each class
4
+ // once, in the bridge of the header defining it, and a binding naming a class no bridge registers fails only when it is
5
+ // called. So a header binds the classes its imported names reach: through their signatures, the bases of those classes
6
+ // and the signatures of their public members, in this header and in the headers defining them.
7
+
8
+ // SWIG leaves `>` unescaped inside attribute values (std::vector&lt;(T)>), so a tag ends only outside a quoted value.
9
+ const TAG = /<(\/?)([A-Za-z_][\w-]*)((?:\s+[\w:-]+="[^"]*")*)\s*(\/?)>/g;
10
+ const ATTRIBUTE = /([\w:-]+)="([^"]*)"/g;
11
+ const NAME = /[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*/g;
12
+ const ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
13
+ // SWIG's type strings spell pointers, references, qualifiers, functions and arrays as p. r. z. q(...). f(...). a(...).
14
+ const NOT_TYPES = new Set([
15
+ 'p', 'r', 'z', 'q', 'f', 'a', 'const', 'volatile', 'struct', 'class', 'union', 'enum', 'typename',
16
+ 'unsigned', 'signed', 'char', 'short', 'int', 'long', 'float', 'double', 'void', 'bool', 'wchar_t', 'size_t',
17
+ ]);
18
+ const EMPTY_TREE = { types: new Map(), values: new Map(), enumerators: new Map() };
19
+
20
+ const decode = (text) => text.replace(/&(lt|gt|amp|quot|apos);/g, (entity, name) => ENTITIES[name]);
21
+
22
+ function parse(xml) {
23
+ const root = { tag: 'root', attributes: {}, children: [] };
24
+ const open = [root];
25
+ for (const [, closing, tag, rest, selfClosing] of xml.matchAll(TAG)) {
26
+ if (closing) {
27
+ open.pop();
28
+ continue;
29
+ }
30
+ const attributes = Object.fromEntries([...rest.matchAll(ATTRIBUTE)].map(([, key, value]) => [key, decode(value)]));
31
+ const node = { tag, attributes, children: [] };
32
+ open.at(-1).children.push(node);
33
+ if (!selfClosing) open.push(node);
34
+ }
35
+ return root;
36
+ }
37
+
38
+ // A node's own attributes are the <attribute> elements of its first <attributelist>.
39
+ const listOf = (node) => node.children.find((child) => child.tag === 'attributelist')?.children ?? [];
40
+ const attributesOf = (node) => Object.fromEntries(listOf(node).filter((child) => child.tag === 'attribute')
41
+ .map((child) => [child.attributes.name, child.attributes.value]));
42
+ const basesOf = (node) => listOf(node).filter((child) => child.tag === 'baselist')
43
+ .flatMap((list) => list.children.filter((child) => child.tag === 'base').map((base) => base.attributes.name));
44
+ const parametersOf = (node) => listOf(node).filter((child) => child.tag === 'parmlist')
45
+ .flatMap((list) => list.children.filter((child) => child.tag === 'parm').map((parm) => attributesOf(parm).type ?? ''));
46
+
47
+ // What a header declares: its classes, enums and typedefs by qualified name, each with the type strings it uses, its free
48
+ // functions and variables by name, and the enum of each enumerator. SWIG binds no nested class and no member a class
49
+ // keeps private, so neither reaches anything.
50
+ export function readSwigTree(xml) {
51
+ const types = new Map();
52
+ const values = new Map();
53
+ const enumerators = new Map();
54
+ // C names a struct and its typedef alike (`typedef struct PJ_INFO PJ_INFO;` ahead of `struct PJ_INFO {...}`), and the
55
+ // name stands for the struct.
56
+ const declare = (name, entry) => {
57
+ if (!types.has(name) || (types.get(name).kind === 'typedef' && entry.kind !== 'typedef')) types.set(name, entry);
58
+ };
59
+ const visit = (node, namespace, owner) => {
60
+ // %$isclass leaves class templates alone, so the selection does too.
61
+ if (node.tag === 'template') return;
62
+ const own = node.tag === 'root' ? {} : attributesOf(node);
63
+ const isPublic = own.access !== 'private' && own.access !== 'protected';
64
+ // SWIG spells a class or enum by its full name; a typedef inside a namespace or class may be spelled without it.
65
+ const qualified = (name) => (name.includes('::') || !namespace.length ? name : [...namespace, name].join('::'));
66
+ if (node.tag === 'namespace') {
67
+ node.children.forEach((child) => visit(child, own.name ? [...namespace, own.name] : namespace, null));
68
+ } else if (node.tag === 'class') {
69
+ if (!own.name || !isPublic || own.nested === '1') return;
70
+ const name = qualified(own.name);
71
+ // The name SWIG binds it under: a C struct's typedef name (X509V3_CONF_METHOD for X509V3_CONF_METHOD_st).
72
+ const bound = own.sym_name ?? own['sym:name'] ?? own.tdname ?? name.split('::').at(-1);
73
+ const entry = { kind: 'class', scope: name.split('::'), bases: basesOf(node), uses: [], bound };
74
+ declare(name, entry);
75
+ node.children.forEach((child) => visit(child, namespace, entry));
76
+ } else if (node.tag === 'enum') {
77
+ if (!own.name || !isPublic) return;
78
+ const name = owner && !own.name.includes('::') ? [...owner.scope, own.name].join('::') : qualified(own.name);
79
+ declare(name, { kind: 'enum', scope: name.split('::'), bases: [], uses: [] });
80
+ node.children.filter((child) => child.tag === 'enumitem').forEach((item) => {
81
+ const enumerator = attributesOf(item).name;
82
+ if (enumerator && !enumerators.has(enumerator)) enumerators.set(enumerator, name);
83
+ });
84
+ } else if (node.tag === 'cdecl') {
85
+ if (!own.name || !isPublic || own.storage === 'friend') return;
86
+ const uses = [own.type ?? '', own.decl ?? '', ...parametersOf(node)];
87
+ if (own.storage === 'typedef') {
88
+ const name = owner ? [...owner.scope, own.name].join('::') : qualified(own.name);
89
+ declare(name, { kind: 'typedef', scope: name.split('::'), bases: [], uses });
90
+ } else if (owner) {
91
+ owner.uses.push(...uses);
92
+ } else {
93
+ // An import of an overloaded name binds every overload.
94
+ values.set(own.name, [...(values.get(own.name) ?? []), ...uses]);
95
+ }
96
+ } else if (node.tag === 'constructor') {
97
+ if (owner && isPublic) owner.uses.push(own.decl ?? '', ...parametersOf(node));
98
+ } else {
99
+ node.children.forEach((child) => visit(child, namespace, owner));
100
+ }
101
+ };
102
+ visit(parse(xml), [], null);
103
+ return { types, values, enumerators };
104
+ }
105
+
106
+ // The classes and enums `names` reach from `header`, by qualified name, each with its kind and the header defining it.
107
+ // `treeOf` reads a header's parse; a name no header read so far declares is looked up through `definingHeader`, whose
108
+ // header is read in turn. ALL_NAMES starts from everything the header declares, as a header bound whole does.
109
+ export function typeClosure(header, names, { treeOf, definingHeader }) {
110
+ const trees = new Map();
111
+ const read = (file) => {
112
+ if (!trees.has(file)) trees.set(file, treeOf(file) ?? EMPTY_TREE);
113
+ return trees.get(file);
114
+ };
115
+ const root = read(header);
116
+ const lookedUp = new Set();
117
+ const inTrees = (candidates, wanted) => {
118
+ for (const candidate of candidates) {
119
+ for (const [file, tree] of trees) {
120
+ const entry = tree.types.get(candidate);
121
+ if (entry && wanted(entry)) return { name: candidate, file, entry };
122
+ }
123
+ }
124
+ return null;
125
+ };
126
+ const isClassOrEnum = (entry) => entry.kind !== 'typedef';
127
+ // A class or enum wins over a typedef of the same name, even one another header declares first (a forward header's
128
+ // `typedef struct Foo Foo;`), so a header that may define the name is read before a typedef stands for it.
129
+ const find = (candidates) => {
130
+ const found = inTrees(candidates, isClassOrEnum);
131
+ if (found) return found;
132
+ for (const candidate of candidates) {
133
+ if (lookedUp.has(candidate)) continue;
134
+ lookedUp.add(candidate);
135
+ const file = definingHeader(candidate);
136
+ if (file && !trees.has(file)) read(file);
137
+ }
138
+ return inTrees(candidates, isClassOrEnum) ?? inTrees(candidates, () => true);
139
+ };
140
+ const queue = [];
141
+ const reach = (text, scope) => {
142
+ for (const [name] of text.matchAll(NAME)) {
143
+ if (!NOT_TYPES.has(name)) queue.push({ name, scope });
144
+ }
145
+ };
146
+ const start = names === ALL_NAMES ? [...root.values.keys(), ...root.types.keys()] : names;
147
+ for (const name of start) {
148
+ if (root.values.has(name)) root.values.get(name).forEach((use) => reach(use, []));
149
+ else queue.push({ name: root.enumerators.get(name) ?? name, scope: [] });
150
+ }
151
+ const reached = new Map();
152
+ const visited = new Set();
153
+ while (queue.length) {
154
+ const { name, scope } = queue.pop();
155
+ // As in C++ lookup, a name used inside a class or namespace is tried there first.
156
+ const candidates = [...scope.map((_, depth) => [...scope.slice(0, scope.length - depth), name].join('::')), name];
157
+ const found = find(candidates);
158
+ if (!found || visited.has(found.name)) continue;
159
+ visited.add(found.name);
160
+ const { entry } = found;
161
+ if (entry.kind === 'class') reached.set(found.name, { kind: entry.kind, header: found.file, bound: entry.bound });
162
+ else if (entry.kind === 'enum') reached.set(found.name, { kind: entry.kind, header: found.file });
163
+ const enclosing = entry.scope.slice(0, -1);
164
+ // SWIG binds an enum declared in a class only with that class.
165
+ if (entry.kind === 'enum' && enclosing.length) queue.push({ name: enclosing.join('::'), scope: [] });
166
+ entry.bases.forEach((base) => queue.push({ name: base, scope: enclosing }));
167
+ entry.uses.forEach((use) => reach(use, entry.kind === 'class' ? entry.scope : enclosing));
168
+ }
169
+ return reached;
170
+ }