crossbind 2.0.0-beta.56 → 2.0.0-beta.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,412 @@
1
+ import upath from 'upath';
2
+
3
+ // Libraries whose headers are not self-contained (spatialite expects sqlite3.h first) list the
4
+ // prerequisites in export.headerPrelude of the package that ships the header: its output or project
5
+ // tree, or a directory it lists under paths.header (the conformance kit's headers sit in a sibling
6
+ // package); the deepest root wins. An object keys the lists by include path, '*' applying to every
7
+ // header.
8
+ export function findHeaderPrelude(headerFile, configs, headerPath) {
9
+ return findHeaderOption('headerPrelude', headerFile, configs, headerPath);
10
+ }
11
+
12
+ // A library that declares what it never defines (GDAL's VRTAverageFilteredSource) lists those names in
13
+ // export.ignoredDeclarations, in the forms headerPrelude takes, so the bindings of the header still link.
14
+ export function findIgnoredDeclarations(headerFile, configs, headerPath) {
15
+ return findHeaderOption('ignoredDeclarations', headerFile, configs, headerPath);
16
+ }
17
+
18
+ function findHeaderOption(option, headerFile, configs, headerPath) {
19
+ const file = upath.normalize(headerFile);
20
+ let owner = null;
21
+ let ownerRoot = '';
22
+ for (const config of configs) {
23
+ for (const root of [config?.paths?.output, config?.paths?.project, ...(config?.paths?.header ?? [])]) {
24
+ if (!root) continue;
25
+ const normalized = upath.normalize(root);
26
+ if (file.startsWith(`${normalized}/`) && normalized.length > ownerRoot.length) {
27
+ owner = config;
28
+ ownerRoot = normalized;
29
+ }
30
+ }
31
+ }
32
+ const lists = owner?.export?.[option] ?? [];
33
+ return Array.isArray(lists) ? lists : [...(lists['*'] ?? []), ...(lists[headerPath] ?? [])];
34
+ }
35
+
36
+ export function interfaceIncludes(headerPath, prelude = []) {
37
+ return [...prelude.filter((header) => header !== headerPath), headerPath];
38
+ }
39
+
40
+ // The prelude and the completing includes reach only the compiled wrapper and swigMacros only SWIG's parse: SWIG still
41
+ // wraps just the imported header. The ignored declarations follow the macros, so the retry without macros keeps them.
42
+ export function buildInterfaceContent({ moduleName, headerPath, prelude = [], completing = [], swigMacros = [], ignored = [] }) {
43
+ const includes = [...new Set([...interfaceIncludes(headerPath, prelude), ...completing])]
44
+ .map((header) => `#include "${header}"`)
45
+ .join('\n');
46
+ const macros = swigMacros.length ? `${swigMacros.join('\n')}\n\n` : '';
47
+ const ignores = ignored.length ? `${ignored.map((name) => `%ignore ${name};`).join('\n')}\n\n` : '';
48
+ const module = moduleName.replace(/\W/g, '_');
49
+ return `#ifndef _${module}_I
50
+ #define _${module}_I
51
+
52
+ %module ${module}
53
+
54
+ %{
55
+ ${includes}
56
+ %}
57
+
58
+ %feature("shared_ptr");
59
+ %feature("polymorphic_shared_ptr");
60
+
61
+ ${macros}${ignores}%include "${headerPath}"
62
+
63
+ #endif
64
+ `;
65
+ }
66
+
67
+ export function withoutSwigMacros(content) {
68
+ return content.replace(/(%feature\("polymorphic_shared_ptr"\);\n\n)(?:#define [^\n]*\n)+\n/, '$1');
69
+ }
70
+
71
+ // SWIG exits with 1 when it rejects the interface; docker failures and killed processes exit with other codes.
72
+ const SWIG_REJECTED_EXIT_CODE = 1;
73
+
74
+ export function interfaceToRetryWithoutMacros(content, error) {
75
+ const plain = withoutSwigMacros(content);
76
+ return plain !== content && error?.cause?.status === SWIG_REJECTED_EXIT_CODE ? plain : null;
77
+ }
78
+
79
+ const TOKEN = /[A-Za-z_]\w*|\d[\w.]*|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\[\[|\]\]|##|\S/g;
80
+ const IDENTIFIER = /^[A-Za-z_]/;
81
+ const CONDITIONAL_DIRECTIVES = new Set(['if', 'elif', 'ifdef', 'ifndef', 'elifdef', 'elifndef']);
82
+ const NEUTRALIZERS = new Map([
83
+ ['__attribute__', '#define __attribute__(x)'],
84
+ ['__attribute', '#define __attribute(x)'],
85
+ ['__declspec', '#define __declspec(x)'],
86
+ ['__asm__', '#define __asm__(x)'],
87
+ ['__asm', '#define __asm(x)'],
88
+ ['_Pragma', '#define _Pragma(x)'],
89
+ ['__extension__', '#define __extension__'],
90
+ ['__restrict', '#define __restrict'],
91
+ ['__restrict__', '#define __restrict__'],
92
+ ['restrict', '#define restrict'],
93
+ ['__inline', '#define __inline inline'],
94
+ ['__inline__', '#define __inline__ inline'],
95
+ ['_Noreturn', '#define _Noreturn'],
96
+ ['__cdecl', '#define __cdecl'],
97
+ ['__stdcall', '#define __stdcall'],
98
+ ['__fastcall', '#define __fastcall'],
99
+ ['_Nullable', '#define _Nullable'],
100
+ ['_Nonnull', '#define _Nonnull'],
101
+ ['_Null_unspecified', '#define _Null_unspecified'],
102
+ ['_Thread_local', '#define _Thread_local'],
103
+ ]);
104
+ const SYNTAX_TOKENS = new Set([
105
+ 'extern', 'static', 'inline', 'const', 'volatile', 'register', 'typedef', 'virtual', 'explicit', 'friend',
106
+ 'mutable', 'constexpr', 'noexcept', 'throw', 'override', 'final', 'public', 'protected', 'private', '{', '}', ';', '[[',
107
+ ...NEUTRALIZERS.keys(),
108
+ ]);
109
+
110
+ const tokenize = (text) => text.match(TOKEN) ?? [];
111
+ const identifiers = (text) => tokenize(text).filter((token) => IDENTIFIER.test(token));
112
+ // Lines are spliced before comments go, as in the compiler, and a comment marker inside a literal is text.
113
+ const LITERAL_OR_COMMENT = /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
114
+ const stripComments = (text) => text
115
+ .replace(/\\\r?\n/g, ' ')
116
+ .replace(LITERAL_OR_COMMENT, (match) => (match.startsWith('/') ? ' ' : match));
117
+
118
+ export function parseMacroDump(text) {
119
+ const macros = new Map();
120
+ for (const line of text.split('\n')) {
121
+ const match = line.match(/^#define ([A-Za-z_]\w*)(?:\(([^)]*)\))?(.*)$/);
122
+ if (!match) continue;
123
+ const [, name, paramText, rest] = match;
124
+ const body = rest.trim();
125
+ macros.set(name, {
126
+ params: paramText === undefined ? null : paramText.split(',').map((param) => param.trim()).filter(Boolean),
127
+ body,
128
+ line: `#define ${paramText === undefined ? name : `${name}(${paramText})`}${body ? ` ${body}` : ''}`,
129
+ });
130
+ }
131
+ return macros;
132
+ }
133
+
134
+ function headerReferences(headerText) {
135
+ const declarations = new Set();
136
+ const conditions = new Set();
137
+ const defined = new Set();
138
+ for (const line of stripComments(headerText).split('\n')) {
139
+ const directive = line.match(/^\s*#\s*(\w+)(.*)$/);
140
+ if (!directive) {
141
+ identifiers(line).forEach((name) => declarations.add(name));
142
+ } else if (CONDITIONAL_DIRECTIVES.has(directive[1])) {
143
+ identifiers(directive[2]).forEach((name) => conditions.add(name));
144
+ } else if (directive[1] === 'define') {
145
+ const [, name, body] = directive[2].match(/^\s*([A-Za-z_]\w*)(?:\([^)]*\))?(.*)$/) ?? [];
146
+ if (!name) continue;
147
+ defined.add(name);
148
+ identifiers(body).forEach((token) => declarations.add(token));
149
+ }
150
+ }
151
+ return { declarations, conditions, defined };
152
+ }
153
+
154
+ // SWIG parses only the imported header, so export, linkage and attribute macros from its #includes read as syntax
155
+ // errors. Those come from the compiler's macro table, with the flags and versions its #if lines test; type and value
156
+ // macros stay undefined so every platform's compiler resolves them.
157
+ export function selectSwigMacros({ headerText, macros, predefined }) {
158
+ const memoize = (classify) => {
159
+ const results = new Map();
160
+ const check = (name) => {
161
+ if (!results.has(name)) {
162
+ results.set(name, false);
163
+ results.set(name, classify(macros.get(name), check));
164
+ }
165
+ return results.get(name);
166
+ };
167
+ return check;
168
+ };
169
+ const isSyntax = memoize((macro, check) => {
170
+ if (macro.params) return true;
171
+ const body = tokenize(macro.body);
172
+ return body.length === 0 || body.some((token, index) => SYNTAX_TOKENS.has(token)
173
+ || (macros.has(token) && (macros.get(token).params ? body[index + 1] === '(' : check(token))));
174
+ });
175
+ const isConstant = memoize((macro, check) => {
176
+ const params = new Set(macro.params ?? []);
177
+ return tokenize(macro.body).every((token) => !IDENTIFIER.test(token) || params.has(token) || token === 'defined'
178
+ || (macros.has(token) && check(token)));
179
+ });
180
+
181
+ // A macro the header defines itself stays the header's: predefining its include guard would hide the header, and
182
+ // SWIG rejects a redefinition with a different body.
183
+ const { declarations, conditions, defined } = headerReferences(headerText);
184
+ const selected = new Set();
185
+ const forward = (name, accepts) => {
186
+ if (selected.has(name) || defined.has(name) || predefined.has(name) || !macros.has(name) || !accepts(name)) return;
187
+ selected.add(name);
188
+ const params = new Set(macros.get(name).params ?? []);
189
+ identifiers(macros.get(name).body).filter((token) => !params.has(token)).forEach((token) => forward(token, accepts));
190
+ };
191
+ declarations.forEach((name) => forward(name, isSyntax));
192
+ conditions.forEach((name) => forward(name, isConstant));
193
+
194
+ const visible = new Set([
195
+ ...tokenize(stripComments(headerText)),
196
+ ...[...selected].flatMap((name) => tokenize(macros.get(name).body)),
197
+ ]);
198
+ const neutralizers = [...NEUTRALIZERS]
199
+ .filter(([token]) => visible.has(token) && !selected.has(token) && !defined.has(token))
200
+ .map(([, line]) => line);
201
+ return [...neutralizers, ...[...selected].sort().map((name) => macros.get(name).line)];
202
+ }
203
+
204
+ const SCOPE_TOKEN = /[A-Za-z_]\w*|::|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\S/g;
205
+ const CLASS_KEYWORDS = new Set(['class', 'struct', 'union']);
206
+ const ATTRIBUTE_CALLS = new Set(['__attribute__', '__attribute', '__declspec', 'alignas']);
207
+ const TYPE_QUALIFIERS = new Set(['const', 'volatile', 'class', 'struct', 'union', 'enum', 'typename']);
208
+ const BLOCK = { kind: 'block', names: [] };
209
+ // An export or finality macro can follow the class name, as in GDAL's `class CPL_DLL MEMDataset CPL_NON_FINAL : ...`.
210
+ const isTrailingMacro = (group) => group.length === 1 && (group[0] === 'final' || /^[A-Z][A-Z\d]*_[A-Z\d_]*$/.test(group[0]));
211
+
212
+ const BRANCH_MARKERS = new Map([
213
+ ['if', '#if'], ['ifdef', '#if'], ['ifndef', '#if'],
214
+ ['elif', '#else'], ['elifdef', '#else'], ['elifndef', '#else'], ['else', '#else'],
215
+ ['endif', '#endif'],
216
+ ]);
217
+
218
+ const codeTokens = (text) => stripComments(text).split('\n').flatMap((line) => {
219
+ const directive = line.match(/^\s*#\s*(\w*)/);
220
+ if (!directive) return line.match(SCOPE_TOKEN) ?? [];
221
+ return BRANCH_MARKERS.has(directive[1]) ? [BRANCH_MARKERS.get(directive[1])] : [];
222
+ });
223
+
224
+ function closingIndex(tokens, index) {
225
+ const [open, close] = { '[': ['[', ']'], '{': ['{', '}'] }[tokens[index]] ?? ['(', ')'];
226
+ let depth = 0;
227
+ for (let at = index; at < tokens.length; at += 1) {
228
+ if (tokens[at] === open) depth += 1;
229
+ else if (tokens[at] === close && (depth -= 1) === 0) return at;
230
+ }
231
+ return tokens.length;
232
+ }
233
+
234
+ // The name segments a class head defines, or null for a specialization, an enum or a function returning an elaborated type.
235
+ function classHeadName(head) {
236
+ let depth = 0;
237
+ let keyword = -1;
238
+ for (let index = 0; index < head.length && keyword < 0; index += 1) {
239
+ if (head[index] === '<') depth += 1;
240
+ else if (head[index] === '>') depth -= 1;
241
+ else if (depth === 0 && CLASS_KEYWORDS.has(head[index]) && head[index - 1] !== 'enum') keyword = index;
242
+ }
243
+ if (keyword < 0) return null;
244
+ const groups = [];
245
+ for (let index = keyword + 1; index < head.length && head[index] !== ':'; index += 1) {
246
+ const token = head[index];
247
+ if (token === '[' || ATTRIBUTE_CALLS.has(token)) {
248
+ index = closingIndex(head, token === '[' ? index : index + 1);
249
+ } else if (token !== '::') {
250
+ if (!IDENTIFIER.test(token)) return null;
251
+ if (head[index - 1] === '::' && groups.length) groups.at(-1).push(token);
252
+ else groups.push([token]);
253
+ }
254
+ }
255
+ while (groups.length > 1 && isTrailingMacro(groups.at(-1))) groups.pop();
256
+ return groups.at(-1) ?? null;
257
+ }
258
+
259
+ // The name segments an enum head defines (`enum class Location : char`), or null for an anonymous enum.
260
+ function enumHeadName(head) {
261
+ const keyword = head.indexOf('enum');
262
+ if (keyword < 0) return null;
263
+ const segments = [];
264
+ for (let index = keyword + 1; index < head.length && head[index] !== ':'; index += 1) {
265
+ const token = head[index];
266
+ if (token === 'class' || token === 'struct' || token === '::') continue;
267
+ if (!IDENTIFIER.test(token) || (segments.length && head[index - 1] !== '::')) return null;
268
+ segments.push(token);
269
+ }
270
+ return segments.length ? segments : null;
271
+ }
272
+
273
+ // The names a typedef gives the type whose body opens at openIndex: `} GDALRPCInfoV2, *GDALRPCInfoPtr;` names only
274
+ // GDALRPCInfoV2, since a pointer typedef names no type.
275
+ function typedefNames(tokens, openIndex) {
276
+ const names = [];
277
+ for (let index = closingIndex(tokens, openIndex) + 1; index < tokens.length && tokens[index] !== ';'; index += 1) {
278
+ if (IDENTIFIER.test(tokens[index]) && tokens[index - 1] !== '*' && tokens[index - 1] !== '&') names.push(tokens[index]);
279
+ }
280
+ return names;
281
+ }
282
+
283
+ // The tokens since the previous ;, { or } tell what a { opens: namespaces and classes qualify the names inside, extern "C"
284
+ // is transparent, and anything else (a function body, an enum, an initializer) is a block.
285
+ function openedScope(head) {
286
+ if (head.at(-2) === 'extern' && head.at(-1).startsWith('"')) return { kind: 'extern', names: [] };
287
+ const namespace = head.lastIndexOf('namespace');
288
+ if (namespace >= 0) {
289
+ const names = head.slice(namespace + 1).filter((token) => token !== '::' && token !== 'inline');
290
+ return names.every((name) => IDENTIFIER.test(name)) ? { kind: 'namespace', names } : BLOCK;
291
+ }
292
+ const names = classHeadName(head);
293
+ return names ? { kind: 'class', names } : BLOCK;
294
+ }
295
+
296
+ // Each #else or #elif branch restarts from the scopes open at its #if: branches that each open a scope closed after
297
+ // #endif (GEOS's LineIntersector.h) would otherwise leave one open for the rest of the file.
298
+ function walkScopes(text, visit) {
299
+ const tokens = codeTokens(text);
300
+ const branches = [];
301
+ let scopes = [];
302
+ let head = [];
303
+ tokens.forEach((token, index) => {
304
+ if (token === '#if') {
305
+ branches.push({ scopes: [...scopes], head: [...head] });
306
+ } else if (token === '#else') {
307
+ if (branches.length) [scopes, head] = [[...branches.at(-1).scopes], [...branches.at(-1).head]];
308
+ } else if (token === '#endif') {
309
+ branches.pop();
310
+ } else {
311
+ const opening = token === '{' ? head : null;
312
+ if (token === '{') scopes.push(openedScope(head));
313
+ if (token === '}') scopes.pop();
314
+ if (token === '{' || token === '}' || token === ';') head = [];
315
+ else head.push(token);
316
+ visit(token, index, tokens, scopes, opening);
317
+ }
318
+ });
319
+ }
320
+
321
+ // Classes, structs and enums by qualified name, including the names C typedefs give them, each mapped to the first header
322
+ // defining it. Preprocessor branches are all read.
323
+ export function indexTypeDefinitions(files) {
324
+ const definitions = new Map();
325
+ for (const { path, text } of files) {
326
+ const record = (segments) => {
327
+ const name = segments.join('::');
328
+ if (!definitions.has(name)) definitions.set(name, path);
329
+ };
330
+ walkScopes(text, (token, index, tokens, scopes, opening) => {
331
+ const outer = scopes.slice(0, -1);
332
+ if (token !== '{' || outer.some((scope) => scope.kind === 'block')) return;
333
+ const enclosing = outer.flatMap((scope) => scope.names);
334
+ const opened = scopes.at(-1);
335
+ if (opened.kind === 'class') record([...enclosing, ...opened.names]);
336
+ const enumName = enumHeadName(opening);
337
+ if (enumName) record([...enclosing, ...enumName]);
338
+ const isTypeBody = opened.kind === 'class' || opening.some((head) => head === 'enum' || CLASS_KEYWORDS.has(head));
339
+ if (opening.includes('typedef') && isTypeBody) typedefNames(tokens, index).forEach((name) => record([...enclosing, name]));
340
+ });
341
+ }
342
+ return definitions;
343
+ }
344
+
345
+ function heldClassName(tokens, start) {
346
+ const parts = [];
347
+ let isGlobal = false;
348
+ for (let index = start; index < tokens.length; index += 1) {
349
+ const token = tokens[index];
350
+ if (token === '::') {
351
+ isGlobal ||= parts.length === 0;
352
+ } else if (!TYPE_QUALIFIERS.has(token)) {
353
+ if (!IDENTIFIER.test(token) || (parts.length && tokens[index - 1] !== '::')) break;
354
+ parts.push(token);
355
+ }
356
+ }
357
+ return parts.length ? { name: parts.join('::'), isGlobal } : null;
358
+ }
359
+
360
+ // As in C++ lookup, the innermost declaration of a name wins, and an alias names another type.
361
+ function resolveType({ name, isGlobal }, enclosing, definitions, aliases, visited = new Set()) {
362
+ const candidates = isGlobal ? [name]
363
+ : [...enclosing.map((_, depth) => [...enclosing.slice(0, enclosing.length - depth), name].join('::')), name];
364
+ for (const candidate of candidates) {
365
+ if (definitions.has(candidate)) return candidate;
366
+ const alias = aliases.get(candidate);
367
+ if (alias) {
368
+ return visited.has(candidate) ? null
369
+ : resolveType(alias.target, alias.enclosing, definitions, aliases, new Set([...visited, candidate]));
370
+ }
371
+ }
372
+ return null;
373
+ }
374
+
375
+ // The other headers defining the types `pick` names in a header, looked up through its enclosing namespaces and classes
376
+ // and its alias and using declarations.
377
+ function definingHeaders({ headerText, headerPath, definitions }, pick) {
378
+ const headers = new Set();
379
+ const aliases = new Map();
380
+ walkScopes(headerText, (token, index, tokens, scopes) => {
381
+ const enclosing = () => scopes.flatMap((scope) => scope.names);
382
+ if (token === 'using' && tokens[index + 1] !== 'namespace') {
383
+ const isAlias = tokens[index + 2] === '=';
384
+ const target = heldClassName(tokens, index + (isAlias ? 3 : 1));
385
+ const name = isAlias ? tokens[index + 1] : target?.name.split('::').at(-1);
386
+ if (target) aliases.set([...enclosing(), name].join('::'), { target, enclosing: enclosing() });
387
+ return;
388
+ }
389
+ const picked = pick(token, index, tokens, scopes);
390
+ const found = picked && resolveType(picked, enclosing(), definitions, aliases);
391
+ if (found && definitions.get(found) !== headerPath) headers.add(definitions.get(found));
392
+ });
393
+ return [...headers];
394
+ }
395
+
396
+ // The bridge destroys what a std::unique_ptr holds, so that class must be complete there even when the header only
397
+ // declares it (GEOS's GeometryNoder holds a Noder).
398
+ export function completingIncludes(context) {
399
+ return definingHeaders(context, (token, index, tokens) => {
400
+ const isStd = tokens[index - 1] !== '::' || tokens[index - 2] === 'std';
401
+ return token === 'unique_ptr' && tokens[index + 1] === '<' && isStd ? heldClassName(tokens, index + 2) : null;
402
+ });
403
+ }
404
+
405
+ // The headers defining the classes and enums a header's declarations use outside function bodies: embind registers each
406
+ // type once, in the bridge of the header defining it (GDAL's CPLErr lives in cpl_error.h).
407
+ export function referencedTypeHeaders(context) {
408
+ return definingHeaders(context, (token, index, tokens, scopes) => {
409
+ if (!IDENTIFIER.test(token) || tokens[index - 1] === '::' || scopes.some((scope) => scope.kind === 'block')) return null;
410
+ return heldClassName(tokens, index);
411
+ });
412
+ }