@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.
@@ -0,0 +1,116 @@
1
+ export declare const TRIPLE = "wasm32-unknown-wasip1";
2
+ /** The pieces of the Swift SDK for wasm32-unknown-wasip1 the jobs name. */
3
+ export interface SdkPaths {
4
+ /** The WASI sysroot: wasi-libc, libc++ and the crt objects. */
5
+ sysroot: string;
6
+ /** The static resource directory: the stdlib's modules and archives. */
7
+ resourceDir: string;
8
+ /** Clang's resource directory inside it: headers and the builtins archive. */
9
+ clangResourceDir: string;
10
+ /** Where the Swift runtime archives and swiftrt.o are. */
11
+ libDir: string;
12
+ swiftrt: string;
13
+ crt1: string;
14
+ builtins: string;
15
+ /** The clang flags every static executable links with (a response file). */
16
+ staticExecutableArgs: string;
17
+ }
18
+ /**
19
+ * The layout of the Swift SDK's `wasm32-unknown-wasip1` directory (in the
20
+ * artifact bundle, `<bundle>/<name>/wasm32-unknown-wasip1`).
21
+ */
22
+ export declare function sdkPaths(root: string): SdkPaths;
23
+ export type ToolName = 'swift-frontend' | 'wasm-ld' | 'clang';
24
+ export interface Job {
25
+ tool: ToolName;
26
+ args: string[];
27
+ /** The files the job writes, for the next job to read. */
28
+ outputs: string[];
29
+ }
30
+ export interface FrontendOptions {
31
+ sdk: SdkPaths;
32
+ moduleName: string;
33
+ /** The module's source files, in order. */
34
+ sources: readonly string[];
35
+ /** `compile` writes one object per source into `objectDir`; `typecheck` writes nothing. */
36
+ action: 'compile' | 'typecheck';
37
+ objectDir?: string;
38
+ /**
39
+ * The native toolchain's macro plugin server and plugin directories, which
40
+ * the native driver passes on every job. The WebAssembly frontend has none
41
+ * yet (docs/in-browser-compiler.md, Milestone 2).
42
+ */
43
+ plugins?: {
44
+ inProcessServer: string;
45
+ paths: readonly string[];
46
+ };
47
+ /**
48
+ * Where Clang's modules are cached. The native driver leaves this to the
49
+ * frontend, which derives it from the home directory; under WASI there is
50
+ * none, so the hosts pass a directory they keep between jobs.
51
+ */
52
+ moduleCachePath?: string;
53
+ /**
54
+ * One job for the whole module, as SwiftPM's release builds run (`-wmo`):
55
+ * no primary files, one object named after the module.
56
+ */
57
+ wholeModule?: boolean;
58
+ /**
59
+ * Flags for every job beyond the driver's own: `-O`, `-swift-version 5`,
60
+ * `-D NAME`, `-I DIR`, `-parse-as-library` and the like, as a build system
61
+ * passes them.
62
+ */
63
+ flags?: readonly string[];
64
+ }
65
+ /** The object file the job for `source` writes (unique even when two sources share a name). */
66
+ export declare function objectPath(objectDir: string, sources: readonly string[], index: number): string;
67
+ /**
68
+ * The frontend jobs `swiftc` runs without batch mode at -Onone: one per
69
+ * source, which is the job's primary file, with the other sources listed so
70
+ * the module type-checks as a whole. With `wholeModule`, one job for all of
71
+ * them.
72
+ */
73
+ export declare function frontendJobs(options: FrontendOptions): Job[];
74
+ export interface ClangOptions {
75
+ sdk: SdkPaths;
76
+ /** The C (or C++) source to compile. */
77
+ source: string;
78
+ /** The object to write. */
79
+ output: string;
80
+ /** The build's own flags: optimization, defines, include paths, a language standard. */
81
+ flags?: readonly string[];
82
+ /** Compile as C++ (clang's g++ driver mode, libc++ from the sysroot). */
83
+ cxx?: boolean;
84
+ }
85
+ /** One `clang -c`, as SwiftPM runs it for a C target's source, against the SDK's sysroot and clang headers. */
86
+ export declare function clangJob(options: ClangOptions): Job;
87
+ /**
88
+ * What clang passes on to `wasm-ld` for the lines of a clang response file
89
+ * like static-executable-args.lnk: `-Xlinker` values go through, `-lstdc++`
90
+ * is libc++ on WASI, and `-static` is what wasm-ld does anyway.
91
+ */
92
+ export declare function clangLinkerArgs(responseFile: string): string[];
93
+ export interface LinkOptions {
94
+ sdk: SdkPaths;
95
+ objects: readonly string[];
96
+ /** The objects' autolink entries, as `extractAutolink` returns them. */
97
+ autolink: readonly string[];
98
+ /** The contents of `sdk.staticExecutableArgs`. */
99
+ staticExecutableArgs: string;
100
+ output: string;
101
+ /** The shadow stack size; the Swift driver's default for WASI is 128 KB. */
102
+ stackSize?: number;
103
+ /**
104
+ * The build's own linker flags (what `-Xlinker` passes): exports,
105
+ * `--gc-sections`, archives to link whole. They follow the driver's flags,
106
+ * so a later `-z stack-size` wins.
107
+ */
108
+ flags?: readonly string[];
109
+ }
110
+ /**
111
+ * The `wasm-ld` command clang runs for the Swift driver's link job, in the
112
+ * same order: crt1 and swiftrt.o, the objects, the autolinked libraries, the
113
+ * SDK's static-executable flags, the Swift driver's own linker flags, libc
114
+ * and the compiler builtins last.
115
+ */
116
+ export declare function linkJob(options: LinkOptions): Job;
package/dist/driver.js ADDED
@@ -0,0 +1,165 @@
1
+ // The Swift driver's job planning for wasm32-unknown-wasip1, in TypeScript.
2
+ //
3
+ // `swiftc` (swift-driver) only turns a command line into tool invocations:
4
+ // one `swift-frontend` job per primary file, `swift-autolink-extract`, and a
5
+ // clang link job that runs `wasm-ld`. Porting it would bring Foundation,
6
+ // Dispatch and process spawning along, so this file reproduces the jobs
7
+ // instead. test/fixtures/*-jobs.json hold what the native 6.4 driver and
8
+ // clang run (toolchain/record-invocations.mjs); test/driver.test.ts checks
9
+ // these functions against them, argument for argument.
10
+ //
11
+ // Every path here is a path as the tools see it: a real path for native
12
+ // tools, a path inside the WASI file system for the WebAssembly ones.
13
+ export const TRIPLE = 'wasm32-unknown-wasip1';
14
+ /**
15
+ * The layout of the Swift SDK's `wasm32-unknown-wasip1` directory (in the
16
+ * artifact bundle, `<bundle>/<name>/wasm32-unknown-wasip1`).
17
+ */
18
+ export function sdkPaths(root) {
19
+ const sysroot = `${root}/WASI.sdk`;
20
+ const resourceDir = `${root}/swift.xctoolchain/usr/lib/swift_static`;
21
+ const clangResourceDir = `${resourceDir}/clang`;
22
+ const libDir = `${resourceDir}/wasi`;
23
+ return {
24
+ sysroot,
25
+ resourceDir,
26
+ clangResourceDir,
27
+ libDir,
28
+ swiftrt: `${libDir}/wasm32/swiftrt.o`,
29
+ crt1: `${sysroot}/lib/wasm32-wasip1/crt1-command.o`,
30
+ builtins: `${clangResourceDir}/lib/wasip1/libclang_rt.builtins-wasm32.a`,
31
+ staticExecutableArgs: `${libDir}/static-executable-args.lnk`,
32
+ };
33
+ }
34
+ function basenameWithoutExtension(file) {
35
+ const base = file.slice(file.lastIndexOf('/') + 1);
36
+ const dot = base.lastIndexOf('.');
37
+ return dot > 0 ? base.slice(0, dot) : base;
38
+ }
39
+ /** The object file the job for `source` writes (unique even when two sources share a name). */
40
+ export function objectPath(objectDir, sources, index) {
41
+ const name = basenameWithoutExtension(sources[index]);
42
+ const clash = sources.some((other, i) => i !== index && basenameWithoutExtension(other) === name);
43
+ return `${objectDir}/${clash ? `${name}-${index}` : name}.o`;
44
+ }
45
+ /**
46
+ * The frontend jobs `swiftc` runs without batch mode at -Onone: one per
47
+ * source, which is the job's primary file, with the other sources listed so
48
+ * the module type-checks as a whole. With `wholeModule`, one job for all of
49
+ * them.
50
+ */
51
+ export function frontendJobs(options) {
52
+ const { sources } = options;
53
+ if (sources.length === 0)
54
+ throw new Error('no Swift sources');
55
+ if (options.action === 'compile' && !options.objectDir)
56
+ throw new Error('compile needs an objectDir');
57
+ if (options.wholeModule) {
58
+ const output = options.action === 'compile' ? `${options.objectDir}/${options.moduleName}.o` : null;
59
+ return [frontendJob(options, sources, output)];
60
+ }
61
+ return sources.map((primary, index) => {
62
+ const inputs = sources.flatMap((source) => (source === primary ? ['-primary-file', source] : [source]));
63
+ return frontendJob(options, inputs, options.action === 'compile' ? objectPath(options.objectDir, sources, index) : null);
64
+ });
65
+ }
66
+ function frontendJob(options, inputs, output) {
67
+ const { sdk } = options;
68
+ const args = [
69
+ '-frontend',
70
+ options.action === 'compile' ? '-c' : '-typecheck',
71
+ ...inputs,
72
+ '-target', TRIPLE,
73
+ '-disable-objc-interop',
74
+ '-sdk', sdk.sysroot,
75
+ '-sysroot', sdk.sysroot,
76
+ '-no-color-diagnostics',
77
+ '-Xcc', '-fno-color-diagnostics',
78
+ '-empty-abi-descriptor',
79
+ '-resource-dir', sdk.resourceDir,
80
+ '-use-static-resource-dir',
81
+ '-Xcc', '-resource-dir', '-Xcc', sdk.clangResourceDir,
82
+ '-no-auto-bridging-header-chaining',
83
+ '-module-name', options.moduleName,
84
+ ...(options.flags ?? []),
85
+ ...(options.plugins
86
+ ? ['-in-process-plugin-server-path', options.plugins.inProcessServer, ...options.plugins.paths.flatMap((p) => ['-plugin-path', p])]
87
+ : []),
88
+ ...(options.moduleCachePath ? ['-module-cache-path', options.moduleCachePath] : []),
89
+ ...(output ? ['-o', output] : []),
90
+ ];
91
+ return { tool: 'swift-frontend', args, outputs: output ? [output] : [] };
92
+ }
93
+ /** One `clang -c`, as SwiftPM runs it for a C target's source, against the SDK's sysroot and clang headers. */
94
+ export function clangJob(options) {
95
+ const { sdk } = options;
96
+ const args = [
97
+ ...(options.cxx ? ['--driver-mode=g++'] : []),
98
+ `--target=${TRIPLE}`,
99
+ `--sysroot=${sdk.sysroot}`,
100
+ '-resource-dir', sdk.clangResourceDir,
101
+ '-fno-color-diagnostics',
102
+ ...(options.flags ?? []),
103
+ '-c', options.source,
104
+ '-o', options.output,
105
+ ];
106
+ return { tool: 'clang', args, outputs: [options.output] };
107
+ }
108
+ /**
109
+ * What clang passes on to `wasm-ld` for the lines of a clang response file
110
+ * like static-executable-args.lnk: `-Xlinker` values go through, `-lstdc++`
111
+ * is libc++ on WASI, and `-static` is what wasm-ld does anyway.
112
+ */
113
+ export function clangLinkerArgs(responseFile) {
114
+ const words = responseFile.split(/\s+/).filter((word) => word.length > 0);
115
+ const out = [];
116
+ for (let i = 0; i < words.length; i++) {
117
+ const word = words[i];
118
+ if (word === '-Xlinker') {
119
+ const value = words[++i];
120
+ if (value === undefined)
121
+ throw new Error('-Xlinker without a value');
122
+ out.push(value);
123
+ }
124
+ else if (word === '-static') {
125
+ continue;
126
+ }
127
+ else if (word === '-lstdc++') {
128
+ out.push('-lc++', '-lc++abi');
129
+ }
130
+ else if (word.startsWith('-l') || word.startsWith('-L')) {
131
+ out.push(word);
132
+ }
133
+ else {
134
+ throw new Error(`unexpected flag in a linker response file: ${word}`);
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ /**
140
+ * The `wasm-ld` command clang runs for the Swift driver's link job, in the
141
+ * same order: crt1 and swiftrt.o, the objects, the autolinked libraries, the
142
+ * SDK's static-executable flags, the Swift driver's own linker flags, libc
143
+ * and the compiler builtins last.
144
+ */
145
+ export function linkJob(options) {
146
+ const { sdk } = options;
147
+ const args = [
148
+ '-m', 'wasm32',
149
+ `-L${sdk.libDir}`,
150
+ `-L${sdk.sysroot}/lib/wasm32-wasip1`,
151
+ sdk.crt1,
152
+ sdk.swiftrt,
153
+ ...options.objects,
154
+ ...options.autolink,
155
+ ...clangLinkerArgs(options.staticExecutableArgs),
156
+ '--global-base=4096',
157
+ '--table-base=4096',
158
+ '-z', `stack-size=${options.stackSize ?? 128 * 1024}`,
159
+ ...(options.flags ?? []),
160
+ '-lc',
161
+ sdk.builtins,
162
+ '-o', options.output,
163
+ ];
164
+ return { tool: 'wasm-ld', args, outputs: [options.output] };
165
+ }
package/dist/host.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { SdkPaths, ToolName } from './driver.js';
2
+ export interface RunResult {
3
+ exitCode: number;
4
+ /** stdout and stderr, interleaved as written. */
5
+ output: string;
6
+ /**
7
+ * The size of the tool's linear memory when it exited, for a WebAssembly
8
+ * tool: its peak, since a memory only grows.
9
+ */
10
+ memoryBytes?: number;
11
+ }
12
+ export interface Host {
13
+ /** One line for logs: which tools, from where. */
14
+ readonly description: string;
15
+ /** The Swift SDK as the tools see it. */
16
+ readonly sdk: SdkPaths;
17
+ /** The native driver's plugin flags, when the tools have plugins. */
18
+ readonly plugins?: {
19
+ inProcessServer: string;
20
+ paths: readonly string[];
21
+ };
22
+ /** Where the frontend caches Clang's modules, as the tools see it, when the host chooses. */
23
+ readonly moduleCachePath?: string;
24
+ run(tool: ToolName, args: readonly string[]): Promise<RunResult>;
25
+ /** Reads a file by the path the tools use. */
26
+ readFile(path: string): Promise<Uint8Array>;
27
+ }
package/dist/host.js ADDED
@@ -0,0 +1,7 @@
1
+ // Where the tools run. The driver plans jobs; a host runs them and reads
2
+ // what they wrote. Two kinds exist (src/node): the native toolchain, which
3
+ // pins the driver against the real thing, and the WebAssembly tools under
4
+ // WASI. The browser gets a third one over an in-memory file system
5
+ // (docs/in-browser-compiler.md, Milestone 3); nothing above this interface
6
+ // knows which it has.
7
+ export {};
@@ -0,0 +1,11 @@
1
+ export { TRIPLE, clangJob, clangLinkerArgs, frontendJobs, linkJob, objectPath, sdkPaths } from './driver.js';
2
+ export type { ClangOptions, FrontendOptions, Job, LinkOptions, SdkPaths, ToolName } from './driver.js';
3
+ export { compile, compileC } from './compile.js';
4
+ export { dataObject } from './object.js';
5
+ export { pluginHost } from './plugins.js';
6
+ export type { PluginHost, PluginHostOptions } from './plugins.js';
7
+ export type { CompileCRequest, CompileRequest, CompileResult, CompileStep } from './compile.js';
8
+ export type { Host, RunResult } from './host.js';
9
+ export { AUTOLINK_SECTION, SWIFT_RUNTIME_LIBRARIES_ORDERED, autolinkEntries, customSections, extractAutolink } from './wasm.js';
10
+ export { directoryFiles, memoryDirectory, memoryHost, runMemoryCommand } from './memory-host.js';
11
+ export type { MemoryCommand, MemoryHost, MemoryHostOptions } from './memory-host.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // The parts that run anywhere, the browser included: the driver's job
2
+ // planning, the autolink reader and the compile sequence over a Host. The
3
+ // Node hosts are the package's `./node` entry point (src/node/index.ts).
4
+ export { TRIPLE, clangJob, clangLinkerArgs, frontendJobs, linkJob, objectPath, sdkPaths } from './driver.js';
5
+ export { compile, compileC } from './compile.js';
6
+ export { dataObject } from './object.js';
7
+ export { pluginHost } from './plugins.js';
8
+ export { AUTOLINK_SECTION, SWIFT_RUNTIME_LIBRARIES_ORDERED, autolinkEntries, customSections, extractAutolink } from './wasm.js';
9
+ export { directoryFiles, memoryDirectory, memoryHost, runMemoryCommand } from './memory-host.js';
@@ -0,0 +1,50 @@
1
+ import { Directory, OpenFile } from '@bjorn3/browser_wasi_shim';
2
+ import { type ToolName } from './driver.js';
3
+ import type { Host, RunResult } from './host.js';
4
+ /** A Directory holding `files`, keyed by `/`-separated paths relative to it. */
5
+ export declare function memoryDirectory(files: Iterable<readonly [string, Uint8Array]>): Directory;
6
+ /** The files under `dir`, as `memoryDirectory` takes them: the inverse of it. */
7
+ export declare function directoryFiles(dir: Directory, prefix?: string): [string, Uint8Array][];
8
+ /** Writes `data` at `offset` of an open file, growing its buffer by doubling. Exported for the tests. */
9
+ export declare function writeAt(open: OpenFile, offset: number, data: Uint8Array): void;
10
+ export interface MemoryCommand {
11
+ module: WebAssembly.Module;
12
+ /** argv, the program name first. */
13
+ args: readonly string[];
14
+ /** Guest directory → its contents. */
15
+ mounts: Record<string, Directory>;
16
+ env?: Record<string, string>;
17
+ /** Every path the command opens, as a guest path. */
18
+ onOpen?: (path: string) => void;
19
+ /** Macro plugins by the guest path the compiler names (plugins.ts). */
20
+ plugins?: Record<string, WebAssembly.Module>;
21
+ }
22
+ /** Runs a WASI command over in-memory directories on the calling thread. */
23
+ export declare function runMemoryCommand(command: MemoryCommand): Promise<RunResult>;
24
+ export interface MemoryHostOptions {
25
+ /** The compiled tool modules. */
26
+ tools: Partial<Record<ToolName, WebAssembly.Module>>;
27
+ /**
28
+ * Guest directory → its contents. An empty / (the frontend's working
29
+ * directory) and /tmp (temporary files and the module cache, kept between
30
+ * jobs) are added when missing.
31
+ */
32
+ mounts: Record<string, Directory>;
33
+ /** The Swift SDK's `wasm32-unknown-wasip1` directory, as a guest path. */
34
+ sdkRoot: string;
35
+ onOpen?: (path: string) => void;
36
+ /** Macro plugins by the guest path the compiler names; the file must exist under a mount too. */
37
+ plugins?: Record<string, WebAssembly.Module>;
38
+ /**
39
+ * Clang's module cache to start from, as `moduleCacheFiles` returned it
40
+ * (docs/in-browser-compiler.md, Milestone 2's cached PCMs). Every file
41
+ * here has the same timestamp, so a cache built for the same files and
42
+ * flags is valid in any session and nothing is rebuilt.
43
+ */
44
+ moduleCache?: Iterable<readonly [string, Uint8Array]>;
45
+ }
46
+ export interface MemoryHost extends Host {
47
+ /** Clang's module cache as it is now, for a later host's `moduleCache`. */
48
+ moduleCacheFiles(): [string, Uint8Array][];
49
+ }
50
+ export declare function memoryHost(options: MemoryHostOptions): MemoryHost;
@@ -0,0 +1,195 @@
1
+ // The WebAssembly tools over an in-memory file system: the host the browser
2
+ // will use (docs/in-browser-compiler.md, Milestone 3), with
3
+ // @bjorn3/browser_wasi_shim, the WASI implementation the renderer already
4
+ // runs apps with. It runs on the calling thread; in a page, call it from a
5
+ // worker so the tools' deep recursion has a worker's stack. No Node APIs.
6
+ //
7
+ // Each mount is a top-level guest directory (/sdk, /work, ...) backed by a
8
+ // Directory; files a tool writes stay in it, so the next tool (and
9
+ // readFile) sees them. `onOpen` reports every path a tool opens, which is
10
+ // how the SDK files a compile needs are found.
11
+ import { ConsoleStdout, Directory, File, Inode, OpenFile, PreopenDirectory, WASI, wasi as wasiDefs } from '@bjorn3/browser_wasi_shim';
12
+ import { sdkPaths } from './driver.js';
13
+ import { pluginHost } from './plugins.js';
14
+ /** A Directory holding `files`, keyed by `/`-separated paths relative to it. */
15
+ export function memoryDirectory(files) {
16
+ const root = new Directory(new Map());
17
+ for (const [file, data] of files) {
18
+ const parts = file.split('/').filter((part) => part.length > 0);
19
+ let dir = root;
20
+ for (const part of parts.slice(0, -1)) {
21
+ let next = dir.contents.get(part);
22
+ if (!next) {
23
+ next = new Directory(new Map());
24
+ dir.contents.set(part, next);
25
+ }
26
+ if (!(next instanceof Directory))
27
+ throw new Error(`${file}: ${part} is a file`);
28
+ dir = next;
29
+ }
30
+ dir.contents.set(parts[parts.length - 1], new File(data));
31
+ }
32
+ return root;
33
+ }
34
+ /** The files under `dir`, as `memoryDirectory` takes them: the inverse of it. */
35
+ export function directoryFiles(dir, prefix = '') {
36
+ return [...dir.contents.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).flatMap(([name, node]) => {
37
+ const file = prefix ? `${prefix}/${name}` : name;
38
+ if (node instanceof Directory)
39
+ return directoryFiles(node, file);
40
+ return node instanceof File ? [[file, node.data]] : [];
41
+ });
42
+ }
43
+ /** Writes `data` at `offset` of an open file, growing its buffer by doubling. Exported for the tests. */
44
+ export function writeAt(open, offset, data) {
45
+ const file = open.file;
46
+ const end = offset + data.length;
47
+ if (end > file.data.byteLength) {
48
+ const buffer = file.data.buffer;
49
+ if (end > buffer.byteLength - file.data.byteOffset || file.data.byteOffset !== 0) {
50
+ const grown = new Uint8Array(new ArrayBuffer(Math.max(end, 2 * buffer.byteLength, 4096)));
51
+ grown.set(file.data);
52
+ file.data = grown.subarray(0, end);
53
+ }
54
+ else {
55
+ file.data = new Uint8Array(buffer, 0, end);
56
+ }
57
+ }
58
+ file.data.set(data, offset);
59
+ }
60
+ function lookup(mounts, guest) {
61
+ const mount = Object.keys(mounts)
62
+ .filter((dir) => guest === dir || guest.startsWith(`${dir}/`))
63
+ .sort((a, b) => b.length - a.length)[0];
64
+ if (mount === undefined)
65
+ return undefined;
66
+ let node = mounts[mount];
67
+ for (const part of guest.slice(mount.length).split('/').filter((p) => p.length > 0)) {
68
+ if (!(node instanceof Directory))
69
+ return undefined;
70
+ node = node.contents.get(part);
71
+ }
72
+ return node;
73
+ }
74
+ /** Runs a WASI command over in-memory directories on the calling thread. */
75
+ export async function runMemoryCommand(command) {
76
+ const decoder = new TextDecoder();
77
+ let output = '';
78
+ const collect = (bytes) => {
79
+ output += decoder.decode(bytes, { stream: true });
80
+ };
81
+ const names = Object.keys(command.mounts);
82
+ const fds = [
83
+ new OpenFile(new File(new Uint8Array())),
84
+ new ConsoleStdout(collect),
85
+ new ConsoleStdout(collect),
86
+ ...names.map((name) => new PreopenDirectory(name, command.mounts[name].contents)),
87
+ ];
88
+ const env = Object.entries(command.env ?? {}).map(([key, value]) => `${key}=${value}`);
89
+ const wasi = new WASI([...command.args], env, fds, { debug: false });
90
+ const imports = { ...wasi.wasiImport };
91
+ // The in-memory file system has no symbolic links, and says readlink is
92
+ // not supported; a real one says a path is not a link (EINVAL), which is
93
+ // what resolving a path (the compiler's plugins, for one) expects.
94
+ const pathReadlink = imports['path_readlink'];
95
+ imports['path_readlink'] = ((...args) => {
96
+ const result = pathReadlink(...args);
97
+ return result === wasiDefs.ERRNO_NOTSUP ? wasiDefs.ERRNO_INVAL : result;
98
+ });
99
+ // Its files grow to exactly the size of each write, copying the whole file
100
+ // every time: writing a 10 MB module in small writes is quadratic. Writes
101
+ // to an open file go into a buffer that doubles instead, with the file's
102
+ // data a view of the part written.
103
+ const fdWrite = imports['fd_write'];
104
+ imports['fd_write'] = ((fd, iovs, count, written) => {
105
+ const open = wasi.fds[fd];
106
+ if (!(open instanceof OpenFile) || open.file.readonly)
107
+ return fdWrite(fd, iovs, count, written);
108
+ const memory = instance.exports['memory'].buffer;
109
+ const view = new DataView(memory);
110
+ let total = 0;
111
+ for (let i = 0; i < count; i++) {
112
+ const pointer = view.getUint32(iovs + i * 8, true);
113
+ const length = view.getUint32(iovs + i * 8 + 4, true);
114
+ writeAt(open, Number(open.file_pos), new Uint8Array(memory, pointer, length));
115
+ open.file_pos += BigInt(length);
116
+ total += length;
117
+ }
118
+ view.setUint32(written, total, true);
119
+ return wasiDefs.ERRNO_SUCCESS;
120
+ });
121
+ // It also grants no rights, and wasi-libc's access() checks a path against
122
+ // its directory's: report every right, as a preopened directory under a
123
+ // real WASI host has (the compiler checks that a plugin is readable).
124
+ const fdstatGet = imports['fd_fdstat_get'];
125
+ imports['fd_fdstat_get'] = ((fd, pointer) => {
126
+ const result = fdstatGet(fd, pointer);
127
+ if (result === wasiDefs.ERRNO_SUCCESS) {
128
+ const view = new DataView(instance.exports['memory'].buffer);
129
+ // fdstat: filetype u8, flags u16, rights_base u64 at 8, rights_inheriting u64 at 16.
130
+ view.setBigUint64(pointer + 8, 0xffffffffffffffffn, true);
131
+ view.setBigUint64(pointer + 16, 0xffffffffffffffffn, true);
132
+ }
133
+ return result;
134
+ });
135
+ if (command.onOpen) {
136
+ const pathOpen = imports['path_open'];
137
+ const onOpen = command.onOpen;
138
+ // path_open(dirfd, dirflags, path, path_len, ...): the preopens are fds 3 and up.
139
+ imports['path_open'] = ((...args) => {
140
+ const [dirfd, , pathPtr, pathLength] = args;
141
+ const memory = instance.exports['memory'].buffer;
142
+ const relative = decoder.decode(new Uint8Array(memory, pathPtr, pathLength));
143
+ const base = names[dirfd - 3];
144
+ onOpen(base === undefined ? relative : `${base}/${relative}`);
145
+ return pathOpen(...args);
146
+ });
147
+ }
148
+ const plugins = WebAssembly.Module.imports(command.module).some((entry) => entry.module === 'swiftbrowser')
149
+ ? pluginHost({ module: (path) => command.plugins?.[path] ?? null, onStderr: (text) => (output += text) })
150
+ : null;
151
+ const instance = await WebAssembly.instantiate(command.module, { wasi_snapshot_preview1: imports, ...(plugins?.imports ?? {}) });
152
+ plugins?.bind(instance);
153
+ let exitCode;
154
+ try {
155
+ exitCode = wasi.start(instance);
156
+ }
157
+ catch (error) {
158
+ output += decoder.decode();
159
+ return { exitCode: 1, output: `${output}${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n` };
160
+ }
161
+ return { exitCode, output: output + decoder.decode() };
162
+ }
163
+ /** The module cache's directory under the host's /tmp. */
164
+ const MODULE_CACHE = 'module-cache';
165
+ export function memoryHost(options) {
166
+ const tmp = new Directory(new Map());
167
+ if (options.moduleCache)
168
+ tmp.contents.set(MODULE_CACHE, memoryDirectory(options.moduleCache));
169
+ const mounts = { '/': new Directory(new Map()), '/tmp': tmp, ...options.mounts };
170
+ return {
171
+ moduleCacheFiles() {
172
+ const cache = mounts['/tmp'].contents.get(MODULE_CACHE);
173
+ return cache instanceof Directory ? directoryFiles(cache) : [];
174
+ },
175
+ description: 'WebAssembly tools over an in-memory file system',
176
+ sdk: sdkPaths(options.sdkRoot),
177
+ moduleCachePath: `/tmp/${MODULE_CACHE}`,
178
+ async readFile(file) {
179
+ const node = lookup(mounts, file);
180
+ if (!(node instanceof File))
181
+ throw new Error(`${file}: no such file`);
182
+ return node.data;
183
+ },
184
+ run(tool, args) {
185
+ const module = options.tools[tool];
186
+ if (!module)
187
+ throw new Error(`no WebAssembly module for ${tool}`);
188
+ return runMemoryCommand({
189
+ module, args: [tool, ...args], mounts, env: { TMPDIR: '/tmp' },
190
+ ...(options.onOpen ? { onOpen: options.onOpen } : {}),
191
+ ...(options.plugins ? { plugins: options.plugins } : {}),
192
+ });
193
+ },
194
+ };
195
+ }
@@ -0,0 +1,5 @@
1
+ export { nativeHost } from './native-host.js';
2
+ export type { NativeHostOptions } from './native-host.js';
3
+ export { findManifestApi, findPlugins, findTools, packagedToolsDir, toolFile } from './tools.js';
4
+ export { runWasiCommand, wasiHost } from './wasi-host.js';
5
+ export type { WasiCommand, WasiHost, WasiHostOptions } from './wasi-host.js';
@@ -0,0 +1,4 @@
1
+ // The Node hosts: the native toolchain and the WebAssembly tools under WASI.
2
+ export { nativeHost } from './native-host.js';
3
+ export { findManifestApi, findPlugins, findTools, packagedToolsDir, toolFile } from './tools.js';
4
+ export { runWasiCommand, wasiHost } from './wasi-host.js';
@@ -0,0 +1,8 @@
1
+ import type { Host } from '../host.js';
2
+ export interface NativeHostOptions {
3
+ /** The toolchain directory, the one holding `usr/bin/swift-frontend`. */
4
+ toolchain: string;
5
+ /** The Swift SDK's `wasm32-unknown-wasip1` directory. */
6
+ sdkRoot: string;
7
+ }
8
+ export declare function nativeHost(options: NativeHostOptions): Host;
@@ -0,0 +1,31 @@
1
+ // The native Swift toolchain as a host: the same jobs the WebAssembly tools
2
+ // run, run by the real `swift-frontend` and `wasm-ld`. It checks the driver
3
+ // end to end on a machine with Swift, and is the baseline the WebAssembly
4
+ // tools are compared with.
5
+ import { spawn } from 'node:child_process';
6
+ import { readFile } from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import { sdkPaths } from '../driver.js';
9
+ export function nativeHost(options) {
10
+ const bin = path.join(options.toolchain, 'usr/bin');
11
+ return {
12
+ description: `native tools in ${bin}`,
13
+ sdk: sdkPaths(options.sdkRoot),
14
+ plugins: {
15
+ inProcessServer: path.join(options.toolchain, 'usr/lib/swift/host/libSwiftInProcPluginServer.so'),
16
+ paths: [path.join(options.toolchain, 'usr/lib/swift/host/plugins'), path.join(options.toolchain, 'usr/local/lib/swift/host/plugins')],
17
+ },
18
+ run: (tool, args) => runProcess(path.join(bin, tool), args),
19
+ readFile: (file) => readFile(file),
20
+ };
21
+ }
22
+ function runProcess(command, args) {
23
+ return new Promise((resolve, reject) => {
24
+ const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
25
+ let output = '';
26
+ child.stdout.on('data', (chunk) => (output += chunk.toString('utf8')));
27
+ child.stderr.on('data', (chunk) => (output += chunk.toString('utf8')));
28
+ child.on('error', reject);
29
+ child.on('close', (code, signal) => resolve({ exitCode: code ?? 1, output: signal ? `${output}${command} ended with ${signal}\n` : output }));
30
+ });
31
+ }
@@ -0,0 +1,15 @@
1
+ export interface PluginStreams {
2
+ /** Fills `buffer` from `offset` with at most `length` bytes; 0 at the end. */
3
+ read(buffer: Uint8Array, offset: number, length: number): number;
4
+ write(bytes: Uint8Array): void;
5
+ error(bytes: Uint8Array): void;
6
+ }
7
+ /** Stdin, stdout and stderr, read and written synchronously: the compiler waits on each reply. */
8
+ export declare const processStreams: PluginStreams;
9
+ /** The plugin's module: a `.wasm` file, or a brotli-compressed `.wasm.br`. */
10
+ export declare function pluginModule(file: string): WebAssembly.Module;
11
+ /**
12
+ * Serves the compiler's messages until it closes the pipe or sends the empty
13
+ * message that asks a plugin to finish; returns the exit code.
14
+ */
15
+ export declare function servePlugin(module: WebAssembly.Module, name: string, streams?: PluginStreams): number;