@swiftbrowser/wasm-compiler 0.0.0-stage → 0.8.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/README.md +299 -2
- package/bin/swiftbrowser-wasmc.js +18 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +147 -0
- package/dist/compile.d.ts +69 -0
- package/dist/compile.js +89 -0
- package/dist/driver.d.ts +116 -0
- package/dist/driver.js +165 -0
- package/dist/host.d.ts +27 -0
- package/dist/host.js +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +9 -0
- package/dist/memory-host.d.ts +50 -0
- package/dist/memory-host.js +195 -0
- package/dist/node/index.d.ts +5 -0
- package/dist/node/index.js +4 -0
- package/dist/node/native-host.d.ts +8 -0
- package/dist/node/native-host.js +31 -0
- package/dist/node/plugin-executable.d.ts +15 -0
- package/dist/node/plugin-executable.js +97 -0
- package/dist/node/tools.d.ts +24 -0
- package/dist/node/tools.js +66 -0
- package/dist/node/wasi-host.d.ts +53 -0
- package/dist/node/wasi-host.js +189 -0
- package/dist/object.d.ts +5 -0
- package/dist/object.js +67 -0
- package/dist/plugins.d.ts +15 -0
- package/dist/plugins.js +147 -0
- package/dist/wasm.d.ts +24 -0
- package/dist/wasm.js +90 -0
- package/package.json +50 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// A macro plugin built as WebAssembly, run as the executable plugin a native
|
|
2
|
+
// compiler spawns (`-load-plugin-executable`): `node plugin-executable.js
|
|
3
|
+
// <plugin.wasm>`. The compiler's messages arrive on stdin and the replies go
|
|
4
|
+
// to stdout, framed alike (an 8-byte little-endian length, then JSON), and
|
|
5
|
+
// each message is handed to the module's `swift_wasm_macro_v1_pump`, as the
|
|
6
|
+
// WebAssembly compiler's host does (../plugins.ts). The kit's macros reach a
|
|
7
|
+
// native toolchain this way (the CLI's kit.ts), the same file on every host.
|
|
8
|
+
import { readFileSync, readSync, realpathSync, writeSync } from 'node:fs';
|
|
9
|
+
import { pathToFileURL } from 'node:url';
|
|
10
|
+
import { brotliDecompressSync } from 'node:zlib';
|
|
11
|
+
import { ConsoleStdout, WASI, wasi } from '@bjorn3/browser_wasi_shim';
|
|
12
|
+
const HEADER = 8;
|
|
13
|
+
/** Stdin the executable fills with one message before each pump. */
|
|
14
|
+
class MessageInput extends ConsoleStdout {
|
|
15
|
+
queue = new Uint8Array();
|
|
16
|
+
constructor() {
|
|
17
|
+
super(() => { });
|
|
18
|
+
}
|
|
19
|
+
set(bytes) {
|
|
20
|
+
this.queue = bytes;
|
|
21
|
+
}
|
|
22
|
+
fd_read(size) {
|
|
23
|
+
const data = this.queue.slice(0, size);
|
|
24
|
+
this.queue = this.queue.slice(data.length);
|
|
25
|
+
return { ret: wasi.ERRNO_SUCCESS, data };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Stdin, stdout and stderr, read and written synchronously: the compiler waits on each reply. */
|
|
29
|
+
export const processStreams = {
|
|
30
|
+
read: (buffer, offset, length) => readSync(0, buffer, offset, length, null),
|
|
31
|
+
write: (bytes) => {
|
|
32
|
+
for (let written = 0; written < bytes.length;)
|
|
33
|
+
written += writeSync(1, bytes, written);
|
|
34
|
+
},
|
|
35
|
+
error: (bytes) => {
|
|
36
|
+
for (let written = 0; written < bytes.length;)
|
|
37
|
+
written += writeSync(2, bytes, written);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
/** The plugin's module: a `.wasm` file, or a brotli-compressed `.wasm.br`. */
|
|
41
|
+
export function pluginModule(file) {
|
|
42
|
+
const bytes = readFileSync(file);
|
|
43
|
+
return new WebAssembly.Module(file.endsWith('.br') ? brotliDecompressSync(bytes) : bytes);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Serves the compiler's messages until it closes the pipe or sends the empty
|
|
47
|
+
* message that asks a plugin to finish; returns the exit code.
|
|
48
|
+
*/
|
|
49
|
+
export function servePlugin(module, name, streams = processStreams) {
|
|
50
|
+
const stdin = new MessageInput();
|
|
51
|
+
const shim = new WASI([name], [], [stdin, new ConsoleStdout(streams.write), new ConsoleStdout(streams.error)], { debug: false });
|
|
52
|
+
const instance = new WebAssembly.Instance(module, { wasi_snapshot_preview1: shim.wasiImport });
|
|
53
|
+
// `main` installs the message handler and returns.
|
|
54
|
+
const code = shim.start(instance);
|
|
55
|
+
if (code !== 0)
|
|
56
|
+
return code;
|
|
57
|
+
const pump = instance.exports['swift_wasm_macro_v1_pump'];
|
|
58
|
+
if (!pump) {
|
|
59
|
+
streams.error(new TextEncoder().encode(`${name}: not a macro plugin built as WebAssembly (no swift_wasm_macro_v1_pump)\n`));
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const readExactly = (size) => {
|
|
63
|
+
const buffer = new Uint8Array(size);
|
|
64
|
+
for (let offset = 0; offset < size;) {
|
|
65
|
+
const count = streams.read(buffer, offset, size - offset);
|
|
66
|
+
if (count === 0)
|
|
67
|
+
return null;
|
|
68
|
+
offset += count;
|
|
69
|
+
}
|
|
70
|
+
return buffer;
|
|
71
|
+
};
|
|
72
|
+
for (;;) {
|
|
73
|
+
const header = readExactly(HEADER);
|
|
74
|
+
if (!header)
|
|
75
|
+
return 0;
|
|
76
|
+
const size = Number(new DataView(header.buffer).getBigUint64(0, true));
|
|
77
|
+
if (size === 0)
|
|
78
|
+
return 0;
|
|
79
|
+
const body = readExactly(size);
|
|
80
|
+
if (!body)
|
|
81
|
+
return 0;
|
|
82
|
+
const message = new Uint8Array(HEADER + size);
|
|
83
|
+
message.set(header);
|
|
84
|
+
message.set(body, HEADER);
|
|
85
|
+
stdin.set(message);
|
|
86
|
+
pump();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Run as a script (through a link, too: npm links workspaces).
|
|
90
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
91
|
+
const file = process.argv[2];
|
|
92
|
+
if (!file) {
|
|
93
|
+
process.stderr.write('usage: plugin-executable <plugin.wasm>\n');
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
process.exit(servePlugin(pluginModule(file), file));
|
|
97
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ToolName } from '../driver.js';
|
|
2
|
+
/** `<tool>.wasm` in `dir`, or else its brotli-compressed `<tool>.wasm.br`. */
|
|
3
|
+
export declare function toolFile(dir: string, tool: ToolName): string;
|
|
4
|
+
/** Where @swiftbrowser/wasm-tools keeps the tools, when it is installed with them. */
|
|
5
|
+
export declare function packagedToolsDir(): string;
|
|
6
|
+
/**
|
|
7
|
+
* The tools in `dir`, or else in the installed @swiftbrowser/wasm-tools:
|
|
8
|
+
* the compiler and the linker, and clang for C targets when the tools have it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findTools(dir?: string): Record<'swift-frontend' | 'wasm-ld', string> & {
|
|
11
|
+
clang?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The macro plugins built as WebAssembly in `<dir>/plugins`
|
|
15
|
+
* (toolchain/build-plugins.sh): module name → file, `.wasm` or `.wasm.br`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findPlugins(dir?: string): Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* PackageDescription built for wasm32-wasip1 in `<dir>/manifest-api`
|
|
20
|
+
* (toolchain/build-manifest-api.sh): the directory with
|
|
21
|
+
* `PackageDescription.swiftmodule` and `libPackageDescription.a` that a
|
|
22
|
+
* manifest is compiled against and linked with; null when the tools have none.
|
|
23
|
+
*/
|
|
24
|
+
export declare function findManifestApi(dir?: string): string | null;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Where the WebAssembly tools are on this machine: a directory of
|
|
2
|
+
// `<tool>.wasm` (a build's toolchain/install-tools.sh output), or the
|
|
3
|
+
// brotli-compressed `<tool>.wasm.br` @swiftbrowser/wasm-tools installs.
|
|
4
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
/** `<tool>.wasm` in `dir`, or else its brotli-compressed `<tool>.wasm.br`. */
|
|
8
|
+
export function toolFile(dir, tool) {
|
|
9
|
+
const file = path.resolve(dir, `${tool}.wasm`);
|
|
10
|
+
if (existsSync(file) || !existsSync(`${file}.br`))
|
|
11
|
+
return file;
|
|
12
|
+
return `${file}.br`;
|
|
13
|
+
}
|
|
14
|
+
/** Where @swiftbrowser/wasm-tools keeps the tools, when it is installed with them. */
|
|
15
|
+
export function packagedToolsDir() {
|
|
16
|
+
let dir;
|
|
17
|
+
try {
|
|
18
|
+
dir = path.dirname(createRequire(import.meta.url).resolve('@swiftbrowser/wasm-tools/package.json'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error('no WebAssembly tools: pass --tools, set SWIFTBROWSER_WASM_TOOLS or install @swiftbrowser/wasm-tools');
|
|
22
|
+
}
|
|
23
|
+
if (!existsSync(path.join(dir, 'swift-frontend.wasm.br'))) {
|
|
24
|
+
throw new Error(`@swiftbrowser/wasm-tools in ${dir} has no tools; in this repository, run \`node fetch.mjs\` there`);
|
|
25
|
+
}
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The tools in `dir`, or else in the installed @swiftbrowser/wasm-tools:
|
|
30
|
+
* the compiler and the linker, and clang for C targets when the tools have it.
|
|
31
|
+
*/
|
|
32
|
+
export function findTools(dir) {
|
|
33
|
+
const root = dir ?? packagedToolsDir();
|
|
34
|
+
const clang = toolFile(root, 'clang');
|
|
35
|
+
return {
|
|
36
|
+
'swift-frontend': toolFile(root, 'swift-frontend'),
|
|
37
|
+
'wasm-ld': toolFile(root, 'wasm-ld'),
|
|
38
|
+
...(existsSync(clang) ? { clang } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The macro plugins built as WebAssembly in `<dir>/plugins`
|
|
43
|
+
* (toolchain/build-plugins.sh): module name → file, `.wasm` or `.wasm.br`.
|
|
44
|
+
*/
|
|
45
|
+
export function findPlugins(dir) {
|
|
46
|
+
const root = path.join(dir ?? packagedToolsDir(), 'plugins');
|
|
47
|
+
if (!existsSync(root))
|
|
48
|
+
return {};
|
|
49
|
+
const plugins = {};
|
|
50
|
+
for (const name of readdirSync(root).sort()) {
|
|
51
|
+
const match = /^(.+)\.wasm(\.br)?$/.exec(name);
|
|
52
|
+
if (match && !plugins[match[1]])
|
|
53
|
+
plugins[match[1]] = path.join(root, name);
|
|
54
|
+
}
|
|
55
|
+
return plugins;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* PackageDescription built for wasm32-wasip1 in `<dir>/manifest-api`
|
|
59
|
+
* (toolchain/build-manifest-api.sh): the directory with
|
|
60
|
+
* `PackageDescription.swiftmodule` and `libPackageDescription.a` that a
|
|
61
|
+
* manifest is compiled against and linked with; null when the tools have none.
|
|
62
|
+
*/
|
|
63
|
+
export function findManifestApi(dir) {
|
|
64
|
+
const root = path.join(dir ?? packagedToolsDir(), 'manifest-api');
|
|
65
|
+
return existsSync(path.join(root, 'libPackageDescription.a')) && existsSync(path.join(root, 'PackageDescription.swiftmodule')) ? root : null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ToolName } from '../driver.js';
|
|
2
|
+
import type { Host, RunResult } from '../host.js';
|
|
3
|
+
export interface WasiHostOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The tool modules by name: `swift-frontend.wasm`, `wasm-ld.wasm`, or
|
|
6
|
+
* their brotli-compressed `.wasm.br` (as @swiftbrowser/wasm-tools has them).
|
|
7
|
+
*/
|
|
8
|
+
tools: Partial<Record<ToolName, string>>;
|
|
9
|
+
/** Guest directory → host directory. */
|
|
10
|
+
mounts: Record<string, string>;
|
|
11
|
+
/** The Swift SDK's `wasm32-unknown-wasip1` directory, as a guest path. */
|
|
12
|
+
sdkRoot: string;
|
|
13
|
+
/** The worker's native stack, in MB. */
|
|
14
|
+
stackSizeMb?: number;
|
|
15
|
+
/**
|
|
16
|
+
* A host directory for Clang's module cache, mounted at /module-cache and
|
|
17
|
+
* kept between jobs, so SwiftShims and wasi-libc's modules are built once.
|
|
18
|
+
* Defaults to a temporary directory for the host's lifetime.
|
|
19
|
+
*/
|
|
20
|
+
moduleCacheDir?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Macro plugins built as WebAssembly, by the guest path the compiler names
|
|
23
|
+
* in `-load-plugin-executable` (under a mounted directory, since the
|
|
24
|
+
* compiler checks the file) → the module's host path (`.wasm` or `.wasm.br`).
|
|
25
|
+
*/
|
|
26
|
+
plugins?: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
export interface WasiCommand {
|
|
29
|
+
module: WebAssembly.Module;
|
|
30
|
+
/** argv, the program name first. */
|
|
31
|
+
args: readonly string[];
|
|
32
|
+
/** Guest directory → host directory. */
|
|
33
|
+
mounts: Record<string, string>;
|
|
34
|
+
env?: Record<string, string>;
|
|
35
|
+
/** The worker's native stack, in MB. */
|
|
36
|
+
stackSizeMb?: number;
|
|
37
|
+
/** Macro plugins by guest path (`WasiHostOptions.plugins`), compiled. */
|
|
38
|
+
plugins?: Record<string, WebAssembly.Module>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Runs a WASI command in a worker thread and returns its exit status and its
|
|
42
|
+
* stdout and stderr, interleaved. A host directory is mounted at /tmp (and
|
|
43
|
+
* TMPDIR points there) for the command's temporary files, and an empty one at
|
|
44
|
+
* / unless a mount covers it: the Swift frontend makes / its working
|
|
45
|
+
* directory, which needs a preopened directory there.
|
|
46
|
+
*/
|
|
47
|
+
export declare function runWasiCommand(command: WasiCommand): Promise<RunResult>;
|
|
48
|
+
/** The Node host: the tools in worker threads, and macro plugins that can be added as a build makes them. */
|
|
49
|
+
export interface WasiHost extends Host {
|
|
50
|
+
/** A macro plugin built after the host was made: its guest path (as `-load-plugin-executable` names it) and its file. */
|
|
51
|
+
addPlugin(guest: string, file: string): void;
|
|
52
|
+
}
|
|
53
|
+
export declare function wasiHost(options: WasiHostOptions): WasiHost;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// The WebAssembly tools as a host, under Node's WASI: step 1.5 of
|
|
2
|
+
// docs/in-browser-compiler.md. Host directories are mounted at guest paths
|
|
3
|
+
// (/sdk, /work, ...), and every path the jobs name is a guest path.
|
|
4
|
+
//
|
|
5
|
+
// Each tool runs in a worker thread. A worker gets its own, larger native
|
|
6
|
+
// stack: compiled WebAssembly frames live on it, and the frontend's type
|
|
7
|
+
// checker and the SIL passes recurse deeply. The tool's own shadow stack is
|
|
8
|
+
// in its linear memory and was sized when it was linked. A compiled module
|
|
9
|
+
// is kept per tool and sent to each worker, so it is compiled once.
|
|
10
|
+
import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
14
|
+
import v8 from 'node:v8';
|
|
15
|
+
import { Worker } from 'node:worker_threads';
|
|
16
|
+
import { brotliDecompress } from 'node:zlib';
|
|
17
|
+
import { sdkPaths } from '../driver.js';
|
|
18
|
+
// Node 22's V8 crashes in a garbage collection that walks the stack while a
|
|
19
|
+
// WebAssembly module is calling node:wasi through V8's fast API calls
|
|
20
|
+
// (InnerPointerToCodeCache::GetCacheEntry, from IterateTurbofanOptimizedFrame);
|
|
21
|
+
// the Swift frontend triggers it on every real compile. Without fast API
|
|
22
|
+
// calls it runs correctly. V8 flags are per process, so this applies to the
|
|
23
|
+
// workers too.
|
|
24
|
+
v8.setFlagsFromString('--no-turbo-fast-api-calls');
|
|
25
|
+
// Runs one WASI command and reports its exit status. CommonJS, evaluated as
|
|
26
|
+
// the worker's script, so it runs the same from the TypeScript sources (in
|
|
27
|
+
// tests) and from dist.
|
|
28
|
+
const WORKER = `
|
|
29
|
+
// Each worker would print node:wasi's ExperimentalWarning again.
|
|
30
|
+
const emitWarning = process.emitWarning;
|
|
31
|
+
process.emitWarning = (warning, ...rest) => {
|
|
32
|
+
if (String(warning).startsWith('WASI is an experimental feature')) return;
|
|
33
|
+
emitWarning.call(process, warning, ...rest);
|
|
34
|
+
};
|
|
35
|
+
const { workerData, parentPort } = require('node:worker_threads');
|
|
36
|
+
const { WASI } = require('node:wasi');
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const { module, args, env, preopens, outputFile, plugins, pluginHostUrl } = workerData;
|
|
39
|
+
const fd = fs.openSync(outputFile, 'w');
|
|
40
|
+
(async () => {
|
|
41
|
+
const wasi = new WASI({ version: 'preview1', args, env, preopens, stdin: 0, stdout: fd, stderr: fd, returnOnExit: true });
|
|
42
|
+
const imports = wasi.getImportObject();
|
|
43
|
+
// A program may close its stdio (a Swift manifest fcloses stdout after
|
|
44
|
+
// writing its description there). The descriptors are the process's, shared
|
|
45
|
+
// with every thread: closing one frees its number for another thread's next
|
|
46
|
+
// open, which this worker's own close would then close. So closing 0-2 is
|
|
47
|
+
// a no-op; what was written is already in the file.
|
|
48
|
+
const preview1 = imports.wasi_snapshot_preview1;
|
|
49
|
+
const fdClose = preview1.fd_close;
|
|
50
|
+
preview1.fd_close = (descriptor) => (descriptor <= 2 ? 0 : fdClose(descriptor));
|
|
51
|
+
// The Swift frontend runs macro plugins through the host (plugins.ts).
|
|
52
|
+
let host = null;
|
|
53
|
+
if (WebAssembly.Module.imports(module).some((entry) => entry.module === 'swiftbrowser')) {
|
|
54
|
+
const { pluginHost } = await import(pluginHostUrl);
|
|
55
|
+
host = pluginHost({ module: (path) => (plugins && plugins[path]) || null, onStderr: (text) => fs.writeSync(fd, text) });
|
|
56
|
+
Object.assign(imports, host.imports);
|
|
57
|
+
}
|
|
58
|
+
const instance = new WebAssembly.Instance(module, imports);
|
|
59
|
+
if (host) host.bind(instance);
|
|
60
|
+
const exitCode = wasi.start(instance);
|
|
61
|
+
const memory = instance.exports.memory;
|
|
62
|
+
parentPort.postMessage({ exitCode, memoryBytes: memory ? memory.buffer.byteLength : undefined });
|
|
63
|
+
})().catch((error) => {
|
|
64
|
+
parentPort.postMessage({ exitCode: 1, error: String(error && error.stack || error) });
|
|
65
|
+
}).finally(() => {
|
|
66
|
+
fs.closeSync(fd);
|
|
67
|
+
});
|
|
68
|
+
`;
|
|
69
|
+
// The plugin host the worker imports: plugins.js next to this module's
|
|
70
|
+
// directory, or, when this runs from the TypeScript sources (the tests), the
|
|
71
|
+
// build's copy, since a plain worker cannot import TypeScript.
|
|
72
|
+
const PLUGIN_HOST_URL = import.meta.url.endsWith('.ts')
|
|
73
|
+
? new URL('../../dist/plugins.js', import.meta.url).href
|
|
74
|
+
: new URL('../plugins.js', import.meta.url).href;
|
|
75
|
+
/**
|
|
76
|
+
* Runs a WASI command in a worker thread and returns its exit status and its
|
|
77
|
+
* stdout and stderr, interleaved. A host directory is mounted at /tmp (and
|
|
78
|
+
* TMPDIR points there) for the command's temporary files, and an empty one at
|
|
79
|
+
* / unless a mount covers it: the Swift frontend makes / its working
|
|
80
|
+
* directory, which needs a preopened directory there.
|
|
81
|
+
*/
|
|
82
|
+
export async function runWasiCommand(command) {
|
|
83
|
+
const scratch = await mkdtemp(path.join(tmpdir(), 'wasmc-'));
|
|
84
|
+
const outputFile = path.join(scratch, 'output');
|
|
85
|
+
const tmp = path.join(scratch, 'tmp');
|
|
86
|
+
const root = path.join(scratch, 'root');
|
|
87
|
+
await mkdir(tmp);
|
|
88
|
+
await mkdir(root);
|
|
89
|
+
try {
|
|
90
|
+
const message = await new Promise((resolve, reject) => {
|
|
91
|
+
const worker = new Worker(WORKER, {
|
|
92
|
+
eval: true,
|
|
93
|
+
workerData: {
|
|
94
|
+
module: command.module,
|
|
95
|
+
args: command.args,
|
|
96
|
+
env: { TMPDIR: '/tmp', ...command.env },
|
|
97
|
+
preopens: { '/': root, ...command.mounts, '/tmp': tmp },
|
|
98
|
+
outputFile,
|
|
99
|
+
plugins: command.plugins ?? {},
|
|
100
|
+
pluginHostUrl: PLUGIN_HOST_URL,
|
|
101
|
+
},
|
|
102
|
+
resourceLimits: { stackSizeMb: command.stackSizeMb ?? 256 },
|
|
103
|
+
});
|
|
104
|
+
let settled = false;
|
|
105
|
+
worker.once('message', (m) => {
|
|
106
|
+
settled = true;
|
|
107
|
+
resolve(m);
|
|
108
|
+
});
|
|
109
|
+
worker.once('error', reject);
|
|
110
|
+
worker.once('exit', (code) => {
|
|
111
|
+
if (!settled)
|
|
112
|
+
resolve({ exitCode: code || 1, error: `${command.args[0]}: the worker exited with ${code}` });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
const output = await readFile(outputFile, 'utf8');
|
|
116
|
+
return {
|
|
117
|
+
exitCode: message.exitCode,
|
|
118
|
+
output: message.error ? `${output}${message.error}\n` : output,
|
|
119
|
+
...(message.memoryBytes !== undefined ? { memoryBytes: message.memoryBytes } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await rm(scratch, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Modules compiled in this process, by file, size and modification time. */
|
|
127
|
+
const compiled = new Map();
|
|
128
|
+
/** A module from a `.wasm` file, or a brotli-compressed `.wasm.br`; compiled once per process while the file is unchanged. */
|
|
129
|
+
async function compileFile(file) {
|
|
130
|
+
const info = await stat(file);
|
|
131
|
+
const key = `${path.resolve(file)}\0${info.size}\0${info.mtimeMs}`;
|
|
132
|
+
let module = compiled.get(key);
|
|
133
|
+
if (!module) {
|
|
134
|
+
module = readFile(file).then(async (bytes) => WebAssembly.compile(file.endsWith('.br') ? await promisify(brotliDecompress)(bytes) : bytes));
|
|
135
|
+
// A failed compile is not kept: the next run tries again.
|
|
136
|
+
module.catch(() => compiled.delete(key));
|
|
137
|
+
compiled.set(key, module);
|
|
138
|
+
}
|
|
139
|
+
return module;
|
|
140
|
+
}
|
|
141
|
+
export function wasiHost(options) {
|
|
142
|
+
let moduleCache;
|
|
143
|
+
const moduleCacheDir = () => (moduleCache ??= options.moduleCacheDir
|
|
144
|
+
? mkdir(options.moduleCacheDir, { recursive: true }).then(() => options.moduleCacheDir)
|
|
145
|
+
: mkdtemp(path.join(tmpdir(), 'wasmc-module-cache-')));
|
|
146
|
+
const modules = new Map();
|
|
147
|
+
const moduleFor = (tool) => {
|
|
148
|
+
let module = modules.get(tool);
|
|
149
|
+
if (!module) {
|
|
150
|
+
const file = options.tools[tool];
|
|
151
|
+
if (!file)
|
|
152
|
+
throw new Error(`no WebAssembly module for ${tool}`);
|
|
153
|
+
module = compileFile(file);
|
|
154
|
+
modules.set(tool, module);
|
|
155
|
+
}
|
|
156
|
+
return module;
|
|
157
|
+
};
|
|
158
|
+
// The plugins, those added since the host was made (`addPlugin`)
|
|
159
|
+
// included; each module is compiled once (compileFile).
|
|
160
|
+
const pluginFiles = { ...options.plugins };
|
|
161
|
+
const compiledPlugins = () => Promise.all(Object.entries(pluginFiles).map(async ([guest, file]) => [guest, await compileFile(file)]))
|
|
162
|
+
.then((entries) => Object.fromEntries(entries));
|
|
163
|
+
const toHost = (guest) => {
|
|
164
|
+
const mount = Object.keys(options.mounts)
|
|
165
|
+
.filter((dir) => guest === dir || guest.startsWith(`${dir}/`))
|
|
166
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
167
|
+
if (mount === undefined)
|
|
168
|
+
throw new Error(`${guest} is not under a mounted directory`);
|
|
169
|
+
return path.join(options.mounts[mount], guest.slice(mount.length));
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
description: `WebAssembly tools (${Object.values(options.tools).join(', ')})`,
|
|
173
|
+
sdk: sdkPaths(options.sdkRoot),
|
|
174
|
+
moduleCachePath: '/module-cache',
|
|
175
|
+
readFile: (file) => readFile(toHost(file)),
|
|
176
|
+
async run(tool, args) {
|
|
177
|
+
const module = await moduleFor(tool);
|
|
178
|
+
const mounts = { ...options.mounts, '/module-cache': await moduleCacheDir() };
|
|
179
|
+
return runWasiCommand({
|
|
180
|
+
module, args: [tool, ...args], mounts,
|
|
181
|
+
...(options.stackSizeMb ? { stackSizeMb: options.stackSizeMb } : {}),
|
|
182
|
+
...(tool === 'swift-frontend' && Object.keys(pluginFiles).length > 0 ? { plugins: await compiledPlugins() } : {}),
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
addPlugin(guest, file) {
|
|
186
|
+
pluginFiles[guest] = file;
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
package/dist/object.d.ts
ADDED
package/dist/object.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// A relocatable WebAssembly object holding one global data symbol: what
|
|
2
|
+
// clang makes of `.incbin` in an assembly file, without clang. The CLI stages
|
|
3
|
+
// Foundation's ICU data this way (packages/cli/src/icu.ts), and wasm-ld links
|
|
4
|
+
// it like any other object.
|
|
5
|
+
//
|
|
6
|
+
// The layout is clang's: the linear memory imported as env.__linear_memory,
|
|
7
|
+
// one active data segment at offset 0, and a `linking` section (version 2)
|
|
8
|
+
// with the symbol table and the segment's name and alignment.
|
|
9
|
+
const WASM_MAGIC = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00];
|
|
10
|
+
const SECTION = { custom: 0, import: 2, data: 11, dataCount: 12 };
|
|
11
|
+
const LINKING = { version: 2, segmentInfo: 5, symbolTable: 8 };
|
|
12
|
+
const SYMTAB_DATA = 1;
|
|
13
|
+
const PAGE = 65536;
|
|
14
|
+
function uleb(value) {
|
|
15
|
+
const out = [];
|
|
16
|
+
do {
|
|
17
|
+
let byte = value & 0x7f;
|
|
18
|
+
value = Math.floor(value / 128);
|
|
19
|
+
if (value !== 0)
|
|
20
|
+
byte |= 0x80;
|
|
21
|
+
out.push(byte);
|
|
22
|
+
} while (value !== 0);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function name(text) {
|
|
26
|
+
const bytes = [...new TextEncoder().encode(text)];
|
|
27
|
+
return [...uleb(bytes.length), ...bytes];
|
|
28
|
+
}
|
|
29
|
+
function section(id, body) {
|
|
30
|
+
const head = [id, ...uleb(body.length)];
|
|
31
|
+
const out = new Uint8Array(head.length + body.length);
|
|
32
|
+
out.set(head);
|
|
33
|
+
out.set(body, head.length);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function concat(parts) {
|
|
37
|
+
const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
|
|
38
|
+
let offset = 0;
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
out.set(part, offset);
|
|
41
|
+
offset += part.length;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* An object defining the global `symbol` as `data`, in a segment named
|
|
47
|
+
* `.data.<symbol>` aligned to `2 ** alignLog2` bytes.
|
|
48
|
+
*/
|
|
49
|
+
export function dataObject(symbol, data, alignLog2 = 4) {
|
|
50
|
+
const memoryImport = [...uleb(1), ...name('env'), ...name('__linear_memory'), 0x02, 0x00, ...uleb(Math.ceil(data.length / PAGE))];
|
|
51
|
+
const segmentHead = [...uleb(1), 0x00, 0x41, 0x00, 0x0b, ...uleb(data.length)];
|
|
52
|
+
const symbolTable = [...uleb(1), SYMTAB_DATA, ...uleb(0), ...name(symbol), ...uleb(0), ...uleb(0), ...uleb(data.length)];
|
|
53
|
+
const segmentInfo = [...uleb(1), ...name(`.data.${symbol}`), ...uleb(alignLog2), ...uleb(0)];
|
|
54
|
+
const linking = [
|
|
55
|
+
...name('linking'),
|
|
56
|
+
...uleb(LINKING.version),
|
|
57
|
+
LINKING.symbolTable, ...uleb(symbolTable.length), ...symbolTable,
|
|
58
|
+
LINKING.segmentInfo, ...uleb(segmentInfo.length), ...segmentInfo,
|
|
59
|
+
];
|
|
60
|
+
return concat([
|
|
61
|
+
new Uint8Array(WASM_MAGIC),
|
|
62
|
+
section(SECTION.import, memoryImport),
|
|
63
|
+
section(SECTION.dataCount, uleb(1)),
|
|
64
|
+
section(SECTION.data, concat([new Uint8Array(segmentHead), data])),
|
|
65
|
+
section(SECTION.custom, linking),
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface PluginHostOptions {
|
|
2
|
+
/** The compiled plugin for a guest path, or null when there is none. */
|
|
3
|
+
module(path: string): WebAssembly.Module | null;
|
|
4
|
+
/** What plugins write to stderr (their own diagnostics and crashes). */
|
|
5
|
+
onStderr?: (text: string) => void;
|
|
6
|
+
}
|
|
7
|
+
/** The `swiftbrowser` imports, and the hook that gives them the compiler's memory. */
|
|
8
|
+
export interface PluginHost {
|
|
9
|
+
imports: {
|
|
10
|
+
swiftbrowser: Record<string, (...args: number[]) => number | void>;
|
|
11
|
+
};
|
|
12
|
+
/** Call with the compiler's instance before it runs. */
|
|
13
|
+
bind(instance: WebAssembly.Instance): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function pluginHost(options: PluginHostOptions): PluginHost;
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Macro plugins for the WebAssembly compiler. A native compiler runs an
|
|
2
|
+
// executable plugin as a process and talks to it over a pipe; under WASI the
|
|
3
|
+
// compiler asks the host instead (toolchain/patches/swift, PluginRegistry.cpp),
|
|
4
|
+
// through four imports of the `swiftbrowser` module:
|
|
5
|
+
//
|
|
6
|
+
// plugin_spawn(path, length) -> handle start the plugin at a guest path
|
|
7
|
+
// plugin_write(handle, buffer, size) the compiler's messages to it
|
|
8
|
+
// plugin_read(handle, buffer, size) its replies
|
|
9
|
+
// plugin_close(handle)
|
|
10
|
+
//
|
|
11
|
+
// The plugin is a WebAssembly module built with swift-syntax for WASI
|
|
12
|
+
// (toolchain/build-plugins.sh). Its `main` installs a message handler and
|
|
13
|
+
// returns; the host then calls its `swift_wasm_macro_v1_pump` export once per
|
|
14
|
+
// message, with the message on its stdin, and takes the reply from its
|
|
15
|
+
// stdout. Messages are framed as on the pipe: an 8-byte little-endian length,
|
|
16
|
+
// then that many bytes of JSON. Everything is synchronous, so a plugin runs
|
|
17
|
+
// in the compiler's own thread, in Node and in a browser worker alike.
|
|
18
|
+
import { ConsoleStdout, Fd, WASI, wasi } from '@bjorn3/browser_wasi_shim';
|
|
19
|
+
/** Stdin the host fills before each pump. */
|
|
20
|
+
class MessageInput extends ConsoleStdout {
|
|
21
|
+
queue = new Uint8Array();
|
|
22
|
+
constructor() {
|
|
23
|
+
super(() => { });
|
|
24
|
+
}
|
|
25
|
+
push(bytes) {
|
|
26
|
+
const next = new Uint8Array(this.queue.length + bytes.length);
|
|
27
|
+
next.set(this.queue);
|
|
28
|
+
next.set(bytes, this.queue.length);
|
|
29
|
+
this.queue = next;
|
|
30
|
+
}
|
|
31
|
+
fd_read(size) {
|
|
32
|
+
const data = this.queue.slice(0, size);
|
|
33
|
+
this.queue = this.queue.slice(data.length);
|
|
34
|
+
return { ret: wasi.ERRNO_SUCCESS, data };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Bytes appended to and taken from the front of a buffer. */
|
|
38
|
+
class ByteQueue {
|
|
39
|
+
bytes = new Uint8Array(0);
|
|
40
|
+
get length() {
|
|
41
|
+
return this.bytes.length;
|
|
42
|
+
}
|
|
43
|
+
push(data) {
|
|
44
|
+
const next = new Uint8Array(this.bytes.length + data.length);
|
|
45
|
+
next.set(this.bytes);
|
|
46
|
+
next.set(data, this.bytes.length);
|
|
47
|
+
this.bytes = next;
|
|
48
|
+
}
|
|
49
|
+
take(count) {
|
|
50
|
+
const taken = this.bytes.slice(0, count);
|
|
51
|
+
this.bytes = this.bytes.slice(taken.length);
|
|
52
|
+
return taken;
|
|
53
|
+
}
|
|
54
|
+
peek() {
|
|
55
|
+
return this.bytes;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const HEADER = 8;
|
|
59
|
+
export function pluginHost(options) {
|
|
60
|
+
let memory = null;
|
|
61
|
+
const plugins = new Map();
|
|
62
|
+
let next = 3;
|
|
63
|
+
const bytes = (pointer, size) => new Uint8Array(memory.buffer, pointer, size);
|
|
64
|
+
const decoder = new TextDecoder();
|
|
65
|
+
const spawn = (path) => {
|
|
66
|
+
const module = options.module(path);
|
|
67
|
+
if (!module)
|
|
68
|
+
return -1;
|
|
69
|
+
const stdin = new MessageInput();
|
|
70
|
+
const replies = new ByteQueue();
|
|
71
|
+
const stderr = (data) => options.onStderr?.(decoder.decode(data));
|
|
72
|
+
const fds = [stdin, new ConsoleStdout((data) => replies.push(data)), new ConsoleStdout(stderr)];
|
|
73
|
+
const shim = new WASI([path], [], fds, { debug: false });
|
|
74
|
+
const instance = new WebAssembly.Instance(module, { wasi_snapshot_preview1: shim.wasiImport });
|
|
75
|
+
// `main` installs the message handler and returns, which exits.
|
|
76
|
+
const code = shim.start(instance);
|
|
77
|
+
if (code !== 0)
|
|
78
|
+
return -1;
|
|
79
|
+
const handle = next++;
|
|
80
|
+
plugins.set(handle, { path, instance, stdin, pending: new ByteQueue(), replies });
|
|
81
|
+
return handle;
|
|
82
|
+
};
|
|
83
|
+
// Hands every complete message the compiler wrote to the plugin, one pump each.
|
|
84
|
+
const deliver = (plugin) => {
|
|
85
|
+
const pump = plugin.instance.exports['swift_wasm_macro_v1_pump'];
|
|
86
|
+
for (;;) {
|
|
87
|
+
const head = plugin.pending.peek();
|
|
88
|
+
if (head.length < HEADER)
|
|
89
|
+
return;
|
|
90
|
+
const view = new DataView(head.buffer, head.byteOffset, head.byteLength);
|
|
91
|
+
const size = Number(view.getBigUint64(0, true));
|
|
92
|
+
// An empty message asks the plugin to finish: nothing to pump.
|
|
93
|
+
if (size === 0) {
|
|
94
|
+
plugin.pending.take(HEADER);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (head.length < HEADER + size)
|
|
98
|
+
return;
|
|
99
|
+
plugin.stdin.push(plugin.pending.take(HEADER + size));
|
|
100
|
+
pump();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
bind(instance) {
|
|
105
|
+
memory = instance.exports['memory'];
|
|
106
|
+
},
|
|
107
|
+
imports: {
|
|
108
|
+
swiftbrowser: {
|
|
109
|
+
plugin_spawn(pointer, size) {
|
|
110
|
+
try {
|
|
111
|
+
return spawn(decoder.decode(bytes(pointer, size)));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
options.onStderr?.(`plugin: ${String(error.stack ?? error)}\n`);
|
|
115
|
+
return -1;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
plugin_write(handle, pointer, size) {
|
|
119
|
+
const plugin = plugins.get(handle);
|
|
120
|
+
if (!plugin)
|
|
121
|
+
return -1;
|
|
122
|
+
plugin.pending.push(bytes(pointer, size).slice());
|
|
123
|
+
try {
|
|
124
|
+
deliver(plugin);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
options.onStderr?.(`${plugin.path}: ${String(error.stack ?? error)}\n`);
|
|
128
|
+
plugins.delete(handle);
|
|
129
|
+
return -1;
|
|
130
|
+
}
|
|
131
|
+
return size;
|
|
132
|
+
},
|
|
133
|
+
plugin_read(handle, pointer, size) {
|
|
134
|
+
const plugin = plugins.get(handle);
|
|
135
|
+
if (!plugin)
|
|
136
|
+
return -1;
|
|
137
|
+
const reply = plugin.replies.take(size);
|
|
138
|
+
bytes(pointer, reply.length).set(reply);
|
|
139
|
+
return reply.length;
|
|
140
|
+
},
|
|
141
|
+
plugin_close(handle) {
|
|
142
|
+
plugins.delete(handle);
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|