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.
- package/LICENSE +1 -1
- package/README.md +2 -2
- package/docs/assembly.js +190 -0
- package/docs/browser-wasmedge-isomorphic.html +6 -1
- package/docs/byo-wasm-quickstart.html +6 -1
- package/docs/conformance.html +6 -1
- package/docs/events-abi.html +6 -1
- package/docs/families/analytics.html +7 -2
- package/docs/families/attitude.html +7 -2
- package/docs/families/behavior.html +7 -2
- package/docs/families/breakup.html +7 -2
- package/docs/families/conjunction.html +7 -2
- package/docs/families/data-source.html +7 -2
- package/docs/families/effects.html +7 -2
- package/docs/families/environment.html +7 -2
- package/docs/families/estimation.html +7 -2
- package/docs/families/gnc.html +7 -2
- package/docs/families/maneuver.html +7 -2
- package/docs/families/obstruction.html +7 -2
- package/docs/families/propagator.html +7 -2
- package/docs/families/propulsion.html +7 -2
- package/docs/families/reentry.html +7 -2
- package/docs/families/rf.html +7 -2
- package/docs/families/scheduler.html +7 -2
- package/docs/families/sensor.html +7 -2
- package/docs/families/signature.html +7 -2
- package/docs/flatsql-host-contract.html +7 -2
- package/docs/flatsql-host-contract.md +2 -2
- package/docs/flatsql-streaming-standard.html +6 -1
- package/docs/gpu-module-abi.html +6 -1
- package/docs/index.html +21 -14
- package/docs/isomorphic-pthreads.html +31 -2
- package/docs/isomorphic-pthreads.md +104 -1
- package/docs/isomorphic-sdn-runtime-plan.html +6 -1
- package/docs/language-runtime-matrix.html +6 -1
- package/docs/llms.txt +1 -1
- package/docs/module-bundle-runtime-plan.html +6 -1
- package/docs/module-publication-standard.html +6 -1
- package/docs/node-red-default-node-parity.html +6 -1
- package/docs/protect-and-sign.html +6 -1
- package/docs/protocol-installation.html +6 -1
- package/docs/publication-submission.html +6 -1
- package/docs/secrets-capability.html +6 -1
- package/docs/styles.css +32 -0
- package/docs/testing-harness.html +6 -1
- package/docs/tri-runtime-parity-gate.html +6 -1
- package/docs/tri-runtime-parity.html +6 -1
- package/package.json +1 -1
- package/src/compiler/invokeGlue.js +38 -0
- package/src/host/browserModuleHarness.js +58 -35
- package/src/host/isomorphicLoader.js +21 -6
- package/src/host/wasiShim.js +57 -13
- package/src/host/wasiThreadBrowserWorker.mjs +4 -0
- package/src/host/wasiThreadHost.js +19 -3
- package/src/host/wasiThreadWorker.mjs +25 -2
- package/src/host/wasiThreadWorkerRuntime.js +2 -1
- package/src/index.d.ts +2 -0
- package/src/runtime/constants.js +6 -0
- package/src/testing/browserModuleHarness.js +2 -0
- package/src/testing/buildWasmEdgeRunner.js +147 -4
- package/src/testing/docker/wasmedge-parity.Dockerfile +23 -1
- package/src/testing/isomorphicHarness.js +5 -0
- package/src/testing/native/wasmedge-0.16.4-atomic-wait.patch +111 -0
- package/src/testing/native/wasmedge_wasi_threads_runner.c +353 -0
- package/src/testing/parityBrowserRunner.js +12 -4
- package/src/testing/parityHarness.js +17 -3
- package/src/testing/parityLanes.js +48 -5
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// WASI preview1 command host for the SDK parity lanes. WasmEdge 0.16.4's
|
|
2
|
+
// CLI enables atomics but does not implement wasi.thread-spawn. Like the SDN
|
|
3
|
+
// Go host, use a fresh instance per worker, shared memory, and ONE executor:
|
|
4
|
+
// atomic.wait/notify waiters belong to the executor in this runtime revision.
|
|
5
|
+
#define _POSIX_C_SOURCE 200809L
|
|
6
|
+
#include <wasmedge/wasmedge.h>
|
|
7
|
+
#include <pthread.h>
|
|
8
|
+
#include <stdbool.h>
|
|
9
|
+
#include <stdint.h>
|
|
10
|
+
#include <stdio.h>
|
|
11
|
+
#include <stdlib.h>
|
|
12
|
+
#include <string.h>
|
|
13
|
+
#include <time.h>
|
|
14
|
+
|
|
15
|
+
#define MAX_WORKERS 32
|
|
16
|
+
|
|
17
|
+
typedef struct {
|
|
18
|
+
WasmEdge_ModuleInstanceContext *instance;
|
|
19
|
+
WasmEdge_Async *async;
|
|
20
|
+
} Worker;
|
|
21
|
+
|
|
22
|
+
typedef struct {
|
|
23
|
+
WasmEdge_ExecutorContext *executor;
|
|
24
|
+
WasmEdge_StoreContext *store;
|
|
25
|
+
WasmEdge_ASTModuleContext *ast;
|
|
26
|
+
pthread_mutex_t mutex;
|
|
27
|
+
Worker workers[MAX_WORKERS];
|
|
28
|
+
int32_t next_tid;
|
|
29
|
+
bool closed;
|
|
30
|
+
bool failed;
|
|
31
|
+
bool exited;
|
|
32
|
+
} Threads;
|
|
33
|
+
|
|
34
|
+
static WasmEdge_String name(const char *s) {
|
|
35
|
+
return WasmEdge_StringWrap(s, (uint32_t)strlen(s));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static uint64_t monotonic_ms(void) {
|
|
39
|
+
struct timespec now;
|
|
40
|
+
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
41
|
+
return (uint64_t)now.tv_sec * 1000 + (uint64_t)now.tv_nsec / 1000000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static bool wait_until(WasmEdge_Async *call, uint64_t deadline) {
|
|
45
|
+
uint64_t now = monotonic_ms();
|
|
46
|
+
return WasmEdge_AsyncWaitFor(call, now < deadline ? deadline - now : 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static void require_result(const char *step, WasmEdge_Result result) {
|
|
50
|
+
if (!WasmEdge_ResultOK(result)) {
|
|
51
|
+
fprintf(stderr, "wasm trap: %s: %s\n", step,
|
|
52
|
+
WasmEdge_ResultGetMessage(result));
|
|
53
|
+
exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Called with the mutex held. Only release instances after execution ends.
|
|
58
|
+
static void reap(Threads *g) {
|
|
59
|
+
for (size_t i = 0; i < MAX_WORKERS; ++i) {
|
|
60
|
+
Worker *w = &g->workers[i];
|
|
61
|
+
if (!w->async || !WasmEdge_AsyncWaitFor(w->async, 0)) continue;
|
|
62
|
+
WasmEdge_Result result = WasmEdge_AsyncGet(w->async, NULL, 0);
|
|
63
|
+
if (WasmEdge_ResultGetCode(result) == WasmEdge_ErrCode_Terminated) {
|
|
64
|
+
g->exited = true;
|
|
65
|
+
g->closed = true;
|
|
66
|
+
} else if (!WasmEdge_ResultOK(result)) {
|
|
67
|
+
fprintf(stderr, "wasm trap: wasi worker: %s\n",
|
|
68
|
+
WasmEdge_ResultGetMessage(result));
|
|
69
|
+
g->failed = true;
|
|
70
|
+
g->closed = true;
|
|
71
|
+
}
|
|
72
|
+
WasmEdge_AsyncDelete(w->async);
|
|
73
|
+
WasmEdge_ModuleInstanceDelete(w->instance);
|
|
74
|
+
*w = (Worker){0};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static WasmEdge_Result thread_spawn(void *data,
|
|
79
|
+
const WasmEdge_CallingFrameContext *frame, const WasmEdge_Value *in,
|
|
80
|
+
WasmEdge_Value *out) {
|
|
81
|
+
Threads *g = data;
|
|
82
|
+
out[0] = WasmEdge_ValueGenI32(-6); // WASI EAGAIN: caller may retry or run inline.
|
|
83
|
+
pthread_mutex_lock(&g->mutex);
|
|
84
|
+
if (g->closed) goto done;
|
|
85
|
+
reap(g);
|
|
86
|
+
if (g->closed || g->next_tid >= (1 << 29)) goto done;
|
|
87
|
+
Worker *w = NULL;
|
|
88
|
+
for (size_t i = 0; i < MAX_WORKERS; ++i) {
|
|
89
|
+
if (!g->workers[i].async) { w = &g->workers[i]; break; }
|
|
90
|
+
}
|
|
91
|
+
if (!w) goto done;
|
|
92
|
+
WasmEdge_Result result = WasmEdge_ExecutorInstantiate(
|
|
93
|
+
WasmEdge_CallingFrameGetExecutor(frame), &w->instance, g->store, g->ast);
|
|
94
|
+
if (!WasmEdge_ResultOK(result)) goto discard;
|
|
95
|
+
const WasmEdge_FunctionInstanceContext *entry =
|
|
96
|
+
WasmEdge_ModuleInstanceFindFunction(w->instance, name("wasi_thread_start"));
|
|
97
|
+
if (!entry) goto discard;
|
|
98
|
+
int32_t tid = g->next_tid++;
|
|
99
|
+
WasmEdge_Value args[] = {WasmEdge_ValueGenI32(tid), in[0]};
|
|
100
|
+
w->async = WasmEdge_ExecutorAsyncInvoke(WasmEdge_CallingFrameGetExecutor(frame), entry, args, 2);
|
|
101
|
+
if (!w->async) goto discard;
|
|
102
|
+
out[0] = WasmEdge_ValueGenI32(tid);
|
|
103
|
+
goto done;
|
|
104
|
+
discard:
|
|
105
|
+
if (w->instance) WasmEdge_ModuleInstanceDelete(w->instance);
|
|
106
|
+
*w = (Worker){0};
|
|
107
|
+
done:
|
|
108
|
+
pthread_mutex_unlock(&g->mutex);
|
|
109
|
+
return WasmEdge_Result_Success;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static WasmEdge_ModuleInstanceContext *memory_import(Threads *g) {
|
|
113
|
+
uint32_t count = WasmEdge_ASTModuleListImportsLength(g->ast);
|
|
114
|
+
const WasmEdge_ImportTypeContext **imports = calloc(count, sizeof(*imports));
|
|
115
|
+
if (count && !imports) exit(1);
|
|
116
|
+
WasmEdge_ASTModuleListImports(g->ast, imports, count);
|
|
117
|
+
WasmEdge_ModuleInstanceContext *env = NULL;
|
|
118
|
+
bool threaded = false;
|
|
119
|
+
for (uint32_t i = 0; i < count; ++i) {
|
|
120
|
+
WasmEdge_String mod = WasmEdge_ImportTypeGetModuleName(imports[i]);
|
|
121
|
+
WasmEdge_String field = WasmEdge_ImportTypeGetExternalName(imports[i]);
|
|
122
|
+
if (WasmEdge_StringIsEqual(mod, name("wasi")) &&
|
|
123
|
+
WasmEdge_StringIsEqual(field, name("thread-spawn"))) threaded = true;
|
|
124
|
+
if (WasmEdge_ImportTypeGetExternalType(imports[i]) !=
|
|
125
|
+
WasmEdge_ExternalType_Memory) continue;
|
|
126
|
+
if (env || !WasmEdge_StringIsEqual(mod, name("env")) ||
|
|
127
|
+
!WasmEdge_StringIsEqual(field, name("memory"))) {
|
|
128
|
+
fprintf(stderr, "unsupported imported memory: expected one env.memory\n");
|
|
129
|
+
exit(1);
|
|
130
|
+
}
|
|
131
|
+
const WasmEdge_MemoryTypeContext *type =
|
|
132
|
+
WasmEdge_ImportTypeGetMemoryType(g->ast, imports[i]);
|
|
133
|
+
WasmEdge_Limit limit = WasmEdge_MemoryTypeGetLimit(type);
|
|
134
|
+
if (!limit.Shared || !limit.HasMax) {
|
|
135
|
+
fprintf(stderr, "WASI threads require bounded shared memory\n");
|
|
136
|
+
exit(1);
|
|
137
|
+
}
|
|
138
|
+
env = WasmEdge_ModuleInstanceCreate(name("env"));
|
|
139
|
+
WasmEdge_MemoryInstanceContext *memory = WasmEdge_MemoryInstanceCreate(type);
|
|
140
|
+
if (!env || !memory) exit(1);
|
|
141
|
+
WasmEdge_ModuleInstanceAddMemory(env, name("memory"), memory);
|
|
142
|
+
require_result("register memory", WasmEdge_ExecutorRegisterImport(
|
|
143
|
+
g->executor, g->store, env));
|
|
144
|
+
}
|
|
145
|
+
free(imports);
|
|
146
|
+
if (threaded && !env) {
|
|
147
|
+
fprintf(stderr, "WASI threads require imported shared memory\n");
|
|
148
|
+
exit(1);
|
|
149
|
+
}
|
|
150
|
+
return env;
|
|
151
|
+
}
|
|
152
|
+
|
|
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).
|
|
159
|
+
static uint32_t stage_direct_request(Threads *g,
|
|
160
|
+
WasmEdge_ModuleInstanceContext *instance,
|
|
161
|
+
WasmEdge_MemoryInstanceContext *memory, WasmEdge_Value args[3]) {
|
|
162
|
+
size_t size = 0, capacity = 4096;
|
|
163
|
+
uint8_t *input = malloc(capacity);
|
|
164
|
+
if (!input) exit(1);
|
|
165
|
+
for (;;) {
|
|
166
|
+
size += fread(input + size, 1, capacity - size, stdin);
|
|
167
|
+
if (ferror(stdin)) { fprintf(stderr, "failed to read stdin\n"); exit(1); }
|
|
168
|
+
if (feof(stdin)) break;
|
|
169
|
+
if (capacity >= (1U << 30)) { fprintf(stderr, "request exceeds 1 GiB\n"); exit(1); }
|
|
170
|
+
capacity *= 2;
|
|
171
|
+
uint8_t *grown = realloc(input, capacity);
|
|
172
|
+
if (!grown) exit(1);
|
|
173
|
+
input = grown;
|
|
174
|
+
}
|
|
175
|
+
const WasmEdge_FunctionInstanceContext *init =
|
|
176
|
+
WasmEdge_ModuleInstanceFindFunction(instance, name("_initialize"));
|
|
177
|
+
if (!init) init = WasmEdge_ModuleInstanceFindFunction(instance, name("__wasm_call_ctors"));
|
|
178
|
+
if (init) require_result("initialize", WasmEdge_ExecutorInvoke(g->executor, init, NULL, 0, NULL, 0));
|
|
179
|
+
const WasmEdge_FunctionInstanceContext *alloc =
|
|
180
|
+
WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_alloc"));
|
|
181
|
+
if (!alloc || !memory) { fprintf(stderr, "direct module requires plugin_alloc and memory\n"); exit(1); }
|
|
182
|
+
uint32_t padded = ((uint32_t)size + 7U) & ~7U;
|
|
183
|
+
WasmEdge_Value allocation_size = WasmEdge_ValueGenI32((int32_t)(padded + 4));
|
|
184
|
+
WasmEdge_Value pointer;
|
|
185
|
+
require_result("allocate", WasmEdge_ExecutorInvoke(g->executor, alloc, &allocation_size, 1, &pointer, 1));
|
|
186
|
+
uint32_t ptr = (uint32_t)WasmEdge_ValueGetI32(pointer);
|
|
187
|
+
if (!ptr || ptr % 8) { fprintf(stderr, "invalid direct allocation\n"); exit(1); }
|
|
188
|
+
uint32_t length_ptr = ptr + padded;
|
|
189
|
+
const uint8_t zero[4] = {0};
|
|
190
|
+
require_result("copy request", WasmEdge_MemoryInstanceSetData(memory, input, ptr, (uint32_t)size));
|
|
191
|
+
require_result("clear response length", WasmEdge_MemoryInstanceSetData(memory, zero, length_ptr, 4));
|
|
192
|
+
free(input);
|
|
193
|
+
args[0] = pointer;
|
|
194
|
+
args[1] = WasmEdge_ValueGenI32((int32_t)size);
|
|
195
|
+
args[2] = WasmEdge_ValueGenI32((int32_t)length_ptr);
|
|
196
|
+
return length_ptr;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
static bool write_direct_response(WasmEdge_MemoryInstanceContext *memory,
|
|
200
|
+
uint32_t ptr, uint32_t length_ptr) {
|
|
201
|
+
uint8_t bytes[4];
|
|
202
|
+
require_result("read response length", WasmEdge_MemoryInstanceGetData(memory, bytes, length_ptr, 4));
|
|
203
|
+
uint32_t length = (uint32_t)bytes[0] | (uint32_t)bytes[1] << 8 |
|
|
204
|
+
(uint32_t)bytes[2] << 16 | (uint32_t)bytes[3] << 24;
|
|
205
|
+
if (!ptr || ptr % 8 || !length) {
|
|
206
|
+
fprintf(stderr, "invalid direct response\n");
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const uint8_t *data = WasmEdge_MemoryInstanceGetPointerConst(memory, ptr, length);
|
|
210
|
+
if (!data) { fprintf(stderr, "direct response is out of bounds\n"); return false; }
|
|
211
|
+
return fwrite(data, 1, length, stdout) == length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
int main(int argc, char **argv) {
|
|
215
|
+
if (strcmp(WasmEdge_VersionGet(), "0.16.4") != 0) {
|
|
216
|
+
fprintf(stderr, "WASI threads runner requires WasmEdge 0.16.4; linked %s\n", WasmEdge_VersionGet());
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
219
|
+
if (argc == 2 && strcmp(argv[1], "--version") == 0) {
|
|
220
|
+
printf("wasmedge version %s\nsdm-wasi-threads-runner 1\n",
|
|
221
|
+
WasmEdge_VersionGet());
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
// WasmEdge's default logger writes to stdout. Guest stdout is binary; all
|
|
225
|
+
// host diagnostics below are emitted to stderr instead.
|
|
226
|
+
WasmEdge_LogOff();
|
|
227
|
+
const char **envs = calloc((size_t)argc, sizeof(*envs));
|
|
228
|
+
if (!envs) return 1;
|
|
229
|
+
uint32_t env_count = 0;
|
|
230
|
+
bool stats = false;
|
|
231
|
+
bool force_direct = false;
|
|
232
|
+
int first = 1;
|
|
233
|
+
for (; first < argc; ++first) {
|
|
234
|
+
if (strcmp(argv[first], "--enable-threads") == 0) continue;
|
|
235
|
+
if (strcmp(argv[first], "--sdm-thread-stats") == 0) { stats = true; continue; }
|
|
236
|
+
if (strcmp(argv[first], "--sdm-direct") == 0) { force_direct = true; continue; }
|
|
237
|
+
if (strcmp(argv[first], "--env") == 0 && first + 1 < argc) {
|
|
238
|
+
envs[env_count++] = argv[++first];
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
if (first >= argc || argv[first][0] == '-') {
|
|
244
|
+
fprintf(stderr, "usage: %s [--sdm-direct] [--env NAME=VALUE] module.wasm [args...]\n", argv[0]);
|
|
245
|
+
free(envs);
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
WasmEdge_ConfigureContext *conf = WasmEdge_ConfigureCreate();
|
|
249
|
+
WasmEdge_ConfigureAddProposal(conf, WasmEdge_Proposal_Threads);
|
|
250
|
+
WasmEdge_LoaderContext *loader = WasmEdge_LoaderCreate(conf);
|
|
251
|
+
WasmEdge_ValidatorContext *validator = WasmEdge_ValidatorCreate(conf);
|
|
252
|
+
Threads g = {.executor = WasmEdge_ExecutorCreate(conf, NULL),
|
|
253
|
+
.store = WasmEdge_StoreCreate(), .next_tid = 1};
|
|
254
|
+
if (pthread_mutex_init(&g.mutex, NULL) != 0) return 1;
|
|
255
|
+
require_result("parse", WasmEdge_LoaderParseFromFile(loader, &g.ast, argv[first]));
|
|
256
|
+
require_result("validate", WasmEdge_ValidatorValidate(validator, g.ast));
|
|
257
|
+
WasmEdge_ModuleInstanceContext *wasi = WasmEdge_ModuleInstanceCreateWASI(
|
|
258
|
+
(const char *const *)(argv + first), (uint32_t)(argc - first),
|
|
259
|
+
envs, env_count, NULL, 0);
|
|
260
|
+
free(envs);
|
|
261
|
+
if (!wasi) return 1;
|
|
262
|
+
require_result("register WASI", WasmEdge_ExecutorRegisterImport(
|
|
263
|
+
g.executor, g.store, wasi));
|
|
264
|
+
WasmEdge_ModuleInstanceContext *env = memory_import(&g);
|
|
265
|
+
WasmEdge_ModuleInstanceContext *threads = WasmEdge_ModuleInstanceCreate(name("wasi"));
|
|
266
|
+
WasmEdge_ValType i32 = WasmEdge_ValTypeGenI32();
|
|
267
|
+
WasmEdge_FunctionTypeContext *type = WasmEdge_FunctionTypeCreate(&i32, 1, &i32, 1);
|
|
268
|
+
WasmEdge_FunctionInstanceContext *spawn = WasmEdge_FunctionInstanceCreate(
|
|
269
|
+
type, thread_spawn, &g, 0);
|
|
270
|
+
WasmEdge_FunctionTypeDelete(type);
|
|
271
|
+
if (!threads || !spawn) return 1;
|
|
272
|
+
WasmEdge_ModuleInstanceAddFunction(threads, name("thread-spawn"), spawn);
|
|
273
|
+
require_result("register wasi threads", WasmEdge_ExecutorRegisterImport(
|
|
274
|
+
g.executor, g.store, threads));
|
|
275
|
+
WasmEdge_ModuleInstanceContext *instance = NULL;
|
|
276
|
+
require_result("instantiate", WasmEdge_ExecutorInstantiate(
|
|
277
|
+
g.executor, &instance, g.store, g.ast));
|
|
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 :
|
|
281
|
+
WasmEdge_ModuleInstanceFindFunction(instance, name("_start"));
|
|
282
|
+
bool direct = start == NULL;
|
|
283
|
+
WasmEdge_Value direct_args[3], response_pointer;
|
|
284
|
+
uint32_t response_length_ptr = 0;
|
|
285
|
+
WasmEdge_MemoryInstanceContext *memory = env
|
|
286
|
+
? WasmEdge_ModuleInstanceFindMemory(env, name("memory"))
|
|
287
|
+
: WasmEdge_ModuleInstanceFindMemory(instance, name("memory"));
|
|
288
|
+
if (direct) {
|
|
289
|
+
start = WasmEdge_ModuleInstanceFindFunction(instance, name("plugin_invoke_stream"));
|
|
290
|
+
if (!start) { fprintf(stderr, "module has neither _start nor plugin_invoke_stream\n"); return 1; }
|
|
291
|
+
response_length_ptr = stage_direct_request(&g, instance, memory, direct_args);
|
|
292
|
+
}
|
|
293
|
+
WasmEdge_Async *main_call = WasmEdge_ExecutorAsyncInvoke(g.executor, start,
|
|
294
|
+
direct ? direct_args : NULL, direct ? 3 : 0);
|
|
295
|
+
if (!main_call) return 1;
|
|
296
|
+
for (;;) {
|
|
297
|
+
bool finished = WasmEdge_AsyncWaitFor(main_call, 1);
|
|
298
|
+
pthread_mutex_lock(&g.mutex);
|
|
299
|
+
reap(&g);
|
|
300
|
+
bool stop = finished || g.failed || g.exited;
|
|
301
|
+
if (stop) g.closed = true;
|
|
302
|
+
pthread_mutex_unlock(&g.mutex);
|
|
303
|
+
if (stop) break;
|
|
304
|
+
}
|
|
305
|
+
// Exit/trap in any thread ends the whole command. Cancel all executions
|
|
306
|
+
// before releasing shared memory; a stuck cancellation ends this process
|
|
307
|
+
// without freeing objects underneath live native workers.
|
|
308
|
+
uint64_t deadline = monotonic_ms() + 2000;
|
|
309
|
+
if (!WasmEdge_AsyncWaitFor(main_call, 0)) WasmEdge_AsyncCancel(main_call);
|
|
310
|
+
pthread_mutex_lock(&g.mutex);
|
|
311
|
+
for (size_t i = 0; i < MAX_WORKERS; ++i) {
|
|
312
|
+
if (g.workers[i].async) WasmEdge_AsyncCancel(g.workers[i].async);
|
|
313
|
+
}
|
|
314
|
+
pthread_mutex_unlock(&g.mutex);
|
|
315
|
+
if (!wait_until(main_call, deadline)) {
|
|
316
|
+
fprintf(stderr, "wasm trap: main cancellation timed out\n");
|
|
317
|
+
fflush(NULL);
|
|
318
|
+
_Exit(1);
|
|
319
|
+
}
|
|
320
|
+
WasmEdge_Result result = WasmEdge_AsyncGet(main_call,
|
|
321
|
+
direct ? &response_pointer : NULL, direct ? 1 : 0);
|
|
322
|
+
bool failed = g.failed || (!WasmEdge_ResultOK(result) && !g.exited);
|
|
323
|
+
if (failed) fprintf(stderr, "wasm trap: command: %s\n", WasmEdge_ResultGetMessage(result));
|
|
324
|
+
for (size_t i = 0; i < MAX_WORKERS; ++i) {
|
|
325
|
+
Worker *w = &g.workers[i];
|
|
326
|
+
if (!w->async) continue;
|
|
327
|
+
if (!wait_until(w->async, deadline)) {
|
|
328
|
+
fprintf(stderr, "wasm trap: worker cancellation timed out\n");
|
|
329
|
+
fflush(NULL);
|
|
330
|
+
_Exit(1);
|
|
331
|
+
}
|
|
332
|
+
WasmEdge_AsyncDelete(w->async);
|
|
333
|
+
WasmEdge_ModuleInstanceDelete(w->instance);
|
|
334
|
+
}
|
|
335
|
+
if (stats) fprintf(stderr, "sdm-wasi-threads: spawned=%d\n", g.next_tid - 1);
|
|
336
|
+
if (direct && !failed && !g.exited) {
|
|
337
|
+
failed = !write_direct_response(memory, (uint32_t)WasmEdge_ValueGetI32(response_pointer), response_length_ptr);
|
|
338
|
+
}
|
|
339
|
+
int exit_code = failed ? 1 : (int)WasmEdge_ModuleInstanceWASIGetExitCode(wasi);
|
|
340
|
+
WasmEdge_AsyncDelete(main_call);
|
|
341
|
+
WasmEdge_ModuleInstanceDelete(instance);
|
|
342
|
+
WasmEdge_ModuleInstanceDelete(threads);
|
|
343
|
+
if (env) WasmEdge_ModuleInstanceDelete(env);
|
|
344
|
+
WasmEdge_ModuleInstanceDelete(wasi);
|
|
345
|
+
WasmEdge_StoreDelete(g.store);
|
|
346
|
+
WasmEdge_ASTModuleDelete(g.ast);
|
|
347
|
+
WasmEdge_ExecutorDelete(g.executor);
|
|
348
|
+
WasmEdge_ValidatorDelete(validator);
|
|
349
|
+
WasmEdge_LoaderDelete(loader);
|
|
350
|
+
WasmEdge_ConfigureDelete(conf);
|
|
351
|
+
pthread_mutex_destroy(&g.mutex);
|
|
352
|
+
return exit_code;
|
|
353
|
+
}
|
|
@@ -91,6 +91,7 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
|
|
|
91
91
|
let stdout = new Uint8Array(0);
|
|
92
92
|
let stderr = new Uint8Array(0);
|
|
93
93
|
let harness = null;
|
|
94
|
+
let spawnCount = null;
|
|
94
95
|
try {
|
|
95
96
|
// Plaintext hygiene: no per-case copy. createBrowserModuleHarness
|
|
96
97
|
// compiles directly from a caller-supplied buffer without copying it
|
|
@@ -101,7 +102,10 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
|
|
|
101
102
|
// would only add a fresh, unzeroed plaintext copy per run.
|
|
102
103
|
harness = await createBrowserModuleHarness({
|
|
103
104
|
wasmSource: moduleBytes,
|
|
104
|
-
surface: "command",
|
|
105
|
+
surface: plan.surface ?? "command",
|
|
106
|
+
enableBrowserWasiThreads: true,
|
|
107
|
+
maxThreads: threadCount,
|
|
108
|
+
wasiThreadWorkerUrl: new URL("/wasi-thread-worker.js", globalThis.location.href).href,
|
|
105
109
|
args: planCase.args,
|
|
106
110
|
env: {
|
|
107
111
|
...planCase.env,
|
|
@@ -113,8 +117,11 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
|
|
|
113
117
|
} catch (error) {
|
|
114
118
|
({ exitClass, exitDetail } = classifyBrowserError(error));
|
|
115
119
|
} finally {
|
|
120
|
+
stderr = harness?.wasi?.stderr ?? stderr;
|
|
121
|
+
if (exitClass !== OK) stdout = harness?.wasi?.stdout ?? stdout;
|
|
122
|
+
spawnCount = harness?.threadHost?.spawnCount() ?? null;
|
|
116
123
|
try {
|
|
117
|
-
harness?.destroy?.();
|
|
124
|
+
await harness?.destroy?.();
|
|
118
125
|
} catch {
|
|
119
126
|
/* teardown must never mask the run result */
|
|
120
127
|
}
|
|
@@ -124,6 +131,7 @@ export async function runParityPlanInBrowser({ baseUrl = "" } = {}) {
|
|
|
124
131
|
threadCount,
|
|
125
132
|
exitClass,
|
|
126
133
|
exitDetail,
|
|
134
|
+
spawnCount,
|
|
127
135
|
stdoutBase64: bytesToBase64(stdout),
|
|
128
136
|
stderrBase64: bytesToBase64(stderr),
|
|
129
137
|
});
|
|
@@ -144,8 +152,8 @@ async function postDone(body) {
|
|
|
144
152
|
// Auto-run only inside a real browser page (the served runner). Node imports
|
|
145
153
|
// of this module (tests, bundlers) never auto-run.
|
|
146
154
|
if (
|
|
147
|
-
typeof globalThis.
|
|
148
|
-
typeof globalThis.
|
|
155
|
+
typeof globalThis.self !== "undefined" &&
|
|
156
|
+
typeof globalThis.location !== "undefined" &&
|
|
149
157
|
globalThis.__SDM_PARITY_AUTORUN__ !== false
|
|
150
158
|
) {
|
|
151
159
|
runParityPlanInBrowser({})
|
|
@@ -71,14 +71,18 @@ export function loadWasmEdgePin() {
|
|
|
71
71
|
if (!repository) {
|
|
72
72
|
throw new Error("wasmedgePin.json dockerImageRepository is required.");
|
|
73
73
|
}
|
|
74
|
+
const runnerIdentity = createHash("sha256")
|
|
75
|
+
.update(readFileSync(path.join(__dirname, "native/wasmedge_wasi_threads_runner.c")))
|
|
76
|
+
.update(readFileSync(path.join(__dirname, "native/wasmedge-0.16.4-atomic-wait.patch")))
|
|
77
|
+
.update(readFileSync(path.join(__dirname, String(pin.dockerfile))))
|
|
78
|
+
.digest("hex").slice(0, 16);
|
|
74
79
|
return Object.freeze({
|
|
75
80
|
wasmedgeVersion: version,
|
|
76
81
|
dockerImageRepository: repository,
|
|
77
82
|
dockerImage: `${repository}:${version}`,
|
|
83
|
+
dockerRunnerImage: `${repository}:${version}-threads-${runnerIdentity}`,
|
|
78
84
|
dockerfilePath: path.join(__dirname, String(pin.dockerfile)),
|
|
79
|
-
dockerfileContextDir:
|
|
80
|
-
path.join(__dirname, String(pin.dockerfile)),
|
|
81
|
-
),
|
|
85
|
+
dockerfileContextDir: __dirname,
|
|
82
86
|
pinPath,
|
|
83
87
|
});
|
|
84
88
|
}
|
|
@@ -495,6 +499,11 @@ function applyInjectedDivergence(runs, injectDivergence) {
|
|
|
495
499
|
* @param {string[]} [options.lanes] - subset of PARITY_LANES. Defaults to ALL
|
|
496
500
|
* THREE. Narrowing is always an explicit caller decision — lanes are never
|
|
497
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.
|
|
498
507
|
* @param {Object} [options.laneRunners] - {laneName: async (context) => runs[]}
|
|
499
508
|
* override for tests. Default runners come from parityLanes.js.
|
|
500
509
|
* @param {string} [options.injectDivergence] - fire-drill: XOR one output byte
|
|
@@ -550,6 +559,10 @@ export async function runParityHarness(options = {}) {
|
|
|
550
559
|
log: options.log ?? (() => {}),
|
|
551
560
|
chromeBinary: options.chromeBinary,
|
|
552
561
|
wasmedgeBinary: options.wasmedgeBinary,
|
|
562
|
+
wasmEdgeRunnerBinary: options.wasmEdgeRunnerBinary,
|
|
563
|
+
surface: options.surface ?? null,
|
|
564
|
+
browserSurface: options.browserSurface ?? options.surface ??
|
|
565
|
+
(WebAssembly.Module.exports(await WebAssembly.compile(loadableBytes)).some((entry) => entry.name === "_start") ? "command" : "direct"),
|
|
553
566
|
dockerBinary: options.dockerBinary,
|
|
554
567
|
dockerPlatform: options.dockerPlatform,
|
|
555
568
|
autoBuildDockerImage: options.autoBuildDockerImage !== false,
|
|
@@ -609,6 +622,7 @@ export async function runParityHarness(options = {}) {
|
|
|
609
622
|
threadCount: run.threadCount,
|
|
610
623
|
exitClass: run.exitClass,
|
|
611
624
|
exitDetail: run.exitDetail ?? null,
|
|
625
|
+
spawnCount: run.spawnCount ?? null,
|
|
612
626
|
stdoutSha256: sha256Hex(toUint8Array(run.stdout) ?? new Uint8Array(0)),
|
|
613
627
|
stdoutLength: (toUint8Array(run.stdout) ?? new Uint8Array(0)).length,
|
|
614
628
|
})),
|
|
@@ -30,6 +30,8 @@ import { promisify } from "node:util";
|
|
|
30
30
|
|
|
31
31
|
import { ExitClass, assertWasmEdgeVersionMatchesPin } from "./parityHarness.js";
|
|
32
32
|
import { normalizeWasmEdgeOutcome } from "./wasmedgeOutput.js";
|
|
33
|
+
import { isWasiThreadsModule } from "../host/wasiThreadHost.js";
|
|
34
|
+
import { resolveWasmEdgeWasiThreadsRunner } from "./buildWasmEdgeRunner.js";
|
|
33
35
|
|
|
34
36
|
const execFile = promisify(execFileCallback);
|
|
35
37
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -128,6 +130,21 @@ function wasmedgeInvocationArgs(planCase, plan, threadCount) {
|
|
|
128
130
|
return args;
|
|
129
131
|
}
|
|
130
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
|
+
|
|
131
148
|
async function stageModuleWorkdir(context, label) {
|
|
132
149
|
const workdir = await mkdtemp(path.join(os.tmpdir(), `sdm-parity-${label}-`));
|
|
133
150
|
// Stage the canonical loadable bytes (publication records stripped by the
|
|
@@ -162,7 +179,10 @@ export async function resolveWasmEdgeBinary(context = {}) {
|
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
export async function runNativeWasmEdgeLane(context) {
|
|
165
|
-
const
|
|
182
|
+
const threaded = isWasiThreadsModule(await WebAssembly.compile(context.loadableBytes ?? context.wasmBytes));
|
|
183
|
+
const binary = threaded
|
|
184
|
+
? await resolveWasmEdgeWasiThreadsRunner(context)
|
|
185
|
+
: await resolveWasmEdgeBinary(context);
|
|
166
186
|
let versionOutput;
|
|
167
187
|
try {
|
|
168
188
|
versionOutput = (await execFile(binary, ["--version"])).stdout;
|
|
@@ -178,6 +198,7 @@ export async function runNativeWasmEdgeLane(context) {
|
|
|
178
198
|
`native binary ${binary}`,
|
|
179
199
|
);
|
|
180
200
|
|
|
201
|
+
const directArgs = surfaceArgs(context, threaded, "native WasmEdge lane");
|
|
181
202
|
const workdir = await stageModuleWorkdir(context, "native");
|
|
182
203
|
const runs = [];
|
|
183
204
|
try {
|
|
@@ -185,7 +206,9 @@ export async function runNativeWasmEdgeLane(context) {
|
|
|
185
206
|
for (const threadCount of planCase.threadCounts) {
|
|
186
207
|
const outcome = await spawnWithStdin(
|
|
187
208
|
binary,
|
|
188
|
-
|
|
209
|
+
[...(threaded ? ["--sdm-thread-stats"] : []),
|
|
210
|
+
...directArgs,
|
|
211
|
+
...wasmedgeInvocationArgs(planCase, context.plan, threadCount)],
|
|
189
212
|
{
|
|
190
213
|
cwd: workdir,
|
|
191
214
|
env: { PATH: process.env.PATH ?? "" },
|
|
@@ -208,6 +231,7 @@ export async function runNativeWasmEdgeLane(context) {
|
|
|
208
231
|
exitDetail,
|
|
209
232
|
stdout: normalized.stdout,
|
|
210
233
|
stderr: normalized.stderr,
|
|
234
|
+
spawnCount: threaded ? Number(/sdm-wasi-threads: spawned=(\d+)/.exec(normalized.diagnosticText)?.[1] ?? NaN) : null,
|
|
211
235
|
stateFiles: null,
|
|
212
236
|
});
|
|
213
237
|
}
|
|
@@ -271,6 +295,8 @@ export async function ensureDockerParityImage(context) {
|
|
|
271
295
|
}
|
|
272
296
|
|
|
273
297
|
export async function runDockerWasmEdgeLane(context) {
|
|
298
|
+
const threaded = isWasiThreadsModule(await WebAssembly.compile(context.loadableBytes ?? context.wasmBytes));
|
|
299
|
+
if (threaded) context = { ...context, pin: { ...context.pin, dockerImage: context.pin.dockerRunnerImage } };
|
|
274
300
|
const dockerBinary = context.dockerBinary ?? "docker";
|
|
275
301
|
try {
|
|
276
302
|
await execFile(dockerBinary, ["--version"]);
|
|
@@ -279,6 +305,7 @@ export async function runDockerWasmEdgeLane(context) {
|
|
|
279
305
|
`docker WasmEdge lane: cannot execute "${dockerBinary}" (${error.message}).`,
|
|
280
306
|
);
|
|
281
307
|
}
|
|
308
|
+
const directArgs = surfaceArgs(context, threaded, "docker WasmEdge lane");
|
|
282
309
|
const image = await ensureDockerParityImage(context);
|
|
283
310
|
|
|
284
311
|
const workdir = await stageModuleWorkdir(context, "docker");
|
|
@@ -300,8 +327,11 @@ export async function runDockerWasmEdgeLane(context) {
|
|
|
300
327
|
if (context.dockerPlatform) {
|
|
301
328
|
dockerArgs.push("--platform", String(context.dockerPlatform));
|
|
302
329
|
}
|
|
330
|
+
if (threaded) dockerArgs.push("--entrypoint", "/opt/wasmedge/bin/sdm-wasi-threads-runner");
|
|
303
331
|
dockerArgs.push(
|
|
304
332
|
image,
|
|
333
|
+
...(threaded ? ["--sdm-thread-stats"] : []),
|
|
334
|
+
...directArgs,
|
|
305
335
|
...wasmedgeInvocationArgs(planCase, context.plan, threadCount),
|
|
306
336
|
);
|
|
307
337
|
const outcome = await spawnWithStdin(dockerBinary, dockerArgs, {
|
|
@@ -323,6 +353,7 @@ export async function runDockerWasmEdgeLane(context) {
|
|
|
323
353
|
exitDetail,
|
|
324
354
|
stdout: normalized.stdout,
|
|
325
355
|
stderr: normalized.stderr,
|
|
356
|
+
spawnCount: threaded ? Number(/sdm-wasi-threads: spawned=(\d+)/.exec(normalized.diagnosticText)?.[1] ?? NaN) : null,
|
|
326
357
|
stateFiles: null,
|
|
327
358
|
});
|
|
328
359
|
}
|
|
@@ -373,11 +404,17 @@ async function buildBrowserRunnerBundle() {
|
|
|
373
404
|
external: ["node:*", "hd-wallet-wasm"],
|
|
374
405
|
logLevel: "silent",
|
|
375
406
|
});
|
|
376
|
-
|
|
407
|
+
const threadWorker = await esbuild.build({
|
|
408
|
+
entryPoints: [path.join(__dirname, "../host/wasiThreadBrowserWorker.mjs")],
|
|
409
|
+
bundle: true, write: false, format: "esm", platform: "browser", target: ["chrome110"],
|
|
410
|
+
external: ["node:*", "hd-wallet-wasm"], logLevel: "silent",
|
|
411
|
+
});
|
|
412
|
+
return { runner: result.outputFiles[0].text, threadWorker: threadWorker.outputFiles[0].text };
|
|
377
413
|
}
|
|
378
414
|
|
|
379
415
|
function browserPlanPayload(context) {
|
|
380
416
|
return JSON.stringify({
|
|
417
|
+
surface: context.browserSurface ?? "command",
|
|
381
418
|
threadEnvVar: context.plan.threadEnvVar,
|
|
382
419
|
cases: context.plan.cases.map((planCase) => ({
|
|
383
420
|
id: planCase.id,
|
|
@@ -393,7 +430,7 @@ const RUNNER_HTML = `<!doctype html>
|
|
|
393
430
|
<html>
|
|
394
431
|
<head><meta charset="utf-8"><title>sdm parity runner</title></head>
|
|
395
432
|
<body><pre id="status">parity runner booting…</pre>
|
|
396
|
-
<script type="module"
|
|
433
|
+
<script type="module">new Worker("/runner.js", { type: "module" });</script></body>
|
|
397
434
|
</html>`;
|
|
398
435
|
|
|
399
436
|
export async function runBrowserLane(context) {
|
|
@@ -447,7 +484,12 @@ export async function runBrowserLane(context) {
|
|
|
447
484
|
...securityHeaders,
|
|
448
485
|
"Content-Type": "text/javascript; charset=utf-8",
|
|
449
486
|
});
|
|
450
|
-
response.end(runnerBundle);
|
|
487
|
+
response.end(runnerBundle.runner);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (url.pathname === "/wasi-thread-worker.js") {
|
|
491
|
+
response.writeHead(200, { ...securityHeaders, "Content-Type": "text/javascript; charset=utf-8" });
|
|
492
|
+
response.end(runnerBundle.threadWorker);
|
|
451
493
|
return;
|
|
452
494
|
}
|
|
453
495
|
if (url.pathname === "/plan") {
|
|
@@ -522,6 +564,7 @@ export async function runBrowserLane(context) {
|
|
|
522
564
|
threadCount: Number(run.threadCount),
|
|
523
565
|
exitClass: String(run.exitClass),
|
|
524
566
|
exitDetail: run.exitDetail ?? null,
|
|
567
|
+
spawnCount: run.spawnCount ?? null,
|
|
525
568
|
stdout: new Uint8Array(Buffer.from(String(run.stdoutBase64 ?? ""), "base64")),
|
|
526
569
|
stderr: new Uint8Array(Buffer.from(String(run.stderrBase64 ?? ""), "base64")),
|
|
527
570
|
stateFiles: null,
|