space-data-module-sdk 0.8.20 → 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.
- package/docs/isomorphic-pthreads.html +12 -0
- package/docs/isomorphic-pthreads.md +45 -0
- package/package.json +1 -1
- package/src/compiler/invokeGlue.js +38 -0
- package/src/host/browserModuleHarness.js +24 -5
- package/src/host/isomorphicLoader.js +10 -2
- package/src/host/wasiThreadBrowserWorker.mjs +3 -0
- package/src/host/wasiThreadHost.js +3 -2
- package/src/host/wasiThreadWorker.mjs +23 -1
- package/src/index.d.ts +2 -0
- package/src/runtime/constants.js +6 -0
- package/src/testing/native/wasmedge_wasi_threads_runner.c +13 -5
- package/src/testing/parityHarness.js +7 -1
- package/src/testing/parityLanes.js +19 -0
|
@@ -146,6 +146,16 @@
|
|
|
146
146
|
<div class="codeblock"><div class="codeblock-head">sh</div><pre><code>SPACE_DATA_MODULE_SDK_ENABLE_WASMEDGE_PARITY=1 \
|
|
147
147
|
SPACE_DATA_MODULE_SDK_ENABLE_TRI_RUNTIME_PARITY=1 \
|
|
148
148
|
node --test test/wasi-threads-command.test.js</code></pre></div>
|
|
149
|
+
<h3 id="sdk-0821-constructors-on-the-direct-surface"><a class="anchor" href="#sdk-0821-constructors-on-the-direct-surface" aria-hidden="true">#</a>SDK 0.8.21: constructors on the direct surface</h3>
|
|
150
|
+
<p>An artifact built with both the <code>direct</code> and the <code>command</code> surface links the WASI command runtime. Its <code>_start</code> sets up the main thread's pthread descriptor, runs the global constructors, then runs <code>main</code>, which reads stdin. A host that serves the direct surface never enters <code>_start</code>. From 0.8.21 such an artifact also exports <code>__wasm_call_ctors</code>: the same descriptor setup and constructors, without <code>main</code>, at most once per instance. Reactors keep <code>_initialize</code>.</p>
|
|
151
|
+
<p>A direct host runs <code>_initialize</code> if the module exports it, otherwise <code>__wasm_call_ctors</code>, once per instance, before the first direct call. The browser harness does this for <code>surface: "direct"</code>; a command instance only enters <code>_start</code>. The wasi-threads runner serves the direct surface of a command artifact with <code>--sdm-direct</code>, selected by <code>createStandaloneHarness("wasmedge", path, { surface: "direct" })</code> and by <code>runParityHarness({ surface: "direct" })</code>. The SDN node uses the same order.</p>
|
|
152
|
+
<p>Artifacts built with 0.8.20 or earlier have no such export. On their direct surface the constructors never run, in every runtime, and a threaded artifact's main thread has no pthread descriptor, so a recursive mutex held by the main thread does not exclude other threads. Rebuild them with 0.8.21.</p>
|
|
153
|
+
<div class="codeblock"><div class="codeblock-head">sh</div><pre><code>SPACE_DATA_MODULE_SDK_ENABLE_WASMEDGE_PARITY=1 \
|
|
154
|
+
SPACE_DATA_MODULE_SDK_ENABLE_TRI_RUNTIME_PARITY=1 \
|
|
155
|
+
node --test test/direct-call-constructors.test.js</code></pre></div>
|
|
156
|
+
<h3 id="guest-thread-faults"><a class="anchor" href="#guest-thread-faults" aria-hidden="true">#</a>Guest thread faults</h3>
|
|
157
|
+
<p>A guest thread that traps never finishes the pthread exit protocol. WasmEdge cancels the whole command. In the browser and Node harnesses the joining thread stays blocked inside the guest and cannot run the worker's error event, so the worker itself writes <code>[wasi-thread] guest thread N trapped: ...</code> to stderr (Node) or the console (browser). The call still does not return.</p>
|
|
158
|
+
<p>V8 (Node 20 to 25) checks bulk memory operations, and every access when the WebAssembly trap handler is off (Node on Linux arm64), against a per-instance copy of a shared memory's size. That copy is refreshed asynchronously after another thread grows the memory, so a thread that writes into memory another thread has just grown can trap with "memory access out of bounds", even after synchronizing with the growing thread. The guest's allocator uses the heap the artifact was linked with and then grows the memory, so a larger imported initial memory does not prevent it.</p>
|
|
149
159
|
<p>The old source path <code>src/testing/browserModuleHarness.js</code> remains a pure compatibility re-export. New browser consumers should use the public <code>space-data-module-sdk/host/browser-module</code> entry point.</p>
|
|
150
160
|
<h2 id="4-integrators-the-browser-worker-anchor-required-when-you-bundle"><a class="anchor" href="#4-integrators-the-browser-worker-anchor-required-when-you-bundle" aria-hidden="true">#</a>4. Integrators: the browser worker anchor (REQUIRED when you bundle)</h2>
|
|
151
161
|
<p>The browser leg of the wasi-threads host runs each guest pthread on a pooled module <code>Worker</code>. That worker is a <strong>served asset</strong>, and the SDK cannot guess where your build published it.</p>
|
|
@@ -204,6 +214,8 @@ setBrowserWasiThreadWorkerBase("js/vendor/space-data-module-sdk/src/host/&q
|
|
|
204
214
|
<li><a class="depth-2" href="#guest-link-symbol-namespacing-collision-proof-metadata-authoritative">Guest-link symbol namespacing (collision-proof, metadata-authoritative)</a></li>
|
|
205
215
|
<li><a class="depth-2" href="#3-compile-time-vs-runtime-an-honest-boundary">3. Compile-Time vs. Runtime: an honest boundary</a></li>
|
|
206
216
|
<li><a class="depth-3" href="#sdk-0820-command-hosts">SDK 0.8.20 command hosts</a></li>
|
|
217
|
+
<li><a class="depth-3" href="#sdk-0821-constructors-on-the-direct-surface">SDK 0.8.21: constructors on the direct surface</a></li>
|
|
218
|
+
<li><a class="depth-3" href="#guest-thread-faults">Guest thread faults</a></li>
|
|
207
219
|
<li><a class="depth-2" href="#4-integrators-the-browser-worker-anchor-required-when-you-bundle">4. Integrators: the browser worker anchor (REQUIRED when you bundle)</a></li>
|
|
208
220
|
<li><a class="depth-2" href="#tests">Tests</a></li>
|
|
209
221
|
<li><a class="depth-2" href="#see-also">See also</a></li></ul></nav>
|
|
@@ -216,6 +216,51 @@ SPACE_DATA_MODULE_SDK_ENABLE_TRI_RUNTIME_PARITY=1 \
|
|
|
216
216
|
node --test test/wasi-threads-command.test.js
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
+
### SDK 0.8.21: constructors on the direct surface
|
|
220
|
+
|
|
221
|
+
An artifact built with both the `direct` and the `command` surface links the
|
|
222
|
+
WASI command runtime. Its `_start` sets up the main thread's pthread descriptor,
|
|
223
|
+
runs the global constructors, then runs `main`, which reads stdin. A host that
|
|
224
|
+
serves the direct surface never enters `_start`. From 0.8.21 such an artifact
|
|
225
|
+
also exports `__wasm_call_ctors`: the same descriptor setup and constructors,
|
|
226
|
+
without `main`, at most once per instance. Reactors keep `_initialize`.
|
|
227
|
+
|
|
228
|
+
A direct host runs `_initialize` if the module exports it, otherwise
|
|
229
|
+
`__wasm_call_ctors`, once per instance, before the first direct call. The
|
|
230
|
+
browser harness does this for `surface: "direct"`; a command instance only
|
|
231
|
+
enters `_start`. The wasi-threads runner serves the direct surface of a command
|
|
232
|
+
artifact with `--sdm-direct`, selected by
|
|
233
|
+
`createStandaloneHarness("wasmedge", path, { surface: "direct" })` and by
|
|
234
|
+
`runParityHarness({ surface: "direct" })`. The SDN node uses the same order.
|
|
235
|
+
|
|
236
|
+
Artifacts built with 0.8.20 or earlier have no such export. On their direct
|
|
237
|
+
surface the constructors never run, in every runtime, and a threaded artifact's
|
|
238
|
+
main thread has no pthread descriptor, so a recursive mutex held by the main
|
|
239
|
+
thread does not exclude other threads. Rebuild them with 0.8.21.
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
SPACE_DATA_MODULE_SDK_ENABLE_WASMEDGE_PARITY=1 \
|
|
243
|
+
SPACE_DATA_MODULE_SDK_ENABLE_TRI_RUNTIME_PARITY=1 \
|
|
244
|
+
node --test test/direct-call-constructors.test.js
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Guest thread faults
|
|
248
|
+
|
|
249
|
+
A guest thread that traps never finishes the pthread exit protocol. WasmEdge
|
|
250
|
+
cancels the whole command. In the browser and Node harnesses the joining thread
|
|
251
|
+
stays blocked inside the guest and cannot run the worker's error event, so the
|
|
252
|
+
worker itself writes `[wasi-thread] guest thread N trapped: ...` to stderr
|
|
253
|
+
(Node) or the console (browser). The call still does not return.
|
|
254
|
+
|
|
255
|
+
V8 (Node 20 to 25) checks bulk memory operations, and every access when the
|
|
256
|
+
WebAssembly trap handler is off (Node on Linux arm64), against a per-instance
|
|
257
|
+
copy of a shared memory's size. That copy is refreshed asynchronously after
|
|
258
|
+
another thread grows the memory, so a thread that writes into memory another
|
|
259
|
+
thread has just grown can trap with "memory access out of bounds", even after
|
|
260
|
+
synchronizing with the growing thread. The guest's allocator uses the heap the
|
|
261
|
+
artifact was linked with and then grows the memory, so a larger imported initial
|
|
262
|
+
memory does not prevent it.
|
|
263
|
+
|
|
219
264
|
The old source path `src/testing/browserModuleHarness.js` remains a pure
|
|
220
265
|
compatibility re-export. New browser consumers should use the public
|
|
221
266
|
`space-data-module-sdk/host/browser-module` entry point.
|
package/package.json
CHANGED
|
@@ -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;
|
|
@@ -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 (
|
|
@@ -412,11 +434,7 @@ async function instantiateBrowserModule(options = {}) {
|
|
|
412
434
|
}
|
|
413
435
|
wasi.setMemory(memory);
|
|
414
436
|
}
|
|
415
|
-
|
|
416
|
-
instance.exports._initialize();
|
|
417
|
-
}
|
|
418
|
-
// Command exports initialize their CRT on entry. Calling _start here would
|
|
419
|
-
// consume stdin before invocation; direct-only modules use _initialize.
|
|
437
|
+
initializeInstance(instance, options.directSurface === true);
|
|
420
438
|
} catch (error) {
|
|
421
439
|
await threadHost?.terminateAll();
|
|
422
440
|
throw error;
|
|
@@ -543,6 +561,7 @@ export async function createBrowserModuleHarness(options = {}) {
|
|
|
543
561
|
|
|
544
562
|
const activeContext = await instantiateBrowserModule({
|
|
545
563
|
wasmModule,
|
|
564
|
+
directSurface: surface === "direct",
|
|
546
565
|
host,
|
|
547
566
|
hostcallDispatch: options.hostcallDispatch,
|
|
548
567
|
args: options.args,
|
|
@@ -67,8 +67,15 @@ async function createWasmEdgeCommandHarness(options = {}) {
|
|
|
67
67
|
|
|
68
68
|
const threaded = inspection.imports.some((entry) =>
|
|
69
69
|
entry.module === "wasi" && entry.name === "thread-spawn");
|
|
70
|
-
const
|
|
71
|
-
|
|
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)) {
|
|
72
79
|
throw new Error(
|
|
73
80
|
"Standalone WasmEdge loading requires a command-surface artifact with the _start export.",
|
|
74
81
|
);
|
|
@@ -91,6 +98,7 @@ async function createWasmEdgeCommandHarness(options = {}) {
|
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
const args = [
|
|
101
|
+
...(surface === "direct" && hasCommand ? ["--sdm-direct"] : []),
|
|
94
102
|
...(options.enableThreads === false ? [] : ["--enable-threads"]),
|
|
95
103
|
...Object.entries(options.env ?? {}).flatMap(([key, value]) => ["--env", `${key}=${value}`]),
|
|
96
104
|
launchWasmPath,
|
|
@@ -38,6 +38,9 @@ self.onmessage = (event) => {
|
|
|
38
38
|
instance.exports.wasi_thread_start(message.tid, message.startArg);
|
|
39
39
|
} catch (error) {
|
|
40
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);
|
|
41
44
|
self.postMessage({
|
|
42
45
|
t: "error",
|
|
43
46
|
tid: message.tid,
|
|
@@ -400,8 +400,9 @@ export async function createWasiThreadSpawn({
|
|
|
400
400
|
osThreadIds.add(worker.threadId);
|
|
401
401
|
}
|
|
402
402
|
worker.on("error", (error) => {
|
|
403
|
-
// A worker crash cannot be surfaced to the guest synchronously
|
|
404
|
-
//
|
|
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.
|
|
405
406
|
// eslint-disable-next-line no-console
|
|
406
407
|
console.error("[wasi-thread] worker error:", error);
|
|
407
408
|
});
|
|
@@ -8,24 +8,46 @@
|
|
|
8
8
|
// memory atomics (memory.atomic.wait/notify emitted by the guest) — no
|
|
9
9
|
// messages are needed for correctness.
|
|
10
10
|
|
|
11
|
+
import { writeSync } from "node:fs";
|
|
11
12
|
import { workerData } from "node:worker_threads";
|
|
12
13
|
|
|
13
14
|
import { createWasiThreadWorkerRuntime } from "./wasiThreadWorkerRuntime.js";
|
|
14
15
|
|
|
15
16
|
const { wasmModule, memory, tid, startArg, hostcallChannel, processState } = workerData;
|
|
17
|
+
|
|
18
|
+
// A thread that faults never completes the pthread exit protocol, so the
|
|
19
|
+
// thread joining it blocks for good inside the guest. When that joiner is the
|
|
20
|
+
// thread that owns this worker, it can never run this worker's "error" event.
|
|
21
|
+
// Write the fault to stderr from here, synchronously, so it is always seen.
|
|
22
|
+
function reportGuestThreadFault(what, error) {
|
|
23
|
+
try {
|
|
24
|
+
writeSync(2, `[wasi-thread] guest thread ${tid} ${what}: ${error?.stack ?? error}\n`);
|
|
25
|
+
} catch {
|
|
26
|
+
// stderr unavailable; the worker error event still carries the fault
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
const runtime = createWasiThreadWorkerRuntime({
|
|
17
31
|
wasmModule,
|
|
18
32
|
memory,
|
|
19
33
|
hostcallChannel,
|
|
20
34
|
processState,
|
|
21
35
|
});
|
|
22
|
-
|
|
36
|
+
let instance;
|
|
37
|
+
try {
|
|
38
|
+
instance = runtime.instantiate();
|
|
39
|
+
} catch (error) {
|
|
40
|
+
reportGuestThreadFault("failed to instantiate", error);
|
|
41
|
+
runtime.close();
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
23
44
|
|
|
24
45
|
try {
|
|
25
46
|
instance.exports.wasi_thread_start(tid, startArg);
|
|
26
47
|
} catch (error) {
|
|
27
48
|
// WASI proc_exit surfaces as WasiExitError; a clean thread return is normal.
|
|
28
49
|
if (!(error && error.name === "WasiExitError")) {
|
|
50
|
+
reportGuestThreadFault("trapped", error);
|
|
29
51
|
throw error;
|
|
30
52
|
}
|
|
31
53
|
} finally {
|
package/src/index.d.ts
CHANGED
package/src/runtime/constants.js
CHANGED
|
@@ -74,4 +74,10 @@ export const DefaultInvokeExports = Object.freeze({
|
|
|
74
74
|
allocSymbol: "plugin_alloc",
|
|
75
75
|
freeSymbol: "plugin_free",
|
|
76
76
|
commandSymbol: "_start",
|
|
77
|
+
// Direct-surface initialization. A reactor exports `_initialize`; a command
|
|
78
|
+
// artifact that also serves the direct surface exports `__wasm_call_ctors`
|
|
79
|
+
// (its `_start` would run main). A direct host calls the first one present,
|
|
80
|
+
// once per instance, before the first direct call.
|
|
81
|
+
reactorInitializeSymbol: "_initialize",
|
|
82
|
+
constructorsSymbol: "__wasm_call_ctors",
|
|
77
83
|
});
|
|
@@ -150,9 +150,12 @@ static WasmEdge_ModuleInstanceContext *memory_import(Threads *g) {
|
|
|
150
150
|
return env;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
153
|
+
// Direct calls use the same PIV request/response bytes as commands. Stage the
|
|
154
|
+
// request through the public allocator, then monitor the direct call with the
|
|
155
|
+
// same worker-group lifecycle as _start. No guest ABI is reimplemented. The
|
|
156
|
+
// module initializes first, once: a reactor through _initialize, a command
|
|
157
|
+
// artifact serving the direct surface through __wasm_call_ctors (its _start
|
|
158
|
+
// would run main).
|
|
156
159
|
static uint32_t stage_direct_request(Threads *g,
|
|
157
160
|
WasmEdge_ModuleInstanceContext *instance,
|
|
158
161
|
WasmEdge_MemoryInstanceContext *memory, WasmEdge_Value args[3]) {
|
|
@@ -171,6 +174,7 @@ static uint32_t stage_direct_request(Threads *g,
|
|
|
171
174
|
}
|
|
172
175
|
const WasmEdge_FunctionInstanceContext *init =
|
|
173
176
|
WasmEdge_ModuleInstanceFindFunction(instance, name("_initialize"));
|
|
177
|
+
if (!init) init = WasmEdge_ModuleInstanceFindFunction(instance, name("__wasm_call_ctors"));
|
|
174
178
|
if (init) require_result("initialize", WasmEdge_ExecutorInvoke(g->executor, init, NULL, 0, NULL, 0));
|
|
175
179
|
const WasmEdge_FunctionInstanceContext *alloc =
|
|
176
180
|
WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_alloc"));
|
|
@@ -224,10 +228,12 @@ int main(int argc, char **argv) {
|
|
|
224
228
|
if (!envs) return 1;
|
|
225
229
|
uint32_t env_count = 0;
|
|
226
230
|
bool stats = false;
|
|
231
|
+
bool force_direct = false;
|
|
227
232
|
int first = 1;
|
|
228
233
|
for (; first < argc; ++first) {
|
|
229
234
|
if (strcmp(argv[first], "--enable-threads") == 0) continue;
|
|
230
235
|
if (strcmp(argv[first], "--sdm-thread-stats") == 0) { stats = true; continue; }
|
|
236
|
+
if (strcmp(argv[first], "--sdm-direct") == 0) { force_direct = true; continue; }
|
|
231
237
|
if (strcmp(argv[first], "--env") == 0 && first + 1 < argc) {
|
|
232
238
|
envs[env_count++] = argv[++first];
|
|
233
239
|
continue;
|
|
@@ -235,7 +241,7 @@ int main(int argc, char **argv) {
|
|
|
235
241
|
break;
|
|
236
242
|
}
|
|
237
243
|
if (first >= argc || argv[first][0] == '-') {
|
|
238
|
-
fprintf(stderr, "usage: %s [--env NAME=VALUE] module.wasm [args...]\n", argv[0]);
|
|
244
|
+
fprintf(stderr, "usage: %s [--sdm-direct] [--env NAME=VALUE] module.wasm [args...]\n", argv[0]);
|
|
239
245
|
free(envs);
|
|
240
246
|
return 2;
|
|
241
247
|
}
|
|
@@ -269,7 +275,9 @@ int main(int argc, char **argv) {
|
|
|
269
275
|
WasmEdge_ModuleInstanceContext *instance = NULL;
|
|
270
276
|
require_result("instantiate", WasmEdge_ExecutorInstantiate(
|
|
271
277
|
g.executor, &instance, g.store, g.ast));
|
|
272
|
-
|
|
278
|
+
// --sdm-direct serves the direct surface of an artifact that also has a
|
|
279
|
+
// command entry; a reactor (no _start) is always served directly.
|
|
280
|
+
const WasmEdge_FunctionInstanceContext *start = force_direct ? NULL :
|
|
273
281
|
WasmEdge_ModuleInstanceFindFunction(instance, name("_start"));
|
|
274
282
|
bool direct = start == NULL;
|
|
275
283
|
WasmEdge_Value direct_args[3], response_pointer;
|
|
@@ -499,6 +499,11 @@ function applyInjectedDivergence(runs, injectDivergence) {
|
|
|
499
499
|
* @param {string[]} [options.lanes] - subset of PARITY_LANES. Defaults to ALL
|
|
500
500
|
* THREE. Narrowing is always an explicit caller decision — lanes are never
|
|
501
501
|
* skipped silently, and a single-lane run can never claim parity.
|
|
502
|
+
* @param {string} [options.surface] - "direct" runs EVERY lane on the direct
|
|
503
|
+
* surface: the browser harness, and the wasi-threads runner with
|
|
504
|
+
* --sdm-direct (the WasmEdge CLI has no direct surface, so a single-thread
|
|
505
|
+
* artifact cannot be compared on it). Omitted: the browser lane follows
|
|
506
|
+
* browserSurface and the WasmEdge lanes run the artifact's default surface.
|
|
502
507
|
* @param {Object} [options.laneRunners] - {laneName: async (context) => runs[]}
|
|
503
508
|
* override for tests. Default runners come from parityLanes.js.
|
|
504
509
|
* @param {string} [options.injectDivergence] - fire-drill: XOR one output byte
|
|
@@ -555,7 +560,8 @@ export async function runParityHarness(options = {}) {
|
|
|
555
560
|
chromeBinary: options.chromeBinary,
|
|
556
561
|
wasmedgeBinary: options.wasmedgeBinary,
|
|
557
562
|
wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
|
|
558
|
-
|
|
563
|
+
surface: options.surface ?? null,
|
|
564
|
+
browserSurface: options.browserSurface ?? options.surface ??
|
|
559
565
|
(WebAssembly.Module.exports(await WebAssembly.compile(loadableBytes)).some((entry) => entry.name === "_start") ? "command" : "direct"),
|
|
560
566
|
dockerBinary: options.dockerBinary,
|
|
561
567
|
dockerPlatform: options.dockerPlatform,
|
|
@@ -130,6 +130,21 @@ function wasmedgeInvocationArgs(planCase, plan, threadCount) {
|
|
|
130
130
|
return args;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// Runner flags that select the surface. Only the wasi-threads runner serves the
|
|
134
|
+
// direct surface of an artifact with a command entry; the WasmEdge CLI runs
|
|
135
|
+
// `_start` or nothing, so a direct-surface comparison on it would silently
|
|
136
|
+
// compare two different surfaces.
|
|
137
|
+
function surfaceArgs(context, threaded, lane) {
|
|
138
|
+
if (context.surface !== "direct") return [];
|
|
139
|
+
if (!threaded) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`${lane}: the WasmEdge CLI serves only the command surface of a single-thread artifact; ` +
|
|
142
|
+
"it cannot take part in a direct-surface parity run.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return ["--sdm-direct"];
|
|
146
|
+
}
|
|
147
|
+
|
|
133
148
|
async function stageModuleWorkdir(context, label) {
|
|
134
149
|
const workdir = await mkdtemp(path.join(os.tmpdir(), `sdm-parity-${label}-`));
|
|
135
150
|
// Stage the canonical loadable bytes (publication records stripped by the
|
|
@@ -183,6 +198,7 @@ export async function runNativeWasmEdgeLane(context) {
|
|
|
183
198
|
`native binary ${binary}`,
|
|
184
199
|
);
|
|
185
200
|
|
|
201
|
+
const directArgs = surfaceArgs(context, threaded, "native WasmEdge lane");
|
|
186
202
|
const workdir = await stageModuleWorkdir(context, "native");
|
|
187
203
|
const runs = [];
|
|
188
204
|
try {
|
|
@@ -191,6 +207,7 @@ export async function runNativeWasmEdgeLane(context) {
|
|
|
191
207
|
const outcome = await spawnWithStdin(
|
|
192
208
|
binary,
|
|
193
209
|
[...(threaded ? ["--sdm-thread-stats"] : []),
|
|
210
|
+
...directArgs,
|
|
194
211
|
...wasmedgeInvocationArgs(planCase, context.plan, threadCount)],
|
|
195
212
|
{
|
|
196
213
|
cwd: workdir,
|
|
@@ -288,6 +305,7 @@ export async function runDockerWasmEdgeLane(context) {
|
|
|
288
305
|
`docker WasmEdge lane: cannot execute "${dockerBinary}" (${error.message}).`,
|
|
289
306
|
);
|
|
290
307
|
}
|
|
308
|
+
const directArgs = surfaceArgs(context, threaded, "docker WasmEdge lane");
|
|
291
309
|
const image = await ensureDockerParityImage(context);
|
|
292
310
|
|
|
293
311
|
const workdir = await stageModuleWorkdir(context, "docker");
|
|
@@ -313,6 +331,7 @@ export async function runDockerWasmEdgeLane(context) {
|
|
|
313
331
|
dockerArgs.push(
|
|
314
332
|
image,
|
|
315
333
|
...(threaded ? ["--sdm-thread-stats"] : []),
|
|
334
|
+
...directArgs,
|
|
316
335
|
...wasmedgeInvocationArgs(planCase, context.plan, threadCount),
|
|
317
336
|
);
|
|
318
337
|
const outcome = await spawnWithStdin(dockerBinary, dockerArgs, {
|