@swiftbrowser/wasm-compiler 0.0.0-stage → 0.7.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.
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * An object defining the global `symbol` as `data`, in a segment named
3
+ * `.data.<symbol>` aligned to `2 ** alignLog2` bytes.
4
+ */
5
+ export declare function dataObject(symbol: string, data: Uint8Array, alignLog2?: number): Uint8Array<ArrayBuffer>;
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;
@@ -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
+ }
package/dist/wasm.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /** The custom section the Swift frontend fills with `-l` flags for the linker. */
2
+ export declare const AUTOLINK_SECTION = ".swift1_autolink_entries";
3
+ /** Every custom section of a module or object file, by name, in order. */
4
+ export declare function customSections(module: Uint8Array): {
5
+ name: string;
6
+ payload: Uint8Array;
7
+ }[];
8
+ /**
9
+ * The linker flags an object asks for (`-lswiftCore` and so on), as
10
+ * `swift-autolink-extract` prints them: NUL-separated in the section.
11
+ */
12
+ export declare function autolinkEntries(object: Uint8Array): string[];
13
+ /**
14
+ * The runtime libraries `swift-autolink-extract` recognizes, in the order it
15
+ * prints them (lib/DriverTool/autolink_extract_main.cpp, Swift 6.4): after
16
+ * every other flag, each once, so a library comes before the ones it uses.
17
+ */
18
+ export declare const SWIFT_RUNTIME_LIBRARIES_ORDERED: readonly string[];
19
+ /**
20
+ * What `swift-autolink-extract` prints for these objects: every flag that is
21
+ * not a known runtime library, in order and repeats kept, then the runtime
22
+ * libraries any object named, once each, in SWIFT_RUNTIME_LIBRARIES_ORDERED.
23
+ */
24
+ export declare function extractAutolink(objects: readonly Uint8Array[]): string[];
package/dist/wasm.js ADDED
@@ -0,0 +1,90 @@
1
+ // Reading a WebAssembly object file's custom sections, for the autolink
2
+ // record the Swift frontend writes into every object. `swift-autolink-extract`
3
+ // does this for the native driver; it is a few lines here instead of one more
4
+ // program to port. Plain Uint8Array, no Node APIs, so the browser can use it.
5
+ // The same section layout is parsed by packages/cli/src/wasm.ts for linked
6
+ // modules.
7
+ const MAGIC = [0x00, 0x61, 0x73, 0x6d];
8
+ /** The custom section the Swift frontend fills with `-l` flags for the linker. */
9
+ export const AUTOLINK_SECTION = '.swift1_autolink_entries';
10
+ function readLeb(bytes, pos) {
11
+ let result = 0;
12
+ let shift = 0;
13
+ let byte;
14
+ do {
15
+ if (pos.at >= bytes.length)
16
+ throw new Error('truncated LEB128 number');
17
+ byte = bytes[pos.at++];
18
+ result += (byte & 0x7f) * 2 ** shift;
19
+ shift += 7;
20
+ } while (byte & 0x80);
21
+ return result;
22
+ }
23
+ /** Every custom section of a module or object file, by name, in order. */
24
+ export function customSections(module) {
25
+ if (module.length < 8 || MAGIC.some((b, i) => module[i] !== b))
26
+ throw new Error('not a WebAssembly module');
27
+ const sections = [];
28
+ const decoder = new TextDecoder();
29
+ const pos = { at: 8 };
30
+ while (pos.at < module.length) {
31
+ const id = module[pos.at++];
32
+ const size = readLeb(module, pos);
33
+ const end = pos.at + size;
34
+ if (end > module.length)
35
+ throw new Error(`section ${id} runs past the end of the module`);
36
+ if (id === 0) {
37
+ const nameLength = readLeb(module, pos);
38
+ const name = decoder.decode(module.subarray(pos.at, pos.at + nameLength));
39
+ sections.push({ name, payload: module.subarray(pos.at + nameLength, end) });
40
+ }
41
+ pos.at = end;
42
+ }
43
+ return sections;
44
+ }
45
+ /**
46
+ * The linker flags an object asks for (`-lswiftCore` and so on), as
47
+ * `swift-autolink-extract` prints them: NUL-separated in the section.
48
+ */
49
+ export function autolinkEntries(object) {
50
+ const section = customSections(object).find((s) => s.name === AUTOLINK_SECTION);
51
+ if (!section)
52
+ return [];
53
+ return new TextDecoder().decode(section.payload).split('\0').filter((entry) => entry.length > 0);
54
+ }
55
+ /**
56
+ * The runtime libraries `swift-autolink-extract` recognizes, in the order it
57
+ * prints them (lib/DriverTool/autolink_extract_main.cpp, Swift 6.4): after
58
+ * every other flag, each once, so a library comes before the ones it uses.
59
+ */
60
+ export const SWIFT_RUNTIME_LIBRARIES_ORDERED = [
61
+ '-lswiftSwiftOnoneSupport', '-lswiftCore', '-lswiftObservation', '-lswift_Concurrency',
62
+ '-lswift_StringProcessing', '-lswiftRegexBuilder', '-lswift_RegexParser', '-lswift_Builtin_float',
63
+ '-lswift_math', '-lswiftRuntime', '-lswiftSynchronization', '-lswiftGlibc', '-lswiftAndroid',
64
+ '-lswiftWASILibc', '-lBlocksRuntime', '-ldispatch', '-lDispatchStubs', '-lswiftDispatch',
65
+ '-l_FoundationICU', '-lCoreFoundation', '-lFoundation', '-lFoundationEssentials',
66
+ '-lFoundationInternationalization', '-lFoundationNetworking', '-lFoundationXML',
67
+ '-l_CFXMLInterface', '-l_FoundationCShims', '-l_FoundationCollections', '-lcurl', '-lxml2',
68
+ '-luuid', '-lTesting', '-lXCTest', '-l_TestingInternals', '-l_TestDiscovery',
69
+ '-l_Testing_Foundation', '-lm', '-lpthread', '-lutil', '-ldl', '-lz',
70
+ '-lwasi-emulated-getpid', '-lwasi-emulated-mman', '-lwasi-emulated-signal',
71
+ ];
72
+ /**
73
+ * What `swift-autolink-extract` prints for these objects: every flag that is
74
+ * not a known runtime library, in order and repeats kept, then the runtime
75
+ * libraries any object named, once each, in SWIFT_RUNTIME_LIBRARIES_ORDERED.
76
+ */
77
+ export function extractAutolink(objects) {
78
+ const runtime = new Set(SWIFT_RUNTIME_LIBRARIES_ORDERED);
79
+ const flags = [];
80
+ const seenRuntime = new Set();
81
+ for (const object of objects) {
82
+ for (const entry of autolinkEntries(object)) {
83
+ if (runtime.has(entry))
84
+ seenRuntime.add(entry);
85
+ else
86
+ flags.push(entry);
87
+ }
88
+ }
89
+ return [...flags, ...SWIFT_RUNTIME_LIBRARIES_ORDERED.filter((lib) => seenRuntime.has(lib))];
90
+ }