space-data-module-sdk 0.8.19 → 0.8.21

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 (67) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -2
  3. package/docs/assembly.js +190 -0
  4. package/docs/browser-wasmedge-isomorphic.html +6 -1
  5. package/docs/byo-wasm-quickstart.html +6 -1
  6. package/docs/conformance.html +6 -1
  7. package/docs/events-abi.html +6 -1
  8. package/docs/families/analytics.html +7 -2
  9. package/docs/families/attitude.html +7 -2
  10. package/docs/families/behavior.html +7 -2
  11. package/docs/families/breakup.html +7 -2
  12. package/docs/families/conjunction.html +7 -2
  13. package/docs/families/data-source.html +7 -2
  14. package/docs/families/effects.html +7 -2
  15. package/docs/families/environment.html +7 -2
  16. package/docs/families/estimation.html +7 -2
  17. package/docs/families/gnc.html +7 -2
  18. package/docs/families/maneuver.html +7 -2
  19. package/docs/families/obstruction.html +7 -2
  20. package/docs/families/propagator.html +7 -2
  21. package/docs/families/propulsion.html +7 -2
  22. package/docs/families/reentry.html +7 -2
  23. package/docs/families/rf.html +7 -2
  24. package/docs/families/scheduler.html +7 -2
  25. package/docs/families/sensor.html +7 -2
  26. package/docs/families/signature.html +7 -2
  27. package/docs/flatsql-host-contract.html +7 -2
  28. package/docs/flatsql-host-contract.md +2 -2
  29. package/docs/flatsql-streaming-standard.html +6 -1
  30. package/docs/gpu-module-abi.html +6 -1
  31. package/docs/index.html +21 -14
  32. package/docs/isomorphic-pthreads.html +31 -2
  33. package/docs/isomorphic-pthreads.md +104 -1
  34. package/docs/isomorphic-sdn-runtime-plan.html +6 -1
  35. package/docs/language-runtime-matrix.html +6 -1
  36. package/docs/llms.txt +1 -1
  37. package/docs/module-bundle-runtime-plan.html +6 -1
  38. package/docs/module-publication-standard.html +6 -1
  39. package/docs/node-red-default-node-parity.html +6 -1
  40. package/docs/protect-and-sign.html +6 -1
  41. package/docs/protocol-installation.html +6 -1
  42. package/docs/publication-submission.html +6 -1
  43. package/docs/secrets-capability.html +6 -1
  44. package/docs/styles.css +32 -0
  45. package/docs/testing-harness.html +6 -1
  46. package/docs/tri-runtime-parity-gate.html +6 -1
  47. package/docs/tri-runtime-parity.html +6 -1
  48. package/package.json +1 -1
  49. package/src/compiler/invokeGlue.js +38 -0
  50. package/src/host/browserModuleHarness.js +58 -35
  51. package/src/host/isomorphicLoader.js +21 -6
  52. package/src/host/wasiShim.js +57 -13
  53. package/src/host/wasiThreadBrowserWorker.mjs +4 -0
  54. package/src/host/wasiThreadHost.js +19 -3
  55. package/src/host/wasiThreadWorker.mjs +25 -2
  56. package/src/host/wasiThreadWorkerRuntime.js +2 -1
  57. package/src/index.d.ts +2 -0
  58. package/src/runtime/constants.js +6 -0
  59. package/src/testing/browserModuleHarness.js +2 -0
  60. package/src/testing/buildWasmEdgeRunner.js +147 -4
  61. package/src/testing/docker/wasmedge-parity.Dockerfile +23 -1
  62. package/src/testing/isomorphicHarness.js +5 -0
  63. package/src/testing/native/wasmedge-0.16.4-atomic-wait.patch +111 -0
  64. package/src/testing/native/wasmedge_wasi_threads_runner.c +353 -0
  65. package/src/testing/parityBrowserRunner.js +12 -4
  66. package/src/testing/parityHarness.js +17 -3
  67. package/src/testing/parityLanes.js +48 -5
@@ -2,10 +2,12 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <script>try{var t=localStorage.getItem("sdn-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}</script>
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
7
  <meta name="description" content="This repo now includes two related testing/runtime surfaces in" />
7
8
  <title>Testing harness — Space Data Module SDK</title>
8
9
  <link rel="stylesheet" href="./styles.css" />
10
+ <script src="./assembly.js" defer></script>
9
11
  <!-- SDN_CONSUMER_ASSETS_START -->
10
12
  <link rel="stylesheet" href="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.c3f68d1cfd88478f10d836a5e829d1dfc6a10157972cf0f7d4d319d0636f2cc4.css" integrity="sha384-fICuhN4I9xqOK1F5vGGzl26opuO+xQIvTZXMSV76lWTBiQ6AfzztMGLLDw8yRT8i" crossorigin="anonymous">
11
13
  <script defer src="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.f611e7e151a3b5c38384272f8894a4c6634f0a29bf925b0e02e0e27f8db0bfff.js" integrity="sha384-R+QqsKoWJIS7iWrtPmgJ49DVnb1hSGjZokupbLInJd1PLp8RwQhtF/hnaerc33ci" crossorigin="anonymous" data-sdn-wallet-public-client="v1"></script>
@@ -13,6 +15,7 @@
13
15
  <!-- SDN_CONSUMER_ASSETS_END -->
14
16
  </head>
15
17
  <body>
18
+ <canvas id="assembly" aria-hidden="true"></canvas>
16
19
  <a class="skip-link" href="#main">Skip to content</a>
17
20
  <sdn-stack-nav active="module-sdk"></sdn-stack-nav>
18
21
  <header class="site-header">
@@ -22,12 +25,13 @@
22
25
  <a href="./conformance.html">Conformance</a>
23
26
  <a href="https://github.com/DigitalArsenal/space-data-module-sdk">GitHub</a>
24
27
  <a class="stack-button" href="#stack">Stack</a>
28
+ <button class="theme-toggle" type="button" aria-label="Switch between light and dark theme" title="Light or dark theme"><svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg><svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg></button>
25
29
  </nav>
26
30
  </header>
27
31
  <div class="shell">
28
32
  <nav class="sidebar" id="sidebar" aria-label="Documentation">
29
33
  <h2>Start here</h2><ul>
30
- <li><a href="./index.html"><span>Harness family matrix</span></a></li>
34
+ <li><a href="./index.html"><span>Module types</span></a></li>
31
35
  <li><a href="./byo-wasm-quickstart.html"><span>BYO-wasm quickstart</span></a></li>
32
36
  <li><a href="./conformance.html"><span>Conformance kit</span></a></li>
33
37
  <li><a href="./protect-and-sign.html"><span>Protect and sign</span></a></li>
@@ -403,6 +407,7 @@ npm run benchmark:module-stream-1gib</code></pre></div>
403
407
  <footer class="site-footer">
404
408
  Space Data Module SDK — Apache-2.0. Every ABI page renders a Markdown
405
409
  source in <code>docs/</code>; edit the Markdown, never the HTML.
410
+ Contact <a href="mailto:tj@edgesource.com">tj@edgesource.com</a>.
406
411
  </footer>
407
412
  </body>
408
413
  </html>
@@ -2,10 +2,12 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <script>try{var t=localStorage.getItem("sdn-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}</script>
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
7
  <meta name="description" content="docs/tri-runtime-parity.md describes the parity harness: one" />
7
8
  <title>Tri-runtime parity gate — Space Data Module SDK</title>
8
9
  <link rel="stylesheet" href="./styles.css" />
10
+ <script src="./assembly.js" defer></script>
9
11
  <!-- SDN_CONSUMER_ASSETS_START -->
10
12
  <link rel="stylesheet" href="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.c3f68d1cfd88478f10d836a5e829d1dfc6a10157972cf0f7d4d319d0636f2cc4.css" integrity="sha384-fICuhN4I9xqOK1F5vGGzl26opuO+xQIvTZXMSV76lWTBiQ6AfzztMGLLDw8yRT8i" crossorigin="anonymous">
11
13
  <script defer src="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.f611e7e151a3b5c38384272f8894a4c6634f0a29bf925b0e02e0e27f8db0bfff.js" integrity="sha384-R+QqsKoWJIS7iWrtPmgJ49DVnb1hSGjZokupbLInJd1PLp8RwQhtF/hnaerc33ci" crossorigin="anonymous" data-sdn-wallet-public-client="v1"></script>
@@ -13,6 +15,7 @@
13
15
  <!-- SDN_CONSUMER_ASSETS_END -->
14
16
  </head>
15
17
  <body>
18
+ <canvas id="assembly" aria-hidden="true"></canvas>
16
19
  <a class="skip-link" href="#main">Skip to content</a>
17
20
  <sdn-stack-nav active="module-sdk"></sdn-stack-nav>
18
21
  <header class="site-header">
@@ -22,12 +25,13 @@
22
25
  <a href="./conformance.html">Conformance</a>
23
26
  <a href="https://github.com/DigitalArsenal/space-data-module-sdk">GitHub</a>
24
27
  <a class="stack-button" href="#stack">Stack</a>
28
+ <button class="theme-toggle" type="button" aria-label="Switch between light and dark theme" title="Light or dark theme"><svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg><svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg></button>
25
29
  </nav>
26
30
  </header>
27
31
  <div class="shell">
28
32
  <nav class="sidebar" id="sidebar" aria-label="Documentation">
29
33
  <h2>Start here</h2><ul>
30
- <li><a href="./index.html"><span>Harness family matrix</span></a></li>
34
+ <li><a href="./index.html"><span>Module types</span></a></li>
31
35
  <li><a href="./byo-wasm-quickstart.html"><span>BYO-wasm quickstart</span></a></li>
32
36
  <li><a href="./conformance.html"><span>Conformance kit</span></a></li>
33
37
  <li><a href="./protect-and-sign.html"><span>Protect and sign</span></a></li>
@@ -177,6 +181,7 @@ space-data-module parity-gate --gate-manifest ./parity/negative-control.json --e
177
181
  <footer class="site-footer">
178
182
  Space Data Module SDK — Apache-2.0. Every ABI page renders a Markdown
179
183
  source in <code>docs/</code>; edit the Markdown, never the HTML.
184
+ Contact <a href="mailto:tj@edgesource.com">tj@edgesource.com</a>.
180
185
  </footer>
181
186
  </body>
182
187
  </html>
@@ -2,10 +2,12 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
+ <script>try{var t=localStorage.getItem("sdn-theme");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}</script>
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
7
  <meta name="description" content="ONE dist/isomorphic/module.wasm, THREE runtimes, IDENTICAL behavior:" />
7
8
  <title>Tri-runtime parity — Space Data Module SDK</title>
8
9
  <link rel="stylesheet" href="./styles.css" />
10
+ <script src="./assembly.js" defer></script>
9
11
  <!-- SDN_CONSUMER_ASSETS_START -->
10
12
  <link rel="stylesheet" href="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.c3f68d1cfd88478f10d836a5e829d1dfc6a10157972cf0f7d4d319d0636f2cc4.css" integrity="sha384-fICuhN4I9xqOK1F5vGGzl26opuO+xQIvTZXMSV76lWTBiQ6AfzztMGLLDw8yRT8i" crossorigin="anonymous">
11
13
  <script defer src="https://static.spacedatanetwork.org/assets/hd-wallet-ui/2.0.28/sdn-wallet-public-client.f611e7e151a3b5c38384272f8894a4c6634f0a29bf925b0e02e0e27f8db0bfff.js" integrity="sha384-R+QqsKoWJIS7iWrtPmgJ49DVnb1hSGjZokupbLInJd1PLp8RwQhtF/hnaerc33ci" crossorigin="anonymous" data-sdn-wallet-public-client="v1"></script>
@@ -13,6 +15,7 @@
13
15
  <!-- SDN_CONSUMER_ASSETS_END -->
14
16
  </head>
15
17
  <body>
18
+ <canvas id="assembly" aria-hidden="true"></canvas>
16
19
  <a class="skip-link" href="#main">Skip to content</a>
17
20
  <sdn-stack-nav active="module-sdk"></sdn-stack-nav>
18
21
  <header class="site-header">
@@ -22,12 +25,13 @@
22
25
  <a href="./conformance.html">Conformance</a>
23
26
  <a href="https://github.com/DigitalArsenal/space-data-module-sdk">GitHub</a>
24
27
  <a class="stack-button" href="#stack">Stack</a>
28
+ <button class="theme-toggle" type="button" aria-label="Switch between light and dark theme" title="Light or dark theme"><svg class="sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg><svg class="moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z"/></svg></button>
25
29
  </nav>
26
30
  </header>
27
31
  <div class="shell">
28
32
  <nav class="sidebar" id="sidebar" aria-label="Documentation">
29
33
  <h2>Start here</h2><ul>
30
- <li><a href="./index.html"><span>Harness family matrix</span></a></li>
34
+ <li><a href="./index.html"><span>Module types</span></a></li>
31
35
  <li><a href="./byo-wasm-quickstart.html"><span>BYO-wasm quickstart</span></a></li>
32
36
  <li><a href="./conformance.html"><span>Conformance kit</span></a></li>
33
37
  <li><a href="./protect-and-sign.html"><span>Protect and sign</span></a></li>
@@ -198,6 +202,7 @@
198
202
  <footer class="site-footer">
199
203
  Space Data Module SDK — Apache-2.0. Every ABI page renders a Markdown
200
204
  source in <code>docs/</code>; edit the Markdown, never the HTML.
205
+ Contact <a href="mailto:tj@edgesource.com">tj@edgesource.com</a>.
201
206
  </footer>
202
207
  </body>
203
208
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.8.19",
3
+ "version": "0.8.21",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
6
6
  "type": "module",
@@ -302,6 +302,43 @@ function renderMethodDescriptors(methods) {
302
302
  .join("\n");
303
303
  }
304
304
 
305
+ // Direct-surface initializer for command-model artifacts (wasi toolchains).
306
+ //
307
+ // A command artifact links crt1-command: its _start sets up the main-thread
308
+ // descriptor (__wasi_init_tp, threads sysroot) and runs the global
309
+ // constructors, then main. A host that serves the direct surface never enters
310
+ // _start, so without this entry namespace-scope C++ objects stay zero-filled
311
+ // and the main thread has no pthread descriptor. It is exported under the
312
+ // conventional __wasm_call_ctors name that direct hosts call when a module has
313
+ // no reactor _initialize. It runs once per instance and is a no-op on an
314
+ // instance whose constructors have already run.
315
+ const COMMAND_DIRECT_INITIALIZER_SOURCE = `#if defined(__wasi__)
316
+ extern "C" void __wasm_call_ctors(void);
317
+ extern "C" __attribute__((weak)) void __wasi_init_tp(void);
318
+
319
+ namespace {
320
+ // volatile: the optimizer would otherwise evaluate the marking constructor at
321
+ // compile time and emit the flag as already set.
322
+ volatile int g_sdm_constructors_ran = 0;
323
+
324
+ __attribute__((constructor)) void MarkConstructorsRan() {
325
+ __atomic_store_n(&g_sdm_constructors_ran, 1, __ATOMIC_SEQ_CST);
326
+ }
327
+ } // namespace
328
+
329
+ extern "C" __attribute__((export_name("__wasm_call_ctors")))
330
+ void sdm_command_direct_initialize(void) {
331
+ if (__atomic_exchange_n(&g_sdm_constructors_ran, 1, __ATOMIC_SEQ_CST) != 0) {
332
+ return;
333
+ }
334
+ if (__wasi_init_tp) {
335
+ __wasi_init_tp();
336
+ }
337
+ __wasm_call_ctors();
338
+ }
339
+ #endif
340
+ `;
341
+
305
342
  export function generateInvokeSupportSource({ manifest = {}, includeCommandMain = true } = {}) {
306
343
  const methods = Array.isArray(manifest.methods) ? manifest.methods : [];
307
344
  return `#include <algorithm>
@@ -1812,6 +1849,7 @@ extern "C" uint32_t plugin_invoke_stream(
1812
1849
  return response_ptr;
1813
1850
  }
1814
1851
 
1852
+ ${includeCommandMain ? COMMAND_DIRECT_INITIALIZER_SOURCE : ""}
1815
1853
  ${includeCommandMain
1816
1854
  ? `int main(int argc, char **argv) {
1817
1855
  const char *shortcut_method = nullptr;
@@ -20,7 +20,7 @@
20
20
  * stdout for the response bytes.
21
21
  */
22
22
 
23
- import { createBrowserWasiShim, WasiExitError } from "./wasiShim.js";
23
+ import { createBrowserWasiShim, createSharedWasiProcess, WasiExitError } from "./wasiShim.js";
24
24
  import {
25
25
  createWasiThreadSpawn,
26
26
  isWasiThreadsModule,
@@ -297,6 +297,28 @@ function resolveManifestSurface(manifest) {
297
297
  return null;
298
298
  }
299
299
 
300
+ // Run the module's initializer, once per instance, before any direct call.
301
+ // A reactor exports `_initialize` (constructors + WASI init) and runs it on
302
+ // every instance, as before. A command artifact runs its constructors inside
303
+ // `_start`, which then runs main and consumes stdin, so a command instance
304
+ // only ever enters `_start`. An instance that serves the DIRECT surface of a
305
+ // command artifact runs the artifact's `__wasm_call_ctors` export instead:
306
+ // constructors (and main-thread setup) without main. Artifacts built before
307
+ // that export existed have no constructor entry a host can call without main;
308
+ // their direct surface stays uninitialized in every runtime alike.
309
+ function initializeInstance(instance, directSurface) {
310
+ const exports = instance.exports;
311
+ const reactorInitialize = exports[DefaultInvokeExports.reactorInitializeSymbol];
312
+ if (typeof reactorInitialize === "function") {
313
+ reactorInitialize();
314
+ return;
315
+ }
316
+ const constructors = exports[DefaultInvokeExports.constructorsSymbol];
317
+ if (directSurface && typeof constructors === "function") {
318
+ constructors();
319
+ }
320
+ }
321
+
300
322
  async function instantiateBrowserModule(options = {}) {
301
323
  let providedMemory = options.wasmMemory ?? options.memory ?? null;
302
324
  if (
@@ -307,7 +329,10 @@ async function instantiateBrowserModule(options = {}) {
307
329
  "Browser module harness memory must be a WebAssembly.Memory.",
308
330
  );
309
331
  }
332
+ const isThreaded = isWasiThreadsModule(options.wasmModule);
333
+ const processState = isThreaded ? createSharedWasiProcess(options) : null;
310
334
  const wasi = createBrowserWasiShim({
335
+ processState,
311
336
  args: options.args ?? [],
312
337
  env: options.env ?? {},
313
338
  stdinBytes: options.stdinBytes ?? new Uint8Array(),
@@ -323,7 +348,6 @@ async function instantiateBrowserModule(options = {}) {
323
348
  // `wasi.thread-spawn` over a SHARED imported memory. Such a module can only be
324
349
  // instantiated with shared memory, so force it here regardless of the caller's
325
350
  // sharedMemory flag; single-thread artifacts are unaffected.
326
- const isThreaded = isWasiThreadsModule(options.wasmModule);
327
351
  const memoryImports = moduleImports.filter((entry) => entry.kind === "memory");
328
352
  if (!providedMemory && memoryImports.length > 0) {
329
353
  providedMemory = createImportedMemory(
@@ -356,6 +380,7 @@ async function instantiateBrowserModule(options = {}) {
356
380
  threadHost = await createWasiThreadSpawn({
357
381
  wasmModule: options.wasmModule,
358
382
  memory: providedMemory,
383
+ processState,
359
384
  // Browser warm-pool sizing: cap pooled workers at how many guest threads
360
385
  // the module will actually ask for. Ignored by the Node lazy path.
361
386
  requestedThreads: options.maxThreads,
@@ -395,35 +420,24 @@ async function instantiateBrowserModule(options = {}) {
395
420
  Object.assign(importObject, bridge.imports);
396
421
  }
397
422
 
398
- instance = await WebAssembly.instantiate(options.wasmModule, importObject);
399
- memory = instance.exports.memory ?? providedMemory;
400
- if (memory) {
401
- if (
402
- options.sharedMemory === true &&
403
- !isSharedArrayBufferLike(memory.buffer)
404
- ) {
405
- throw new Error(
406
- "Browser module harness sharedMemory requires shared WebAssembly.Memory backing.",
407
- );
408
- }
409
- wasi.setMemory(memory);
410
- }
411
- if (instance.exports._initialize) {
412
- instance.exports._initialize();
413
- } else if (isThreaded && typeof instance.exports._start === "function") {
414
- // wasi-threads artifacts link the wasi command crt (exports `_start`), not a
415
- // reactor (`_initialize`). Run `_start` ONCE to execute global constructors
416
- // + WASI init before any direct invoke. The module declares no command
417
- // surface, so its main is an empty stub — `_start` runs ctors and proc_exit's
418
- // out; swallow that exit. Direct-invoke exports remain callable afterward
419
- // (the linear memory + globals persist).
420
- try {
421
- instance.exports._start();
422
- } catch (error) {
423
- if (!(error instanceof WasiExitError)) {
424
- throw error;
423
+ try {
424
+ instance = await WebAssembly.instantiate(options.wasmModule, importObject);
425
+ memory = instance.exports.memory ?? providedMemory;
426
+ if (memory) {
427
+ if (
428
+ options.sharedMemory === true &&
429
+ !isSharedArrayBufferLike(memory.buffer)
430
+ ) {
431
+ throw new Error(
432
+ "Browser module harness sharedMemory requires shared WebAssembly.Memory backing.",
433
+ );
425
434
  }
435
+ wasi.setMemory(memory);
426
436
  }
437
+ initializeInstance(instance, options.directSurface === true);
438
+ } catch (error) {
439
+ await threadHost?.terminateAll();
440
+ throw error;
427
441
  }
428
442
 
429
443
  return {
@@ -547,6 +561,7 @@ export async function createBrowserModuleHarness(options = {}) {
547
561
 
548
562
  const activeContext = await instantiateBrowserModule({
549
563
  wasmModule,
564
+ directSurface: surface === "direct",
550
565
  host,
551
566
  hostcallDispatch: options.hostcallDispatch,
552
567
  args: options.args,
@@ -561,8 +576,11 @@ export async function createBrowserModuleHarness(options = {}) {
561
576
  maxThreads: options.maxThreads,
562
577
  threadHostcallChannel: options.threadHostcallChannel,
563
578
  enableBrowserWasiThreads: options.enableBrowserWasiThreads,
579
+ wasiThreadWorkerBaseUrl: options.wasiThreadWorkerBaseUrl,
580
+ wasiThreadWorkerUrl: options.wasiThreadWorkerUrl,
564
581
  });
565
582
  const { instance, bridge, wasi, memory, threadHost } = activeContext;
583
+ let lastCommandContext = null;
566
584
  const allowRawInvoke = options.allowRawInvoke !== false;
567
585
 
568
586
  // --- Invoke helpers ---
@@ -805,6 +823,8 @@ export async function createBrowserModuleHarness(options = {}) {
805
823
  async function invokeCommandRaw(stdinBytes) {
806
824
  const commandContext = await instantiateBrowserModule({
807
825
  wasmModule,
826
+ // Command CRT startup consumes stdin and is not reentrant. Run it only
827
+ // below, after this fresh instance has received the actual request.
808
828
  host,
809
829
  hostcallDispatch: options.hostcallDispatch,
810
830
  args: options.args,
@@ -820,7 +840,10 @@ export async function createBrowserModuleHarness(options = {}) {
820
840
  maxThreads: options.maxThreads,
821
841
  threadHostcallChannel: options.threadHostcallChannel,
822
842
  enableBrowserWasiThreads: options.enableBrowserWasiThreads,
843
+ wasiThreadWorkerBaseUrl: options.wasiThreadWorkerBaseUrl,
844
+ wasiThreadWorkerUrl: options.wasiThreadWorkerUrl,
823
845
  });
846
+ lastCommandContext = commandContext;
824
847
  try {
825
848
  const commandExport = commandContext.instance.exports[DefaultInvokeExports.commandSymbol];
826
849
  if (typeof commandExport !== "function") {
@@ -833,6 +856,8 @@ export async function createBrowserModuleHarness(options = {}) {
833
856
  if (!(error instanceof WasiExitError) || error.code !== 0) {
834
857
  throw error;
835
858
  }
859
+ } finally {
860
+ await commandContext.threadHost?.terminateAll();
836
861
  }
837
862
  return commandContext.wasi.stdout;
838
863
  }
@@ -903,11 +928,9 @@ export async function createBrowserModuleHarness(options = {}) {
903
928
  return new Uint8Array(memory.buffer, ptr, size).slice();
904
929
  }
905
930
 
906
- function destroy() {
931
+ async function destroy() {
907
932
  wasi.flushOutput();
908
- if (threadHost) {
909
- void threadHost.terminateAll();
910
- }
933
+ await threadHost?.terminateAll();
911
934
  }
912
935
 
913
936
  return {
@@ -920,12 +943,12 @@ export async function createBrowserModuleHarness(options = {}) {
920
943
  module: wasmModule,
921
944
  host,
922
945
  bridge,
923
- wasi,
946
+ get wasi() { return lastCommandContext?.wasi ?? wasi; },
924
947
  memory,
925
948
  // Present only for isomorphic-pthreads (wasi-threads) artifacts: exposes the
926
949
  // spawn host so callers can observe real thread activity
927
950
  // (spawnCount/distinctOsThreadCount).
928
- threadHost,
951
+ get threadHost() { return lastCommandContext?.threadHost ?? threadHost; },
929
952
  callHost,
930
953
  invokeDirect,
931
954
  invoke,
@@ -65,12 +65,25 @@ async function createWasmEdgeCommandHarness(options = {}) {
65
65
  const loadableBytes = toLoadableWasmBytes(wasmBytes);
66
66
  const inspection = await inspectModule(loadableBytes);
67
67
 
68
- if (!inspection.exports.includes(DefaultInvokeExports.commandSymbol)) {
68
+ const threaded = inspection.imports.some((entry) =>
69
+ entry.module === "wasi" && entry.name === "thread-spawn");
70
+ const hasCommand = inspection.exports.includes(DefaultInvokeExports.commandSymbol);
71
+ const hasDirect = inspection.exports.includes(DefaultInvokeExports.invokeSymbol);
72
+ // The one-shot wasi-threads runner serves the direct surface of a reactor,
73
+ // and of a command artifact when the caller asks for it (--sdm-direct): the
74
+ // module's constructors then run through its initializer export, never main.
75
+ const surface = hasCommand && !(options.surface === "direct" && threaded && hasDirect)
76
+ ? "command"
77
+ : "direct";
78
+ if (surface === "direct" && (!threaded || !hasDirect)) {
69
79
  throw new Error(
70
80
  "Standalone WasmEdge loading requires a command-surface artifact with the _start export.",
71
81
  );
72
82
  }
73
83
 
84
+ const command = threaded
85
+ ? await (await import("../testing/buildWasmEdgeRunner.js")).resolveWasmEdgeWasiThreadsRunner(options)
86
+ : options.wasmEdgeRunnerBinary ?? options.wasmEdgeBinary ?? "wasmedge";
74
87
  let launchWasmPath = wasmPath;
75
88
  let tempArtifactDir = null;
76
89
  if (loadableBytes.byteLength !== wasmBytes.byteLength) {
@@ -84,9 +97,10 @@ async function createWasmEdgeCommandHarness(options = {}) {
84
97
  await writeFile(launchWasmPath, loadableBytes);
85
98
  }
86
99
 
87
- const command = options.wasmEdgeBinary ?? "wasmedge";
88
100
  const args = [
101
+ ...(surface === "direct" && hasCommand ? ["--sdm-direct"] : []),
89
102
  ...(options.enableThreads === false ? [] : ["--enable-threads"]),
103
+ ...Object.entries(options.env ?? {}).flatMap(([key, value]) => ["--env", `${key}=${value}`]),
90
104
  launchWasmPath,
91
105
  ...(Array.isArray(options.args) ? options.args : []),
92
106
  ];
@@ -156,7 +170,7 @@ async function createWasmEdgeCommandHarness(options = {}) {
156
170
  runtime: {
157
171
  kind: "wasmedge",
158
172
  profile: inspection.profile,
159
- surface: "command",
173
+ surface,
160
174
  },
161
175
  launchPlan,
162
176
  invokeRaw,
@@ -225,8 +239,7 @@ export async function loadModule(options = {}) {
225
239
  // byte-level question.
226
240
  const needsCompiledModule =
227
241
  runtimeKind === "wasmedge" &&
228
- !runtimeHostRequested &&
229
- !options.wasmEdgeRunnerBinary;
242
+ !runtimeHostRequested;
230
243
  const needsBytes = Boolean(signaturePolicy) || runtimeKind === "wasmedge";
231
244
  let artifactBytes = null;
232
245
  let wasmModule = null;
@@ -264,9 +277,11 @@ export async function loadModule(options = {}) {
264
277
  });
265
278
  if (needsCompiledModule) {
266
279
  const inspection = await inspectModule(wasmModule);
280
+ const threaded = inspection.imports.some((entry) => entry.module === "wasi" && entry.name === "thread-spawn");
267
281
  if (
268
282
  (inspection.profile === "standalone" || inspection.profile === "module-host-abi") &&
269
- inspection.exports.includes("_start")
283
+ (inspection.exports.includes("_start") || (threaded && inspection.exports.includes("plugin_invoke_stream"))) &&
284
+ (!options.wasmEdgeRunnerBinary || options.surface === "command" || threaded)
270
285
  ) {
271
286
  return attachHostDispatch(
272
287
  await createWasmEdgeCommandHarness(options),
@@ -34,10 +34,34 @@ export class WasiExitError extends Error {
34
34
  }
35
35
  }
36
36
 
37
+ // One process owns its descriptors, even when libc executes main on a pthread.
38
+ // Offsets are atomically reserved so concurrent readers/writers never duplicate
39
+ // input or overwrite another thread's output. Overflow is an explicit WASI error.
40
+ export function createSharedWasiProcess({ args = [], env = {}, stdinBytes = [], maxOutputBytes = 16 * 1024 * 1024 } = {}) {
41
+ const bytes = new Uint8Array(stdinBytes);
42
+ const input = new Uint8Array(new SharedArrayBuffer(bytes.length));
43
+ input.set(bytes);
44
+ return {
45
+ args, env, input,
46
+ control: new Int32Array(new SharedArrayBuffer(5 * 4)),
47
+ stdout: new Uint8Array(new SharedArrayBuffer(maxOutputBytes)),
48
+ stderr: new Uint8Array(new SharedArrayBuffer(maxOutputBytes)),
49
+ };
50
+ }
51
+
52
+ function reserve(control, index, length, capacity) {
53
+ for (;;) {
54
+ const offset = Atomics.load(control, index);
55
+ if (length > capacity - offset) return -1;
56
+ if (Atomics.compareExchange(control, index, offset, offset + length) === offset) return offset;
57
+ }
58
+ }
59
+
37
60
  export function createBrowserWasiShim(options = {}) {
38
- const args = options.args ?? [];
39
- const env = options.env ?? {};
40
- const stdinBytes = new Uint8Array(options.stdinBytes ?? []);
61
+ const shared = options.processState;
62
+ const args = shared?.args ?? options.args ?? [];
63
+ const env = shared?.env ?? options.env ?? {};
64
+ const stdinBytes = shared?.input ?? new Uint8Array(options.stdinBytes ?? []);
41
65
  const logOutput = options.logOutput === true;
42
66
  const performanceApi = options.performance ?? globalThis.performance ?? {
43
67
  now: () => Date.now(),
@@ -100,7 +124,14 @@ export function createBrowserWasiShim(options = {}) {
100
124
  const base = iovsPtr + i * 8;
101
125
  const ptr = dv.getUint32(base, true);
102
126
  const len = dv.getUint32(base + 4, true);
103
- target.push(bytes.slice(ptr, ptr + len));
127
+ if (shared) {
128
+ const output = fd === 1 ? shared.stdout : shared.stderr;
129
+ const offset = reserve(shared.control, fd, len, output.length);
130
+ if (offset < 0) return 51; // NOSPC: never silently truncate command output.
131
+ output.set(bytes.subarray(ptr, ptr + len), offset);
132
+ } else {
133
+ target.push(bytes.slice(ptr, ptr + len));
134
+ }
104
135
  totalWritten += len;
105
136
  }
106
137
 
@@ -119,16 +150,25 @@ export function createBrowserWasiShim(options = {}) {
119
150
  let totalRead = 0;
120
151
 
121
152
  for (let i = 0; i < iovsLen; i += 1) {
122
- if (stdinOffset >= stdinBytes.length) {
123
- break;
124
- }
125
153
  const base = iovsPtr + i * 8;
126
154
  const ptr = dv.getUint32(base, true);
127
155
  const len = dv.getUint32(base + 4, true);
128
- const remaining = stdinBytes.length - stdinOffset;
129
- const count = Math.min(len, remaining);
130
- bytes.set(stdinBytes.subarray(stdinOffset, stdinOffset + count), ptr);
131
- stdinOffset += count;
156
+ let offset, count;
157
+ if (shared) {
158
+ do {
159
+ offset = Atomics.load(shared.control, 0);
160
+ count = Math.min(len, stdinBytes.length - offset);
161
+ } while (Atomics.compareExchange(shared.control, 0, offset, offset + count) !== offset);
162
+ } else {
163
+ offset = stdinOffset;
164
+ count = Math.min(len, stdinBytes.length - offset);
165
+ stdinOffset += count;
166
+ }
167
+ if (count === 0) {
168
+ if (offset >= stdinBytes.length) break;
169
+ continue; // An empty iovec does not mean end of file.
170
+ }
171
+ bytes.set(stdinBytes.subarray(offset, offset + count), ptr);
132
172
  totalRead += count;
133
173
  }
134
174
 
@@ -243,6 +283,10 @@ export function createBrowserWasiShim(options = {}) {
243
283
  }
244
284
 
245
285
  function proc_exit(code) {
286
+ if (shared) {
287
+ Atomics.store(shared.control, 4, code);
288
+ Atomics.store(shared.control, 3, 1);
289
+ }
246
290
  if (logOutput) {
247
291
  flushOutput();
248
292
  }
@@ -317,10 +361,10 @@ export function createBrowserWasiShim(options = {}) {
317
361
  getMemory,
318
362
  flushOutput,
319
363
  get stdout() {
320
- return concatChunks(stdoutChunks);
364
+ return shared ? shared.stdout.slice(0, Atomics.load(shared.control, 1)) : concatChunks(stdoutChunks);
321
365
  },
322
366
  get stderr() {
323
- return concatChunks(stderrChunks);
367
+ return shared ? shared.stderr.slice(0, Atomics.load(shared.control, 2)) : concatChunks(stderrChunks);
324
368
  },
325
369
  };
326
370
  }
@@ -16,6 +16,7 @@ self.onmessage = (event) => {
16
16
  wasmModule,
17
17
  memory,
18
18
  hostcallChannel,
19
+ processState: message.processState,
19
20
  });
20
21
  runtime.instantiate();
21
22
  self.postMessage({ t: "ready", ok: true });
@@ -37,6 +38,9 @@ self.onmessage = (event) => {
37
38
  instance.exports.wasi_thread_start(message.tid, message.startArg);
38
39
  } catch (error) {
39
40
  if (!(error && error.name === "WasiExitError")) {
41
+ // The thread that joins this one may be blocked in the guest and never
42
+ // handle the message below; report the fault from this worker as well.
43
+ console.error(`[wasi-thread] guest thread ${message.tid} trapped:`, error);
40
44
  self.postMessage({
41
45
  t: "error",
42
46
  tid: message.tid,
@@ -202,7 +202,7 @@ const BROWSER_POOL_PROBE_TIMEOUT_MS = 1500;
202
202
  // surfacing) for the life of the pool.
203
203
  function armBrowserPool(
204
204
  created,
205
- { wasmModule, memory, hostcallChannel, timeoutMs, onExit },
205
+ { wasmModule, memory, hostcallChannel, processState, timeoutMs, onExit },
206
206
  ) {
207
207
  return new Promise((resolve) => {
208
208
  let remaining = created.length;
@@ -266,6 +266,7 @@ function armBrowserPool(
266
266
  wasmModule,
267
267
  memory,
268
268
  hostcallChannel: hostcallChannel ?? null,
269
+ processState,
269
270
  });
270
271
  }
271
272
  });
@@ -333,6 +334,7 @@ export async function createWasiThreadSpawn({
333
334
  memory,
334
335
  requestedThreads,
335
336
  hostcallChannel,
337
+ processState,
336
338
  requiresHostcalls = false,
337
339
  enableBrowserThreads,
338
340
  browserWorkerBaseUrl,
@@ -367,17 +369,29 @@ export async function createWasiThreadSpawn({
367
369
  );
368
370
  const NodeWorker = workerThreads.Worker;
369
371
  const nodeWorkerUrl = new URL("./wasiThreadWorker.mjs", import.meta.url);
372
+ // An --input-type parent runs eval/stdin, but this worker loads a file.
373
+ // Inheriting that flag makes the worker fail while the guest is blocked
374
+ // synchronously in pthread_join and cannot receive the error event.
375
+ const parentArgs = globalThis.process.execArgv;
376
+ const sourceFlags = ["--input-type", "--eval", "-e", "--print", "-p"];
377
+ const execArgv = parentArgs.some((arg) => arg.startsWith("--input-type"))
378
+ ? parentArgs.filter((arg, index, args) =>
379
+ !sourceFlags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)) &&
380
+ !(index > 0 && sourceFlags.includes(args[index - 1])))
381
+ : undefined;
370
382
 
371
383
  const threadSpawn = (startArg) => {
372
384
  const tid = (nextTid += 1);
373
385
  try {
374
386
  const worker = new NodeWorker(nodeWorkerUrl, {
387
+ execArgv,
375
388
  workerData: {
376
389
  wasmModule,
377
390
  memory,
378
391
  tid,
379
392
  startArg,
380
393
  hostcallChannel: hostcallChannel ?? null,
394
+ processState,
381
395
  },
382
396
  });
383
397
  // Node exposes the OS-thread id per Worker — distinct ids are direct
@@ -386,8 +400,9 @@ export async function createWasiThreadSpawn({
386
400
  osThreadIds.add(worker.threadId);
387
401
  }
388
402
  worker.on("error", (error) => {
389
- // A worker crash cannot be surfaced to the guest synchronously; log it
390
- // so a hung pthread_join is diagnosable rather than silent.
403
+ // A worker crash cannot be surfaced to the guest synchronously. The
404
+ // worker itself writes the fault to stderr first, because this
405
+ // handler never runs while this thread is blocked in pthread_join.
391
406
  // eslint-disable-next-line no-console
392
407
  console.error("[wasi-thread] worker error:", error);
393
408
  });
@@ -491,6 +506,7 @@ export async function createWasiThreadSpawn({
491
506
  wasmModule,
492
507
  memory,
493
508
  hostcallChannel,
509
+ processState,
494
510
  timeoutMs: BROWSER_POOL_PROBE_TIMEOUT_MS,
495
511
  onExit: returnWorkerToIdle,
496
512
  });