crossbind 2.0.0-beta.60 → 2.0.0-beta.63

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.
Files changed (118) hide show
  1. package/README.md +8 -6
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +37 -9
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildJs.js +1 -1
  6. package/src/actions/buildLib.js +27 -8
  7. package/src/actions/buildNativeCommand.js +84 -0
  8. package/src/actions/buildNode.js +190 -0
  9. package/src/actions/buildTypes.js +8 -2
  10. package/src/actions/buildWasiCommand.js +0 -4
  11. package/src/actions/buildWasm.js +92 -95
  12. package/src/actions/cloudCommands.js +133 -0
  13. package/src/actions/createImportedBridges.js +64 -0
  14. package/src/actions/createInterface.js +318 -65
  15. package/src/actions/createLib.js +60 -43
  16. package/src/actions/getAllBridges.js +1 -1
  17. package/src/actions/getData.js +4 -1
  18. package/src/actions/getLinkInputs.js +70 -0
  19. package/src/actions/isSourceNewer.js +2 -0
  20. package/src/actions/licenses.js +175 -17
  21. package/src/actions/prepareConanDependencies.js +51 -0
  22. package/src/actions/publishBinary.js +32 -0
  23. package/src/actions/run.js +124 -32
  24. package/src/actions/runnerCommands.js +421 -0
  25. package/src/actions/writeRuntimeEntry.js +69 -0
  26. package/src/assets/cmake/CMakeLists.txt +1 -1
  27. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  28. package/src/assets/cmake/command/command.cpp +1 -0
  29. package/src/assets/cmake/dist.cmake +2 -1
  30. package/src/assets/configure/install.sh +22 -0
  31. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  32. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  33. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -56
  34. package/src/assets/js-runtime/core.js +11 -0
  35. package/src/assets/node-import-hooks.mjs +61 -0
  36. package/src/assets/toolchain-digests.json +27 -13
  37. package/src/assets/wasi-runtime/stubs.c +29 -0
  38. package/src/bin.js +290 -37
  39. package/src/index.js +6 -1
  40. package/src/integration/bindHeaderImports.js +18 -0
  41. package/src/integration/getCrossbindScript.js +53 -19
  42. package/src/integration/getDependFilePath.js +18 -12
  43. package/src/integration/resolveNativeImport.js +14 -0
  44. package/src/node/dev.mjs +19 -0
  45. package/src/runner/blobs.js +135 -0
  46. package/src/runner/files.js +193 -0
  47. package/src/runner/server.js +320 -0
  48. package/src/state/attachConanDependencies.js +56 -0
  49. package/src/state/index.js +31 -20
  50. package/src/state/loadConfig.js +36 -24
  51. package/src/state/refreshBuiltDependencies.js +20 -0
  52. package/src/state/refreshConanDependencies.js +17 -0
  53. package/src/utils/androidToolchain.js +4 -0
  54. package/src/utils/appRustCrates.js +17 -1
  55. package/src/utils/appSources.js +112 -0
  56. package/src/utils/archiveFlags.js +12 -0
  57. package/src/utils/boundHeaders.js +79 -0
  58. package/src/utils/bridgeExtras.js +486 -0
  59. package/src/utils/bridgeRuntimeFixes.js +48 -0
  60. package/src/utils/bundledLicenses.js +26 -0
  61. package/src/utils/cargoImport.js +44 -0
  62. package/src/utils/cargoTarget.js +23 -3
  63. package/src/utils/cloudAccount.js +55 -0
  64. package/src/utils/conanDependencies.js +66 -0
  65. package/src/utils/conanImport.js +58 -0
  66. package/src/utils/conanInstall.js +154 -0
  67. package/src/utils/conanProfile.js +203 -0
  68. package/src/utils/conanStage.js +292 -0
  69. package/src/utils/copyToDistSource.js +13 -0
  70. package/src/utils/cppDts.js +134 -55
  71. package/src/utils/darwinToolchain.js +11 -0
  72. package/src/utils/dirLock.js +137 -28
  73. package/src/utils/downloadAndExtractFile.js +34 -7
  74. package/src/utils/embindArgumentGuards.js +84 -0
  75. package/src/utils/embindCallArguments.js +19 -0
  76. package/src/utils/embindJspiQueue.js +25 -0
  77. package/src/utils/embindRsFingerprint.js +26 -0
  78. package/src/utils/execContainer.js +49 -0
  79. package/src/utils/headerImports.js +56 -0
  80. package/src/utils/hostPlatform.js +21 -0
  81. package/src/utils/inputStamp.js +23 -0
  82. package/src/utils/iosToolchain.js +6 -0
  83. package/src/utils/isSourceCmakePackage.js +10 -0
  84. package/src/utils/licenseReport.js +11 -2
  85. package/src/utils/linkLayout.js +8 -1
  86. package/src/utils/linuxToolchain.js +24 -0
  87. package/src/utils/makeTreeWritable.js +12 -0
  88. package/src/utils/nativeImports.js +23 -0
  89. package/src/utils/nativeSourceStamp.js +36 -0
  90. package/src/utils/nodeDevBuild.js +92 -0
  91. package/src/utils/packageLicense.js +101 -0
  92. package/src/utils/pthreadMainScript.js +14 -0
  93. package/src/utils/pullDockerImage.js +10 -5
  94. package/src/utils/relocatePrebuilt.js +119 -0
  95. package/src/utils/remoteClient.js +127 -0
  96. package/src/utils/remoteOutputs.js +39 -0
  97. package/src/utils/remoteRunner.js +97 -0
  98. package/src/utils/remoteUpload.js +134 -0
  99. package/src/utils/replaceFile.js +8 -0
  100. package/src/utils/resolveCompanionPackage.js +51 -0
  101. package/src/utils/resolveEmbindNapi.js +22 -0
  102. package/src/utils/resolveEmbindRust.js +17 -52
  103. package/src/utils/runCargo.js +51 -53
  104. package/src/utils/runConan.js +257 -0
  105. package/src/utils/runtimeEntries.js +56 -0
  106. package/src/utils/rustBridgeGen.js +903 -253
  107. package/src/utils/rustMt.js +4 -0
  108. package/src/utils/scopedEmbind.js +22 -0
  109. package/src/utils/selectRunner.js +81 -0
  110. package/src/utils/sourceFingerprint.js +4 -3
  111. package/src/utils/swigInterface.js +86 -6
  112. package/src/utils/systemKeys.js +43 -3
  113. package/src/utils/targets.js +104 -3
  114. package/src/utils/toolchainNotices.js +5 -0
  115. package/src/utils/wasiToolchain.js +14 -1
  116. package/src/utils/windowsToolchain.js +18 -0
  117. package/vitest.config.js +11 -0
  118. package/src/utils/cppFieldBindings.js +0 -23
@@ -0,0 +1,486 @@
1
+ import { ALL_NAMES } from './headerImports.js';
2
+ import { parseMacroDump, stripComments, declaresAtFileScope } from './swigInterface.js';
3
+
4
+ // SWIG binds no function-like macro, variadic function or mutable global. crossbind binds them for the names an app
5
+ // imports by name: a function-like macro takes the parameter types of the one function it calls, a macro naming a
6
+ // function binds that function under its own name, a variadic function takes its extra arguments by their JavaScript
7
+ // type, and a mutable global binds as a handle to its address. A macro binds only through a function SWIG bound, the
8
+ // one place that proves the name is a single declared function.
9
+
10
+ const C_IDENTIFIER = /^[A-Za-z_]\w*$/;
11
+ const MACRO_LINE = /^\/\/ crossbind:macro ([A-Za-z_]\w*) (#define .*)$/gm;
12
+ const ALIAS_LINE = /^\/\/ crossbind:alias ([A-Za-z_]\w*) ([A-Za-z_]\w*)$/gm;
13
+ const NOTE_MARKER = '// crossbind:note ';
14
+ const EXTRA_LINES = /^(?:\/\/ crossbind:(?:macro|alias|note) .*\n)+\n/m;
15
+ // The arguments one call may add to a variadic function: each registration count dispatches over 2^n argument kinds.
16
+ const MAX_VARIADIC_ARGUMENTS = 6;
17
+ const VARIADIC_WARNING = /^Variable length arguments are not supported by embind, (\S+) skipped\.$/;
18
+ const GLOBAL_WARNING = /^Global (\S+) is not const, so embind cannot bind it as a constant; skipped\.$/;
19
+ const CAST = /^\(\s*[A-Za-z_]\w*(?:\s+[A-Za-z_]\w*)*\s*\**\s*\)\s*(.+)$/;
20
+ const C_KEYWORDS = new Set([
21
+ 'auto', 'bool', 'char', 'const', 'double', 'enum', 'extern', 'float', 'int', 'long', 'register', 'restrict', 'short',
22
+ 'signed', 'static', 'struct', 'typedef', 'union', 'unsigned', 'void', 'volatile', 'inline', 'true', 'false', 'nullptr',
23
+ ]);
24
+ export const POINTER_RUNTIME_ANCHOR = 'crossbindPointerRuntime';
25
+
26
+ // The index of the quote that closes the string or character literal opening at `at`.
27
+ function literalEnd(text, at) {
28
+ let end = at + 1;
29
+ while (end < text.length && text[end] !== text[at]) end += text[end] === '\\' ? 2 : 1;
30
+ return end;
31
+ }
32
+
33
+ function closingParen(text, open) {
34
+ let depth = 0;
35
+ for (let at = open; at < text.length; at += 1) {
36
+ if (text[at] === '"' || text[at] === "'") at = literalEnd(text, at);
37
+ else if (text[at] === '(') depth += 1;
38
+ else if (text[at] === ')' && (depth -= 1) === 0) return at;
39
+ }
40
+ return -1;
41
+ }
42
+
43
+ const isWrapped = (text) => text.startsWith('(') && closingParen(text, 0) === text.length - 1;
44
+
45
+ function splitArguments(text) {
46
+ const parts = [];
47
+ let depth = 0;
48
+ let start = 0;
49
+ for (let at = 0; at < text.length; at += 1) {
50
+ if (text[at] === '"' || text[at] === "'") at = literalEnd(text, at);
51
+ else if (text[at] === '(') depth += 1;
52
+ else if (text[at] === ')') depth -= 1;
53
+ else if (text[at] === ',' && depth === 0) {
54
+ parts.push(text.slice(start, at));
55
+ start = at + 1;
56
+ }
57
+ }
58
+ parts.push(text.slice(start));
59
+ return parts.map((part) => part.trim());
60
+ }
61
+
62
+ // `(pp)`, `(char *)(pp)` and `(void *)pp` all pass pp whole.
63
+ function wholeArgument(argument) {
64
+ let text = argument;
65
+ for (;;) {
66
+ if (isWrapped(text)) {
67
+ text = text.slice(1, -1).trim();
68
+ } else {
69
+ const cast = text.match(CAST);
70
+ if (!cast) return text;
71
+ text = cast[1].trim();
72
+ }
73
+ }
74
+ }
75
+
76
+ // The one function a function-like macro calls, and where each of its parameters goes in that call; null for any
77
+ // other body.
78
+ export function macroCall(macro) {
79
+ if (!macro?.params || macro.params.some((param) => !C_IDENTIFIER.test(param))) return null;
80
+ let body = macro.body.trim();
81
+ while (isWrapped(body)) body = body.slice(1, -1).trim();
82
+ const open = body.indexOf('(');
83
+ const callee = body.slice(0, Math.max(open, 0)).trim();
84
+ if (open < 0 || !C_IDENTIFIER.test(callee) || closingParen(body, open) !== body.length - 1) return null;
85
+ const args = body.slice(open + 1, -1).trim() ? splitArguments(body.slice(open + 1, -1)) : [];
86
+ const positions = macro.params.map((param) => args.findIndex((arg) => wholeArgument(arg) === param));
87
+ return positions.every((position) => position >= 0) ? { callee, positions } : null;
88
+ }
89
+
90
+ // The identifier an object-like macro stands for, as in `#define iconv_open libiconv_open`.
91
+ const identifierOf = (macro) => (macro && !macro.params && C_IDENTIFIER.test(macro.body) ? macro.body : null);
92
+
93
+ // What the interface carries for the names an app imports by name, read by the bridge once SWIG has run: their
94
+ // function-like macros and their macros naming a function, and the functions they reach, which SWIG has to bind.
95
+ export function interfaceExtras(names, macros) {
96
+ if (names === ALL_NAMES || !names.length) return { lines: [], functions: [] };
97
+ const lines = [];
98
+ const functions = [];
99
+ for (const name of names) {
100
+ const macro = macros.get(name);
101
+ if (!macro) continue;
102
+ if (!macro.params) {
103
+ const target = identifierOf(macro);
104
+ if (target && target !== name && !macros.has(target) && !C_KEYWORDS.has(target)) {
105
+ lines.push(`// crossbind:alias ${name} ${target}`);
106
+ functions.push(target);
107
+ }
108
+ continue;
109
+ }
110
+ const call = macroCall(macro);
111
+ const callee = call && macros.get(call.callee);
112
+ if (callee?.params) {
113
+ lines.push(`${NOTE_MARKER}${name}: a function-like macro binds only when it calls a function, not the macro ${call.callee}; skipped.`);
114
+ } else {
115
+ // The compiler expands a callee that is an object-like macro, while SWIG knows the function by its target.
116
+ const declared = (call && identifierOf(callee)) ?? call?.callee ?? name;
117
+ lines.push(`// crossbind:macro ${declared} ${macro.line}`);
118
+ if (call) functions.push(declared);
119
+ }
120
+ }
121
+ return { lines, functions };
122
+ }
123
+
124
+ // A function SWIG registered once; one it bound for a single argument count of several overloads warns about the rest.
125
+ const bindsOneFunction = (bridgeText, warnings, name) => bridgeText.split(`emscripten::function("${name}",`).length === 2
126
+ && !warnings.some((line) => line.includes(`another overload of ${name} taking`));
127
+
128
+ // A function whose extra arguments arrive as a va_list cannot be called with them.
129
+ const takesVaList = (headerText, name) => new RegExp(`\\b${name}\\b[^;{}]*\\bva_list\\b`).test(stripComments(headerText));
130
+
131
+ function warningsAbout(warnings, pattern, imported) {
132
+ const found = new Map();
133
+ for (const line of warnings) {
134
+ const name = line.match(/^.*:\d+: (.*)$/)?.[1].match(pattern)?.[1];
135
+ if (name && imported.has(name)) found.set(name, line);
136
+ }
137
+ return found;
138
+ }
139
+
140
+ const macroStruct = (name, params) => (params.length ? `struct crossbind_macro_${name} {
141
+ template<${params.map((_, i) => `typename A${i}`).join(', ')}>
142
+ static auto call(${params.map((_, i) => `A${i} a${i}`).join(', ')}) { return ${name}(${params.map((_, i) => `a${i}`).join(', ')}); }
143
+ };` : `struct crossbind_macro_${name} {
144
+ static auto call() { return ${name}(); }
145
+ };`);
146
+
147
+ // What the bridge of `interfaceText` adds to SWIG's output in `bridgeText` for the names the app imports by name: the C++
148
+ // to append, the names it exports, the warnings it answers and the ones it adds.
149
+ export function bridgeExtras({ interfaceText = '', named = [], bridgeText = '', warnings = [], headerText = '', module }) {
150
+ const imported = new Set(named);
151
+ const notes = interfaceText.split('\n').filter((line) => line.startsWith(NOTE_MARKER)).map((line) => line.slice(NOTE_MARKER.length));
152
+ const macros = [];
153
+ for (const [, declared, line] of interfaceText.matchAll(MACRO_LINE)) {
154
+ const [[name, macro]] = parseMacroDump(line);
155
+ const call = macroCall(macro);
156
+ if (!call) notes.push(`${name}: a function-like macro binds only when its body calls one function with each of its parameters as a whole argument; skipped.`);
157
+ else if (!bindsOneFunction(bridgeText, warnings, declared)) notes.push(`${name}: a function-like macro binds only when it calls one function the header binds, which ${declared} is not; skipped.`);
158
+ else macros.push({ name, params: macro.params, ...call });
159
+ }
160
+ const aliases = [];
161
+ for (const [, name, target] of interfaceText.matchAll(ALIAS_LINE)) {
162
+ if (bindsOneFunction(bridgeText, warnings, target)) aliases.push({ name, target });
163
+ // A macro naming an enum member SWIG binds as a constant already.
164
+ else if (!bridgeText.includes(`bindConstant("${name}"`)) notes.push(`${name}: a macro naming ${target} binds only when ${target} is one function the header binds; skipped.`);
165
+ }
166
+ const variadicWarnings = warningsAbout(warnings, VARIADIC_WARNING, imported);
167
+ const variadics = [...variadicWarnings.keys()].filter((name) => !takesVaList(headerText, name));
168
+ const globalWarnings = warningsAbout(warnings, GLOBAL_WARNING, imported);
169
+ const globals = [...globalWarnings.keys()].filter((name) => declaresAtFileScope(headerText, name));
170
+ const answered = new Set([...variadics.map((name) => variadicWarnings.get(name)), ...globals.map((name) => globalWarnings.get(name))]);
171
+ if (!macros.length && !aliases.length && !variadics.length && !globals.length) return { code: '', exports: [], answered, notes };
172
+
173
+ // The macros register ahead of the fallbacks below, which would make a function a variadic shares its name with an
174
+ // overload set.
175
+ const code = [
176
+ EXTRAS_RUNTIME,
177
+ ...macros.map(({ name, params }) => macroStruct(name, params)),
178
+ `EMSCRIPTEN_BINDINGS(CrossbindMacros_${module}) {`,
179
+ ...macros.map(({ name, callee, positions }) => ` crossbind::bindMacro<crossbind_macro_${name}, decltype(&${callee})${positions.map((p) => `, ${p}`).join('')}>("${name}");`),
180
+ ...aliases.map(({ name, target }) => ` crossbind::bindAlias<decltype(&${target}), &${target}>("${name}");`),
181
+ '}',
182
+ // A name SWIG reported only for a class member reaches this fallback, which binds nothing.
183
+ ...variadics.map((name) => `inline crossbind::Unbound (${name})(crossbind::Unbound) { return {}; }`),
184
+ `EMSCRIPTEN_BINDINGS(CrossbindExtras_${module}) {`,
185
+ ...variadics.map((name) => ` { using F = decltype(crossbind::variadicType(&${name}, 0)); crossbind::bindVariadic<F, static_cast<F>(&${name}), ${MAX_VARIADIC_ARGUMENTS}>("${name}"); }`),
186
+ ...globals.map((name) => ` crossbind::bindGlobal("${name}", &${name});`),
187
+ '}',
188
+ '',
189
+ ].join('\n');
190
+ const exports = [...macros.map(({ name }) => name), ...aliases.map(({ name }) => name), ...variadics, ...globals, ...(variadics.length ? ['vaDouble'] : [])];
191
+ return { code, exports, answered, notes };
192
+ }
193
+
194
+ export const hasPointerRuntime = (bridgeText) => bridgeText.includes('struct NativePointer {');
195
+
196
+ // SWIG writes its pointer runtime only into a bridge with a pointer binding, so a bridge whose extras need it binds
197
+ // this function too, and the export list leaves it out.
198
+ const POINTER_RUNTIME_BINDING = `%rename("%s") ${POINTER_RUNTIME_ANCHOR};
199
+ %inline %{
200
+ static void ${POINTER_RUNTIME_ANCHOR}(void *) {}
201
+ %}
202
+
203
+ `;
204
+ export const withPointerRuntimeAnchor = (interfaceText) => interfaceText.replace(/^%include\b/m, `${POINTER_RUNTIME_BINDING}%include`);
205
+
206
+ // What the interface carries for the bridge alone, which a SWIG run over the whole header for the editor leaves out.
207
+ export const withoutBridgeExtras = (interfaceText) => interfaceText.replace(EXTRA_LINES, '').replace(POINTER_RUNTIME_BINDING, '');
208
+
209
+ export const EXTRAS_RUNTIME = `// crossbind extras
210
+ #include <array>
211
+ #include <cmath>
212
+ #include <cstdint>
213
+ #include <cstring>
214
+ #include <limits>
215
+ #include <stdexcept>
216
+ #include <string>
217
+ #include <tuple>
218
+ #include <type_traits>
219
+ #include <utility>
220
+ namespace crossbind {
221
+ struct Unbound {};
222
+ template<size_t> using JsValue = emscripten::val;
223
+
224
+ // A number, a boolean, or an enum member, which embind hands over as an object with a numeric value.
225
+ inline bool readNumber(const emscripten::val &value, double &number) {
226
+ if (value.isNumber()) number = value.as<double>();
227
+ else if (value.isTrue() || value.isFalse()) number = value.isTrue() ? 1 : 0;
228
+ else if (value.isNull() || value.isUndefined() || value.typeOf().as<std::string>() != "object" || !value["value"].isNumber()) return false;
229
+ else number = value["value"].as<double>();
230
+ return true;
231
+ }
232
+
233
+ // A BigInt is compared as one: converting first would wrap a value beyond 64 bits.
234
+ template<typename Wide> Wide bigIntFromJs(const emscripten::val &value, Wide min, Wide max) {
235
+ if (value < emscripten::val(min) || value > emscripten::val(max)) throw std::range_error("crossbind: an integer argument is out of range");
236
+ return value.as<Wide>();
237
+ }
238
+
239
+ // 2^digits is one past the largest value of I, and the negative of its smallest when it is signed; a double holds both.
240
+ template<typename I> bool fitsInteger(double number) {
241
+ const double limit = std::ldexp(1.0, std::numeric_limits<I>::digits);
242
+ return number < limit && number >= (std::is_signed_v<I> ? -limit : 0.0);
243
+ }
244
+
245
+ template<typename I> I integerFromJs(const emscripten::val &value) {
246
+ double number = 0;
247
+ if (!value.isNumber() && value.typeOf().as<std::string>() == "bigint") {
248
+ using Wide = std::conditional_t<std::is_signed_v<I>, long long, unsigned long long>;
249
+ return static_cast<I>(bigIntFromJs<Wide>(value, std::numeric_limits<I>::min(), std::numeric_limits<I>::max()));
250
+ }
251
+ if (!readNumber(value, number) || std::trunc(number) != number) throw std::invalid_argument("crossbind: an integer argument takes an integer, a BigInt or an enum member");
252
+ if (!fitsInteger<I>(number)) throw std::range_error("crossbind: an integer argument is out of range");
253
+ return static_cast<I>(number);
254
+ }
255
+
256
+ template<typename T> constexpr bool isJsArgument = std::is_pointer_v<std::remove_cv_t<T>> || std::is_enum_v<std::remove_cv_t<T>>
257
+ || (std::is_arithmetic_v<std::remove_cv_t<T>> && !std::is_same_v<std::remove_cv_t<T>, long double>);
258
+ template<typename R> constexpr bool isJsResult = std::is_void_v<R> || isJsArgument<R>;
259
+
260
+ // What converts to the parameter: a CStringArg keeps a string's copy alive for the call.
261
+ template<typename T> auto argumentFromJs(const emscripten::val &value) {
262
+ using U = std::remove_cv_t<T>;
263
+ if constexpr (std::is_same_v<U, const char *>) return fromJsCString<const char>(value);
264
+ else if constexpr (std::is_pointer_v<U>) return fieldFromJs<U>(value);
265
+ else if constexpr (std::is_same_v<U, bool>) {
266
+ double number = 0;
267
+ if (!readNumber(value, number)) throw std::invalid_argument("crossbind: a bool argument takes a boolean or a number");
268
+ return number != 0;
269
+ } else if constexpr (std::is_enum_v<U>) return static_cast<U>(integerFromJs<std::underlying_type_t<U>>(value));
270
+ else if constexpr (std::is_integral_v<U>) return integerFromJs<U>(value);
271
+ else {
272
+ double number = 0;
273
+ if (!readNumber(value, number)) throw std::invalid_argument("crossbind: a floating-point argument takes a number");
274
+ return static_cast<U>(number);
275
+ }
276
+ }
277
+
278
+ template<typename R> auto resultToJs(R result) {
279
+ using U = std::remove_cv_t<R>;
280
+ if constexpr (std::is_same_v<U, const char *>) return toJsCString(result);
281
+ else if constexpr (std::is_pointer_v<U>) return toJs(result);
282
+ else if constexpr (std::is_enum_v<U>) return static_cast<std::underlying_type_t<U>>(result);
283
+ else return result;
284
+ }
285
+
286
+ template<typename Call, size_t... I> void registerCall(const char *name, std::index_sequence<I...>) {
287
+ if (!claimFunction(name, sizeof...(I))) return;
288
+ emscripten::function(name, +[](JsValue<I>... args) { return Call::invoke(std::array<emscripten::val, sizeof...(I)>{args...}); }, emscripten::allow_raw_pointers());
289
+ }
290
+
291
+ template<size_t... I> void registerUnconvertible(const char *name, std::index_sequence<I...>) {
292
+ if (!claimFunction(name, sizeof...(I))) return;
293
+ emscripten::function(name, +[](JsValue<I>...) {
294
+ throw std::invalid_argument("crossbind: this binding takes or returns a type only a C++ wrapper reaches (numbers, enums, booleans, strings and pointers cross)");
295
+ });
296
+ }
297
+
298
+ template<typename F> struct Signature;
299
+ template<typename R, typename... A> struct Signature<R (*)(A...)> { using Result = R; using Params = std::tuple<A...>; };
300
+ template<typename R, typename... A> struct Signature<R (*)(A..., ...)> { using Result = R; using Params = std::tuple<A...>; };
301
+ template<typename R, typename... A> struct Signature<R (*)(A...) noexcept> : Signature<R (*)(A...)> {};
302
+ template<typename R, typename... A> struct Signature<R (*)(A..., ...) noexcept> : Signature<R (*)(A..., ...)> {};
303
+ template<typename Params, size_t P, typename = void> struct ParamAt { using type = Unbound; };
304
+ template<typename Params, size_t P> struct ParamAt<Params, P, std::enable_if_t<(P < std::tuple_size_v<Params>)>> { using type = std::tuple_element_t<P, Params>; };
305
+ template<typename Tuple> struct AllJsArguments;
306
+ template<typename... A> struct AllJsArguments<std::tuple<A...>> : std::bool_constant<(isJsArgument<A> && ...)> {};
307
+
308
+ // A function under another name, a macro's.
309
+ template<auto f, typename Params> struct FunctionCall;
310
+ template<auto f, typename... A> struct FunctionCall<f, std::tuple<A...>> {
311
+ template<size_t... I> static auto call(const std::array<emscripten::val, sizeof...(A)> &args, std::index_sequence<I...>) {
312
+ auto converted = std::make_tuple(argumentFromJs<A>(args[I])...);
313
+ using R = decltype(f(static_cast<A>(std::get<I>(converted))...));
314
+ if constexpr (std::is_void_v<R>) f(static_cast<A>(std::get<I>(converted))...);
315
+ else return resultToJs(f(static_cast<A>(std::get<I>(converted))...));
316
+ }
317
+ static auto invoke(const std::array<emscripten::val, sizeof...(A)> &args) { return call(args, std::index_sequence_for<A...>{}); }
318
+ };
319
+
320
+ template<typename F, F f> void bindAlias(const char *name) {
321
+ using S = Signature<F>;
322
+ constexpr size_t arity = std::tuple_size_v<typename S::Params>;
323
+ if constexpr (AllJsArguments<typename S::Params>::value && isJsResult<typename S::Result>) registerCall<FunctionCall<f, typename S::Params>>(name, std::make_index_sequence<arity>{});
324
+ else registerUnconvertible(name, std::make_index_sequence<arity>{});
325
+ }
326
+
327
+ // A function-like macro called with the parameter types of the function it calls, read at the positions it passes them.
328
+ template<typename Macro, typename Params, size_t... P> struct MacroCall {
329
+ template<size_t... I> static auto call(const std::array<emscripten::val, sizeof...(P)> &args, std::index_sequence<I...>) {
330
+ auto converted = std::make_tuple(argumentFromJs<typename ParamAt<Params, P>::type>(args[I])...);
331
+ using R = decltype(Macro::call(static_cast<typename ParamAt<Params, P>::type>(std::get<I>(converted))...));
332
+ if constexpr (std::is_void_v<R>) Macro::call(static_cast<typename ParamAt<Params, P>::type>(std::get<I>(converted))...);
333
+ else return resultToJs(Macro::call(static_cast<typename ParamAt<Params, P>::type>(std::get<I>(converted))...));
334
+ }
335
+ static auto invoke(const std::array<emscripten::val, sizeof...(P)> &args) { return call(args, std::make_index_sequence<sizeof...(P)>{}); }
336
+ };
337
+
338
+ template<typename Macro, typename Params, size_t... P> constexpr bool macroResultConverts() {
339
+ return isJsResult<decltype(Macro::call(std::declval<typename ParamAt<Params, P>::type>()...))>;
340
+ }
341
+
342
+ template<typename Macro, typename Callee, size_t... P> void bindMacro(const char *name) {
343
+ using Params = typename Signature<Callee>::Params;
344
+ if constexpr ((isJsArgument<typename ParamAt<Params, P>::type> && ...)) {
345
+ if constexpr (macroResultConverts<Macro, Params, P...>()) registerCall<MacroCall<Macro, Params, P...>>(name, std::make_index_sequence<sizeof...(P)>{});
346
+ else registerUnconvertible(name, std::make_index_sequence<sizeof...(P)>{});
347
+ } else registerUnconvertible(name, std::make_index_sequence<sizeof...(P)>{});
348
+ }
349
+
350
+ // A variadic argument goes where the C ABI puts its type: on wasm32 a long or pointer is one 4-byte slot and a long long
351
+ // or double one 8-byte slot, while 64-bit targets pass integers and pointers in general registers and doubles in
352
+ // floating-point ones. Each extra argument is one of two kinds, so a call with n of them dispatches over 2^n calls.
353
+ #if UINTPTR_MAX == 0xFFFFFFFFu
354
+ using VarWord = std::uint32_t;
355
+ using VarWide = std::uint64_t;
356
+ #else
357
+ using VarWord = std::uint64_t;
358
+ using VarWide = double;
359
+ #endif
360
+ struct VarArgument {
361
+ bool wide = false;
362
+ VarWord word = 0;
363
+ VarWide wideValue = 0;
364
+ CStringArg text;
365
+ PointerHandle handle;
366
+ // A short string lives inside its CStringArg, so its address is read where the argument ended up.
367
+ VarWord passed() const { return text.isText ? static_cast<VarWord>(reinterpret_cast<std::uintptr_t>(text.text.c_str())) : word; }
368
+ };
369
+
370
+ inline void setDouble(VarArgument &arg, double number) {
371
+ arg.wide = true;
372
+ if constexpr (std::is_same_v<VarWide, double>) arg.wideValue = number;
373
+ else std::memcpy(&arg.wideValue, &number, sizeof number);
374
+ }
375
+
376
+ inline void setLongLong(VarArgument &arg, long long number) {
377
+ if constexpr (sizeof(VarWord) == sizeof(long long)) {
378
+ arg.word = static_cast<VarWord>(number);
379
+ } else {
380
+ arg.wide = true;
381
+ arg.wideValue = static_cast<VarWide>(static_cast<unsigned long long>(number));
382
+ }
383
+ }
384
+
385
+ // An integer number is a C long, which is 32 bits on wasm32 and Windows; a larger one takes a BigInt (long long).
386
+ inline VarArgument varArgumentFromJs(const emscripten::val &value) {
387
+ VarArgument arg;
388
+ if (value.isNull() || value.isUndefined()) return arg;
389
+ if (value.isString()) {
390
+ arg.text = fromJsCString<const char>(value);
391
+ return arg;
392
+ }
393
+ std::string type = value.isNumber() ? "number" : value.typeOf().as<std::string>();
394
+ if (type == "bigint") {
395
+ // A long long or an unsigned one: the same 64 bits either way.
396
+ if (value < emscripten::val(0LL)) setLongLong(arg, bigIntFromJs<long long>(value, std::numeric_limits<long long>::min(), 0));
397
+ else setLongLong(arg, static_cast<long long>(bigIntFromJs<unsigned long long>(value, 0, std::numeric_limits<unsigned long long>::max())));
398
+ return arg;
399
+ }
400
+ if (type == "object" && value["crossbindVaDouble"].isNumber()) {
401
+ setDouble(arg, value["crossbindVaDouble"].as<double>());
402
+ return arg;
403
+ }
404
+ if (type == "object" && value.instanceof(emscripten::val::module_property("NativePointer"))) {
405
+ arg.handle = value.as<PointerHandle>();
406
+ arg.word = static_cast<VarWord>(arg.handle ? arg.handle->address : 0);
407
+ return arg;
408
+ }
409
+ double number = 0;
410
+ if (!readNumber(value, number)) throw std::invalid_argument("crossbind: a variadic argument takes a number, a BigInt, vaDouble(x), a string, a pointer handle, an enum member or null");
411
+ if (!std::isfinite(number) || std::trunc(number) != number) {
412
+ setDouble(arg, number);
413
+ return arg;
414
+ }
415
+ if (!fitsInteger<long>(number) && !fitsInteger<unsigned long>(number)) throw std::range_error("crossbind: a variadic integer beyond a C long takes a BigInt");
416
+ arg.word = static_cast<VarWord>(number < 0 ? static_cast<long long>(number) : static_cast<long long>(static_cast<unsigned long long>(number)));
417
+ return arg;
418
+ }
419
+
420
+ template<size_t N, typename Invoke, typename... Done> decltype(auto) callWithVarArguments(const Invoke &invoke, const VarArgument *extra, Done... done) {
421
+ if constexpr (N == 0) return invoke(done...);
422
+ else if (extra->wide) return callWithVarArguments<N - 1>(invoke, extra + 1, done..., extra->wideValue);
423
+ else return callWithVarArguments<N - 1>(invoke, extra + 1, done..., extra->passed());
424
+ }
425
+
426
+ template<typename F> struct VariadicSignature;
427
+ template<typename R, typename... A> struct VariadicSignature<R (*)(A..., ...)> {
428
+ using Result = R;
429
+ using Fixed = std::tuple<A...>;
430
+ };
431
+ template<typename R, typename... A> struct VariadicSignature<R (*)(A..., ...) noexcept> : VariadicSignature<R (*)(A..., ...)> {};
432
+
433
+ // Picks the variadic function among a name's overloads, or the fallback that binds nothing. A noexcept one (a C
434
+ // library declared with __THROW) needs its own pattern, or the call is ambiguous.
435
+ template<typename R, typename... A> auto variadicType(R (*)(A..., ...), int) -> R (*)(A..., ...);
436
+ template<typename R, typename... A> auto variadicType(R (*)(A..., ...) noexcept, int) -> R (*)(A..., ...) noexcept;
437
+ inline auto variadicType(Unbound (*)(Unbound), long) -> Unbound (*)(Unbound);
438
+
439
+ template<auto f, size_t Extra> struct VariadicCall {
440
+ using S = VariadicSignature<decltype(f)>;
441
+ static constexpr size_t fixedCount = std::tuple_size_v<typename S::Fixed>;
442
+ template<size_t... I, size_t... J> static auto call(const std::array<emscripten::val, fixedCount + Extra> &args, std::index_sequence<I...>, std::index_sequence<J...>) {
443
+ auto fixed = std::make_tuple(argumentFromJs<std::tuple_element_t<I, typename S::Fixed>>(args[I])...);
444
+ std::array<VarArgument, Extra> extra{varArgumentFromJs(args[fixedCount + J])...};
445
+ auto invoke = [&](auto... va) { return f(static_cast<std::tuple_element_t<I, typename S::Fixed>>(std::get<I>(fixed))..., va...); };
446
+ if constexpr (std::is_void_v<typename S::Result>) callWithVarArguments<Extra>(invoke, extra.data());
447
+ else return resultToJs(callWithVarArguments<Extra>(invoke, extra.data()));
448
+ }
449
+ static auto invoke(const std::array<emscripten::val, fixedCount + Extra> &args) {
450
+ return call(args, std::make_index_sequence<fixedCount>{}, std::make_index_sequence<Extra>{});
451
+ }
452
+ };
453
+
454
+ template<auto f, size_t... K> void registerVariadic(const char *name, std::index_sequence<K...>) {
455
+ using S = VariadicSignature<decltype(f)>;
456
+ constexpr size_t fixedCount = std::tuple_size_v<typename S::Fixed>;
457
+ if constexpr (AllJsArguments<typename S::Fixed>::value && isJsResult<typename S::Result>) {
458
+ (registerCall<VariadicCall<f, K>>(name, std::make_index_sequence<fixedCount + K>{}), ...);
459
+ } else {
460
+ (registerUnconvertible(name, std::make_index_sequence<fixedCount + K>{}), ...);
461
+ }
462
+ }
463
+
464
+ inline void bindVaDouble() {
465
+ if (!claimFunction("vaDouble", 1)) return;
466
+ emscripten::function("vaDouble", +[](double number) {
467
+ emscripten::val marked = emscripten::val::object();
468
+ marked.set("crossbindVaDouble", number);
469
+ return marked;
470
+ });
471
+ }
472
+
473
+ template<typename F, F f, size_t MaxExtra> void bindVariadic(const char *name) {
474
+ if constexpr (!std::is_same_v<F, Unbound (*)(Unbound)>) {
475
+ registerVariadic<f>(name, std::make_index_sequence<MaxExtra + 1>{});
476
+ bindVaDouble();
477
+ }
478
+ }
479
+
480
+ template<typename T> void bindGlobal(const char *name, T *address) {
481
+ if (!claimRegistration(std::string("name ") + name)) return;
482
+ if constexpr (std::is_array_v<T>) emscripten::constant(name, toHandle(&(*address)[0]));
483
+ else emscripten::constant(name, toHandle(address));
484
+ }
485
+ }
486
+ `;
@@ -0,0 +1,48 @@
1
+ // Adapt the runtime emitted by the pinned SWIG fork. Kept in the generator so cache rebuilds
2
+ // produce the same runtime in every bridge, including builds on a remote runner.
3
+ export default function fixBridgeRuntime(text) {
4
+ if (!text.includes('template<typename A> auto callbackArgument(')) return text;
5
+ if (!text.includes('void bindFunctionPointerField(')) {
6
+ text = text.replace('template<auto M, bool Instance, typename Builder> void bindField(',
7
+ 'template<auto M, typename Builder> void bindFunctionPointerField(const Builder &cls, const char *name);\n'
8
+ + 'template<auto M, bool Instance, typename Builder> void bindField(');
9
+ text = text.replace('} else if constexpr (std::is_pointer_v<U> && !std::is_function_v<std::remove_pointer_t<U>>) {',
10
+ '} else if constexpr (std::is_pointer_v<U> && std::is_function_v<std::remove_pointer_t<U>>) {\n'
11
+ + ' bindFunctionPointerField<M>(cls, name);\n'
12
+ + ' } else if constexpr (std::is_pointer_v<U>) {');
13
+ text = text.replace('template<typename T> constexpr bool isPassedByAddress =', `template<auto M, typename Builder> void bindFunctionPointerField(const Builder &cls, const char *name) {
14
+ using C = typename MemberOf<decltype(M)>::Class;
15
+ using T = typename MemberOf<decltype(M)>::Type;
16
+ using U = std::remove_cv_t<T>;
17
+ auto read = +[](const C &s) { return toHandle(s.*M); };
18
+ if constexpr (std::is_const_v<T>) cls.property(name, read);
19
+ else cls.property(name, read, +[](C &s, JsCallback<U> value) { s.*M = fromJsCallback<U>(value); });
20
+ }
21
+ template<typename T> constexpr bool isPassedByAddress =`);
22
+ }
23
+ if (!text.includes('auto callbackArgumentAt(')) {
24
+ 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
+ }
35
+ #ifndef __EMSCRIPTEN__
36
+ struct RuntimeCallbacks {`);
37
+ text = text.replaceAll('template<typename R, typename... A> R invokeCallback(size_t slot, A... args) {',
38
+ 'template<typename R, typename... A, size_t... I> R invokeCallbackArguments(size_t slot, std::index_sequence<I...>, A... args) {');
39
+ text = text.replaceAll('callbackArgument<A>(std::declval<std::remove_reference_t<A> &>())',
40
+ 'callbackArgumentAt<I>(std::declval<std::tuple<A &...>>())');
41
+ text = text.replaceAll('callbackArgument<A>(args)', 'callbackArgumentAt<I>(std::tie(args...))');
42
+ text = text.replace('template<typename F> struct Callback {', `template<typename R, typename... A> R invokeCallback(size_t slot, A... args) {
43
+ return invokeCallbackArguments<R, A...>(slot, std::index_sequence_for<A...>{}, std::forward<A>(args)...);
44
+ }
45
+ template<typename F> struct Callback {`);
46
+ }
47
+ return text;
48
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const LICENSES_DIR = 'licenses';
5
+
6
+ // The texts of vendored copies live in the upstream source tree, which an installed package does
7
+ // not have, so the build ships them in dist for the notices to read later.
8
+ export function copyBundledLicenses(entries, sourceDir, outputDir) {
9
+ (entries ?? []).forEach((entry) => {
10
+ (entry.files ?? []).forEach((file) => {
11
+ const from = path.join(sourceDir, file);
12
+ if (!fs.existsSync(from)) {
13
+ throw new Error(`crossbind: ${file}, the license of the vendored ${entry.name}, is not in the source.`);
14
+ }
15
+ const to = path.join(outputDir, LICENSES_DIR, entry.name, file);
16
+ fs.mkdirSync(path.dirname(to), { recursive: true });
17
+ fs.copyFileSync(from, to);
18
+ });
19
+ });
20
+ }
21
+
22
+ export function shippedBundledLicense(packageDirs, entryName, file) {
23
+ return packageDirs
24
+ .map((dir) => path.join(dir, 'dist', LICENSES_DIR, entryName, file))
25
+ .find((candidate) => fs.existsSync(candidate)) ?? null;
26
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import appSourceFiles from './appSources.js';
4
+
5
+ const PREFIX = 'cargo:';
6
+ const RUST_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
7
+ const MODULE_IMPORT = /['"`]cargo:([A-Za-z0-9_-]+)((?:\/[A-Za-z_][A-Za-z0-9_]*)+)['"`]/g;
8
+
9
+ export function parseCargoImport(source) {
10
+ const [crateName, ...modulePath] = source.slice(PREFIX.length).split('/');
11
+ const bad = modulePath.find((segment) => !RUST_IDENT.test(segment));
12
+ if (bad !== undefined) {
13
+ throw new Error(`crossbind: '${source}' - '${bad}' is not a Rust module name; write the path as cargo:<crate>/<module>/<submodule>.`);
14
+ }
15
+ return { crateName, modulePath };
16
+ }
17
+
18
+ // The marker's file name carries the module path after dots, which crate and module names cannot contain.
19
+ export function cargoMarkerName({ crateName, modulePath }) {
20
+ return [crateName, ...modulePath].join('.');
21
+ }
22
+
23
+ export function parseCargoMarkerName(name) {
24
+ const [crateName, ...modulePath] = name.split('.');
25
+ return { crateName, modulePath };
26
+ }
27
+
28
+ export function writeCargoMarker(cacheDir, cargoImport) {
29
+ const marker = path.join(cacheDir, 'rust-crates', `${cargoMarkerName(cargoImport)}.rs`);
30
+ if (!fs.existsSync(marker)) {
31
+ fs.mkdirSync(path.dirname(marker), { recursive: true });
32
+ fs.writeFileSync(marker, `// crossbind cargo crate import marker: ${[cargoImport.crateName, ...cargoImport.modulePath].join('/')}\n`);
33
+ }
34
+ return marker;
35
+ }
36
+
37
+ // A specifier built at run time is not found.
38
+ export function findCargoModuleImports(text) {
39
+ return [...text.matchAll(MODULE_IMPORT)].map((m) => ({ crateName: m[1], modulePath: m[2].slice(1).split('/') }));
40
+ }
41
+
42
+ export function findCargoModuleImportsIn(projectDir) {
43
+ return appSourceFiles(projectDir).flatMap((file) => findCargoModuleImports(fs.readFileSync(file, 'utf8')));
44
+ }
@@ -1,6 +1,6 @@
1
- // Maps a crossbind build target to the cargo target triple. Rust is built on the host (the rust
2
- // toolchain is host-installed), so the host must have the matching linker/SDK: Xcode for apple,
3
- // a sourced emsdk for wasm, an NDK for android.
1
+ // Maps a crossbind build target to the cargo target triple. Apple targets build on the host with
2
+ // Xcode; every other platform builds in the toolchain image that links it, which carries the
3
+ // target's std.
4
4
  export function cargoTripleFor(target) {
5
5
  switch (target.platform) {
6
6
  case 'wasm':
@@ -9,7 +9,27 @@ export function cargoTripleFor(target) {
9
9
  return target.arch === 'iphonesimulator' ? 'aarch64-apple-ios-sim' : 'aarch64-apple-ios';
10
10
  case 'android':
11
11
  return target.arch === 'x86_64' ? 'x86_64-linux-android' : 'aarch64-linux-android';
12
+ case 'darwin':
13
+ return target.arch === 'x64' ? 'x86_64-apple-darwin' : 'aarch64-apple-darwin';
14
+ case 'linux':
15
+ return target.arch === 'x64' ? 'x86_64-unknown-linux-gnu' : 'aarch64-unknown-linux-gnu';
16
+ case 'linuxmusl':
17
+ return target.arch === 'x64' ? 'x86_64-unknown-linux-musl' : 'aarch64-unknown-linux-musl';
18
+ case 'win32':
19
+ return target.arch === 'x64' ? 'x86_64-pc-windows-gnullvm' : 'aarch64-pc-windows-gnullvm';
12
20
  default:
13
21
  return null; // wasi (no rust wasip3 target yet) and anything else: unsupported
14
22
  }
15
23
  }
24
+
25
+ // What `rustc --print native-static-libs` asks a Linux or Windows link to add for a Rust staticlib.
26
+ // The other platforms' toolchains link these by themselves.
27
+ const RUST_STD_LIBS = {
28
+ linux: ['-lgcc_s', '-lutil', '-lrt', '-lpthread', '-lm', '-ldl'],
29
+ linuxmusl: ['-lgcc_s'],
30
+ win32: ['-lkernel32', '-lntdll', '-luserenv', '-lws2_32', '-ldbghelp', '-lunwind'],
31
+ };
32
+
33
+ export function rustStdLibsFor(target) {
34
+ return RUST_STD_LIBS[target.platform] ?? [];
35
+ }