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.
@@ -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
  }
@@ -200,18 +200,15 @@ export function parseCppSurface(source, log = console.log) {
200
200
 
201
201
  export function emitCppDts(model, exportNames, mode = 'sync') {
202
202
  const wrap = (t) => (mode === 'promise' ? `Promise<${t}>` : t);
203
- const out = ['// Generated by crossbind - do not edit. Values are usable after init().', ''];
204
- const usesVector = model.classes.some((cls) => (cls.ctor?.args ?? []).concat(cls.methods.flatMap((m) => [...m.args, { type: m.ret }]))
205
- .some((a) => a.type?.includes('CppVector<')));
206
- if (usesVector) {
207
- out.push('/** An embind std::vector proxy. Convert with Module.toArray / Module.toVector. */');
208
- out.push('export interface CppVector<T> {');
209
- out.push(` size(): ${wrap('number')};`);
210
- out.push(` get(index: number): ${wrap('T')};`);
211
- out.push(` push_back(value: T): ${wrap('void')};`);
212
- out.push(` delete(): ${wrap('void')};`);
213
- out.push('}');
214
- }
203
+ const out = ['// Generated by crossbind - do not edit. Values are usable after initNative() resolves.', ''];
204
+ // The module's toVector and toArray take vectors on every runtime, so the proxy's type is always declared.
205
+ out.push('/** An embind std::vector proxy. Convert with Module.toArray / Module.toVector. */');
206
+ out.push('export interface CppVector<T> {');
207
+ out.push(` size(): ${wrap('number')};`);
208
+ out.push(` get(index: number): ${wrap('T')};`);
209
+ out.push(` push_back(value: T): ${wrap('void')};`);
210
+ out.push(` delete(): ${wrap('void')};`);
211
+ out.push('}');
215
212
  const emitted = new Set();
216
213
  for (const cls of model.classes) {
217
214
  if (!exportNames.includes(cls.name)) continue;
@@ -233,7 +230,16 @@ export function emitCppDts(model, exportNames, mode = 'sync') {
233
230
  if (!emitted.has(name)) out.push(`export declare const ${name}: any;`);
234
231
  }
235
232
  out.push('export declare let AllSymbols: Record<string, unknown>;');
236
- out.push('export declare function initNative(config?: Record<string, unknown>): Promise<unknown>;');
233
+ // A runtime's own members (FS in a wasm build, the file helpers in a browser) differ by runtime; the index signature
234
+ // keeps them reachable while the members every runtime has stay typed.
235
+ out.push('/** What initNative resolves to: the bound classes, toVector and toArray, and the members of the runtime it runs on. */');
236
+ out.push('export interface NativeModule {');
237
+ out.push(' [name: string]: unknown;');
238
+ for (const name of emitted) out.push(` ${name}: typeof ${name};`);
239
+ out.push(` toVector<T>(classOrName: unknown, array?: T[]): ${wrap('CppVector<T>')};`);
240
+ out.push(` toArray<T>(vector: CppVector<T> | T[]): ${wrap('T[]')};`);
241
+ out.push('}');
242
+ out.push('export declare function initNative(config?: Record<string, unknown>): Promise<NativeModule>;');
237
243
  out.push('');
238
244
  return out.join('\n');
239
245
  }
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  import spdxParse from 'spdx-expression-parse';
2
+ import { lgplRows, relinkSection } from './relinking.js';
2
3
 
3
4
  const COPYLEFT_PATTERN = /\b(AGPL|LGPL|GPL|MPL|EPL|EUPL|CDDL)\b/i;
4
5
 
@@ -43,16 +44,20 @@ export function formatNoticeSections(rows) {
43
44
  });
44
45
  }
45
46
 
46
- export function formatNoticesMarkdown(rows) {
47
+ // meta, the package that publishes the artifact (packageMeta), adds the relinking steps an LGPL library asks for.
48
+ export function formatNoticesMarkdown(rows, meta = null) {
47
49
  const sections = formatNoticeSections(rows);
50
+ const relinked = meta?.repository ? lgplRows(rows) : [];
48
51
 
49
52
  return [
50
53
  '# Third-Party Notices',
51
54
  '',
52
- 'GENERATED by `crossbind licenses` from recipe metadata (contract D/E) - do not edit.',
55
+ 'GENERATED by `crossbind licenses` - do not edit. This file states facts and is not legal advice; the',
56
+ 'compliance assessment is yours.',
53
57
  '',
54
58
  'This artifact bundles the following native libraries through crossbind packages.',
55
59
  '',
60
+ ...(relinked.length ? [relinkSection({ ...meta, artifact: 'module', rows: relinked }), ''] : []),
56
61
  sections.join('\n\n'),
57
62
  '',
58
63
  ].join('\n');
@@ -89,11 +94,13 @@ export function formatCycloneDxSbom(rows, target = {}) {
89
94
  },
90
95
  components: [...rows]
91
96
  .sort((a, b) => a.name.localeCompare(b.name))
92
- .map((row) => ({
97
+ .map((row) => ({ row, version: row.nativeVersion ?? row.version }))
98
+ .map(({ row, version }) => ({
93
99
  type: 'library',
94
100
  name: row.name,
95
- ...(row.nativeVersion ? { version: row.nativeVersion } : {}),
96
- ...(row.nativeVersion ? { purl: row.purl ?? `pkg:generic/${row.name}@${row.nativeVersion}` } : {}),
101
+ ...(version ? { version } : {}),
102
+ ...(version ? { purl: row.purl ?? `pkg:generic/${row.name}@${version}` } : {}),
103
+ ...(row.cpe ? { cpe: row.cpe } : {}),
97
104
  ...(row.license ? { licenses: [{ expression: row.license }] } : {}),
98
105
  ...(row.sha256 ? { hashes: [{ alg: 'SHA-256', content: row.sha256 }] } : {}),
99
106
  ...(row.sourceUrl ? { externalReferences: [{ type: 'distribution', url: row.sourceUrl }] } : {}),
@@ -1,14 +1,13 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { deriveLicenseExpression, formatCycloneDxSbom, formatNoticeSections } from './licenseReport.js';
4
+ import { lgplRows, relinkSection } from './relinking.js';
5
+
6
+ export { lgplRows };
4
7
 
5
8
  export const SBOM_FILE = 'sbom.cdx.json';
6
9
  const RULE = '-'.repeat(70);
7
10
 
8
- export function lgplRows(rows) {
9
- return rows.filter((row) => /\bLGPL\b/.test(row.license ?? ''));
10
- }
11
-
12
11
  // The LLVM exception waives the notice for runtime parts compiled into a binary; every other
13
12
  // component needs its text in the package.
14
13
  function assertTexts(name, rows) {
@@ -22,36 +21,20 @@ function assertTexts(name, rows) {
22
21
  }
23
22
  }
24
23
 
25
- const listOf = (names) => (names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names[0]);
26
-
27
- function relinkSection({ name, version, repository, projectPath, addonFile, rows }) {
28
- const tag = `${name}@${version}`;
29
- const repo = repository.replace(/\.git$/, '');
30
- return [
31
- '## Relinking the LGPL libraries',
32
- '',
33
- `This addon links ${listOf(rows.map((row) => row.name))} statically; their sections below give their licenses.`,
34
- 'You may replace them with modified versions and relink the addon:',
35
- '',
36
- `1. Get the corresponding source. This package was built from ${repo} at the tag ${tag}:`,
37
- ` ${repo}/tree/${tag}`,
38
- ' The upstream sources are the tarballs the sections below list, with their SHA-256.',
39
- `2. In ${projectPath}, point the library at your modified source with a crossbind.overrides.js file`,
40
- ' (docs/playbooks/override-dependencies.md in that repository).',
41
- `3. Build the addon with \`pnpm install\` and \`pnpm --dir ${projectPath} build\`. Linux and Windows`,
42
- ' addons build in Docker, macOS addons on a Mac.',
43
- `4. The build writes ${addonFile} in ${projectPath}; it replaces the file of that name in this package.`,
44
- ].join('\n');
24
+ // What a Node-API package's license covers: an addon package what its addon links; a package without an addon crossbind's
25
+ // JavaScript runtime, the bindings of the project's own code and the data of the libraries that declare some.
26
+ export function shippedRows(rows, { addonFile }) {
27
+ return addonFile ? rows : rows.filter((row) => row.isRuntime || !row.isPort || row.shipsData);
45
28
  }
46
29
 
47
- export function nodePackageLicense({ name, version, repository, projectPath, addonFile = null, rows }) {
30
+ export function nodePackageLicense({ name, version, repository, projectPath, addonFile = null, sourceTag = null, rows }) {
48
31
  assertTexts(name, rows);
49
32
  const expression = deriveLicenseExpression(rows);
50
33
  const relinked = addonFile ? lgplRows(rows) : [];
51
34
  const lines = [
52
35
  addonFile
53
36
  ? `${name} ships a Node-API addon that links the components below statically.`
54
- : `${name} ships the JavaScript bindings, types and data of the components below; the addons that link them are in its platform packages.`,
37
+ : `${name} ships the JavaScript bindings, types and data of the components below. Each of its platform packages carries the LICENSE of the addon it holds.`,
55
38
  'The full license text of every component follows.',
56
39
  '',
57
40
  'Effective license expression (also package.json "license"):',
@@ -65,7 +48,9 @@ export function nodePackageLicense({ name, version, repository, projectPath, add
65
48
  '',
66
49
  RULE,
67
50
  '',
68
- ...(relinked.length ? [relinkSection({ name, version, repository, projectPath, addonFile, rows: relinked }), '', RULE, ''] : []),
51
+ ...(relinked.length ? [relinkSection({
52
+ name, version, repository, projectPath, sourceTag, artifact: 'addon', file: addonFile, rows: relinked,
53
+ }), '', RULE, ''] : []),
69
54
  formatNoticeSections(rows).join('\n\n'),
70
55
  '',
71
56
  ];
@@ -91,7 +76,7 @@ export function writeNodePackageLicense(dir, meta) {
91
76
 
92
77
  // What a Node-API package's license names, from its manifest and its place in the repository it builds
93
78
  // from: an addon package's is the addon its own build writes.
94
- export function nodePackageMeta({ package: manifest = {}, paths }) {
79
+ export function nodePackageMeta({ package: manifest = {}, paths, licenses }) {
95
80
  const repository = typeof manifest.repository === 'string' ? manifest.repository : (manifest.repository?.url ?? '');
96
81
  return {
97
82
  name: manifest.name,
@@ -99,5 +84,6 @@ export function nodePackageMeta({ package: manifest = {}, paths }) {
99
84
  repository,
100
85
  projectPath: path.relative(paths.base, paths.project).split(path.sep).join('/') || '.',
101
86
  addonFile: manifest.os && /\.node$/.test(manifest.main ?? '') ? manifest.main : null,
87
+ sourceTag: licenses?.sourceTag ?? null,
102
88
  };
103
89
  }
@@ -0,0 +1,50 @@
1
+ // What a package that links LGPL libraries statically tells its user: where the corresponding source is and how to
2
+ // build the artifact again with a modified library. A Node-API addon's LICENSE and a wasm module's notices both carry it.
3
+ export const OVERRIDE_PLAYBOOK = 'https://github.com/crossbind/crossbind/blob/main/docs/playbooks/override-dependencies.md';
4
+ export const DEFAULT_SOURCE_TAG = '{name}@{version}';
5
+
6
+ export function lgplRows(rows) {
7
+ return rows.filter((row) => /\bLGPL\b/.test(row.license ?? ''));
8
+ }
9
+
10
+ export const sourceTagOf = (format, name, version) => (format || DEFAULT_SOURCE_TAG)
11
+ .replaceAll('{name}', name)
12
+ .replaceAll('{version}', version);
13
+
14
+ const listOf = (names) => (names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names[0]);
15
+
16
+ const ARTIFACTS = {
17
+ addon: (file, projectPath) => ({
18
+ noun: 'addon',
19
+ verb: 'relink the addon',
20
+ where: 'Linux and Windows addons build in Docker, macOS addons on a Mac.',
21
+ result: `The build writes ${file} in ${projectPath}; it replaces the file of that name in this package.`,
22
+ }),
23
+ module: (_file, projectPath) => ({
24
+ noun: 'module',
25
+ verb: 'build the module again',
26
+ where: "The build runs in crossbind's toolchain: Docker, a remote runner or crossbind cloud.",
27
+ result: `The build writes the module's files in ${projectPath}/dist; they replace the files of those names in this package.`,
28
+ }),
29
+ };
30
+
31
+ export function relinkSection({ name, version, repository, projectPath, rows, sourceTag, artifact = 'addon', file = null }) {
32
+ const tag = sourceTagOf(sourceTag, name, version);
33
+ const repo = repository.replace(/\.git$/, '');
34
+ const { noun, verb, where, result } = ARTIFACTS[artifact](file, projectPath);
35
+ return [
36
+ '## Relinking the LGPL libraries',
37
+ '',
38
+ `This ${noun} links ${listOf(rows.map((row) => row.name))} statically; their sections below give their licenses.`,
39
+ `You may replace them with modified versions and ${verb}:`,
40
+ '',
41
+ `1. Get the corresponding source. This package was built from ${repo} at the tag ${tag}:`,
42
+ ` ${repo}/tree/${tag}`,
43
+ ' The upstream sources are the tarballs the sections below list, with their SHA-256.',
44
+ `2. In ${projectPath}, point the library at your modified source with a crossbind.overrides.js file`,
45
+ ` (${OVERRIDE_PLAYBOOK}).`,
46
+ `3. Build the packages the ${noun} links first, as their dist/ folders are not in git, then the ${noun} with`,
47
+ ` \`pnpm install\` and \`pnpm --dir ${projectPath} build\`. ${where}`,
48
+ `4. ${result}`,
49
+ ].join('\n');
50
+ }
@@ -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
+ }