space-data-module-sdk 0.5.4 → 0.5.8
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 +22 -0
- package/package.json +1 -1
- package/src/compiler/compileModule.js +247 -4
- package/src/compiler/emception.d.ts +1 -0
- package/src/compiler/emception.js +5 -0
- package/src/compiler/emceptionNode.js +4 -0
- package/src/compiler/index.d.ts +4 -0
- package/src/compiler/index.js +2 -0
- package/src/compliance/pluginCompliance.js +58 -18
- package/src/host/abi.js +47 -1
- package/src/host/nodeHost.js +4 -4
- package/src/index.d.ts +14 -1
- package/src/manifest/codec.js +91 -0
- package/src/testing/index.js +2 -2
package/README.md
CHANGED
|
@@ -159,6 +159,26 @@ environments with socket/TLS extensions, while plain `wasi` remains the strict
|
|
|
159
159
|
no-wrapper baseline. The Node-RED-oriented parity map lives in
|
|
160
160
|
[`docs/node-red-default-node-parity.md`](./docs/node-red-default-node-parity.md).
|
|
161
161
|
|
|
162
|
+
## WasmEdge Pthreads
|
|
163
|
+
|
|
164
|
+
`space-data-module-sdk` is the source of truth for module thread-model
|
|
165
|
+
selection.
|
|
166
|
+
|
|
167
|
+
- `compileModuleFromSource({ threadModel })` accepts an explicit thread model.
|
|
168
|
+
- If `threadModel` is omitted, the SDK resolves it from `manifest.runtimeTargets`.
|
|
169
|
+
- `runtimeTargets: ["wasmedge"]` defaults to `emscripten-pthreads`.
|
|
170
|
+
- Other targets currently default to `single-thread`.
|
|
171
|
+
|
|
172
|
+
WasmEdge-targeted pthread builds do not use the embedded `sdn-emception`
|
|
173
|
+
toolchain. They require a real system Emscripten installation on `PATH`, and
|
|
174
|
+
the compiler result plus guest-link bundle metadata preserve the selected
|
|
175
|
+
`threadModel`.
|
|
176
|
+
|
|
177
|
+
This SDK does not treat Cesium `TaskProcessor`, ad hoc JS worker pools, or
|
|
178
|
+
host-side fake orchestration as a substitute for guest pthread support. If a
|
|
179
|
+
runtime cannot interoperate with the guest contract directly, document that as a
|
|
180
|
+
wrapper requirement instead of changing the guest artifact semantics.
|
|
181
|
+
|
|
162
182
|
## Install
|
|
163
183
|
|
|
164
184
|
```bash
|
|
@@ -186,6 +206,8 @@ const compilation = await compileModuleFromSource({
|
|
|
186
206
|
manifest,
|
|
187
207
|
sourceCode,
|
|
188
208
|
language: "c",
|
|
209
|
+
// Optional. Defaults from manifest.runtimeTargets.
|
|
210
|
+
// threadModel: "emscripten-pthreads",
|
|
189
211
|
});
|
|
190
212
|
|
|
191
213
|
const bundle = await createSingleFileBundle({
|
package/package.json
CHANGED
|
@@ -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 {
|
|
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 = [
|
|
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
|
|
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:
|
|
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,
|
|
@@ -54,6 +54,7 @@ export interface SharedEmceptionSession {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function createSharedEmceptionSession(): SharedEmceptionSession;
|
|
57
|
+
export function createIsolatedEmceptionSession(): SharedEmceptionSession;
|
|
57
58
|
export function loadSharedEmception(): Promise<unknown>;
|
|
58
59
|
export function withSharedEmception<T>(
|
|
59
60
|
task: (handle: SharedEmceptionHandle) => T | Promise<T>,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
createEmceptionController,
|
|
4
5
|
getSharedEmceptionController,
|
|
5
6
|
loadEmception,
|
|
6
7
|
runWithEmceptionLock,
|
|
@@ -180,6 +181,10 @@ export function createSharedEmceptionSession() {
|
|
|
180
181
|
return new SharedEmceptionSession();
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
export function createIsolatedEmceptionSession() {
|
|
185
|
+
return new SharedEmceptionSession(createEmceptionController());
|
|
186
|
+
}
|
|
187
|
+
|
|
183
188
|
export async function loadSharedEmception() {
|
|
184
189
|
return loadEmception();
|
|
185
190
|
}
|
|
@@ -225,6 +225,10 @@ export function getSharedEmceptionController() {
|
|
|
225
225
|
return sharedEmceptionController;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
export function createEmceptionController() {
|
|
229
|
+
return new EmceptionController();
|
|
230
|
+
}
|
|
231
|
+
|
|
228
232
|
export async function loadEmception() {
|
|
229
233
|
return sharedEmceptionController.load();
|
|
230
234
|
}
|
package/src/compiler/index.d.ts
CHANGED
|
@@ -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
|
|
|
@@ -18,6 +21,7 @@ export type {
|
|
|
18
21
|
} from "./emception.js";
|
|
19
22
|
|
|
20
23
|
export {
|
|
24
|
+
createIsolatedEmceptionSession,
|
|
21
25
|
createSharedEmceptionSession,
|
|
22
26
|
loadSharedEmception,
|
|
23
27
|
withSharedEmception,
|
package/src/compiler/index.js
CHANGED
|
@@ -2,10 +2,12 @@ export {
|
|
|
2
2
|
cleanupCompilation,
|
|
3
3
|
compileModuleFromSource,
|
|
4
4
|
createRecipientKeypairHex,
|
|
5
|
+
ModuleThreadModel,
|
|
5
6
|
protectModuleArtifact,
|
|
6
7
|
} from "./compileModule.js";
|
|
7
8
|
|
|
8
9
|
export {
|
|
10
|
+
createIsolatedEmceptionSession,
|
|
9
11
|
createSharedEmceptionSession,
|
|
10
12
|
loadSharedEmception,
|
|
11
13
|
withSharedEmception,
|
|
@@ -175,6 +175,44 @@ function validateStringField(issues, value, location, label) {
|
|
|
175
175
|
return true;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
function validateCapabilityEntry(capability, issues, location) {
|
|
179
|
+
if (isNonEmptyString(capability)) {
|
|
180
|
+
return capability;
|
|
181
|
+
}
|
|
182
|
+
if (!capability || typeof capability !== "object" || Array.isArray(capability)) {
|
|
183
|
+
pushIssue(
|
|
184
|
+
issues,
|
|
185
|
+
"error",
|
|
186
|
+
"invalid-capability",
|
|
187
|
+
"Capability entries must be non-empty strings or host capability records.",
|
|
188
|
+
location,
|
|
189
|
+
);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (!validateStringField(issues, capability.capability, `${location}.capability`, "Capability id")) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
validateOptionalStringField(
|
|
196
|
+
issues,
|
|
197
|
+
capability.scope,
|
|
198
|
+
`${location}.scope`,
|
|
199
|
+
"Capability scope",
|
|
200
|
+
);
|
|
201
|
+
validateOptionalBooleanField(
|
|
202
|
+
issues,
|
|
203
|
+
capability.required,
|
|
204
|
+
`${location}.required`,
|
|
205
|
+
"Capability required",
|
|
206
|
+
);
|
|
207
|
+
validateOptionalStringField(
|
|
208
|
+
issues,
|
|
209
|
+
capability.description,
|
|
210
|
+
`${location}.description`,
|
|
211
|
+
"Capability description",
|
|
212
|
+
);
|
|
213
|
+
return capability.capability;
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
function validateIntegerField(
|
|
179
217
|
issues,
|
|
180
218
|
value,
|
|
@@ -1066,8 +1104,9 @@ export function validatePluginManifest(manifest, options = {}) {
|
|
|
1066
1104
|
validateStringField(issues, manifest.version, `${sourceName}.version`, "version");
|
|
1067
1105
|
validateStringField(issues, manifest.pluginFamily, `${sourceName}.pluginFamily`, "pluginFamily");
|
|
1068
1106
|
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1107
|
+
const rawDeclaredCapabilities = manifest.capabilities;
|
|
1108
|
+
let declaredCapabilities = null;
|
|
1109
|
+
if (!Array.isArray(rawDeclaredCapabilities)) {
|
|
1071
1110
|
pushIssue(
|
|
1072
1111
|
issues,
|
|
1073
1112
|
"warning",
|
|
@@ -1076,38 +1115,39 @@ export function validatePluginManifest(manifest, options = {}) {
|
|
|
1076
1115
|
`${sourceName}.capabilities`,
|
|
1077
1116
|
);
|
|
1078
1117
|
} else {
|
|
1118
|
+
declaredCapabilities = [];
|
|
1079
1119
|
const seenCapabilities = new Set();
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
);
|
|
1089
|
-
continue;
|
|
1120
|
+
rawDeclaredCapabilities.forEach((capability, index) => {
|
|
1121
|
+
const normalizedCapability = validateCapabilityEntry(
|
|
1122
|
+
capability,
|
|
1123
|
+
issues,
|
|
1124
|
+
`${sourceName}.capabilities[${index}]`,
|
|
1125
|
+
);
|
|
1126
|
+
if (!normalizedCapability) {
|
|
1127
|
+
return;
|
|
1090
1128
|
}
|
|
1091
|
-
|
|
1129
|
+
declaredCapabilities.push(normalizedCapability);
|
|
1130
|
+
if (seenCapabilities.has(normalizedCapability)) {
|
|
1092
1131
|
pushIssue(
|
|
1093
1132
|
issues,
|
|
1094
1133
|
"warning",
|
|
1095
1134
|
"duplicate-capability",
|
|
1096
|
-
`Capability "${
|
|
1135
|
+
`Capability "${normalizedCapability}" is declared more than once.`,
|
|
1097
1136
|
`${sourceName}.capabilities`,
|
|
1098
1137
|
);
|
|
1138
|
+
return;
|
|
1099
1139
|
}
|
|
1100
|
-
seenCapabilities.add(
|
|
1101
|
-
if (!RecommendedCapabilitySet.has(
|
|
1140
|
+
seenCapabilities.add(normalizedCapability);
|
|
1141
|
+
if (!RecommendedCapabilitySet.has(normalizedCapability)) {
|
|
1102
1142
|
pushIssue(
|
|
1103
1143
|
issues,
|
|
1104
1144
|
"warning",
|
|
1105
1145
|
"noncanonical-capability",
|
|
1106
|
-
`Capability "${
|
|
1146
|
+
`Capability "${normalizedCapability}" is not in the current canonical SDN coarse capability set.`,
|
|
1107
1147
|
`${sourceName}.capabilities`,
|
|
1108
1148
|
);
|
|
1109
1149
|
}
|
|
1110
|
-
}
|
|
1150
|
+
});
|
|
1111
1151
|
}
|
|
1112
1152
|
validateRuntimeTargets(
|
|
1113
1153
|
manifest.runtimeTargets,
|
package/src/host/abi.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { bytesToBase64 } from "../utils/encoding.js";
|
|
2
|
+
|
|
1
3
|
const textDecoder = new TextDecoder();
|
|
2
4
|
const textEncoder = new TextEncoder();
|
|
3
5
|
|
|
@@ -14,6 +16,7 @@ export const NodeHostSyncHostcallOperations = Object.freeze([
|
|
|
14
16
|
"clock.now",
|
|
15
17
|
"clock.monotonicNow",
|
|
16
18
|
"clock.nowIso",
|
|
19
|
+
"random.bytes",
|
|
17
20
|
"schedule.parse",
|
|
18
21
|
"schedule.matches",
|
|
19
22
|
"schedule.next",
|
|
@@ -105,6 +108,47 @@ function isPromiseLike(value) {
|
|
|
105
108
|
);
|
|
106
109
|
}
|
|
107
110
|
|
|
111
|
+
function encodeHostcallValue(value) {
|
|
112
|
+
if (value === undefined) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
value instanceof Uint8Array ||
|
|
117
|
+
value instanceof ArrayBuffer ||
|
|
118
|
+
ArrayBuffer.isView(value)
|
|
119
|
+
) {
|
|
120
|
+
const bytes =
|
|
121
|
+
value instanceof Uint8Array
|
|
122
|
+
? value
|
|
123
|
+
: new Uint8Array(
|
|
124
|
+
value.buffer ?? value,
|
|
125
|
+
value.byteOffset ?? 0,
|
|
126
|
+
value.byteLength ?? value.byteLength,
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
__type: "bytes",
|
|
130
|
+
base64: bytesToBase64(bytes),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (value instanceof Date) {
|
|
134
|
+
return value.toISOString();
|
|
135
|
+
}
|
|
136
|
+
if (typeof value === "bigint") {
|
|
137
|
+
return value.toString();
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
return value.map((entry) => encodeHostcallValue(entry));
|
|
141
|
+
}
|
|
142
|
+
if (value && typeof value === "object") {
|
|
143
|
+
return Object.fromEntries(
|
|
144
|
+
Object.entries(value)
|
|
145
|
+
.filter(([, entry]) => entry !== undefined)
|
|
146
|
+
.map(([key, entry]) => [key, encodeHostcallValue(entry)]),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return value;
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
export function dispatchNodeHostSyncOperation(host, operation, params = null) {
|
|
109
153
|
const normalized = assertNonEmptyString(operation, "Hostcall operation");
|
|
110
154
|
switch (normalized) {
|
|
@@ -124,6 +168,8 @@ export function dispatchNodeHostSyncOperation(host, operation, params = null) {
|
|
|
124
168
|
return host.clock.monotonicNow();
|
|
125
169
|
case "clock.nowIso":
|
|
126
170
|
return host.clock.nowIso();
|
|
171
|
+
case "random.bytes":
|
|
172
|
+
return host.random.bytes(params?.length);
|
|
127
173
|
case "schedule.parse":
|
|
128
174
|
return host.schedule.parse(params?.expression);
|
|
129
175
|
case "schedule.matches":
|
|
@@ -202,7 +248,7 @@ export function createJsonHostcallBridge(options = {}) {
|
|
|
202
248
|
}
|
|
203
249
|
setEnvelope(HOSTCALL_STATUS_OK, {
|
|
204
250
|
ok: true,
|
|
205
|
-
result,
|
|
251
|
+
result: encodeHostcallValue(result),
|
|
206
252
|
});
|
|
207
253
|
return HOSTCALL_STATUS_OK;
|
|
208
254
|
} catch (error) {
|
package/src/host/nodeHost.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dgram from "node:dgram";
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { randomBytes as nodeRandomBytes } from "node:crypto";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
7
|
import { performance } from "node:perf_hooks";
|
|
@@ -21,7 +22,6 @@ import {
|
|
|
21
22
|
ed25519Sign,
|
|
22
23
|
ed25519Verify,
|
|
23
24
|
hkdfBytes,
|
|
24
|
-
randomBytes,
|
|
25
25
|
secp256k1PublicKey,
|
|
26
26
|
secp256k1SignDigest,
|
|
27
27
|
secp256k1VerifyDigest,
|
|
@@ -1422,10 +1422,10 @@ export class NodeHost {
|
|
|
1422
1422
|
});
|
|
1423
1423
|
|
|
1424
1424
|
this.random = Object.freeze({
|
|
1425
|
-
bytes:
|
|
1426
|
-
this.#withCapability("random", "random.bytes",
|
|
1425
|
+
bytes: (length) =>
|
|
1426
|
+
this.#withCapability("random", "random.bytes", () => {
|
|
1427
1427
|
const size = assertNonNegativeInteger(length, "Random byte length");
|
|
1428
|
-
return
|
|
1428
|
+
return nodeRandomBytes(size);
|
|
1429
1429
|
}),
|
|
1430
1430
|
});
|
|
1431
1431
|
|
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>;
|
|
@@ -638,6 +650,7 @@ export type {
|
|
|
638
650
|
} from "./compiler/emception.js";
|
|
639
651
|
|
|
640
652
|
export {
|
|
653
|
+
createIsolatedEmceptionSession,
|
|
641
654
|
createSharedEmceptionSession,
|
|
642
655
|
loadSharedEmception,
|
|
643
656
|
withSharedEmception,
|
|
@@ -924,7 +937,7 @@ export class NodeHost {
|
|
|
924
937
|
nowIso(): string;
|
|
925
938
|
};
|
|
926
939
|
random: {
|
|
927
|
-
bytes(length: number):
|
|
940
|
+
bytes(length: number): Uint8Array;
|
|
928
941
|
};
|
|
929
942
|
timers: {
|
|
930
943
|
delay(ms: number, options?: { signal?: unknown }): Promise<void>;
|
package/src/manifest/codec.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as flatbuffers from "flatbuffers";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
CapabilityKind,
|
|
5
|
+
DrainPolicy,
|
|
6
|
+
HostCapabilityT,
|
|
7
|
+
PluginFamily,
|
|
4
8
|
PluginManifest,
|
|
5
9
|
PluginManifestT,
|
|
6
10
|
} from "../generated/orbpro/manifest.js";
|
|
@@ -21,6 +25,88 @@ function toByteBuffer(data) {
|
|
|
21
25
|
);
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
function normalizeEnumName(name, { separator = "_", lowercase = true } = {}) {
|
|
29
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const normalized = name.trim();
|
|
33
|
+
if (!normalized) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const joined = normalized.replace(/_/g, separator);
|
|
37
|
+
return lowercase ? joined.toLowerCase() : joined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizePluginFamilyName(value) {
|
|
41
|
+
if (typeof value === "number" && typeof PluginFamily[value] === "string") {
|
|
42
|
+
return normalizeEnumName(PluginFamily[value], { separator: "_" });
|
|
43
|
+
}
|
|
44
|
+
return normalizeEnumName(value, { separator: "_" });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeDrainPolicyName(value) {
|
|
48
|
+
if (typeof value === "number" && typeof DrainPolicy[value] === "string") {
|
|
49
|
+
return normalizeEnumName(DrainPolicy[value], { separator: "-" });
|
|
50
|
+
}
|
|
51
|
+
return normalizeEnumName(value, { separator: "-" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeCapabilityName(value) {
|
|
55
|
+
if (
|
|
56
|
+
typeof value === "number" &&
|
|
57
|
+
typeof CapabilityKind[value] === "string"
|
|
58
|
+
) {
|
|
59
|
+
return normalizeEnumName(CapabilityKind[value], { separator: "_" });
|
|
60
|
+
}
|
|
61
|
+
return normalizeEnumName(value, { separator: "_" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeDecodedCapabilities(value) {
|
|
65
|
+
if (!Array.isArray(value)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return value
|
|
69
|
+
.map((entry) => {
|
|
70
|
+
if (typeof entry === "string") {
|
|
71
|
+
return normalizeCapabilityName(entry);
|
|
72
|
+
}
|
|
73
|
+
if (!(entry instanceof HostCapabilityT) && (!entry || typeof entry !== "object")) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const capability = normalizeCapabilityName(entry.capability);
|
|
77
|
+
if (!capability) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const scope =
|
|
81
|
+
typeof entry.scope === "string" && entry.scope.trim().length > 0
|
|
82
|
+
? entry.scope.trim()
|
|
83
|
+
: null;
|
|
84
|
+
const description =
|
|
85
|
+
typeof entry.description === "string" &&
|
|
86
|
+
entry.description.trim().length > 0
|
|
87
|
+
? entry.description.trim()
|
|
88
|
+
: null;
|
|
89
|
+
const required = entry.required !== false;
|
|
90
|
+
if (!scope && !description && required) {
|
|
91
|
+
return capability;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
capability,
|
|
95
|
+
...(scope ? { scope } : {}),
|
|
96
|
+
...(required === false ? { required: false } : {}),
|
|
97
|
+
...(description ? { description } : {}),
|
|
98
|
+
};
|
|
99
|
+
})
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeDecodedMethod(method = {}) {
|
|
104
|
+
return {
|
|
105
|
+
...method,
|
|
106
|
+
drainPolicy: normalizeDrainPolicyName(method.drainPolicy),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
24
110
|
export function decodePluginManifest(data) {
|
|
25
111
|
const bb = toByteBuffer(data);
|
|
26
112
|
if (!PluginManifest.bufferHasIdentifier(bb)) {
|
|
@@ -29,6 +115,11 @@ export function decodePluginManifest(data) {
|
|
|
29
115
|
const unpacked = PluginManifest.getRootAsPluginManifest(bb).unpack();
|
|
30
116
|
return {
|
|
31
117
|
...unpacked,
|
|
118
|
+
pluginFamily: normalizePluginFamilyName(unpacked.pluginFamily),
|
|
119
|
+
capabilities: normalizeDecodedCapabilities(unpacked.capabilities),
|
|
120
|
+
methods: Array.isArray(unpacked.methods)
|
|
121
|
+
? unpacked.methods.map((method) => normalizeDecodedMethod(method))
|
|
122
|
+
: [],
|
|
32
123
|
invokeSurfaces: Array.isArray(unpacked.invokeSurfaces)
|
|
33
124
|
? unpacked.invokeSurfaces
|
|
34
125
|
.map((value) => normalizeInvokeSurfaceName(value))
|
package/src/testing/index.js
CHANGED
|
@@ -32,11 +32,11 @@ const CapabilitySurfaceMatrix = Object.freeze({
|
|
|
32
32
|
wasi: true,
|
|
33
33
|
standaloneWasi: true,
|
|
34
34
|
wasmedge: true,
|
|
35
|
-
syncHostcall:
|
|
35
|
+
syncHostcall: true,
|
|
36
36
|
nodeHostApi: true,
|
|
37
37
|
notes: [
|
|
38
38
|
"WASI random_get is available to standalone guests.",
|
|
39
|
-
"The
|
|
39
|
+
"The sync hostcall ABI exposes random.bytes through a canonical base64 byte envelope.",
|
|
40
40
|
],
|
|
41
41
|
}),
|
|
42
42
|
timers: Object.freeze({
|