@daugasauron/dolly 0.0.0-stage → 0.1.4-rc.0
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 +21 -0
- package/README.md +89 -2
- package/dist/dolly-build-id.mjs +1 -0
- package/dist/dolly-dso-0.wasm +0 -0
- package/dist/dolly-image-build-id.mjs +1 -0
- package/dist/dolly-kernel-plugin-abi.mjs +2 -0
- package/dist/dolly-process-0.wasm +0 -0
- package/dist/dolly-process-abi.mjs +1 -0
- package/dist/dolly-process-dso.mjs +1575 -0
- package/dist/dolly-process-gate-0.wasm +0 -0
- package/dist/dolly-process-stack.mjs +50 -0
- package/dist/dolly-process-worker.mjs +275 -0
- package/dist/dolly-threads-0.wasm +0 -0
- package/dist/dolly.wasm +0 -0
- package/host/abi.mjs +5 -0
- package/host/dso/abi.mjs +13 -0
- package/host/dso/dso.mjs +26 -0
- package/host/dso/module.json +15 -0
- package/host/http/abi.mjs +28 -0
- package/host/http/broker.mjs +325 -0
- package/host/http/http.mjs +74 -0
- package/host/http/local-services.mjs +37 -0
- package/host/http/module.json +15 -0
- package/host/http/policy.mjs +293 -0
- package/host/modules.mjs +373 -0
- package/host/nena/abi.mjs +19 -0
- package/host/nena/module.json +15 -0
- package/host/nena/nena.mjs +131 -0
- package/host/packages/module.json +13 -0
- package/host/packages/packages.mjs +15 -0
- package/host/packages/service.mjs +59 -0
- package/host/requirements.mjs +62 -0
- package/host/runtime/abi.mjs +8 -0
- package/host/runtime/boot-files.mjs +45 -0
- package/host/runtime/grower.mjs +14 -0
- package/host/runtime/lease-bridge.mjs +65 -0
- package/host/runtime/module.json +16 -0
- package/host/runtime/runtime.mjs +209 -0
- package/host/snapshot/abi.mjs +16 -0
- package/host/snapshot/module.json +15 -0
- package/host/snapshot/snapshot.mjs +138 -0
- package/host/snapshot/transport.mjs +132 -0
- package/host/snapshot/ui.mjs +207 -0
- package/host/stack/abi.mjs +5 -0
- package/host/stack/module.json +17 -0
- package/host/stack/stack.mjs +19 -0
- package/host/threads/abi.mjs +8 -0
- package/host/threads/module.json +13 -0
- package/host/threads/threads.mjs +36 -0
- package/package.json +12 -4
- package/src/alphabetical.mjs +4 -0
- package/src/browser.mjs +237 -0
- package/src/build-log.mjs +30 -0
- package/src/image-entry.mjs +29 -0
- package/src/image-inputs.mjs +20 -0
- package/src/image-names.mjs +11 -0
- package/src/images.mjs +10 -0
- package/src/kernel-plugin.mjs +77 -0
- package/src/packaged-snapshot.mjs +175 -0
- package/src/page-chords.mjs +43 -0
- package/src/page-indicators.mjs +27 -0
- package/src/process-abi.mjs +69 -0
- package/src/process-constants.mjs +242 -0
- package/src/process-supervisor.mjs +1002 -0
- package/src/runtime-worker.mjs +55 -0
- package/src/session-store.mjs +198 -0
- package/src/snapshot-records.mjs +109 -0
- package/src/static-asset.mjs +97 -0
- package/src/terminal-text.mjs +72 -0
- package/src/version.mjs +3 -0
- package/src/wasm-interface.mjs +440 -0
- package/src/worker-image.mjs +137 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
2
|
+
const valueTypes = new Map([
|
|
3
|
+
[0x7f, "i32"],
|
|
4
|
+
[0x7e, "i64"],
|
|
5
|
+
[0x7d, "f32"],
|
|
6
|
+
[0x7c, "f64"],
|
|
7
|
+
[0x7b, "v128"],
|
|
8
|
+
[0x70, "funcref"],
|
|
9
|
+
[0x6f, "externref"],
|
|
10
|
+
[0x69, "exnref"],
|
|
11
|
+
[0x74, "nullexnref"],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export class Reader {
|
|
15
|
+
constructor(bytes, label = "WebAssembly binary") {
|
|
16
|
+
this.bytes = bytes;
|
|
17
|
+
this.offset = 0;
|
|
18
|
+
this.label = label;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get done() {
|
|
22
|
+
return this.offset === this.bytes.length;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fail(message) {
|
|
26
|
+
throw new Error(`${this.label} at byte ${this.offset}: ${message}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
u8() {
|
|
30
|
+
if (this.offset >= this.bytes.length) this.fail("unexpected end of file");
|
|
31
|
+
return this.bytes[this.offset++];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
take(length) {
|
|
35
|
+
if (!Number.isSafeInteger(length) || length < 0 || this.offset + length > this.bytes.length) {
|
|
36
|
+
this.fail(`invalid byte range of length ${length}`);
|
|
37
|
+
}
|
|
38
|
+
const result = this.bytes.subarray(this.offset, this.offset + length);
|
|
39
|
+
this.offset += length;
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
subreader(length, label) {
|
|
44
|
+
return new Reader(this.take(length), label);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
unsigned(bits = 32) {
|
|
48
|
+
let result = 0n;
|
|
49
|
+
let shift = 0n;
|
|
50
|
+
const limit = Math.ceil(bits / 7);
|
|
51
|
+
for (let index = 0; index < limit; index += 1) {
|
|
52
|
+
const byte = this.u8();
|
|
53
|
+
result |= BigInt(byte & 0x7f) << shift;
|
|
54
|
+
if ((byte & 0x80) === 0) {
|
|
55
|
+
if (result >= (1n << BigInt(bits))) this.fail(`unsigned LEB exceeds ${bits} bits`);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
shift += 7n;
|
|
59
|
+
}
|
|
60
|
+
this.fail(`unterminated unsigned ${bits}-bit LEB`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
signed(bits = 32) {
|
|
64
|
+
let result = 0n;
|
|
65
|
+
let shift = 0n;
|
|
66
|
+
let byte = 0;
|
|
67
|
+
const limit = Math.ceil(bits / 7);
|
|
68
|
+
for (let index = 0; index < limit; index += 1) {
|
|
69
|
+
byte = this.u8();
|
|
70
|
+
result |= BigInt(byte & 0x7f) << shift;
|
|
71
|
+
shift += 7n;
|
|
72
|
+
if ((byte & 0x80) === 0) {
|
|
73
|
+
if ((byte & 0x40) !== 0 && shift < BigInt(bits)) result |= (-1n) << shift;
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
this.fail(`unterminated signed ${bits}-bit LEB`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
u32() {
|
|
81
|
+
return Number(this.unsigned(32));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
string() {
|
|
85
|
+
const length = this.u32();
|
|
86
|
+
try {
|
|
87
|
+
return decoder.decode(this.take(length));
|
|
88
|
+
} catch {
|
|
89
|
+
this.fail("invalid UTF-8 string");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readValueType(reader) {
|
|
95
|
+
const byte = reader.u8();
|
|
96
|
+
const type = valueTypes.get(byte);
|
|
97
|
+
if (!type) reader.fail(`unsupported value type 0x${byte.toString(16)}`);
|
|
98
|
+
return type;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readVector(reader, readElement, result = []) {
|
|
102
|
+
const count = reader.u32();
|
|
103
|
+
if (count > reader.bytes.length - reader.offset) reader.fail("vector count exceeds section size");
|
|
104
|
+
for (let index = 0; index < count; index++) result.push(readElement(reader));
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readLimits(reader) {
|
|
109
|
+
const flags = reader.u32();
|
|
110
|
+
if ((flags & ~0x7) !== 0) reader.fail(`unsupported limits flags 0x${flags.toString(16)}`);
|
|
111
|
+
const hasMaximum = (flags & 0x1) !== 0;
|
|
112
|
+
const shared = (flags & 0x2) !== 0;
|
|
113
|
+
const address64 = (flags & 0x4) !== 0;
|
|
114
|
+
const readBound = () => reader.unsigned(address64 ? 64 : 32);
|
|
115
|
+
const minimum = readBound();
|
|
116
|
+
const maximum = hasMaximum ? readBound() : null;
|
|
117
|
+
if (shared && maximum === null) reader.fail("shared limits require a maximum");
|
|
118
|
+
return { minimum, maximum, shared, address64 };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readTableType(reader) {
|
|
122
|
+
return { kind: "table", element: readValueType(reader), ...readLimits(reader) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readMemoryType(reader) {
|
|
126
|
+
return { kind: "memory", ...readLimits(reader) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readGlobalType(reader) {
|
|
130
|
+
return { kind: "global", value: readValueType(reader), mutable: reader.u8() !== 0 };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readTagType(reader, types) {
|
|
134
|
+
const attribute = reader.u32();
|
|
135
|
+
return { kind: "tag", attribute, type: functionType(types, reader.u32(), reader) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function skipConstantExpression(reader) {
|
|
139
|
+
for (;;) {
|
|
140
|
+
const opcode = reader.u8();
|
|
141
|
+
switch (opcode) {
|
|
142
|
+
case 0x0b:
|
|
143
|
+
return;
|
|
144
|
+
case 0x41:
|
|
145
|
+
reader.signed(32);
|
|
146
|
+
break;
|
|
147
|
+
case 0x42:
|
|
148
|
+
reader.signed(64);
|
|
149
|
+
break;
|
|
150
|
+
case 0x43:
|
|
151
|
+
reader.take(4);
|
|
152
|
+
break;
|
|
153
|
+
case 0x44:
|
|
154
|
+
reader.take(8);
|
|
155
|
+
break;
|
|
156
|
+
case 0x23:
|
|
157
|
+
case 0xd2:
|
|
158
|
+
reader.u32();
|
|
159
|
+
break;
|
|
160
|
+
case 0xd0:
|
|
161
|
+
readValueType(reader);
|
|
162
|
+
break;
|
|
163
|
+
case 0x6a: // i32.add
|
|
164
|
+
case 0x6b: // i32.sub
|
|
165
|
+
case 0x6c: // i32.mul
|
|
166
|
+
case 0x7c: // i64.add
|
|
167
|
+
case 0x7d: // i64.sub
|
|
168
|
+
case 0x7e: // i64.mul
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
reader.fail(`unsupported constant-expression opcode 0x${opcode.toString(16)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// An element of an expression segment: the function a ref.func names, or
|
|
177
|
+
// null for ref.null.
|
|
178
|
+
function readElementExpression(reader) {
|
|
179
|
+
const opcode = reader.u8();
|
|
180
|
+
let index = null;
|
|
181
|
+
if (opcode === 0xd2) index = reader.u32();
|
|
182
|
+
else if (opcode === 0xd0) readValueType(reader);
|
|
183
|
+
else reader.fail(`unsupported element expression opcode 0x${opcode.toString(16)}`);
|
|
184
|
+
if (reader.u8() !== 0x0b) reader.fail("element expression has more than one instruction");
|
|
185
|
+
return index;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The functions an element segment puts into a table: an active segment's,
|
|
189
|
+
// none for a passive or declarative one (flag bit 0).
|
|
190
|
+
function readElementSegment(reader) {
|
|
191
|
+
const flags = reader.u32();
|
|
192
|
+
if (flags > 7) reader.fail(`unsupported element segment flags ${flags}`);
|
|
193
|
+
const active = (flags & 0x1) === 0;
|
|
194
|
+
if (flags === 2 || flags === 6) reader.u32();
|
|
195
|
+
if (active) skipConstantExpression(reader);
|
|
196
|
+
if (flags & 0x4) {
|
|
197
|
+
if (flags !== 4) readValueType(reader);
|
|
198
|
+
const indices = readVector(reader, readElementExpression);
|
|
199
|
+
return active ? indices.filter(index => index !== null) : [];
|
|
200
|
+
}
|
|
201
|
+
if (flags !== 0 && reader.u8() !== 0x00) reader.fail("unsupported element kind");
|
|
202
|
+
const indices = readVector(reader, item => item.u32());
|
|
203
|
+
return active ? indices : [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Binaryen's --fpcast-emu leaves one signature in the table, which every
|
|
207
|
+
// indirect call uses: thunks of i64 parameters, sixteen or more
|
|
208
|
+
// (max-func-params), and an i64 result. Their parameter count, or 0.
|
|
209
|
+
function castEmulationParameters(tableFunctions) {
|
|
210
|
+
const [type] = tableFunctions;
|
|
211
|
+
const thunk = type && type.params.length >= 16 && type.params.every(param => param === "i64") &&
|
|
212
|
+
type.results.length === 1 && type.results[0] === "i64";
|
|
213
|
+
return thunk && tableFunctions.every(other => other === type) ? type.params.length : 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function functionType(types, index, reader) {
|
|
217
|
+
const type = types[index];
|
|
218
|
+
if (!type) reader.fail(`function references missing type ${index}`);
|
|
219
|
+
return type;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function parseWasmInterface(input, label = "WebAssembly binary") {
|
|
223
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
224
|
+
const reader = new Reader(bytes, label);
|
|
225
|
+
const expectedHeader = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
|
|
226
|
+
for (const byte of expectedHeader) {
|
|
227
|
+
if (reader.u8() !== byte) reader.fail("invalid WebAssembly header");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const types = [];
|
|
231
|
+
const imports = [];
|
|
232
|
+
const definedFunctionTypes = [];
|
|
233
|
+
const importedTables = [];
|
|
234
|
+
const definedTables = [];
|
|
235
|
+
const importedMemories = [];
|
|
236
|
+
const definedMemories = [];
|
|
237
|
+
const importedGlobals = [];
|
|
238
|
+
const definedGlobals = [];
|
|
239
|
+
const importedTags = [];
|
|
240
|
+
const definedTags = [];
|
|
241
|
+
const rawExports = [];
|
|
242
|
+
const tableFunctionIndices = [];
|
|
243
|
+
const customSections = [];
|
|
244
|
+
const customSectionData = [];
|
|
245
|
+
|
|
246
|
+
while (!reader.done) {
|
|
247
|
+
const id = reader.u8();
|
|
248
|
+
const size = reader.u32();
|
|
249
|
+
const section = reader.subreader(size, `${label} section ${id}`);
|
|
250
|
+
|
|
251
|
+
if (id === 0) {
|
|
252
|
+
const name = section.string();
|
|
253
|
+
customSections.push(name);
|
|
254
|
+
customSectionData.push({ name, data: section.take(section.bytes.length - section.offset) });
|
|
255
|
+
} else if (id === 1) {
|
|
256
|
+
readVector(section, (item) => {
|
|
257
|
+
if (item.u8() !== 0x60) item.fail("only function types are supported");
|
|
258
|
+
return {
|
|
259
|
+
kind: "func",
|
|
260
|
+
params: readVector(item, readValueType),
|
|
261
|
+
results: readVector(item, readValueType),
|
|
262
|
+
};
|
|
263
|
+
}, types);
|
|
264
|
+
} else if (id === 2) {
|
|
265
|
+
readVector(section, (item) => {
|
|
266
|
+
const module = item.string();
|
|
267
|
+
const name = item.string();
|
|
268
|
+
const externalKind = item.u8();
|
|
269
|
+
let type;
|
|
270
|
+
if (externalKind === 0) {
|
|
271
|
+
const typeIndex = item.u32();
|
|
272
|
+
type = functionType(types, typeIndex, item);
|
|
273
|
+
} else if (externalKind === 1) {
|
|
274
|
+
type = readTableType(item);
|
|
275
|
+
importedTables.push(type);
|
|
276
|
+
} else if (externalKind === 2) {
|
|
277
|
+
type = readMemoryType(item);
|
|
278
|
+
importedMemories.push(type);
|
|
279
|
+
} else if (externalKind === 3) {
|
|
280
|
+
type = readGlobalType(item);
|
|
281
|
+
importedGlobals.push(type);
|
|
282
|
+
} else if (externalKind === 4) {
|
|
283
|
+
type = readTagType(item, types);
|
|
284
|
+
importedTags.push(type);
|
|
285
|
+
} else {
|
|
286
|
+
item.fail(`unknown import kind ${externalKind}`);
|
|
287
|
+
}
|
|
288
|
+
return { module, name, type };
|
|
289
|
+
}, imports);
|
|
290
|
+
} else if (id === 3) {
|
|
291
|
+
readVector(section, (item) => item.u32(), definedFunctionTypes);
|
|
292
|
+
} else if (id === 4) {
|
|
293
|
+
readVector(section, readTableType, definedTables);
|
|
294
|
+
} else if (id === 5) {
|
|
295
|
+
readVector(section, readMemoryType, definedMemories);
|
|
296
|
+
} else if (id === 6) {
|
|
297
|
+
readVector(section, (item) => {
|
|
298
|
+
const type = readGlobalType(item);
|
|
299
|
+
skipConstantExpression(item);
|
|
300
|
+
return type;
|
|
301
|
+
}, definedGlobals);
|
|
302
|
+
} else if (id === 7) {
|
|
303
|
+
readVector(section, (item) => ({
|
|
304
|
+
name: item.string(),
|
|
305
|
+
externalKind: item.u8(),
|
|
306
|
+
index: item.u32(),
|
|
307
|
+
}), rawExports);
|
|
308
|
+
} else if (id === 9) {
|
|
309
|
+
// One by one: a large program's table (Mojo's, Codex's) exceeds the
|
|
310
|
+
// arguments a spread may pass.
|
|
311
|
+
for (const indices of readVector(section, readElementSegment)) for (const index of indices) tableFunctionIndices.push(index);
|
|
312
|
+
} else if (id === 13) {
|
|
313
|
+
readVector(section, item => readTagType(item, types), definedTags);
|
|
314
|
+
} else {
|
|
315
|
+
section.take(section.bytes.length - section.offset);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!section.done) section.fail("section has trailing bytes");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const importedFunctions = imports.filter((entry) => entry.type.kind === "func").map((entry) => entry.type);
|
|
322
|
+
const functions = [
|
|
323
|
+
...importedFunctions,
|
|
324
|
+
...definedFunctionTypes.map((index) => functionType(types, index, reader)),
|
|
325
|
+
];
|
|
326
|
+
const tables = [...importedTables, ...definedTables];
|
|
327
|
+
const memories = [...importedMemories, ...definedMemories];
|
|
328
|
+
const globals = [...importedGlobals, ...definedGlobals];
|
|
329
|
+
const collections = [functions, tables, memories, globals, [...importedTags, ...definedTags]];
|
|
330
|
+
const kindNames = ["func", "table", "memory", "global", "tag"];
|
|
331
|
+
const exports = rawExports.map(({ name, externalKind, index }) => {
|
|
332
|
+
const collection = collections[externalKind];
|
|
333
|
+
const type = collection?.[index];
|
|
334
|
+
if (!type) reader.fail(`export ${name} references unknown ${kindNames[externalKind] ?? externalKind} ${index}`);
|
|
335
|
+
return { name, type };
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const tableFunctions = tableFunctionIndices.map(index =>
|
|
339
|
+
functions[index] ?? reader.fail(`element segment references unknown function ${index}`));
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
label,
|
|
343
|
+
imports,
|
|
344
|
+
exports,
|
|
345
|
+
castEmulationParameters: castEmulationParameters(tableFunctions),
|
|
346
|
+
customSections,
|
|
347
|
+
customSectionData,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function encodeUnsigned(value) {
|
|
352
|
+
let remaining = BigInt(value);
|
|
353
|
+
const bytes = [];
|
|
354
|
+
do {
|
|
355
|
+
let byte = Number(remaining & 0x7fn);
|
|
356
|
+
remaining >>= 7n;
|
|
357
|
+
if (remaining !== 0n) byte |= 0x80;
|
|
358
|
+
bytes.push(byte);
|
|
359
|
+
} while (remaining !== 0n);
|
|
360
|
+
return Uint8Array.from(bytes);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function joinBytes(parts) {
|
|
364
|
+
const length = parts.reduce((total, part) => total + part.length, 0);
|
|
365
|
+
const result = new Uint8Array(length);
|
|
366
|
+
let offset = 0;
|
|
367
|
+
for (const part of parts) {
|
|
368
|
+
result.set(part, offset);
|
|
369
|
+
offset += part.length;
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function appendCustomSection(input, name, data) {
|
|
375
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
376
|
+
const encodedName = new TextEncoder().encode(name);
|
|
377
|
+
const payload = joinBytes([encodeUnsigned(encodedName.length), encodedName, data]);
|
|
378
|
+
const section = joinBytes([Uint8Array.of(0), encodeUnsigned(payload.length), payload]);
|
|
379
|
+
return joinBytes([bytes, section]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function formatLimits(type) {
|
|
383
|
+
const width = type.address64 ? "64" : "32";
|
|
384
|
+
const maximum = type.maximum === null ? "*" : type.maximum;
|
|
385
|
+
return `${width}(min=${type.minimum},max=${maximum}${type.shared ? ",shared" : ""})`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function formatWasmType(type) {
|
|
389
|
+
if (type.kind === "func") {
|
|
390
|
+
return `func(${type.params.join(",")})->(${type.results.join(",")})`;
|
|
391
|
+
}
|
|
392
|
+
if (type.kind === "global") {
|
|
393
|
+
return `global(${type.mutable ? "mut " : ""}${type.value})`;
|
|
394
|
+
}
|
|
395
|
+
if (type.kind === "memory") return `memory${formatLimits(type)}`;
|
|
396
|
+
if (type.kind === "table") return `table${formatLimits(type)}:${type.element}`;
|
|
397
|
+
if (type.kind === "tag") return `tag(${formatWasmType(type.type)})`;
|
|
398
|
+
throw new Error(`unknown WebAssembly type kind ${type.kind}`);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function arraysEqual(left, right) {
|
|
402
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function sameWasmType(left, right) {
|
|
406
|
+
if (left.kind !== right.kind) return false;
|
|
407
|
+
if (left.kind === "func") {
|
|
408
|
+
return arraysEqual(left.params, right.params) && arraysEqual(left.results, right.results);
|
|
409
|
+
}
|
|
410
|
+
if (left.kind === "global") return left.value === right.value && left.mutable === right.mutable;
|
|
411
|
+
if (left.kind === "tag") return left.attribute === right.attribute && sameWasmType(left.type, right.type);
|
|
412
|
+
if (left.kind === "memory") {
|
|
413
|
+
return left.address64 === right.address64
|
|
414
|
+
&& left.shared === right.shared
|
|
415
|
+
&& left.minimum === right.minimum
|
|
416
|
+
&& left.maximum === right.maximum;
|
|
417
|
+
}
|
|
418
|
+
if (left.kind === "table") {
|
|
419
|
+
return left.element === right.element
|
|
420
|
+
&& left.address64 === right.address64
|
|
421
|
+
&& left.minimum === right.minimum
|
|
422
|
+
&& left.maximum === right.maximum;
|
|
423
|
+
}
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function providerSatisfiesImport(provider, required, { dynamicTable = false } = {}) {
|
|
428
|
+
if (provider.kind !== required.kind) return false;
|
|
429
|
+
if (["func", "global", "tag"].includes(provider.kind)) return sameWasmType(provider, required);
|
|
430
|
+
if (provider.kind === "memory" || provider.kind === "table") {
|
|
431
|
+
if (provider.address64 !== required.address64 || provider.shared !== required.shared) return false;
|
|
432
|
+
if (provider.kind === "table" && provider.element !== required.element) return false;
|
|
433
|
+
if (!dynamicTable && provider.minimum < required.minimum) return false;
|
|
434
|
+
if (required.maximum !== null) {
|
|
435
|
+
if (provider.maximum === null || provider.maximum > required.maximum) return false;
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// What a runtime Worker does around the image it runs: it takes the page's
|
|
2
|
+
// configure message, starts the host modules and a kernel, has `image` put the
|
|
3
|
+
// image into that kernel, and runs its ENTRY. runtime-worker.mjs restores an
|
|
4
|
+
// image of the image directory; the site's build-worker.mjs builds one or
|
|
5
|
+
// restores a custom one.
|
|
6
|
+
import { DOLLY_BUILD_ID } from "../dist/dolly-build-id.mjs";
|
|
7
|
+
import { loadImages } from "./images.mjs";
|
|
8
|
+
import { decodeImageEntry } from "./image-entry.mjs";
|
|
9
|
+
import { MAX_SNAPSHOT_BYTES } from "./snapshot-records.mjs";
|
|
10
|
+
import { terminalFailureReason } from "./process-supervisor.mjs";
|
|
11
|
+
import { bootFiles } from "../host/runtime/boot-files.mjs";
|
|
12
|
+
|
|
13
|
+
const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
14
|
+
|
|
15
|
+
// Listen before the host registry loads its manifests: the page's configure
|
|
16
|
+
// message may arrive while that import is still pending.
|
|
17
|
+
const configured = new Promise((resolve, reject) => {
|
|
18
|
+
const timeout = setTimeout(
|
|
19
|
+
() => reject(new Error("Dolly boot configuration was not provided")), 10_000,
|
|
20
|
+
);
|
|
21
|
+
self.addEventListener("message", function configure(event) {
|
|
22
|
+
if (event.data?.type !== "configure") return;
|
|
23
|
+
self.removeEventListener("message", configure);
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
resolve(event.data);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
const { hostRegistry } = await import("../host/modules.mjs");
|
|
29
|
+
|
|
30
|
+
// The runtime's own files: the kernel and the contracts beside this module.
|
|
31
|
+
export const runtimeBase = new URL("../", import.meta.url);
|
|
32
|
+
export const kernelArtifact = path => {
|
|
33
|
+
if (path !== "dolly.wasm" && path !== "dolly.data") throw new Error("unknown fixed kernel artifact");
|
|
34
|
+
return new URL(`dist/${path}`, runtimeBase).href;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function checkedMemoryRange(memory, addressValue, sizeValue) {
|
|
38
|
+
const address = Number(addressValue);
|
|
39
|
+
const size = Number(sizeValue);
|
|
40
|
+
if (!Number.isSafeInteger(address) || !Number.isSafeInteger(size) ||
|
|
41
|
+
address <= 0 || size <= 0 || size > MAX_SNAPSHOT_BYTES ||
|
|
42
|
+
address > memory.buffer.byteLength - size) {
|
|
43
|
+
throw new Error("Wasm published an invalid system snapshot memory range");
|
|
44
|
+
}
|
|
45
|
+
return { address, size };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stage = text => self.postMessage({ type: "bootstrap", text: `${text}\n`, error: false });
|
|
49
|
+
|
|
50
|
+
function acknowledged(type, failure) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const timeout = setTimeout(() => reject(new Error(failure)), 10_000);
|
|
53
|
+
self.addEventListener("message", function acknowledge(event) {
|
|
54
|
+
if (event.data?.type !== type) return;
|
|
55
|
+
self.removeEventListener("message", acknowledge);
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
resolve();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// `image(config, host)` checks what the page asked for and returns its steps:
|
|
63
|
+
// prepare({ kernel, memory, files, stage }) before the kernel's modules start,
|
|
64
|
+
// restore({ kernel, memory, files, stage }) to { status, supervisor? },
|
|
65
|
+
// and an optional finish({ kernel, stage }) once the supervisor runs; `name` is
|
|
66
|
+
// the image the route asked for ("custom" names none).
|
|
67
|
+
export async function runImage(image) {
|
|
68
|
+
let host;
|
|
69
|
+
self.addEventListener("message", event => { void host?.handle(event.data).catch(error => {
|
|
70
|
+
host.dispose();
|
|
71
|
+
self.postMessage({ type: "error", message: error.message });
|
|
72
|
+
}); });
|
|
73
|
+
try {
|
|
74
|
+
const config = await configured;
|
|
75
|
+
if (!Array.isArray(config.modules) || config.modules.length > 64 ||
|
|
76
|
+
!config.modules.every(url => typeof url === "string")) throw new Error("invalid host module list");
|
|
77
|
+
if (typeof config.images !== "string") throw new Error("invalid image directory");
|
|
78
|
+
await loadImages(config.images);
|
|
79
|
+
host = await (await hostRegistry(config.modules)).createHost("worker", config.hostModules, {
|
|
80
|
+
send: (message, transfers = []) => self.postMessage(message, transfers),
|
|
81
|
+
configuration: config.hostConfiguration,
|
|
82
|
+
resources: { runtimeBase },
|
|
83
|
+
});
|
|
84
|
+
const steps = await image(config, host);
|
|
85
|
+
stage("creating wasm64 userspace kernel...");
|
|
86
|
+
const memory = host.kernel.memory;
|
|
87
|
+
await host.kernel.growing;
|
|
88
|
+
// Fixed deployment input, never a filename or URL supplied by Wasm.
|
|
89
|
+
const kernelModule = await WebAssembly.compileStreaming(fetch(kernelArtifact("dolly.wasm")));
|
|
90
|
+
const kernel = new WebAssembly.Instance(kernelModule, host.imports(kernelModule)).exports;
|
|
91
|
+
kernel._initialize();
|
|
92
|
+
const files = bootFiles(kernel, memory);
|
|
93
|
+
await steps.prepare({ kernel, memory, files, stage });
|
|
94
|
+
stage("Dolly runtime loaded");
|
|
95
|
+
await host.start("kernel", { kernel, memory });
|
|
96
|
+
const restored = await steps.restore({ kernel, memory, files, stage });
|
|
97
|
+
if (restored.status !== 0) throw new Error(`Dolly bootstrap failed with status ${restored.status}`);
|
|
98
|
+
if (config.buildOnly) {
|
|
99
|
+
self.close();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const supervisor = restored.supervisor ?? await host.kernel.supervisor(kernel);
|
|
103
|
+
await steps.finish?.({ kernel, stage });
|
|
104
|
+
|
|
105
|
+
await host.imageRestored({ kernel, supervisor, stage, files, image: steps.name });
|
|
106
|
+
if (kernel.dolly_bootstrap_environment() !== 0) throw new Error("Dolly image environment is invalid");
|
|
107
|
+
await host.start("image", { kernel, memory });
|
|
108
|
+
const runtimeImage = decoder.decode(files.read("/etc/dolly/image", 256));
|
|
109
|
+
if (steps.name !== "custom" && runtimeImage !== steps.name) {
|
|
110
|
+
throw new Error(`Dollyfile selected image ${runtimeImage}, expected ${steps.name}`);
|
|
111
|
+
}
|
|
112
|
+
stage("starting image entry...");
|
|
113
|
+
const entryReady = acknowledged("entry-ready-ack", "browser did not acknowledge image startup");
|
|
114
|
+
self.postMessage({ type: "ready", image: runtimeImage, routeImage: steps.name, buildId: DOLLY_BUILD_ID,
|
|
115
|
+
hostModules: host.enabled });
|
|
116
|
+
await entryReady;
|
|
117
|
+
|
|
118
|
+
// The image ends with its ENTRY process, however that ended: a program that
|
|
119
|
+
// failed or could not start is the image's ending, not the runtime's failure.
|
|
120
|
+
const entry = async () => supervisor.spawn(decodeImageEntry(files.read("/etc/dolly/entry", 64 * 1024)), { foreground: true });
|
|
121
|
+
const ending = await entry().catch(error => {
|
|
122
|
+
console.error(error);
|
|
123
|
+
return { status: 126, signal: 0, failure: error.reason ?? terminalFailureReason(error) };
|
|
124
|
+
});
|
|
125
|
+
// The modules' Worker sides let go first: display@0 gives the page its last frame.
|
|
126
|
+
host.dispose();
|
|
127
|
+
self.postMessage({ type: "exited", ...ending });
|
|
128
|
+
} catch (error) {
|
|
129
|
+
self.postMessage({
|
|
130
|
+
type: "error",
|
|
131
|
+
message: error instanceof Error ? error.message : String(error),
|
|
132
|
+
stack: error instanceof Error ? error.stack ?? "" : "",
|
|
133
|
+
});
|
|
134
|
+
} finally {
|
|
135
|
+
host?.dispose();
|
|
136
|
+
}
|
|
137
|
+
}
|