crossbind 2.0.0-beta.63 → 2.0.0-beta.64

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 (50) hide show
  1. package/AGENTS.md +2 -1
  2. package/package.json +2 -2
  3. package/src/actions/buildBinTools.js +0 -0
  4. package/src/actions/buildJs.js +2 -9
  5. package/src/actions/buildNode.js +4 -6
  6. package/src/actions/buildWasiCommand.js +10 -5
  7. package/src/actions/buildWasm.js +24 -3
  8. package/src/actions/createImportedBridges.js +7 -0
  9. package/src/actions/createInterface.js +3 -3
  10. package/src/actions/createLib.js +14 -2
  11. package/src/actions/getData.js +2 -1
  12. package/src/actions/getEnv.js +7 -0
  13. package/src/actions/runnerCommands.js +16 -4
  14. package/src/actions/writeRuntimeEntry.js +18 -7
  15. package/src/assets/cpp-runtime/node.cpp +18 -1
  16. package/src/assets/js-runtime/adapters/worker-comlink.js +51 -0
  17. package/src/assets/js-runtime/core.js +5 -4
  18. package/src/assets/toolchain-digests.json +19 -19
  19. package/src/assets/wasi-runtime/http/README.md +17 -0
  20. package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
  21. package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
  22. package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
  23. package/src/assets/wasi-runtime/http/handler.cpp +298 -0
  24. package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
  25. package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
  26. package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
  27. package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
  28. package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
  29. package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
  30. package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
  31. package/src/bin.js +29 -179
  32. package/src/cli.js +14 -0
  33. package/src/integration/getCrossbindScript.js +4 -3
  34. package/src/program.js +169 -0
  35. package/src/state/index.js +4 -3
  36. package/src/state/loadConfig.js +4 -11
  37. package/src/utils/appRustCrates.js +4 -0
  38. package/src/utils/bridgeExtras.js +74 -6
  39. package/src/utils/cargoLock.js +19 -0
  40. package/src/utils/cppDts.js +2 -0
  41. package/src/utils/embindObjectLifetime.js +35 -0
  42. package/src/utils/packageLicense.js +4 -2
  43. package/src/utils/resetCMakeCacheForSource.js +26 -0
  44. package/src/utils/rustBridgeGen.js +13 -4
  45. package/src/utils/rustMt.js +5 -0
  46. package/src/utils/rustSysroot.js +7 -5
  47. package/src/utils/swigInterface.js +5 -4
  48. package/src/utils/systemConfig.js +13 -0
  49. package/src/utils/targets.js +11 -0
  50. package/src/utils/wasiHttp.js +50 -0
@@ -4,14 +4,16 @@ import { parseMacroDump, stripComments, declaresAtFileScope } from './swigInterf
4
4
  // SWIG binds no function-like macro, variadic function or mutable global. crossbind binds them for the names an app
5
5
  // imports by name: a function-like macro takes the parameter types of the one function it calls, a macro naming a
6
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.
7
+ // type, a mutable global binds as a handle to its address, and a constant SWIG drops for a cast in its value binds as
8
+ // the number the compiler makes of it. A macro binds only through a function SWIG bound, the one place that proves the
9
+ // name is a single declared function.
9
10
 
10
11
  const C_IDENTIFIER = /^[A-Za-z_]\w*$/;
11
12
  const MACRO_LINE = /^\/\/ crossbind:macro ([A-Za-z_]\w*) (#define .*)$/gm;
12
13
  const ALIAS_LINE = /^\/\/ crossbind:alias ([A-Za-z_]\w*) ([A-Za-z_]\w*)$/gm;
14
+ const CONSTANT_LINE = /^\/\/ crossbind:constant ([A-Za-z_]\w*)$/gm;
13
15
  const NOTE_MARKER = '// crossbind:note ';
14
- const EXTRA_LINES = /^(?:\/\/ crossbind:(?:macro|alias|note) .*\n)+\n/m;
16
+ const EXTRA_LINES = /^(?:\/\/ crossbind:(?:macro|alias|constant|note) .*\n)+\n/m;
15
17
  // The arguments one call may add to a variadic function: each registration count dispatches over 2^n argument kinds.
16
18
  const MAX_VARIADIC_ARGUMENTS = 6;
17
19
  const VARIADIC_WARNING = /^Variable length arguments are not supported by embind, (\S+) skipped\.$/;
@@ -90,9 +92,66 @@ export function macroCall(macro) {
90
92
  // The identifier an object-like macro stands for, as in `#define iconv_open libiconv_open`.
91
93
  const identifierOf = (macro) => (macro && !macro.params && C_IDENTIFIER.test(macro.body) ? macro.body : null);
92
94
 
95
+ const NUMBER_TYPE_WORDS = new Set(['unsigned', 'signed', 'short', 'long', 'int', 'char', 'float', 'double', 'bool', '_Bool']);
96
+ const NUMBER_TYPEDEFS = new Set([
97
+ 'size_t', 'ssize_t', 'ptrdiff_t', 'intptr_t', 'uintptr_t', 'intmax_t', 'uintmax_t', 'off_t', 'wchar_t',
98
+ 'int8_t', 'uint8_t', 'int16_t', 'uint16_t', 'int32_t', 'uint32_t', 'int64_t', 'uint64_t',
99
+ ]);
100
+ const VALUE_TOKEN = /0[xX][\dA-Fa-f]+[uUlL]*|\d+(?:\.\d*)?(?:[eE][-+]?\d+)?[uUlLfF]*|'(?:\\.|[^'\\])+'|[A-Za-z_]\w*|<<|>>|\S/g;
101
+ const OPERATOR = /^(?:<<|>>|[-+*/%&|^~!()<>=?:])$/;
102
+ const LITERAL = /^(?:\d|')/;
103
+ const CAST_OPERAND = /^(?:\d|'|[-+~!(])/;
104
+
105
+ // A type named by number words, a standard number typedef, or a typedef of one that the header declares.
106
+ function isNumberType(words, headerText, depth = 0) {
107
+ if (words.every((word) => NUMBER_TYPE_WORDS.has(word))) return true;
108
+ if (words.length !== 1 || depth > 3) return false;
109
+ if (NUMBER_TYPEDEFS.has(words[0])) return true;
110
+ const declared = stripComments(headerText).match(new RegExp(`\\btypedef\\s+([A-Za-z_][\\w\\s]*?)\\s+${words[0]}\\s*;`));
111
+ return Boolean(declared) && isNumberType(declared[1].trim().split(/\s+/), headerText, depth + 1);
112
+ }
113
+
114
+ // The tokens of an object-like macro's value with the object-like macros in it spelled out; null for anything else.
115
+ function expandedTokens(name, macros, depth = 0) {
116
+ const macro = macros.get(name);
117
+ if (!macro || macro.params || depth > 16) return null;
118
+ const tokens = [];
119
+ for (const token of macro.body.match(VALUE_TOKEN) ?? []) {
120
+ if (!C_IDENTIFIER.test(token) || !macros.has(token)) {
121
+ tokens.push(token);
122
+ continue;
123
+ }
124
+ const inner = expandedTokens(token, macros, depth + 1);
125
+ if (!inner) return null;
126
+ tokens.push('(', ...inner, ')');
127
+ }
128
+ return tokens;
129
+ }
130
+
131
+ // SWIG drops a #define whose value holds a name, so every cast. A value of numbers, operators and casts binds through
132
+ // the compiler when every cast is to a number type: { numbers }, or null for a value with no cast or with other names.
133
+ function castConstant(name, macros, headerText) {
134
+ const tokens = expandedTokens(name, macros);
135
+ if (!tokens) return null;
136
+ let casts = 0;
137
+ let numbers = true;
138
+ for (let at = 0; at < tokens.length; at += 1) {
139
+ if (LITERAL.test(tokens[at]) || OPERATOR.test(tokens[at])) continue;
140
+ const close = tokens.indexOf(')', at);
141
+ const words = tokens.slice(at, close);
142
+ const isCast = tokens[at - 1] === '(' && close > at && CAST_OPERAND.test(tokens[close + 1] ?? '')
143
+ && words.every((word) => word === '*' || C_IDENTIFIER.test(word));
144
+ if (!isCast) return null;
145
+ casts += 1;
146
+ numbers &&= !words.includes('*') && isNumberType(words, headerText);
147
+ at = close;
148
+ }
149
+ return casts ? { numbers } : null;
150
+ }
151
+
93
152
  // What the interface carries for the names an app imports by name, read by the bridge once SWIG has run: their
94
153
  // 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) {
154
+ export function interfaceExtras(names, macros, headerText = '') {
96
155
  if (names === ALL_NAMES || !names.length) return { lines: [], functions: [] };
97
156
  const lines = [];
98
157
  const functions = [];
@@ -104,6 +163,10 @@ export function interfaceExtras(names, macros) {
104
163
  if (target && target !== name && !macros.has(target) && !C_KEYWORDS.has(target)) {
105
164
  lines.push(`// crossbind:alias ${name} ${target}`);
106
165
  functions.push(target);
166
+ } else {
167
+ const constant = castConstant(name, macros, headerText);
168
+ if (constant?.numbers) lines.push(`// crossbind:constant ${name}`);
169
+ else if (constant) lines.push(`${NOTE_MARKER}${name}: a constant whose value has a cast binds only when every cast is to a number type the header declares; skipped.`);
107
170
  }
108
171
  continue;
109
172
  }
@@ -168,7 +231,8 @@ export function bridgeExtras({ interfaceText = '', named = [], bridgeText = '',
168
231
  const globalWarnings = warningsAbout(warnings, GLOBAL_WARNING, imported);
169
232
  const globals = [...globalWarnings.keys()].filter((name) => declaresAtFileScope(headerText, name));
170
233
  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 };
234
+ const constants = [...interfaceText.matchAll(CONSTANT_LINE)].map(([, name]) => name).filter((name) => !bridgeText.includes(`bindConstant("${name}"`));
235
+ if (!macros.length && !aliases.length && !variadics.length && !globals.length && !constants.length) return { code: '', exports: [], answered, notes };
172
236
 
173
237
  // The macros register ahead of the fallbacks below, which would make a function a variadic shares its name with an
174
238
  // overload set.
@@ -184,10 +248,13 @@ export function bridgeExtras({ interfaceText = '', named = [], bridgeText = '',
184
248
  `EMSCRIPTEN_BINDINGS(CrossbindExtras_${module}) {`,
185
249
  ...variadics.map((name) => ` { using F = decltype(crossbind::variadicType(&${name}, 0)); crossbind::bindVariadic<F, static_cast<F>(&${name}), ${MAX_VARIADIC_ARGUMENTS}>("${name}"); }`),
186
250
  ...globals.map((name) => ` crossbind::bindGlobal("${name}", &${name});`),
251
+ ...constants.map((name) => ` crossbind::bindConstant("${name}", ${name});`),
187
252
  '}',
188
253
  '',
189
254
  ].join('\n');
190
- const exports = [...macros.map(({ name }) => name), ...aliases.map(({ name }) => name), ...variadics, ...globals, ...(variadics.length ? ['vaDouble'] : [])];
255
+ const exports = [
256
+ ...macros.map(({ name }) => name), ...aliases.map(({ name }) => name), ...variadics, ...globals, ...constants, ...(variadics.length ? ['vaDouble'] : []),
257
+ ];
191
258
  return { code, exports, answered, notes };
192
259
  }
193
260
 
@@ -261,6 +328,7 @@ template<typename R> constexpr bool isJsResult = std::is_void_v<R> || isJsArgume
261
328
  template<typename T> auto argumentFromJs(const emscripten::val &value) {
262
329
  using U = std::remove_cv_t<T>;
263
330
  if constexpr (std::is_same_v<U, const char *>) return fromJsCString<const char>(value);
331
+ else if constexpr (isFunctionPointer<U>) return fromJsCallback<U>(value);
264
332
  else if constexpr (std::is_pointer_v<U>) return fieldFromJs<U>(value);
265
333
  else if constexpr (std::is_same_v<U, bool>) {
266
334
  double number = 0;
@@ -0,0 +1,19 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import writeIfChanged from './writeIfChanged.js';
4
+
5
+ // The versions of every crate the app's Rust links, next to crossbind.config.js and committed like conan.lock, since
6
+ // the manifests cargo resolves are generated under the cache. Not Cargo.lock: a package whose crate sits at the
7
+ // project root already has one.
8
+ const lockOf = (projectDir) => path.join(projectDir, 'crossbind.cargo.lock');
9
+
10
+ // Before cargo resolves a generated manifest, so it keeps every version the project pins.
11
+ export function followCargoLock(projectDir, manifestDir) {
12
+ const lock = lockOf(projectDir);
13
+ if (fs.existsSync(lock)) writeIfChanged(path.join(manifestDir, 'Cargo.lock'), fs.readFileSync(lock, 'utf8'));
14
+ }
15
+
16
+ // After a build: what cargo resolved, a crate the lock did not list yet included.
17
+ export function recordCargoLock(projectDir, manifestDir) {
18
+ writeIfChanged(lockOf(projectDir), fs.readFileSync(path.join(manifestDir, 'Cargo.lock'), 'utf8'));
19
+ }
@@ -225,6 +225,8 @@ export function emitCppDts(model, exportNames, mode = 'sync') {
225
225
  for (const method of cls.methods) {
226
226
  out.push(` ${method.isStatic ? 'static ' : ''}${method.name}(${method.args.map((a) => `${a.name}: ${a.type}`).join(', ')}): ${wrap(method.ret)};`);
227
227
  }
228
+ // Frees the object now instead of when the garbage collector reclaims the handle (lifecycle-and-types.md).
229
+ out.push(` delete(): ${wrap('void')};`);
228
230
  out.push('}');
229
231
  }
230
232
  for (const name of exportNames) {
@@ -0,0 +1,35 @@
1
+ // embind frees an object when JavaScript's handle to it is collected only behind a smart pointer. A value a call
2
+ // returns, which embind copied for JavaScript, and an object JavaScript made through a raw constructor (a vector, a
3
+ // Rust class) were freed by .delete() alone, though they are as much JavaScript's own. This rewrite marks both on the
4
+ // count a handle shares with its clones and gives them the same finalizer. A copy is known only where its converter
5
+ // makes the handle, since embind then types the handle as a pointer to the object's own class. A raw pointer stays
6
+ // its library's. The debug glue's leak warning goes: a collected handle is freed by design. Without
7
+ // FinalizationRegistry (Hermes) nothing is registered, as before.
8
+
9
+ // The release glue spells each on one line, the debug glue one statement per line with comments.
10
+ const FINALIZER = /if\s*\(hasSmartPtr\)\s*\{[\s\S]*?finalizationRegistry\.register\(handle,\s*info,\s*handle\);?/;
11
+ const OWNED_FINALIZER = 'if(hasSmartPtr||$$.count.owned){finalizationRegistry.register(handle,{$$},handle);';
12
+ const LEAK_WARNING = /console\.warn\(info\.leakWarning\);?\s*/;
13
+ const FROM_WIRE = /function RegisteredPointer_fromWireType\(ptr\)\s*\{/g;
14
+ const OWN_FROM_WIRE = 'function crossbindOwnHandle(handle){var $$=handle.$$;if(!$$.smartPtr&&!$$.count.owned){$$.count.owned=true;'
15
+ + 'if(finalizationRegistry)finalizationRegistry.register(handle,{$$},handle)}return handle}'
16
+ + 'function RegisteredPointer_fromWireType(ptr){var handle=crossbindHandleFromWire.call(this,ptr);'
17
+ + 'return this.isReference&&handle?crossbindOwnHandle(handle):handle}'
18
+ + 'function crossbindHandleFromWire(ptr){';
19
+ const CONSTRUCTED = /return body\.apply\(this,\s*args\);?/g;
20
+ const OWN_CONSTRUCTED = 'return crossbindOwnHandle(body.apply(this,args));';
21
+
22
+ const count = (text, pattern) => text.match(pattern)?.length ?? 0;
23
+
24
+ // Returns the rewritten glue and whether it holds embind classes this rewrite does not recognise.
25
+ export function releaseOwnedHandles(glue) {
26
+ if (!glue.includes('attachFinalizer')) return { text: glue, missed: false };
27
+ const recognised = count(glue, FROM_WIRE) === 1 && count(glue, CONSTRUCTED) === 1;
28
+ // Functions, not strings: `$$` in a replacement string is a lone `$`.
29
+ const text = glue
30
+ .replace(FINALIZER, () => OWNED_FINALIZER)
31
+ .replace(LEAK_WARNING, '')
32
+ .replace(FROM_WIRE, () => OWN_FROM_WIRE)
33
+ .replace(CONSTRUCTED, () => OWN_CONSTRUCTED);
34
+ return { text, missed: !recognised || !text.includes(OWNED_FINALIZER) };
35
+ }
@@ -74,9 +74,11 @@ export function nodePackageLicense({ name, version, repository, projectPath, add
74
74
 
75
75
  export function setManifestLicense(dir, expression) {
76
76
  const file = path.join(dir, 'package.json');
77
- const manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
77
+ const text = fs.readFileSync(file, 'utf8');
78
+ const manifest = JSON.parse(text);
78
79
  if (manifest.license === expression) return;
79
- fs.writeFileSync(file, `${JSON.stringify({ ...manifest, license: expression }, null, 4)}\n`);
80
+ const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? 4;
81
+ fs.writeFileSync(file, `${JSON.stringify({ ...manifest, license: expression }, null, indent)}\n`);
80
82
  }
81
83
 
82
84
  export function writeNodePackageLicense(dir, meta) {
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs';
2
+ import state from '../state/index.js';
3
+ import replaceBasePathForDocker from './replaceBasePathForDocker.js';
4
+ import { isLocalRunner } from './selectRunner.js';
5
+ import { HOST_BUILT_PLATFORMS } from './targets.js';
6
+
7
+ const normalize = (dir) => dir.replaceAll('\\', '/').replace(/\/+$/, '');
8
+
9
+ // The directory as the cmake run() starts sees it: a container, Docker or a remote runner, has the project at
10
+ // DOCKER_BASE; macOS and iOS builds and the LOCAL runner see this machine's paths.
11
+ function seenByCMake(dir, target) {
12
+ const isLocal = HOST_BUILT_PLATFORMS.includes(target.platform) || isLocalRunner(state.config.system);
13
+ return isLocal ? dir : replaceBasePathForDocker(dir, state.config.paths.base);
14
+ }
15
+
16
+ // A CMake build tree remembers the source directory it was configured from and refuses another one. The CMake
17
+ // projects crossbind ships move with its version (node_modules/.pnpm/crossbind@<version>/...), so a tree configured
18
+ // from another directory starts afresh, as `cmake --fresh` would.
19
+ export default function resetCMakeCacheForSource(buildPath, sourceDir, target) {
20
+ const cache = `${buildPath}/CMakeCache.txt`;
21
+ if (!fs.existsSync(cache)) return;
22
+ const configuredFrom = fs.readFileSync(cache, 'utf8').match(/^CMAKE_HOME_DIRECTORY:INTERNAL=(.*)$/m)?.[1];
23
+ if (configuredFrom !== undefined && normalize(configuredFrom) === normalize(seenByCMake(sourceDir, target))) return;
24
+ fs.rmSync(cache, { force: true });
25
+ fs.rmSync(`${buildPath}/CMakeFiles`, { recursive: true, force: true });
26
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { followCargoLock } from './cargoLock.js';
4
5
  import resolveEmbindRustRoot, { embindRustVersion } from './resolveEmbindRust.js';
5
6
  import runCargo, { toHostPath } from './runCargo.js';
6
7
  import writeIfChanged from './writeIfChanged.js';
@@ -303,7 +304,9 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
303
304
  // through the root and through a module registers once and is the same JS class for both.
304
305
  const crateSourceCache = new Map();
305
306
  const crateModelCache = new Map();
306
- export function createCrateImportBridge({ crateName, modulePath = [], spec, cacheDir, dtsMode = 'sync', log = console.log }) {
307
+ export function createCrateImportBridge({
308
+ crateName, modulePath = [], spec, cacheDir, projectDir, dtsMode = 'sync', log = console.log,
309
+ }) {
307
310
  const importName = [crateName, ...modulePath].join('/');
308
311
  const crateIdent = crateName.replaceAll('-', '_');
309
312
  const stem = `crate_${crateName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}`;
@@ -343,7 +346,9 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
343
346
  // cargo metadata needs a resolvable lib target before the real bridge exists.
344
347
  if (!fs.existsSync(`${dir}/src/lib.rs`)) writeIfChanged(`${dir}/src/lib.rs`, '// crossbind placeholder\n');
345
348
 
346
- const { srcDir, features } = crateSource({ dir, depLine, crateName, importName });
349
+ const { srcDir, features } = crateSource({
350
+ dir, depLine, crateName, importName, projectDir,
351
+ });
347
352
  const ownKey = modulePath.join('::');
348
353
  const imports = [];
349
354
  for (const importPath of crateImports(cacheDir, crateName, modulePath)) {
@@ -385,10 +390,14 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
385
390
  }
386
391
 
387
392
  // cargo metadata locates the crate's source and the feature set it resolves to (fetching the crate
388
- // on first use); both hold for every import of the crate.
389
- function crateSource({ dir, depLine, crateName, importName }) {
393
+ // on first use); both hold for every import of the crate. It resolves with the project's pins, so
394
+ // the bridge is made from the version the app links.
395
+ function crateSource({
396
+ dir, depLine, crateName, importName, projectDir,
397
+ }) {
390
398
  const cacheKey = `${dir}|${depLine}`;
391
399
  if (crateSourceCache.has(cacheKey)) return crateSourceCache.get(cacheKey);
400
+ followCargoLock(projectDir, dir);
392
401
  let meta;
393
402
  try {
394
403
  const probe = runCargo(['metadata', '--format-version', '1', '--manifest-path', `${dir}/Cargo.toml`], {
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
 
4
5
  // One argv per flag: runCargo joins these with cargo's 0x1F separator, so a flag must not
@@ -39,6 +40,10 @@ export function cargoTargetDirFor(baseDir, target) {
39
40
  // points at whatever version that image pinned, so the CLI carries no version of its own.
40
41
  const CONTAINER_SYSROOT = '/opt/crossbind/rust/current';
41
42
 
43
+ // True only inside the toolchain image. A CLI running there with RUNNER=LOCAL (the playground
44
+ // compiler) has these trees, but not rustup's stock wasm target.
45
+ export const hasContainerSysroot = () => fs.existsSync(CONTAINER_SYSROOT);
46
+
42
47
  export function cargoBuildInvocation({
43
48
  target, triple, targetDir, manifestPath, sysroot = false,
44
49
  }) {
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import withDirLock from './dirLock.js';
6
6
  import fetchOciLayer from './ociArtifact.js';
7
7
  import { cargoRunner } from './runCargo.js';
8
- import { isMtWasm } from './rustMt.js';
8
+ import { hasContainerSysroot, isMtWasm } from './rustMt.js';
9
9
  import PIN from './rustSysrootPin.js';
10
10
 
11
11
  // The second consumption channel for the Rust sysroots. A containerized build gets them as an image
@@ -149,7 +149,7 @@ let resolved = null;
149
149
  // Called from buildDependencies, the one async step every build path awaits before the sync work
150
150
  // starts. Only downloads when a target will actually consume it.
151
151
  export async function prepareRustSysroot(targets, log = console.log) {
152
- if (resolved || !PIN) return resolved;
152
+ if (resolved || !PIN || hasContainerSysroot()) return resolved;
153
153
  if (!targets.some((target) => isMtWasm(target) && cargoRunner(target) === 'LOCAL')) return null;
154
154
  try {
155
155
  resolved = await ensureRustSysroot({ image: PIN.image, index: PIN.index });
@@ -161,11 +161,13 @@ export async function prepareRustSysroot(targets, log = console.log) {
161
161
  return resolved;
162
162
  }
163
163
 
164
- // What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees, an
165
- // absolute path where a downloaded artifact answers, false where mt must rebuild std on nightly.
166
- // st needs nothing - rustup's stock std is already correct for it.
164
+ // What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees (also a
165
+ // LOCAL build running inside the image), an absolute path where a downloaded artifact answers,
166
+ // false where mt must rebuild std on nightly. st needs nothing - rustup's stock std is already
167
+ // correct for it.
167
168
  export function sysrootFor(target) {
168
169
  if (cargoRunner(target) !== 'LOCAL') return true;
170
+ if (target.platform === 'wasm' && hasContainerSysroot()) return true;
169
171
  if (!isMtWasm(target)) return false;
170
172
  return resolved ?? false;
171
173
  }
@@ -75,8 +75,9 @@ export function buildInterfaceContent({
75
75
  // The marker keeps a preamble that starts with #define apart from the macro block the retry drops.
76
76
  const preambleBlock = preamble.length ? `// package preamble\n${preamble.join('\n')}\n\n` : '';
77
77
  const extraBlock = extras.length ? `${extras.join('\n')}\n\n` : '';
78
- // Named constants are asked for ahead of the macros, which may define one. Every constant is asked for after them:
79
- // SWIG binds an interface's own #define lines, and the prelude only carries other headers' values for the parse.
78
+ // Named constants and functions are asked for ahead of the macros, which may define an imported name. Every constant
79
+ // is asked for after them: SWIG binds an interface's own #define lines, and the prelude only carries other headers'
80
+ // values for the parse.
80
81
  const everyConstant = constants === ALL_NAMES;
81
82
  const module = moduleName.replace(/\W/g, '_');
82
83
  return `#ifndef _${module}_I
@@ -88,10 +89,10 @@ ${extraBlock}%{
88
89
  ${includes}
89
90
  %}
90
91
 
91
- ${everyConstant ? '' : constantRequests(constants)}%feature("shared_ptr");
92
+ ${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}%feature("shared_ptr");
92
93
  %feature("polymorphic_shared_ptr");
93
94
 
94
- ${macros}${functionRequests(functions)}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
95
+ ${macros}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
95
96
 
96
97
  #endif
97
98
  `;
@@ -0,0 +1,13 @@
1
+ import os from 'node:os';
2
+ import loadJson from './loadJson.js';
3
+ import systemKeys from './systemKeys.js';
4
+
5
+ export const systemConfigPath = () => `${os.homedir()}/.crossbind.json`;
6
+
7
+ export default function loadSystemConfig() {
8
+ const system = loadJson(systemConfigPath()) || {};
9
+ const defaults = Object.entries(systemKeys)
10
+ .filter(([key]) => !(key in system))
11
+ .map(([key, value]) => [key, value.default]);
12
+ return { ...system, ...Object.fromEntries(defaults) };
13
+ }
@@ -68,6 +68,12 @@ const BASE_TARGETS = [
68
68
  {
69
69
  platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'wasi',
70
70
  },
71
+ {
72
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'http',
73
+ },
74
+ {
75
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'http',
76
+ },
71
77
  {
72
78
  platform: 'android', arch: 'arm64-v8a', runtime: 'mt', buildType: 'release',
73
79
  },
@@ -192,6 +198,11 @@ export function nodeAddonNamesOf(target, projectName) {
192
198
  };
193
199
  }
194
200
 
201
+ // A wasi build is one component, with no JS glue: the command keeps the plain name, an http component adds its own.
202
+ export const wasiNamesOf = (target, projectName) => ({
203
+ wasmName: `${projectName}-${targetPathOf(target)}${target.runtimeEnv === 'http' ? '.http' : ''}.wasm`,
204
+ });
205
+
195
206
  // One executable per platform, arch and build type, named like the addons it replaces.
196
207
  export function nativeCommandNamesOf(target, projectName) {
197
208
  const suffix = target.buildType === 'debug' ? '.debug' : '';
@@ -0,0 +1,50 @@
1
+ // The runtime of an http build: crossbind's wasi:http/handler export, which calls the app's
2
+ // crossbind::http::handle, and the bindings wit-bindgen generated for it (assets/wasi-runtime/http/README.md).
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const RUNTIME = 'assets/wasi-runtime/http';
8
+ // The stack of each request: wasi-libc mallocs one per concurrent request, with no guard below it.
9
+ const STACK_SIZE = 1024 * 1024;
10
+ const HEADER = 'include/crossbind/http.h';
11
+ const LINKED = [
12
+ 'handler.cpp',
13
+ 'bindings/crossbind_http.c',
14
+ 'bindings/crossbind_http.h',
15
+ 'bindings/crossbind_http_component_type.o',
16
+ HEADER,
17
+ ];
18
+
19
+ // paths.cli can lie outside the docker mount, so the files are copied into the build tree first.
20
+ function stage(cliPath, dir, files) {
21
+ files.forEach((file) => {
22
+ fs.mkdirSync(path.dirname(`${dir}/${file}`), { recursive: true });
23
+ fs.copyFileSync(`${cliPath}/${RUNTIME}/${file}`, `${dir}/${file}`);
24
+ });
25
+ }
26
+
27
+ export const wasiHttpFiles = (cliPath) => LINKED.map((file) => `${cliPath}/${RUNTIME}/${file}`);
28
+
29
+ export const wasiHttpHeader = (cliPath) => `${cliPath}/${RUNTIME}/${HEADER}`;
30
+
31
+ // The include dir of a wasi source build, where the app's handler finds <crossbind/http.h>.
32
+ export function stageWasiHttpInclude(cliPath, dir) {
33
+ stage(cliPath, dir, [HEADER]);
34
+ return `${dir}/include`;
35
+ }
36
+
37
+ // What an http link adds to a command's: the handler export, its bindings (C, hence -x c) with the world
38
+ // they implement, the reactor model, which has no main, and a stack as large as the wasm builds'.
39
+ export function stageWasiHttpLink(cliPath, dir) {
40
+ stage(cliPath, dir, LINKED);
41
+ return [
42
+ `-I${dir}/include`,
43
+ `-I${dir}/bindings`,
44
+ `${dir}/handler.cpp`,
45
+ '-x', 'c', `${dir}/bindings/crossbind_http.c`, '-x', 'none',
46
+ `${dir}/bindings/crossbind_http_component_type.o`,
47
+ '-mexec-model=reactor',
48
+ `-Wl,-z,stack-size=${STACK_SIZE}`,
49
+ ];
50
+ }