space-data-module-sdk 0.5.5 → 0.5.9

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.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # Space Data Module SDK
2
2
 
3
- `space-data-module-sdk` defines the canonical module artifact for the Space Data
4
- stack: WebAssembly code, an embedded FlatBuffer manifest, a stable capability
5
- vocabulary, a single-file bundle format, and the signing and transport records
6
- used to move modules between hosts.
3
+ `space-data-module-sdk` defines the canonical Space Data module architecture for
4
+ WebAssembly artifacts: an embedded FlatBuffer manifest, canonical ABI exports,
5
+ a stable capability vocabulary, single-file packaging, and the signing and
6
+ transport records used to move modules between hosts.
7
7
 
8
8
  This repository is the source of truth for module-level concerns:
9
9
 
@@ -11,17 +11,34 @@ This repository is the source of truth for module-level concerns:
11
11
  - embedded manifest exports inside `.wasm` modules
12
12
  - standards-aware compliance and capability validation
13
13
  - module compilation and protection
14
- - the `sds.bundle` single-file custom section
14
+ - the `sds.bundle` single-file container
15
15
  - deployment authorization plus SDS publication records (`REC`, `PNM`, `ENC`)
16
16
  - the first canonical module hostcall/import ABI surface
17
+ - shared module-level testing/runtime harnesses, including the WasmEdge
18
+ process runner used by package-level validation suites
17
19
 
18
20
  <p align="center">
19
21
  <img src="docs/architecture.svg" alt="Module architecture overview" width="820" />
20
22
  </p>
21
23
 
22
- ## Artifact Model
24
+ ## Shared Harness Ownership
23
25
 
24
- A module built with this SDK is a `.wasm` artifact with:
26
+ This repo owns the generic module-side runtime harnesses used across the stack:
27
+
28
+ - `createModuleHarness(...)` for process and WasmEdge-backed module invocation
29
+ - `resolveModuleHarnessLaunchPlan(...)` for portable launch planning
30
+ - `buildWasmEdgeEmscriptenPthreadRunner(...)` plus the shared native runner
31
+ source and runner-level pthread smoke test
32
+
33
+ Flow-specific standalone harnessing lives in `sdn-flow`, which layers flow
34
+ enqueue/drain behavior on top of its compiled standalone runtime host. Package-
35
+ specific validation suites, including conjunction replay/V&V, are expected to
36
+ sit on top of these shared harnesses instead of defining their own runtime
37
+ process model.
38
+
39
+ ## Module Artifact Model
40
+
41
+ A compliant module built with this SDK is always a valid `.wasm` artifact with:
25
42
 
26
43
  - an embedded `PluginManifest.fbs`
27
44
  - exported manifest accessors:
@@ -37,10 +54,15 @@ A module built with this SDK is a `.wasm` artifact with:
37
54
  - resolved deployment plans and input bindings
38
55
  - deployment authorization
39
56
  - auxiliary FlatBuffer or raw payloads
40
- - optional appended SDS `REC` publication trailers carrying:
57
+ - optional appended SDS FlatBuffer publication records in a trailing `REC`
58
+ container carrying:
41
59
  - `PNM` digital-signature/publication metadata
42
60
  - `ENC` encrypted-delivery metadata
43
- - auxiliary FlatBuffer or raw payloads
61
+
62
+ Single-file bundling does not append arbitrary bytes to the end of the wasm
63
+ module. The one-file module container is `sds.bundle`, stored as a standard
64
+ wasm custom section. Appended bytes are reserved for SDS publication records
65
+ such as `PNM` and `ENC`, wrapped in a trailing `REC` FlatBuffer collection.
44
66
 
45
67
  The module contract stays the same whether the artifact is loaded directly,
46
68
  wrapped in a deployment envelope, or shipped as one bundled `.wasm` file.
@@ -65,7 +87,7 @@ same request bytes from guest memory and returns response bytes in guest
65
87
  memory.
66
88
 
67
89
  For simple single-input / single-output methods, command mode also supports a
68
- degenerate raw shortcut:
90
+ raw shortcut:
69
91
 
70
92
  ```bash
71
93
  wasmedge module.wasm --method echo < input.fb > output.fb
@@ -101,8 +123,8 @@ anywhere it can:
101
123
  - read FlatBuffers
102
124
  - honor the module capability and host ABI contract
103
125
 
104
- That target set matches the WebAssembly and FlatBuffers runtime families already
105
- used in the companion `flatbuffers/wasm` work:
126
+ That architecture is intended to stay portable across the common WebAssembly
127
+ and FlatBuffer host environments:
106
128
 
107
129
  - browser
108
130
  - Node.js
@@ -126,11 +148,58 @@ This repo currently includes:
126
148
  - a reference Node host and sync `sdn_host` bridge for the first hostcall
127
149
  surface
128
150
 
151
+ ## Runtime Targets
152
+
153
+ Manifests can declare coarse runtime targets in `manifest.runtimeTargets`.
154
+
155
+ If a manifest declares `runtimeTargets: ["wasi"]`, this SDK treats that as
156
+ "standalone WASI, no host wrapper required." In practice that currently means:
157
+
158
+ - the artifact must declare the `command` invoke surface
159
+ - declared capabilities must stay within the pure WASI subset:
160
+ `logging`, `clock`, `random`, `filesystem`, `pipe`
161
+ - hosted protocols may only use `wasi-pipe` transport
162
+
163
+ If a manifest declares `runtimeTargets: ["wasmedge"]`, this SDK treats that as
164
+ the preferred server-side target when the guest needs network-oriented runtime
165
+ features such as sockets or TLS. Plain `wasi` remains the strict portability
166
+ baseline; `wasmedge` is the practical higher-capability target.
167
+
168
+ ## WasmEdge Pthreads
169
+
170
+ `space-data-module-sdk` is also the source of truth for module thread-model
171
+ selection.
172
+
173
+ - `compileModuleFromSource({ threadModel })` accepts an explicit thread model.
174
+ - If `threadModel` is omitted, the SDK resolves it from `manifest.runtimeTargets`.
175
+ - `runtimeTargets: ["wasmedge"]` defaults to `emscripten-pthreads`.
176
+ - Other targets currently default to `single-thread`.
177
+
178
+ WasmEdge-targeted pthread builds do not use the embedded `sdn-emception`
179
+ toolchain. They require a real system Emscripten installation on `PATH`, and
180
+ the compiler result plus guest-link bundle metadata preserve the selected
181
+ `threadModel`.
182
+
183
+ As emitted by current Emscripten, these pthread artifacts still import
184
+ Emscripten `env.*` host functions plus imported shared memory. That means a
185
+ bare `wasmedge` CLI invocation is not yet the direct execution path for them;
186
+ they currently require a WasmEdge-side host shim that satisfies the Emscripten
187
+ pthread contract.
188
+
189
+ If a runtime cannot interoperate with the guest pthread contract directly,
190
+ document that as a wrapper requirement instead of changing the guest artifact
191
+ semantics.
192
+
129
193
  ## Testing
130
194
 
131
195
  This repo now exposes a manifest-driven harness generator from
132
196
  `space-data-module-sdk/testing` and two complementary integration suites:
133
197
 
198
+ - shared process-level helpers for command-surface runtimes:
199
+ - `createPluginInvokeProcessClient(...)`
200
+ - `resolveWasmEdgePluginLaunchPlan(...)`
201
+ - `buildWasmEdgeEmscriptenPthreadRunner(...)`
202
+
134
203
  - `npm run test:runtime-matrix`
135
204
  - cross-language runtime smoke across the same WASM in Node.js, Go, Python,
136
205
  Rust, Java, C#, and Swift
@@ -145,20 +214,6 @@ The detailed edge cases and the current WASI-vs-host portability boundary are
145
214
  documented in
146
215
  [`docs/testing-harness.md`](./docs/testing-harness.md).
147
216
 
148
- If a manifest declares `runtimeTargets: ["wasi"]`, this SDK now treats that as
149
- "standalone WASI, no host wrapper required." In practice that currently means:
150
-
151
- - the artifact must declare the `command` invoke surface
152
- - declared capabilities must stay within the pure WASI subset:
153
- `logging`, `clock`, `random`, `filesystem`, `pipe`
154
- - hosted protocols may only use `wasi-pipe` transport
155
-
156
- For maximum server-side portability with guest-owned network services, use
157
- `runtimeTargets: ["wasmedge"]`. That target is intended for WasmEdge
158
- environments with socket/TLS extensions, while plain `wasi` remains the strict
159
- no-wrapper baseline. The Node-RED-oriented parity map lives in
160
- [`docs/node-red-default-node-parity.md`](./docs/node-red-default-node-parity.md).
161
-
162
217
  ## Install
163
218
 
164
219
  ```bash
@@ -176,6 +231,8 @@ import {
176
231
  validateManifestWithStandards,
177
232
  } from "space-data-module-sdk";
178
233
 
234
+ manifest.runtimeTargets = ["wasmedge"];
235
+
179
236
  const manifestBytes = encodePluginManifest(manifest);
180
237
  const validation = await validateManifestWithStandards(manifest);
181
238
  if (!validation.ok) {
@@ -186,6 +243,8 @@ const compilation = await compileModuleFromSource({
186
243
  manifest,
187
244
  sourceCode,
188
245
  language: "c",
246
+ // Optional. Defaults from manifest.runtimeTargets.
247
+ // threadModel: "emscripten-pthreads",
189
248
  });
190
249
 
191
250
  const bundle = await createSingleFileBundle({
@@ -243,10 +302,11 @@ The full contract split is documented in
243
302
 
244
303
  `sds.bundle` keeps module delivery to one file without changing WebAssembly
245
304
  loadability for the runtime payload itself. The SDK writes the bundle as a
246
- standard custom section inside the wasm module and, when the artifact is
247
- signed or encrypted for publication, appends an SDS `REC` trailer after the
248
- wasm bytes. Loaders must scan and strip that trailer before handing bytes to a
249
- runtime such as WasmEdge.
305
+ standard custom section inside the wasm module. When the artifact is signed or
306
+ encrypted for publication, SDS appends FlatBuffer publication records after the
307
+ wasm bytes in a trailing `REC` container. Loaders must scan from the end,
308
+ resolve `PNM` / `ENC`, strip or decrypt as needed, and only then hand the
309
+ remaining raw wasm bytes to a runtime such as WasmEdge.
250
310
 
251
311
  The reference path lives in
252
312
  [`examples/single-file-bundle`](./examples/single-file-bundle):
@@ -263,7 +323,7 @@ for resolved protocol installations and producer input bindings.
263
323
 
264
324
  ## Module Publication
265
325
 
266
- Packages that publish SDN modules now use the canonical `sdn-module`
326
+ Packages that publish Space Data modules use the canonical `sdn-module`
267
327
  publication descriptor. That descriptor covers:
268
328
 
269
329
  - standalone module packages
@@ -323,7 +383,7 @@ includes:
323
383
 
324
384
  Manifests can also declare coarse runtime targets for planning and compliance:
325
385
 
326
- `node` `browser` `wasi` `server` `desktop` `edge`
386
+ `node` `browser` `wasi` `wasmedge` `server` `desktop` `edge`
327
387
 
328
388
  ## Environment Notes
329
389
 
@@ -379,7 +439,8 @@ npm run check:compliance
379
439
  ```
380
440
 
381
441
  Node.js `>=20` is required. The compiler uses `sdn-emception` and `flatc-wasm`
382
- by default.
442
+ by default for the embedded toolchain path. WasmEdge pthread builds require a
443
+ system Emscripten toolchain on `PATH`.
383
444
 
384
445
  If another repo needs the same compiler runtime, the package also exposes a
385
446
  shared emception session at `space-data-module-sdk/compiler/emception` with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "space-data-module-sdk",
3
- "version": "0.5.5",
3
+ "version": "0.5.9",
4
4
  "description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
5
5
  "type": "module",
6
6
  "types": "./src/index.d.ts",
@@ -1,7 +1,11 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
3
5
  import {
4
6
  mkdtemp,
7
+ mkdir,
8
+ readFile,
5
9
  rm,
6
10
  writeFile,
7
11
  } from "node:fs/promises";
@@ -24,7 +28,11 @@ import {
24
28
  } from "./flatcSupport.js";
25
29
  import { runWithEmceptionLock } from "./emceptionNode.js";
26
30
  import { encodePluginManifest, toEmbeddedPluginManifest } from "../manifest/index.js";
27
- import { DefaultInvokeExports, InvokeSurface } from "../runtime/constants.js";
31
+ import {
32
+ DefaultInvokeExports,
33
+ InvokeSurface,
34
+ RuntimeTarget,
35
+ } from "../runtime/constants.js";
28
36
  import {
29
37
  appendPublicationRecordCollection,
30
38
  createEncryptedEnvelopePayload,
@@ -55,6 +63,12 @@ import { sha256Bytes } from "../utils/crypto.js";
55
63
  import { getWasmWallet } from "../utils/wasmCrypto.js";
56
64
 
57
65
  const C_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
66
+ const execFileAsync = promisify(execFile);
67
+
68
+ export const ModuleThreadModel = Object.freeze({
69
+ SINGLE_THREAD: "single-thread",
70
+ EMSCRIPTEN_PTHREADS: "emscripten-pthreads",
71
+ });
58
72
 
59
73
  function selectCompiler(language) {
60
74
  const normalized = String(language ?? "c").trim().toLowerCase();
@@ -81,16 +95,63 @@ function buildCompilerArgs(exportedSymbols, options = {}) {
81
95
  (symbol) => "-Wl,--export=" + symbol,
82
96
  );
83
97
  const extraArgs = [];
98
+ const threadArgs = [];
99
+ if (options.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS) {
100
+ threadArgs.push("-pthread");
101
+ }
84
102
  if (options.allowUndefinedImports === true) {
85
103
  extraArgs.push("-s", "ERROR_ON_UNDEFINED_SYMBOLS=0", "-Wl,--allow-undefined");
86
104
  }
87
- const args = ["-O2", "-s", "STANDALONE_WASM=1", ...extraArgs, ...linkerExports];
105
+ const args = [
106
+ "-O2",
107
+ ...threadArgs,
108
+ "-s",
109
+ "STANDALONE_WASM=1",
110
+ ...extraArgs,
111
+ ...linkerExports,
112
+ ];
88
113
  if (options.noEntry === true) {
89
114
  args.splice(1, 0, "--no-entry");
90
115
  }
91
116
  return args;
92
117
  }
93
118
 
119
+ function normalizeThreadModel(value) {
120
+ if (value === undefined || value === null || value === "") {
121
+ return null;
122
+ }
123
+ const normalized = String(value).trim().toLowerCase();
124
+ if (normalized === ModuleThreadModel.SINGLE_THREAD) {
125
+ return ModuleThreadModel.SINGLE_THREAD;
126
+ }
127
+ if (normalized === ModuleThreadModel.EMSCRIPTEN_PTHREADS) {
128
+ return ModuleThreadModel.EMSCRIPTEN_PTHREADS;
129
+ }
130
+ throw new Error(
131
+ `Unsupported threadModel "${value}". Expected "${ModuleThreadModel.SINGLE_THREAD}" or "${ModuleThreadModel.EMSCRIPTEN_PTHREADS}".`,
132
+ );
133
+ }
134
+
135
+ function resolveThreadModel({ manifest, threadModel } = {}) {
136
+ const explicit = normalizeThreadModel(threadModel);
137
+ if (explicit) {
138
+ return explicit;
139
+ }
140
+ const runtimeTargets = Array.isArray(manifest?.runtimeTargets)
141
+ ? manifest.runtimeTargets
142
+ .map((target) => String(target ?? "").trim().toLowerCase())
143
+ .filter(Boolean)
144
+ : [];
145
+ if (runtimeTargets.includes(RuntimeTarget.WASMEDGE)) {
146
+ return ModuleThreadModel.EMSCRIPTEN_PTHREADS;
147
+ }
148
+ return ModuleThreadModel.SINGLE_THREAD;
149
+ }
150
+
151
+ function requiresSystemEmscripten(threadModel) {
152
+ return threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS;
153
+ }
154
+
94
155
  function guestLinkSymbolPrefix(pluginId) {
95
156
  const normalized = String(pluginId ?? "module");
96
157
  const ascii = Array.from(new TextEncoder().encode(normalized))
@@ -280,6 +341,7 @@ function createGuestLinkBundleEntries(guestLink) {
280
341
  methodSymbols: guestLink.methodSymbols,
281
342
  methodIds: Object.keys(guestLink.methodSymbols ?? {}),
282
343
  language: guestLink.language,
344
+ threadModel: guestLink.threadModel ?? ModuleThreadModel.SINGLE_THREAD,
283
345
  },
284
346
  description: "Guest-link metadata for monolithic flow linking.",
285
347
  },
@@ -302,6 +364,14 @@ async function writeFilesToEmception(emception, rootDir, files) {
302
364
  }
303
365
  }
304
366
 
367
+ async function writeFilesToDirectory(rootDir, files) {
368
+ for (const [relativePath, content] of Object.entries(files)) {
369
+ const filePath = path.join(rootDir, relativePath);
370
+ await mkdir(path.dirname(filePath), { recursive: true });
371
+ await writeFile(filePath, content);
372
+ }
373
+ }
374
+
305
375
  function removeEmceptionDirectory(emception, directoryPath) {
306
376
  if (!emception.FS.analyzePath(directoryPath).exists) {
307
377
  return;
@@ -466,6 +536,8 @@ async function compileWithEmception(options = {}) {
466
536
  language,
467
537
  symbolPrefix: guestLink.prefix,
468
538
  methodSymbols: guestLink.methodSymbols,
539
+ threadModel:
540
+ compileOptions.threadModel ?? ModuleThreadModel.SINGLE_THREAD,
469
541
  objectBytes: linkObjectBytes,
470
542
  },
471
543
  };
@@ -483,6 +555,166 @@ async function compileWithEmception(options = {}) {
483
555
  }
484
556
  }
485
557
 
558
+ async function ensureSystemCompilerAvailable(command) {
559
+ try {
560
+ await execFileAsync(command, ["--version"]);
561
+ } catch (error) {
562
+ if (error?.code === "ENOENT") {
563
+ throw new Error(
564
+ `System Emscripten toolchain is required for "${ModuleThreadModel.EMSCRIPTEN_PTHREADS}" builds, but "${command}" was not found on PATH.`,
565
+ );
566
+ }
567
+ throw error;
568
+ }
569
+ }
570
+
571
+ async function runSystemCompiler(command, args, options = {}) {
572
+ try {
573
+ await execFileAsync(command, args, {
574
+ cwd: options.cwd,
575
+ maxBuffer: 32 * 1024 * 1024,
576
+ });
577
+ } catch (error) {
578
+ const stderr = typeof error?.stderr === "string" ? error.stderr.trim() : "";
579
+ const stdout = typeof error?.stdout === "string" ? error.stdout.trim() : "";
580
+ const detail = stderr || stdout || error?.message || "unknown error";
581
+ throw new Error(
582
+ `Compilation failed with ${command} (system emscripten): ${detail}`,
583
+ );
584
+ }
585
+ }
586
+
587
+ async function compileWithSystemEmscripten(options = {}) {
588
+ const {
589
+ manifest,
590
+ language,
591
+ sourceCompilerCommand,
592
+ sourceExtension,
593
+ sourceCode,
594
+ manifestSource,
595
+ invokeHeaderSource,
596
+ invokeSource,
597
+ exportedSymbols,
598
+ outputPath,
599
+ compileOptions,
600
+ } = options;
601
+ const tempDir = await mkdtemp(
602
+ path.join(os.tmpdir(), "space-data-module-sdk-compile-"),
603
+ );
604
+ const resolvedOutputPath = path.resolve(
605
+ outputPath ?? path.join(tempDir, "module.wasm"),
606
+ );
607
+ const runtimeIncludeDir = path.join(tempDir, "flatbuffers-runtime");
608
+ const sourcePath = path.join(tempDir, `module.${sourceExtension}`);
609
+ const manifestSourcePath = path.join(tempDir, "plugin-manifest-exports.cpp");
610
+ const invokeHeaderPath = path.join(tempDir, "space_data_module_invoke.h");
611
+ const invokeSourcePath = path.join(tempDir, "plugin-invoke-bridge.cpp");
612
+ const sourceObjectPath = path.join(tempDir, "module.o");
613
+ const linkObjectPath = path.join(tempDir, "module-link.o");
614
+ const manifestObjectPath = path.join(tempDir, "plugin-manifest-exports.o");
615
+ const invokeObjectPath = path.join(tempDir, "plugin-invoke-bridge.o");
616
+ const wasmOutputPath = path.join(tempDir, "module.wasm");
617
+
618
+ try {
619
+ await ensureSystemCompilerAvailable(sourceCompilerCommand);
620
+ await ensureSystemCompilerAvailable("em++");
621
+ const { runtimeHeaders, schemaHeaders } = await getInvokeCppSupportFiles();
622
+ const args = buildCompilerArgs(exportedSymbols, compileOptions);
623
+ await writeFilesToDirectory(runtimeIncludeDir, runtimeHeaders);
624
+ await writeFilesToDirectory(tempDir, schemaHeaders);
625
+ await writeFile(sourcePath, sourceCode);
626
+ await writeFile(manifestSourcePath, manifestSource);
627
+ await writeFile(invokeHeaderPath, invokeHeaderSource);
628
+ await writeFile(invokeSourcePath, invokeSource);
629
+
630
+ const sourceCompileArgs = [
631
+ "-c",
632
+ sourcePath,
633
+ `-I${tempDir}`,
634
+ ...(compileOptions.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS
635
+ ? ["-pthread"]
636
+ : []),
637
+ "-o",
638
+ sourceObjectPath,
639
+ ];
640
+ await runSystemCompiler(sourceCompilerCommand, sourceCompileArgs);
641
+
642
+ const sourceObjectBytes = new Uint8Array(await readFile(sourceObjectPath));
643
+ const guestLink = deriveGuestLinkRenameArgs({
644
+ objectBytes: sourceObjectBytes,
645
+ pluginId: manifest?.pluginId,
646
+ methodIds: Array.isArray(manifest?.methods)
647
+ ? manifest.methods.map((method) => String(method?.methodId ?? ""))
648
+ : [],
649
+ });
650
+
651
+ const linkCompileArgs = [
652
+ "-c",
653
+ sourcePath,
654
+ `-I${tempDir}`,
655
+ ...(compileOptions.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS
656
+ ? ["-pthread"]
657
+ : []),
658
+ ...guestLink.renameArgs,
659
+ "-o",
660
+ linkObjectPath,
661
+ ];
662
+ await runSystemCompiler(sourceCompilerCommand, linkCompileArgs);
663
+ await runSystemCompiler("em++", [
664
+ "-c",
665
+ manifestSourcePath,
666
+ "-std=c++17",
667
+ `-I${tempDir}`,
668
+ `-I${runtimeIncludeDir}`,
669
+ ...(compileOptions.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS
670
+ ? ["-pthread"]
671
+ : []),
672
+ "-o",
673
+ manifestObjectPath,
674
+ ]);
675
+ await runSystemCompiler("em++", [
676
+ "-c",
677
+ invokeSourcePath,
678
+ "-std=c++17",
679
+ `-I${tempDir}`,
680
+ `-I${runtimeIncludeDir}`,
681
+ ...(compileOptions.threadModel === ModuleThreadModel.EMSCRIPTEN_PTHREADS
682
+ ? ["-pthread"]
683
+ : []),
684
+ "-o",
685
+ invokeObjectPath,
686
+ ]);
687
+ await runSystemCompiler("em++", [
688
+ sourceObjectPath,
689
+ manifestObjectPath,
690
+ invokeObjectPath,
691
+ ...args,
692
+ "-o",
693
+ wasmOutputPath,
694
+ ]);
695
+
696
+ const wasmBytes = new Uint8Array(await readFile(wasmOutputPath));
697
+ const linkObjectBytes = new Uint8Array(await readFile(linkObjectPath));
698
+ await writeFile(resolvedOutputPath, wasmBytes);
699
+ return {
700
+ wasmBytes,
701
+ outputPath: resolvedOutputPath,
702
+ tempDir,
703
+ guestLink: {
704
+ format: "wasm-object",
705
+ language,
706
+ symbolPrefix: guestLink.prefix,
707
+ methodSymbols: guestLink.methodSymbols,
708
+ threadModel: compileOptions.threadModel,
709
+ objectBytes: linkObjectBytes,
710
+ },
711
+ };
712
+ } catch (error) {
713
+ await rm(tempDir, { recursive: true, force: true });
714
+ throw error;
715
+ }
716
+ }
717
+
486
718
  export async function compileModuleFromSource(options = {}) {
487
719
  const manifest = options.manifest ?? {};
488
720
  const sourceCode = String(options.sourceCode ?? "");
@@ -531,11 +763,19 @@ export async function compileModuleFromSource(options = {}) {
531
763
  let wasmBytes;
532
764
  let resolvedOutputPath = null;
533
765
  let tempDir = null;
766
+ const threadModel = resolveThreadModel({
767
+ manifest,
768
+ threadModel: options.threadModel,
769
+ });
534
770
  const compileOptions = {
535
771
  ...options,
536
772
  noEntry: includeCommandMain !== true,
773
+ threadModel,
537
774
  };
538
- const result = await compileWithEmception({
775
+ const compileFunction = requiresSystemEmscripten(threadModel)
776
+ ? compileWithSystemEmscripten
777
+ : compileWithEmception;
778
+ const result = await compileFunction({
539
779
  manifest,
540
780
  language: compiler.language,
541
781
  sourceCompilerCommand: compiler.command,
@@ -559,8 +799,11 @@ export async function compileModuleFromSource(options = {}) {
559
799
  });
560
800
 
561
801
  return {
562
- compiler: "em++ (emception)",
802
+ compiler: requiresSystemEmscripten(threadModel)
803
+ ? "em++ (system emscripten pthreads)"
804
+ : "em++ (emception)",
563
805
  language: compiler.language,
806
+ threadModel,
564
807
  outputPath: resolvedOutputPath,
565
808
  tempDir,
566
809
  wasmBytes,
@@ -1,5 +1,7 @@
1
1
  export type {
2
2
  CompilationResult,
3
+ GuestLinkArtifact,
4
+ ModuleThreadModelName,
3
5
  ProtectedArtifact,
4
6
  } from "../index.js";
5
7
 
@@ -7,6 +9,7 @@ export {
7
9
  cleanupCompilation,
8
10
  compileModuleFromSource,
9
11
  createRecipientKeypairHex,
12
+ ModuleThreadModel,
10
13
  protectModuleArtifact,
11
14
  } from "../index.js";
12
15
 
@@ -2,6 +2,7 @@ export {
2
2
  cleanupCompilation,
3
3
  compileModuleFromSource,
4
4
  createRecipientKeypairHex,
5
+ ModuleThreadModel,
5
6
  protectModuleArtifact,
6
7
  } from "./compileModule.js";
7
8
 
package/src/index.d.ts CHANGED
@@ -556,6 +556,7 @@ export function decodeProtectedBlobBase64(
556
556
  export interface CompilationResult {
557
557
  compiler: string;
558
558
  language: string;
559
+ threadModel: ModuleThreadModelName;
559
560
  outputPath: string | null;
560
561
  tempDir: string | null;
561
562
  wasmBytes: Uint8Array;
@@ -569,9 +570,19 @@ export interface GuestLinkArtifact {
569
570
  language: string;
570
571
  symbolPrefix: string;
571
572
  methodSymbols: Record<string, string>;
573
+ threadModel: ModuleThreadModelName;
572
574
  objectBytes: Uint8Array;
573
575
  }
574
576
 
577
+ export type ModuleThreadModelName =
578
+ | "single-thread"
579
+ | "emscripten-pthreads";
580
+
581
+ export const ModuleThreadModel: {
582
+ readonly SINGLE_THREAD: "single-thread";
583
+ readonly EMSCRIPTEN_PTHREADS: "emscripten-pthreads";
584
+ };
585
+
575
586
  export interface ProtectedArtifact {
576
587
  mnemonic: string;
577
588
  signingPublicKeyHex: string;
@@ -602,6 +613,7 @@ export function compileModuleFromSource(options: {
602
613
  manifest: PluginManifest;
603
614
  sourceCode: string;
604
615
  language?: string;
616
+ threadModel?: ModuleThreadModelName;
605
617
  outputPath?: string;
606
618
  allowUndefinedImports?: boolean;
607
619
  }): Promise<CompilationResult>;
@@ -650,12 +662,25 @@ export type {
650
662
  HarnessInvokeScenario,
651
663
  HarnessRawScenario,
652
664
  ManifestHarnessPlan,
665
+ ModuleHarness,
666
+ ModuleHarnessRuntimeDescriptor,
667
+ PluginInvokeProcessClient,
668
+ PluginInvokeProcessLaunchPlan,
669
+ WasmEdgeRunnerBuildPlan,
653
670
  } from "./testing/index.js";
654
671
 
655
672
  export {
673
+ buildWasmEdgeEmscriptenPthreadRunner,
674
+ buildWasmEdgeSpawnEnv,
675
+ createModuleHarness,
676
+ createPluginInvokeProcessClient,
656
677
  describeCapabilityRuntimeSurface,
657
678
  generateManifestHarnessPlan,
658
679
  materializeHarnessScenario,
680
+ resolveModuleHarnessLaunchPlan,
681
+ resolveWasmEdgeRunnerBuildPlan,
682
+ resolveWasmEdgeRunnerSourcePath,
683
+ resolveWasmEdgePluginLaunchPlan,
659
684
  serializeHarnessPlan,
660
685
  } from "./testing/index.js";
661
686
 
@@ -12,7 +12,7 @@ import { InvokeSurface } from "../generated/orbpro/manifest/invoke-surface.js";
12
12
  import { BufferMutability } from "../generated/orbpro/stream/buffer-mutability.js";
13
13
  import { BufferOwnership } from "../generated/orbpro/stream/buffer-ownership.js";
14
14
  import { FlatBufferTypeRefT } from "../generated/orbpro/stream/flat-buffer-type-ref.js";
15
- import { TypedArenaBuffer, TypedArenaBufferT } from "../generated/orbpro/stream/typed-arena-buffer.js";
15
+ import { TypedArenaBufferT } from "../generated/orbpro/stream/typed-arena-buffer.js";
16
16
  import { toUint8Array } from "../runtime/bufferLike.js";
17
17
 
18
18
  function toByteBuffer(data) {
@@ -149,20 +149,22 @@ function normalizeArenaFrame(frame = {}, offset) {
149
149
 
150
150
  function packArenaFrames(frames = []) {
151
151
  const packedFrames = [];
152
- const arena = [];
152
+ const normalizedFrames = [];
153
153
  let offset = 0;
154
154
  for (const frame of frames) {
155
155
  const normalized = normalizeArenaFrame(frame, offset);
156
- for (let index = 0; index < normalized.padding; index += 1) {
157
- arena.push(0);
158
- }
159
- arena.push(...normalized.payload);
160
156
  offset = normalized.buffer.offset + normalized.buffer.size;
161
157
  packedFrames.push(normalized.buffer);
158
+ normalizedFrames.push(normalized);
159
+ }
160
+
161
+ const arena = new Uint8Array(offset);
162
+ for (const normalized of normalizedFrames) {
163
+ arena.set(normalized.payload, normalized.buffer.offset);
162
164
  }
163
165
  return {
164
166
  frames: packedFrames,
165
- arena: Uint8Array.from(arena),
167
+ arena,
166
168
  };
167
169
  }
168
170