space-data-module-sdk 0.5.18 → 0.5.19
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 +27 -0
- package/package.json +6 -1
- package/src/browser.js +6 -0
- package/src/compiler/compileModule.js +3 -0
- package/src/compliance/pluginCompliance.js +0 -1
- package/src/host/browserEdgeShims.js +355 -0
- package/src/host/browserHost.js +399 -0
- package/src/host/index.js +4 -0
- package/src/host/isomorphicLoader.js +101 -0
- package/src/host/wasiShim.js +275 -0
- package/src/index.d.ts +225 -0
- package/src/testing/browserModuleHarness.js +275 -0
- package/src/testing/index.d.ts +50 -0
- package/src/testing/index.js +4 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side module harness.
|
|
3
|
+
*
|
|
4
|
+
* Loads the same standalone WASI .wasm artifact that WasmEdge runs,
|
|
5
|
+
* instantiating it in the browser with the WASI shim + optional sdn_host
|
|
6
|
+
* bridge. Matches the createModuleHarness() API surface.
|
|
7
|
+
*
|
|
8
|
+
* Supports two invoke paths:
|
|
9
|
+
* 1. "direct" — call plugin_invoke_stream(ptr, len, &outLen) and read
|
|
10
|
+
* the FlatBuffer response from WASM memory.
|
|
11
|
+
* 2. "command" — call _start() with stdin piped via WASI shim, read
|
|
12
|
+
* stdout for the response bytes.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createBrowserWasiShim, WasiExitError } from "../host/wasiShim.js";
|
|
16
|
+
import { createBrowserHost } from "../host/browserHost.js";
|
|
17
|
+
import {
|
|
18
|
+
createJsonHostcallBridge,
|
|
19
|
+
createNodeHostSyncDispatcher,
|
|
20
|
+
DEFAULT_HOSTCALL_IMPORT_MODULE,
|
|
21
|
+
} from "../host/abi.js";
|
|
22
|
+
import {
|
|
23
|
+
DefaultInvokeExports,
|
|
24
|
+
DefaultManifestExports,
|
|
25
|
+
} from "../runtime/constants.js";
|
|
26
|
+
import {
|
|
27
|
+
encodePluginInvokeRequest,
|
|
28
|
+
decodePluginInvokeResponse,
|
|
29
|
+
} from "../invoke/codec.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect artifact profile from WebAssembly.Module imports.
|
|
33
|
+
* Returns "standalone" (WASI-only), "sdn-abi" (WASI + sdn_host),
|
|
34
|
+
* or "emscripten" (env.* with invoke trampolines).
|
|
35
|
+
*/
|
|
36
|
+
export function detectArtifactProfile(wasmModule) {
|
|
37
|
+
const imports = WebAssembly.Module.imports(wasmModule);
|
|
38
|
+
const moduleNames = new Set(imports.map((i) => i.module));
|
|
39
|
+
|
|
40
|
+
if (moduleNames.has("env")) {
|
|
41
|
+
const envImports = imports.filter((i) => i.module === "env");
|
|
42
|
+
const hasInvokeTrampolines = envImports.some((i) => i.name.startsWith("invoke_"));
|
|
43
|
+
const hasPthreads = envImports.some(
|
|
44
|
+
(i) => i.name.includes("pthread") || i.name.includes("thread"),
|
|
45
|
+
);
|
|
46
|
+
if (hasInvokeTrampolines || hasPthreads) {
|
|
47
|
+
return "emscripten";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (moduleNames.has(DEFAULT_HOSTCALL_IMPORT_MODULE)) {
|
|
52
|
+
return "sdn-abi";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (moduleNames.has("wasi_snapshot_preview1") || moduleNames.has("wasi_unstable")) {
|
|
56
|
+
return "standalone";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function compileWasmModule(source) {
|
|
63
|
+
if (source instanceof WebAssembly.Module) {
|
|
64
|
+
return source;
|
|
65
|
+
}
|
|
66
|
+
if (source instanceof Response) {
|
|
67
|
+
return WebAssembly.compileStreaming(source);
|
|
68
|
+
}
|
|
69
|
+
if (typeof source === "string") {
|
|
70
|
+
return WebAssembly.compileStreaming(fetch(source));
|
|
71
|
+
}
|
|
72
|
+
const bytes = source instanceof ArrayBuffer ? new Uint8Array(source) : source;
|
|
73
|
+
return WebAssembly.compile(bytes);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function instantiateBrowserModule(options = {}) {
|
|
77
|
+
const wasi = createBrowserWasiShim({
|
|
78
|
+
args: options.args ?? [],
|
|
79
|
+
env: options.env ?? {},
|
|
80
|
+
stdinBytes: options.stdinBytes ?? new Uint8Array(),
|
|
81
|
+
logOutput: options.logOutput === true,
|
|
82
|
+
performance: options.performance,
|
|
83
|
+
});
|
|
84
|
+
const importObject = { ...wasi.imports };
|
|
85
|
+
const moduleImports = WebAssembly.Module.imports(options.wasmModule);
|
|
86
|
+
const needsHostBridge = moduleImports.some(
|
|
87
|
+
(entry) => entry.module === DEFAULT_HOSTCALL_IMPORT_MODULE,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
let instance = null;
|
|
91
|
+
let bridge = null;
|
|
92
|
+
if (needsHostBridge) {
|
|
93
|
+
const dispatch = createNodeHostSyncDispatcher(options.host);
|
|
94
|
+
bridge = createJsonHostcallBridge({
|
|
95
|
+
dispatch,
|
|
96
|
+
getMemory: () => instance.exports.memory,
|
|
97
|
+
});
|
|
98
|
+
Object.assign(importObject, bridge.imports);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
instance = await WebAssembly.instantiate(options.wasmModule, importObject);
|
|
102
|
+
if (instance.exports.memory) {
|
|
103
|
+
wasi.setMemory(instance.exports.memory);
|
|
104
|
+
}
|
|
105
|
+
if (instance.exports._initialize) {
|
|
106
|
+
instance.exports._initialize();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
instance,
|
|
111
|
+
bridge,
|
|
112
|
+
wasi,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create a browser-side module harness for a standalone WASI artifact.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} options
|
|
120
|
+
* @param {Uint8Array|ArrayBuffer|Response|string} options.wasmSource
|
|
121
|
+
* WASM bytes, ArrayBuffer, fetch Response, or URL string.
|
|
122
|
+
* @param {Object} [options.host] - BrowserHost instance (created if omitted).
|
|
123
|
+
* @param {string[]} [options.args] - WASI args passed to the module.
|
|
124
|
+
* @param {Object} [options.env] - WASI environment variables.
|
|
125
|
+
* @param {string} [options.surface] - "direct" or "command" (default: auto-detect).
|
|
126
|
+
*/
|
|
127
|
+
export async function createBrowserModuleHarness(options = {}) {
|
|
128
|
+
const host = options.host ?? createBrowserHost(options.hostOptions);
|
|
129
|
+
const wasmModule = await compileWasmModule(options.wasmSource);
|
|
130
|
+
|
|
131
|
+
const profile = detectArtifactProfile(wasmModule);
|
|
132
|
+
const moduleExports = WebAssembly.Module.exports(wasmModule);
|
|
133
|
+
const exportNames = new Set(moduleExports.map((e) => e.name));
|
|
134
|
+
|
|
135
|
+
const hasDirectInvoke = exportNames.has(DefaultInvokeExports.invokeSymbol);
|
|
136
|
+
const hasCommand = exportNames.has(DefaultInvokeExports.commandSymbol);
|
|
137
|
+
const surface =
|
|
138
|
+
options.surface ?? (hasDirectInvoke ? "direct" : hasCommand ? "command" : "direct");
|
|
139
|
+
if (profile === "emscripten") {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"Browser harness only supports standalone WASI or sdn_host artifacts. " +
|
|
142
|
+
'Compile shared browser/WasmEdge modules with runtimeTargets: ["browser", "wasmedge"] ' +
|
|
143
|
+
'or override threadModel to "single-thread".',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const activeContext = await instantiateBrowserModule({
|
|
148
|
+
wasmModule,
|
|
149
|
+
host,
|
|
150
|
+
args: options.args,
|
|
151
|
+
env: options.env,
|
|
152
|
+
performance: options.performance ?? host?.performance,
|
|
153
|
+
logOutput: options.logOutput === true,
|
|
154
|
+
});
|
|
155
|
+
const { instance, bridge, wasi } = activeContext;
|
|
156
|
+
|
|
157
|
+
// --- Invoke helpers ---
|
|
158
|
+
function invokeDirectRaw(requestBytes) {
|
|
159
|
+
const alloc = instance.exports[DefaultInvokeExports.allocSymbol];
|
|
160
|
+
const free = instance.exports[DefaultInvokeExports.freeSymbol];
|
|
161
|
+
const invokeStream = instance.exports[DefaultInvokeExports.invokeSymbol];
|
|
162
|
+
const memory = instance.exports.memory;
|
|
163
|
+
if (
|
|
164
|
+
typeof alloc !== "function" ||
|
|
165
|
+
typeof free !== "function" ||
|
|
166
|
+
typeof invokeStream !== "function" ||
|
|
167
|
+
!memory
|
|
168
|
+
) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"Direct browser invoke requires plugin_alloc, plugin_free, plugin_invoke_stream, and memory exports.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const reqLen = requestBytes.length;
|
|
174
|
+
const reqPtr = alloc(reqLen);
|
|
175
|
+
if (!reqPtr) throw new Error("plugin_alloc returned null for request.");
|
|
176
|
+
|
|
177
|
+
new Uint8Array(memory.buffer, reqPtr, reqLen).set(requestBytes);
|
|
178
|
+
|
|
179
|
+
// Allocate space for the response length output
|
|
180
|
+
const outLenPtr = alloc(4);
|
|
181
|
+
if (!outLenPtr) throw new Error("plugin_alloc returned null for response length.");
|
|
182
|
+
|
|
183
|
+
new DataView(memory.buffer).setUint32(outLenPtr, 0, true);
|
|
184
|
+
|
|
185
|
+
const resPtr = invokeStream(reqPtr, reqLen, outLenPtr);
|
|
186
|
+
const resLen = new DataView(memory.buffer).getUint32(outLenPtr, true);
|
|
187
|
+
|
|
188
|
+
free(reqPtr, reqLen);
|
|
189
|
+
free(outLenPtr, 4);
|
|
190
|
+
|
|
191
|
+
if (!resPtr || !resLen) {
|
|
192
|
+
throw new Error("plugin_invoke_stream returned null response.");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const responseBytes = new Uint8Array(memory.buffer, resPtr, resLen).slice();
|
|
196
|
+
free(resPtr, resLen);
|
|
197
|
+
return responseBytes;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function invokeCommandRaw(stdinBytes) {
|
|
201
|
+
const commandContext = await instantiateBrowserModule({
|
|
202
|
+
wasmModule,
|
|
203
|
+
host,
|
|
204
|
+
args: options.args,
|
|
205
|
+
env: options.env,
|
|
206
|
+
stdinBytes,
|
|
207
|
+
performance: options.performance ?? host?.performance,
|
|
208
|
+
logOutput: false,
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
const commandExport = commandContext.instance.exports[DefaultInvokeExports.commandSymbol];
|
|
212
|
+
if (typeof commandExport !== "function") {
|
|
213
|
+
throw new Error(
|
|
214
|
+
`Command-surface browser invoke requires the ${DefaultInvokeExports.commandSymbol} export.`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
commandExport();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (!(error instanceof WasiExitError) || error.code !== 0) {
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return commandContext.wasi.stdout;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// --- Public API ---
|
|
227
|
+
|
|
228
|
+
async function invokeRaw(requestBytes) {
|
|
229
|
+
if (surface === "command") {
|
|
230
|
+
return invokeCommandRaw(requestBytes);
|
|
231
|
+
}
|
|
232
|
+
return invokeDirectRaw(requestBytes);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function invoke(request) {
|
|
236
|
+
const requestBytes = encodePluginInvokeRequest(request);
|
|
237
|
+
const responseBytes = await invokeRaw(requestBytes);
|
|
238
|
+
return decodePluginInvokeResponse(responseBytes);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function readManifest() {
|
|
242
|
+
const getBytesExport =
|
|
243
|
+
instance.exports[DefaultManifestExports.pluginBytesSymbol];
|
|
244
|
+
const getSizeExport =
|
|
245
|
+
instance.exports[DefaultManifestExports.pluginSizeSymbol];
|
|
246
|
+
if (!getBytesExport || !getSizeExport) return null;
|
|
247
|
+
|
|
248
|
+
const ptr = getBytesExport();
|
|
249
|
+
const size = getSizeExport();
|
|
250
|
+
if (!ptr || !size) return null;
|
|
251
|
+
|
|
252
|
+
return new Uint8Array(instance.exports.memory.buffer, ptr, size).slice();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function destroy() {
|
|
256
|
+
wasi.flushOutput();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
runtime: {
|
|
261
|
+
kind: "browser",
|
|
262
|
+
profile,
|
|
263
|
+
surface,
|
|
264
|
+
},
|
|
265
|
+
instance,
|
|
266
|
+
module: wasmModule,
|
|
267
|
+
host,
|
|
268
|
+
bridge,
|
|
269
|
+
wasi,
|
|
270
|
+
invoke,
|
|
271
|
+
invokeRaw,
|
|
272
|
+
readManifest,
|
|
273
|
+
destroy,
|
|
274
|
+
};
|
|
275
|
+
}
|
package/src/testing/index.d.ts
CHANGED
|
@@ -317,6 +317,40 @@ export function createPublicationProtectionDemoSummary(options?: {
|
|
|
317
317
|
};
|
|
318
318
|
}): Promise<PublicationProtectionDemoSummary>;
|
|
319
319
|
|
|
320
|
+
export interface BrowserModuleHarness {
|
|
321
|
+
runtime: {
|
|
322
|
+
kind: "browser";
|
|
323
|
+
profile: string;
|
|
324
|
+
surface: string;
|
|
325
|
+
};
|
|
326
|
+
instance: WebAssembly.Instance;
|
|
327
|
+
module: WebAssembly.Module;
|
|
328
|
+
host: unknown;
|
|
329
|
+
bridge: unknown;
|
|
330
|
+
wasi: {
|
|
331
|
+
imports: Record<string, Record<string, (...args: number[]) => number>>;
|
|
332
|
+
setMemory(mem: { buffer: ArrayBuffer | SharedArrayBuffer }): void;
|
|
333
|
+
getMemory(): { buffer: ArrayBuffer | SharedArrayBuffer } | null;
|
|
334
|
+
flushOutput(): void;
|
|
335
|
+
stdout: Uint8Array;
|
|
336
|
+
stderr: Uint8Array;
|
|
337
|
+
};
|
|
338
|
+
invokeRaw(
|
|
339
|
+
requestBytes: Uint8Array | ArrayBuffer | ArrayBufferView,
|
|
340
|
+
): Promise<Uint8Array>;
|
|
341
|
+
invoke(request: {
|
|
342
|
+
methodId?: string | null;
|
|
343
|
+
inputs?: HarnessInputFrame[];
|
|
344
|
+
}): Promise<{
|
|
345
|
+
statusCode: number;
|
|
346
|
+
errorCode?: string | null;
|
|
347
|
+
errorMessage?: string | null;
|
|
348
|
+
outputs: HarnessInputFrame[];
|
|
349
|
+
}>;
|
|
350
|
+
readManifest(): Uint8Array | null;
|
|
351
|
+
destroy(): void;
|
|
352
|
+
}
|
|
353
|
+
|
|
320
354
|
export function generateManifestHarnessPlan(options: {
|
|
321
355
|
manifest: PluginManifest;
|
|
322
356
|
includeOptionalInputs?: boolean;
|
|
@@ -375,6 +409,22 @@ export function resolveModuleHarnessLaunchPlan(options: {
|
|
|
375
409
|
runtime?: ModuleHarnessRuntimeDescriptor;
|
|
376
410
|
} | ModuleHarnessRuntimeDescriptor): PluginInvokeProcessLaunchPlan;
|
|
377
411
|
|
|
412
|
+
export function detectArtifactProfile(wasmModule: WebAssembly.Module): string;
|
|
413
|
+
|
|
414
|
+
export function createBrowserModuleHarness(options?: {
|
|
415
|
+
wasmSource: Uint8Array | ArrayBuffer | string | WebAssembly.Module | unknown;
|
|
416
|
+
host?: unknown;
|
|
417
|
+
hostOptions?: unknown;
|
|
418
|
+
args?: string[];
|
|
419
|
+
env?: Record<string, string>;
|
|
420
|
+
surface?: "direct" | "command";
|
|
421
|
+
performance?: {
|
|
422
|
+
now(): number;
|
|
423
|
+
timeOrigin: number;
|
|
424
|
+
};
|
|
425
|
+
logOutput?: boolean;
|
|
426
|
+
}): Promise<BrowserModuleHarness>;
|
|
427
|
+
|
|
378
428
|
export function createModuleHarness(options: {
|
|
379
429
|
runtime?: ModuleHarnessRuntimeDescriptor;
|
|
380
430
|
} | ModuleHarnessRuntimeDescriptor): Promise<ModuleHarness>;
|
package/src/testing/index.js
CHANGED
|
@@ -7,6 +7,10 @@ export {
|
|
|
7
7
|
createPublicationProtectionDemoManifest,
|
|
8
8
|
createPublicationProtectionDemoSummary,
|
|
9
9
|
} from "./publicationProtectionDemo.js";
|
|
10
|
+
export {
|
|
11
|
+
createBrowserModuleHarness,
|
|
12
|
+
detectArtifactProfile,
|
|
13
|
+
} from "./browserModuleHarness.js";
|
|
10
14
|
export {
|
|
11
15
|
buildWasmEdgeSpawnEnv,
|
|
12
16
|
createPluginInvokeProcessClient,
|