···11+function is_haskell(block)
22+ return block and block.t == "CodeBlock" and block.classes[1] == "haskell"
33+end
44+55+function Pandoc(doc)
66+ local new_blocks = {}
77+ local i = 1
88+99+ while i <= #doc.blocks do
1010+ local current = doc.blocks[i]
1111+ local next_block = doc.blocks[i + 1]
1212+1313+ if current.t == "Para" and is_haskell(next_block) then
1414+ local code_div = pandoc.Div({next_block}, {class = "code"})
1515+ local row = pandoc.Div({current, code_div}, {class = "row"})
1616+1717+ table.insert(new_blocks, row)
1818+ i = i + 2 -- skip the next block
1919+ elseif current.t == "Para" and not is_haskell(next_block) then
2020+ local empty_div = pandoc.Div({}, {class = "code"})
2121+ local row = pandoc.Div({current, empty_div}, {class = "row"})
2222+ table.insert(new_blocks, row)
2323+ i = i + 1
2424+ else
2525+ table.insert(new_blocks, current)
2626+ i = i + 1
2727+ end
2828+ end
2929+3030+ return pandoc.Pandoc(new_blocks, doc.meta)
3131+end
···11+#!/usr/bin/env -S node --disable-warning=ExperimentalWarning --max-old-space-size=65536 --wasm-lazy-validation
22+33+// Note [The Wasm Dynamic Linker]
44+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
55+//
66+// This script mainly has two roles:
77+//
88+// 1. Message broker: relay iserv messages between host GHC and wasm
99+// iserv (GHCi.Server.defaultServer). This part only runs in
1010+// nodejs.
1111+// 2. Dynamic linker: provide RTS linker interfaces like
1212+// loadDLLs/lookupSymbol etc which are imported by wasm iserv. This
1313+// part can run in browsers as well.
1414+//
1515+// When GHC starts external interpreter for the wasm target, it starts
1616+// this script and passes a pair of pipe fds for iserv messages,
1717+// libHSghci.so path, and command line arguments for wasm iserv. By
1818+// default, the wasm iserv runs in the same node process, so the
1919+// message broker logic is simple: wrap the pipe fds as
2020+// ReadableStream/WritableStream, pass reader/writer callbacks to wasm
2121+// iserv and run it to completion. It doesn't need to intercept or
2222+// parse any message, unlike iserv-proxy.
2323+//
2424+// Things are a bit more interesting with ghci browser mode. All the
2525+// Haskell code and all the runtime runs in the browser, including the
2626+// dynamic linker parts of this script. The host GHC process doeesn't
2727+// need to know about "browser mode" at all as long as iserv messages
2828+// are handled as usual, though obviously we can't pass fds to
2929+// browsers like before! So this script starts an HTTP 1.1 server with
3030+// WebSockets support. The browser side can import a startup script
3131+// served by the server, which will import this script and invoke main
3232+// with the right arguments, hooray isomorphic JavaScript! The browser
3333+// side will proceed to bootstrap wasm iserv, and the iserv messages
3434+// are relayed over the WebSockets. (also ^C signals over a different
3535+// connection)
3636+//
3737+// Under the browser mode, there's more traffic than just the iserv
3838+// message WebSockets. The browser side can fulfill most of the RTS
3939+// linker functionality alone, but it still needs to do stuff like
4040+// searching for a shared library in a bunch of search paths or
4141+// fetching a shared library blob; these side effects require access
4242+// to the same host filesystem that runs GHC, so the HTTP server also
4343+// exposes some rpc endpoints that the browser side can perform
4444+// requests. The server binds to 127.0.0.1 by default for a good
4545+// reason, it doesn't (and shouldn't) have extra logic to try to guard
4646+// against potential malicious requests to scrape your home directory.
4747+//
4848+// So much intro to the message broker part, below are Q/As regarding
4949+// the dynamic linker part:
5050+//
5151+// *** What works right now and what doesn't work yet?
5252+//
5353+// loadDLLs & bytecode interpreter work. Template Haskell & ghci work.
5454+// Profiled dynamic code works. Compiled code and bytecode can all be
5555+// loaded, though the side effects are constrained to what's supported
5656+// by wasi preview1: we map the full host filesystem into wasm cause
5757+// yolo, but things like processes and sockets don't work.
5858+//
5959+// loadArchive/loadObj etc are unsupported and will stay that way. The
6060+// only form of compiled code that can be loaded is wasm shared
6161+// library. There's no code unloading logic. The retain_cafs flag is
6262+// ignored and revertCAFs is a no-op.
6363+//
6464+// JSFFI works. ghci debugger works.
6565+//
6666+// *** What are implications to end users?
6767+//
6868+// Even if you intend to compile fully static wasm modules, you must
6969+// compile everything with -dynamic-too to ensure shared libraries are
7070+// present, otherwise TH doesn't work. In cabal, this is achieved by
7171+// setting `shared: True` in the global cabal config (or under a
7272+// `package *` stanza in your `cabal.project`). You also need to set
7373+// `library-for-ghci: False` since that's unsupported.
7474+//
7575+// *** Why not extend the RTS linker in C like every other new
7676+// platform?
7777+//
7878+// Aside from all the pain of binary manipulation in C, what you can
7979+// do in C on wasm is fairly limited: for instance, you can't manage
8080+// executable memory regions at all. So you need a lot of back and
8181+// forth between C and JS host, totally not worth the extra effort
8282+// just for the sake of making the original C RTS linker interface
8383+// partially work.
8484+//
8585+// *** What kind of wasm shared library can be loaded? What features
8686+// work to what extent?
8787+//
8888+// We support .so files produced by wasm-ld --shared which conforms to
8989+// https://github.com/WebAssembly/tool-conventions/blob/f44d6c526a06a19eec59003a924e475f57f5a6a1/DynamicLinking.md.
9090+// All .so files in the wasm32-wasi sysroot as well as those produced
9191+// by ghc can be loaded.
9292+//
9393+// For simplicity, we don't have any special treatment for weak
9494+// symbols. Any unresolved symbol at link-time will not produce an
9595+// error, they will only trigger an error when they're used at
9696+// run-time and the data/function definition has not been realized by
9797+// then.
9898+//
9999+// There's no dlopen/dlclose etc exposed to the C/C++ world, the
100100+// interfaces here are directly called by JSFFI imports in ghci.
101101+// There's no so unloading logic yet, but it would be fairly easy to
102102+// add once we need it.
103103+//
104104+// No fancy stuff like LD_PRELOAD, LD_LIBRARY_PATH etc.
105105+106106+import { JSValManager, setImmediate } from "./prelude.mjs";
107107+import { parseRecord, parseSections } from "./post-link.mjs";
108108+109109+// Make a consumer callback from a buffer. See Parser class
110110+// constructor comments for what a consumer is.
111111+function makeBufferConsumer(buf) {
112112+ return (len) => {
113113+ if (len > buf.length) {
114114+ throw new Error("not enough bytes");
115115+ }
116116+117117+ const r = buf.subarray(0, len);
118118+ buf = buf.subarray(len);
119119+ return r;
120120+ };
121121+}
122122+123123+// Make a consumer callback from a ReadableStreamDefaultReader.
124124+function makeStreamConsumer(reader) {
125125+ let buf = new Uint8Array();
126126+127127+ return async (len) => {
128128+ while (buf.length < len) {
129129+ const { done, value } = await reader.read();
130130+ if (done) {
131131+ throw new Error("not enough bytes");
132132+ }
133133+ if (buf.length === 0) {
134134+ buf = value;
135135+ continue;
136136+ }
137137+ const tmp = new Uint8Array(buf.length + value.length);
138138+ tmp.set(buf, 0);
139139+ tmp.set(value, buf.length);
140140+ buf = tmp;
141141+ }
142142+143143+ const r = buf.subarray(0, len);
144144+ buf = buf.subarray(len);
145145+ return r;
146146+ };
147147+}
148148+149149+// A simple binary parser
150150+class Parser {
151151+ #cb;
152152+ #consumed = 0;
153153+ #limit;
154154+155155+ // cb is a consumer callback that returns a buffer with exact N
156156+ // bytes for await cb(N). limit indicates how many bytes the Parser
157157+ // may consume at most; it's optional and only used by eof().
158158+ constructor(cb, limit) {
159159+ this.#cb = cb;
160160+ this.#limit = limit;
161161+ }
162162+163163+ eof() {
164164+ return this.#consumed >= this.#limit;
165165+ }
166166+167167+ async skip(len) {
168168+ await this.#cb(len);
169169+ this.#consumed += len;
170170+ }
171171+172172+ async readUInt8() {
173173+ const r = (await this.#cb(1))[0];
174174+ this.#consumed += 1;
175175+ return r;
176176+ }
177177+178178+ async readULEB128() {
179179+ let acc = 0n,
180180+ shift = 0n;
181181+ while (true) {
182182+ const byte = await this.readUInt8();
183183+ acc |= BigInt(byte & 0x7f) << shift;
184184+ shift += 7n;
185185+ if (byte >> 7 === 0) {
186186+ break;
187187+ }
188188+ }
189189+ return Number(acc);
190190+ }
191191+192192+ async readBuffer() {
193193+ const len = await this.readULEB128();
194194+ const r = await this.#cb(len);
195195+ this.#consumed += len;
196196+ return r;
197197+ }
198198+199199+ async readString() {
200200+ return new TextDecoder("utf-8", { fatal: true }).decode(
201201+ await this.readBuffer()
202202+ );
203203+ }
204204+}
205205+206206+// Parse the dylink.0 section of a wasm module
207207+async function parseDyLink0(reader) {
208208+ const p0 = new Parser(makeStreamConsumer(reader));
209209+ // magic, version
210210+ await p0.skip(8);
211211+ // section id
212212+ console.assert((await p0.readUInt8()) === 0);
213213+ const p1_buf = await p0.readBuffer();
214214+ const p1 = new Parser(makeBufferConsumer(p1_buf), p1_buf.length);
215215+ // custom section name
216216+ console.assert((await p1.readString()) === "dylink.0");
217217+218218+ const r = { neededSos: [], exportInfo: [], importInfo: [] };
219219+ while (!p1.eof()) {
220220+ const subsection_type = await p1.readUInt8();
221221+ const p2_buf = await p1.readBuffer();
222222+ const p2 = new Parser(makeBufferConsumer(p2_buf), p2_buf.length);
223223+ switch (subsection_type) {
224224+ case 1: {
225225+ // WASM_DYLINK_MEM_INFO
226226+ r.memSize = await p2.readULEB128();
227227+ r.memP2Align = await p2.readULEB128();
228228+ r.tableSize = await p2.readULEB128();
229229+ r.tableP2Align = await p2.readULEB128();
230230+ break;
231231+ }
232232+ case 2: {
233233+ // WASM_DYLINK_NEEDED
234234+ //
235235+ // There may be duplicate entries. Not a big deal to not
236236+ // dedupe, but why not.
237237+ const n = await p2.readULEB128();
238238+ const acc = new Set();
239239+ for (let i = 0; i < n; ++i) {
240240+ acc.add(await p2.readString());
241241+ }
242242+ r.neededSos = [...acc];
243243+ break;
244244+ }
245245+ case 3: {
246246+ // WASM_DYLINK_EXPORT_INFO
247247+ //
248248+ // Not actually used yet, kept for completeness in case of
249249+ // future usage.
250250+ const n = await p2.readULEB128();
251251+ for (let i = 0; i < n; ++i) {
252252+ const name = await p2.readString();
253253+ const flags = await p2.readULEB128();
254254+ r.exportInfo.push({ name, flags });
255255+ }
256256+ break;
257257+ }
258258+ case 4: {
259259+ // WASM_DYLINK_IMPORT_INFO
260260+ //
261261+ // Same.
262262+ const n = await p2.readULEB128();
263263+ for (let i = 0; i < n; ++i) {
264264+ const module = await p2.readString();
265265+ const name = await p2.readString();
266266+ const flags = await p2.readULEB128();
267267+ r.importInfo.push({ module, name, flags });
268268+ }
269269+ break;
270270+ }
271271+ default: {
272272+ throw new Error(`unknown subsection type ${subsection_type}`);
273273+ }
274274+ }
275275+ }
276276+277277+ return r;
278278+}
279279+280280+// Formats a server.address() result to a URL origin with correct
281281+// handling for IPv6 hostname
282282+function originFromServerAddress({ address, family, port }) {
283283+ const hostname = family === "IPv6" ? `[${address}]` : address;
284284+ return `http://${hostname}:${port}`;
285285+}
286286+287287+// Browser/node portable code stays above this watermark.
288288+const isNode = Boolean(globalThis?.process?.versions?.node && !globalThis.Deno);
289289+290290+// Too cumbersome to only import at use sites. Too troublesome to
291291+// factor out browser-only/node-only logic into different modules. For
292292+// now, just make these global let bindings optionally initialized if
293293+// isNode and be careful to not use them in browser-only logic.
294294+let fs, http, path, require, stream, wasi, ws;
295295+296296+if (isNode) {
297297+ require = (await import("node:module")).createRequire(import.meta.url);
298298+299299+ fs = require("fs");
300300+ http = require("http");
301301+ path = require("path");
302302+ stream = require("stream");
303303+ wasi = require("wasi");
304304+305305+ // Optional npm dependencies loaded via NODE_PATH
306306+ try {
307307+ ws = require("ws");
308308+ } catch {}
309309+} else {
310310+ wasi = await import("https://esm.sh/gh/haskell-wasm/browser_wasi_shim");
311311+}
312312+313313+// A subset of dyld logic that can only be run in the host node
314314+// process and has full access to local filesystem
315315+export class DyLDHost {
316316+ // Deduped absolute paths of directories where we lookup .so files
317317+ #rpaths = new Set();
318318+319319+ constructor({ outFd, inFd }) {
320320+ // When running a non-iserv shared library with node, the DyLDHost
321321+ // instance is created without a pair of fds, so skip creation of
322322+ // readStream/writeStream, they won't be used anyway
323323+ if (!(typeof outFd === "number" && typeof inFd === "number")) {
324324+ return;
325325+ }
326326+ this.readStream = stream.Readable.toWeb(
327327+ fs.createReadStream(undefined, { fd: inFd })
328328+ );
329329+ this.writeStream = stream.Writable.toWeb(
330330+ fs.createWriteStream(undefined, { fd: outFd })
331331+ );
332332+ }
333333+334334+ close() {}
335335+336336+ installSignalHandlers(cb) {
337337+ process.on("SIGINT", cb);
338338+ process.on("SIGQUIT", cb);
339339+ }
340340+341341+ // removeLibrarySearchPath is a no-op in ghci. If you have a use
342342+ // case where it's actually needed, I would like to hear..
343343+ async addLibrarySearchPath(p) {
344344+ this.#rpaths.add(path.resolve(p));
345345+ return null;
346346+ }
347347+348348+ // f can be either just soname or an absolute path, will be
349349+ // canonicalized and checked for file existence here. Throws if
350350+ // non-existent.
351351+ async findSystemLibrary(f) {
352352+ if (path.isAbsolute(f)) {
353353+ await fs.promises.access(f, fs.promises.constants.R_OK);
354354+ return f;
355355+ }
356356+ const r = (
357357+ await Promise.allSettled(
358358+ [...this.#rpaths].map(async (p) => {
359359+ const r = path.resolve(p, f);
360360+ await fs.promises.access(r, fs.promises.constants.R_OK);
361361+ return r;
362362+ })
363363+ )
364364+ ).find(({ status }) => status === "fulfilled");
365365+ console.assert(
366366+ r,
367367+ `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}`
368368+ );
369369+ return r.value;
370370+ }
371371+372372+ // returns a Response for a .so absolute path
373373+ async fetchWasm(p) {
374374+ return new Response(stream.Readable.toWeb(fs.createReadStream(p)), {
375375+ headers: { "Content-Type": "application/wasm" },
376376+ });
377377+ }
378378+}
379379+380380+// Runs in the browser and uses the in-memory vfs, doesn't do any RPC
381381+// calls
382382+export class DyLDBrowserHost {
383383+ // Deduped absolute paths of directories where we lookup .so files
384384+ #rpaths = new Set();
385385+ // The PreopenDirectory object of the root filesystem
386386+ rootfs;
387387+ // Continuations to output a single line to stdout/stderr
388388+ stdout;
389389+ stderr;
390390+391391+ // Given canonicalized absolute file path, returns the File object,
392392+ // or null if absent
393393+ #readFile(p) {
394394+ const { ret, entry } = this.rootfs.dir.get_entry_for_path({
395395+ parts: p.split("/").filter((tok) => tok !== ""),
396396+ is_dir: false,
397397+ });
398398+ return ret === 0 ? entry : null;
399399+ }
400400+401401+ constructor({ rootfs, stdout, stderr }) {
402402+ this.rootfs = rootfs
403403+ ? rootfs
404404+ : new wasi.PreopenDirectory("/", [["tmp", new wasi.Directory([])]]);
405405+ this.stdout = stdout ? stdout : (msg) => console.info(msg);
406406+ this.stderr = stderr ? stderr : (msg) => console.warn(msg);
407407+ }
408408+409409+ // p must be canonicalized absolute path
410410+ async addLibrarySearchPath(p) {
411411+ this.#rpaths.add(p);
412412+ return null;
413413+ }
414414+415415+ async findSystemLibrary(f) {
416416+ if (f.startsWith("/")) {
417417+ if (this.#readFile(f)) {
418418+ return f;
419419+ }
420420+ throw new Error(`findSystemLibrary(${f}): not found in /`);
421421+ }
422422+423423+ for (const rpath of this.#rpaths) {
424424+ const r = `${rpath}/${f}`;
425425+ if (this.#readFile(r)) {
426426+ return r;
427427+ }
428428+ }
429429+430430+ throw new Error(
431431+ `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}`
432432+ );
433433+ }
434434+435435+ async fetchWasm(p) {
436436+ const entry = this.#readFile(p);
437437+ const r = new Response(entry.data, {
438438+ headers: { "Content-Type": "application/wasm" },
439439+ });
440440+ // It's only fetched once, take the chance to prune it in vfs to save memory
441441+ entry.data = new Uint8Array();
442442+ return r;
443443+ }
444444+}
445445+446446+// Fulfill the same functionality as DyLDHost by doing fetch() calls
447447+// to respective RPC endpoints of a host http server. Also manages
448448+// WebSocket connections back to host.
449449+export class DyLDRPC {
450450+ #origin;
451451+ #wsPipe;
452452+ #wsSig;
453453+ #redirectWasiConsole;
454454+ #wsStdout;
455455+ #wsStderr;
456456+457457+ constructor({ origin, redirectWasiConsole }) {
458458+ this.#origin = origin;
459459+460460+ const ws_url = this.#origin.replace("http://", "ws://");
461461+462462+ this.#wsPipe = new WebSocket(ws_url, "pipe");
463463+ this.#wsPipe.binaryType = "arraybuffer";
464464+465465+ this.readStream = new ReadableStream({
466466+ start: (controller) => {
467467+ this.#wsPipe.addEventListener("message", (ev) =>
468468+ controller.enqueue(new Uint8Array(ev.data))
469469+ );
470470+ this.#wsPipe.addEventListener("error", (ev) => controller.error(ev));
471471+ this.#wsPipe.addEventListener("close", () => controller.close());
472472+ },
473473+ });
474474+475475+ this.writeStream = new WritableStream({
476476+ start: (controller) => {
477477+ this.#wsPipe.addEventListener("error", (ev) => controller.error(ev));
478478+ },
479479+ write: (buf) => this.#wsPipe.send(buf),
480480+ });
481481+482482+ this.#wsSig = new WebSocket(ws_url, "sig");
483483+ this.#wsSig.binaryType = "arraybuffer";
484484+485485+ this.#redirectWasiConsole = redirectWasiConsole;
486486+ if (redirectWasiConsole) {
487487+ this.#wsStdout = new WebSocket(ws_url, "stdout");
488488+ this.#wsStderr = new WebSocket(ws_url, "stderr");
489489+ }
490490+491491+ this.opened = Promise.all(
492492+ (redirectWasiConsole
493493+ ? [this.#wsPipe, this.#wsSig, this.#wsStdout, this.#wsStderr]
494494+ : [this.#wsPipe, this.#wsSig]
495495+ ).map(
496496+ (ws) =>
497497+ new Promise((res, rej) => {
498498+ ws.addEventListener("open", res);
499499+ ws.addEventListener("error", rej);
500500+ })
501501+ )
502502+ );
503503+ }
504504+505505+ close() {
506506+ this.#wsPipe.close();
507507+ this.#wsSig.close();
508508+ if (this.#redirectWasiConsole) {
509509+ this.#wsStdout.close();
510510+ this.#wsStderr.close();
511511+ }
512512+ }
513513+514514+ async #rpc(endpoint, ...args) {
515515+ const r = await fetch(`${this.#origin}/rpc/${endpoint}`, {
516516+ method: "POST",
517517+ headers: {
518518+ "Content-Type": "application/json",
519519+ },
520520+ body: JSON.stringify(args),
521521+ });
522522+ if (!r.ok) {
523523+ throw new Error(await r.text());
524524+ }
525525+ return r.json();
526526+ }
527527+528528+ installSignalHandlers(cb) {
529529+ this.#wsSig.addEventListener("message", cb);
530530+ }
531531+532532+ async addLibrarySearchPath(p) {
533533+ return this.#rpc("addLibrarySearchPath", p);
534534+ }
535535+536536+ async findSystemLibrary(f) {
537537+ return this.#rpc("findSystemLibrary", f);
538538+ }
539539+540540+ async fetchWasm(p) {
541541+ return fetch(`${this.#origin}/fs${p}`);
542542+ }
543543+544544+ stdout(msg) {
545545+ if (this.#redirectWasiConsole) {
546546+ this.#wsStdout.send(msg);
547547+ } else {
548548+ console.info(msg);
549549+ }
550550+ }
551551+552552+ stderr(msg) {
553553+ if (this.#redirectWasiConsole) {
554554+ this.#wsStderr.send(msg);
555555+ } else {
556556+ console.warn(msg);
557557+ }
558558+ }
559559+}
560560+561561+// Actual implementation of endpoints used by DyLDRPC
562562+class DyLDRPCServer {
563563+ #dyldHost;
564564+ #server;
565565+ #wss;
566566+567567+ constructor({
568568+ host,
569569+ port,
570570+ dyldPath,
571571+ searchDirs,
572572+ mainSoPath,
573573+ outFd,
574574+ inFd,
575575+ args,
576576+ redirectWasiConsole,
577577+ }) {
578578+ this.#dyldHost = new DyLDHost({ outFd, inFd });
579579+580580+ this.#server = http.createServer(async (req, res) => {
581581+ const origin = originFromServerAddress(await this.listening);
582582+583583+ res.setHeader("Access-Control-Allow-Origin", "*");
584584+ res.setHeader("Access-Control-Allow-Headers", "*");
585585+586586+ if (req.method === "OPTIONS") {
587587+ res.writeHead(204);
588588+ res.end();
589589+ return;
590590+ }
591591+592592+ if (req.url === "/main.html") {
593593+ res.writeHead(200, {
594594+ "Content-Type": "text/html",
595595+ });
596596+ res.end(
597597+ `
598598+<!DOCTYPE html>
599599+<title>wasm ghci</title>
600600+<script type="module" src="./main.js"></script>
601601+`
602602+ );
603603+ return;
604604+ }
605605+606606+ if (req.url === "/main.js") {
607607+ res.writeHead(200, {
608608+ "Content-Type": "application/javascript",
609609+ });
610610+ res.end(
611611+ `
612612+import { DyLDRPC, main } from "./fs${dyldPath}";
613613+const args = ${JSON.stringify({ searchDirs, mainSoPath, args, isIserv: true })};
614614+args.rpc = new DyLDRPC({origin: "${origin}", redirectWasiConsole: ${redirectWasiConsole}});
615615+args.rpc.opened.then(() => main(args));
616616+`
617617+ );
618618+ return;
619619+ }
620620+621621+ if (req.url.startsWith("/fs")) {
622622+ const p = req.url.replace("/fs", "");
623623+624624+ res.setHeader(
625625+ "Content-Type",
626626+ {
627627+ ".mjs": "application/javascript",
628628+ ".so": "application/wasm",
629629+ }[path.extname(p)] || "application/octet-stream"
630630+ );
631631+632632+ res.writeHead(200);
633633+ fs.createReadStream(p).pipe(res);
634634+ return;
635635+ }
636636+637637+ if (req.url.startsWith("/rpc")) {
638638+ const endpoint = req.url.replace("/rpc/", "");
639639+640640+ let body = "";
641641+ for await (const chunk of req) {
642642+ body += chunk;
643643+ }
644644+645645+ res.writeHead(200, {
646646+ "Content-Type": "application/json",
647647+ });
648648+ res.end(
649649+ JSON.stringify(await this.#dyldHost[endpoint](...JSON.parse(body)))
650650+ );
651651+ return;
652652+ }
653653+654654+ res.writeHead(404, {
655655+ "Content-Type": "text/plain",
656656+ });
657657+ res.end("not found");
658658+ });
659659+660660+ this.closed = new Promise((res) => this.#server.on("close", res));
661661+662662+ this.#wss = new ws.WebSocketServer({ server: this.#server });
663663+ this.#wss.on("connection", (ws) => {
664664+ ws.addEventListener("error", () => {
665665+ this.#wss.close();
666666+ this.#server.close();
667667+ });
668668+669669+ ws.addEventListener("close", () => {
670670+ this.#wss.close();
671671+ this.#server.close();
672672+ });
673673+674674+ if (ws.protocol === "pipe") {
675675+ (async () => {
676676+ for await (const buf of this.#dyldHost.readStream) {
677677+ ws.send(buf);
678678+ }
679679+ })();
680680+ const writer = this.#dyldHost.writeStream.getWriter();
681681+ ws.addEventListener("message", (ev) =>
682682+ writer.write(new Uint8Array(ev.data))
683683+ );
684684+ return;
685685+ }
686686+687687+ if (ws.protocol === "sig") {
688688+ this.#dyldHost.installSignalHandlers(() => ws.send(new Uint8Array(0)));
689689+ return;
690690+ }
691691+692692+ if (ws.protocol === "stdout") {
693693+ ws.addEventListener("message", (ev) => console.info(ev.data));
694694+ return;
695695+ }
696696+697697+ if (ws.protocol === "stderr") {
698698+ ws.addEventListener("message", (ev) => console.warn(ev.data));
699699+ return;
700700+ }
701701+702702+ throw new Error(`unknown protocol ${ws.protocol}`);
703703+ });
704704+705705+ this.listening = new Promise((res) =>
706706+ this.#server.listen({ host, port }, () => res(this.#server.address()))
707707+ );
708708+ }
709709+}
710710+711711+// The real stuff
712712+class DyLD {
713713+ // Wasm page size.
714714+ static #pageSize = 0x10000;
715715+716716+ // Placeholder value of GOT.mem addresses that must be imported
717717+ // first and later modified to be the correct relocated pointer.
718718+ // This value is 0xffffffff subtracts one page, so hopefully any
719719+ // memory access near this address will trap immediately.
720720+ //
721721+ // In JS API i32 is signed, hence this layer of redirection.
722722+ static #poison = (0xffffffff - DyLD.#pageSize) | 0;
723723+724724+ // When processing exports, skip the following ones since they're
725725+ // generated by wasm-ld.
726726+ static #ldGeneratedExportNames = new Set([
727727+ "_initialize",
728728+ "__wasm_apply_data_relocs",
729729+ "__wasm_apply_global_relocs",
730730+ "__wasm_call_ctors",
731731+ ]);
732732+733733+ // Handles RPC logic back to host in a browser, or just do plain
734734+ // function calls in node
735735+ #rpc;
736736+737737+ // The WASI instance to provide wasi imports, shared across all wasm
738738+ // instances
739739+ #wasi;
740740+741741+ // Wasm memory & table
742742+ #memory = new WebAssembly.Memory({ initial: 1 });
743743+744744+ #table = new WebAssembly.Table({ element: "anyfunc", initial: 1 });
745745+ // First free slot, might be invalid when it advances to #table.length
746746+ #tableFree = 1;
747747+ // See Note [The evil wasm table grower]
748748+ #tableGrowInstance = new WebAssembly.Instance(
749749+ new WebAssembly.Module(
750750+ new Uint8Array([
751751+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 6, 1, 96, 1, 127, 1, 127, 2, 35, 1, 3,
752752+ 101, 110, 118, 25, 95, 95, 105, 110, 100, 105, 114, 101, 99, 116, 95,
753753+ 102, 117, 110, 99, 116, 105, 111, 110, 95, 116, 97, 98, 108, 101, 1,
754754+ 112, 0, 0, 3, 2, 1, 0, 7, 31, 1, 27, 95, 95, 103, 104, 99, 95, 119, 97,
755755+ 115, 109, 95, 106, 115, 102, 102, 105, 95, 116, 97, 98, 108, 101, 95,
756756+ 103, 114, 111, 119, 0, 0, 10, 11, 1, 9, 0, 208, 112, 32, 0, 252, 15, 0,
757757+ 11,
758758+ ])
759759+ ),
760760+ { env: { __indirect_function_table: this.#table } }
761761+ );
762762+763763+ // __stack_pointer
764764+ #sp = new WebAssembly.Global(
765765+ {
766766+ value: "i32",
767767+ mutable: true,
768768+ },
769769+ DyLD.#pageSize
770770+ );
771771+772772+ // The JSVal manager
773773+ #jsvalManager = new JSValManager();
774774+775775+ // sonames of loaded sos.
776776+ //
777777+ // Note that "soname" is just xxx.so as in file path, not actually
778778+ // parsed from a section in .so file. wasm-ld does accept
779779+ // --soname=<value>, but it just writes the module name to the name
780780+ // section, which can be stripped by wasm-opt and such. We do not
781781+ // rely on the name section at all.
782782+ //
783783+ // Invariant: soname is globally unique!
784784+ #loadedSos = new Set();
785785+786786+ // Mapping from export names to export funcs. It's also passed as
787787+ // __exports in JSFFI code, hence the "memory" special field.
788788+ exportFuncs = { memory: this.#memory };
789789+790790+ // The FinalizationRegistry used by JSFFI.
791791+ #finalizationRegistry = new FinalizationRegistry((sp) =>
792792+ this.exportFuncs.rts_freeStablePtr(sp)
793793+ );
794794+795795+ // The GOT.func table.
796796+ #gotFunc = {};
797797+798798+ // The GOT.mem table. By wasm dylink convention, a wasm global
799799+ // exported by .so is always assumed to be a GOT.mem entry, not a
800800+ // re-exported actual wasm global.
801801+ #gotMem = {};
802802+803803+ // Global STG registers
804804+ #regs = {};
805805+806806+ // Note [The evil wasm table grower]
807807+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
808808+ // We need to grow the wasm table as we load shared libraries in
809809+ // wasm dyld. We used to directly call the table.grow() JS API,
810810+ // which works as expected in Firefox/Chrome, but unfortunately,
811811+ // WebKit's implementation of the table.grow() JS API is broken:
812812+ // https://bugs.webkit.org/show_bug.cgi?id=290681, which means that
813813+ // the wasm dyld simply does not work in WebKit-based browsers like
814814+ // Safari.
815815+ //
816816+ // Now, one simple workaround would be to avoid growing the table at
817817+ // all: just allocate a huge table upfront (current limitation
818818+ // agreed by all vendors is 10000000). To avoid unnecessary space
819819+ // waste on non-WebKit platforms, we could additionally check
820820+ // navigator.userAgent against some regexes and only allocate
821821+ // fixed-length table when there's no blink/gecko mention. But this
822822+ // is fragile and gross, and it's better to stick to a uniform code
823823+ // path for all browsers.
824824+ //
825825+ // Fortunately, it turns out the table.grow wasm instruction work as
826826+ // expected in WebKit! So we can invoke a wasm function that grows
827827+ // the table for us. But don't open a champagne yet, where would
828828+ // that wasm function come from? It can't be put into RTS, or even
829829+ // libc.so, because loading those libraries would require growing
830830+ // the table in the first place! Or perhaps, reserve a table upfront
831831+ // that's just large enough to load RTS and then we can access that
832832+ // function for subsequent table grows? But then we need to
833833+ // experiment for a reasonable initial size, and add a magic number
834834+ // here, which is also fragile and gross and not future-proof!
835835+ //
836836+ // So this special wasm function needs to live in a single wasm
837837+ // module, which is loaded before we load anything else. The full
838838+ // source code for this module is:
839839+ //
840840+ // (module
841841+ // (type (func (param i32) (result i32)))
842842+ // (import "env" "__indirect_function_table" (table 0 funcref))
843843+ // (export "__ghc_wasm_jsffi_table_grow" (func 0))
844844+ // (func (type 0) (param i32) (result i32)
845845+ // ref.null func
846846+ // local.get 0
847847+ // table.grow 0
848848+ // )
849849+ // )
850850+ //
851851+ // This module is 103 bytes so that we can inline its blob in dyld,
852852+ // and use the usually discouraged synchronous
853853+ // WebAssembly.Instance/WebAssembly.Module constructors to load it.
854854+ // On non-WebKit platforms, growing tables this way would introduce
855855+ // a bit of extra JS/Wasm interop overhead, which can be amplified
856856+ // as we used to call table.grow(1, foo) for every GOT.func item.
857857+ // Therefore, unless we're about to exceed the hard limit of table
858858+ // size, we now grow the table exponentially, and use bump
859859+ // allocation to calculate the table index to be returned.
860860+ // Exponential growth is only implemented to minimize the JS/Wasm
861861+ // interop overhead when calling __ghc_wasm_jsffi_table_grow;
862862+ // V8/SpiderMonkey/WebKit already do their own exponential growth of
863863+ // the table's backing buffer in their table growth logic.
864864+ //
865865+ // Invariants: n >= 0; when v is non-null, n === 1
866866+ #tableGrow(n, v) {
867867+ const prev_free = this.#tableFree;
868868+ if (prev_free + n > this.#table.length) {
869869+ const min_delta = prev_free + n - this.#table.length;
870870+ const delta = Math.max(min_delta, this.#table.length);
871871+ this.#tableGrowInstance.exports.__ghc_wasm_jsffi_table_grow(
872872+ this.#table.length + delta <= 10000000 ? delta : min_delta
873873+ );
874874+ }
875875+ if (v) {
876876+ this.#table.set(prev_free, v);
877877+ }
878878+ this.#tableFree += n;
879879+ return prev_free;
880880+ }
881881+882882+ constructor({ args, rpc }) {
883883+ this.#rpc = rpc;
884884+885885+ if (isNode) {
886886+ this.#wasi = new wasi.WASI({
887887+ version: "preview1",
888888+ args,
889889+ env: { PATH: "", PWD: process.cwd() },
890890+ preopens: { "/": "/" },
891891+ });
892892+ } else {
893893+ this.#wasi = new wasi.WASI(
894894+ args,
895895+ [],
896896+ [
897897+ new wasi.OpenFile(
898898+ new wasi.File(new Uint8Array(), { readonly: true })
899899+ ),
900900+ wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stdout(msg)),
901901+ wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stderr(msg)),
902902+ // for ghci browser mode, default to an empty rootfs with
903903+ // /tmp
904904+ this.#rpc instanceof DyLDBrowserHost
905905+ ? this.#rpc.rootfs
906906+ : new wasi.PreopenDirectory("/", [["tmp", new wasi.Directory([])]]),
907907+ ],
908908+ { debug: false }
909909+ );
910910+ }
911911+912912+ // Both wasi implementations we use provide
913913+ // wasi.initialize(instance) to initialize a wasip1 reactor
914914+ // module. However, instance does not really need to be a
915915+ // WebAssembly.Instance object; the wasi implementations only need
916916+ // to access instance.exports.memory for the wasi syscalls to
917917+ // work.
918918+ //
919919+ // Given we'll reuse the same wasi object across different
920920+ // WebAssembly.Instance objects anyway and
921921+ // wasi.initialize(instance) can't be called more than once, we
922922+ // use this simple trick and pass a fake instance object that
923923+ // contains just enough info for the wasi implementation to
924924+ // initialize its internal state. Later when we load each wasm
925925+ // shared library, we can just manually invoke their
926926+ // initialization functions.
927927+ this.#wasi.initialize({
928928+ exports: {
929929+ memory: this.#memory,
930930+ },
931931+ });
932932+933933+ // Keep this in sync with rts/wasm/Wasm.S!
934934+ for (let i = 1; i <= 10; ++i) {
935935+ this.#regs[`__R${i}`] = new WebAssembly.Global({
936936+ value: "i32",
937937+ mutable: true,
938938+ });
939939+ }
940940+941941+ for (let i = 1; i <= 6; ++i) {
942942+ this.#regs[`__F${i}`] = new WebAssembly.Global({
943943+ value: "f32",
944944+ mutable: true,
945945+ });
946946+ }
947947+948948+ for (let i = 1; i <= 6; ++i) {
949949+ this.#regs[`__D${i}`] = new WebAssembly.Global({
950950+ value: "f64",
951951+ mutable: true,
952952+ });
953953+ }
954954+955955+ this.#regs.__L1 = new WebAssembly.Global({ value: "i64", mutable: true });
956956+957957+ for (const k of ["__Sp", "__SpLim", "__Hp", "__HpLim"]) {
958958+ this.#regs[k] = new WebAssembly.Global({ value: "i32", mutable: true });
959959+ }
960960+ }
961961+962962+ async addLibrarySearchPath(p) {
963963+ return this.#rpc.addLibrarySearchPath(p);
964964+ }
965965+966966+ async findSystemLibrary(f) {
967967+ return this.#rpc.findSystemLibrary(f);
968968+ }
969969+970970+ // When we do loadDLLs, we first perform "downsweep" which return a
971971+ // toposorted array of dependencies up to itself, then sequentially
972972+ // load the downsweep result.
973973+ //
974974+ // The rationale of a separate downsweep phase, instead of a simple
975975+ // recursive loadDLLs function is: V8 delegates async
976976+ // WebAssembly.compile to a background worker thread pool. To
977977+ // maintain consistent internal linker state, we *must* load each so
978978+ // file sequentially, but it's okay to kick off compilation asap,
979979+ // store the Promise in downsweep result and await for the actual
980980+ // WebAssembly.Module in loadDLLs logic. This way we can harness some
981981+ // background parallelism.
982982+ async #downsweep(p) {
983983+ const toks = p.split("/");
984984+985985+ const soname = toks[toks.length - 1];
986986+987987+ if (this.#loadedSos.has(soname)) {
988988+ return [];
989989+ }
990990+991991+ // Do this before loading dependencies to break potential cycles.
992992+ this.#loadedSos.add(soname);
993993+994994+ if (p.startsWith("/")) {
995995+ // GHC may attempt to load libghc_tmp_2.so that needs
996996+ // libghc_tmp_1.so in a temporary directory without adding that
997997+ // directory via addLibrarySearchPath
998998+ toks.pop();
999999+ await this.addLibrarySearchPath(toks.join("/"));
10001000+ } else {
10011001+ p = await this.findSystemLibrary(p);
10021002+ }
10031003+10041004+ const resp = await this.#rpc.fetchWasm(p);
10051005+ const resp2 = resp.clone();
10061006+ const modp = WebAssembly.compileStreaming(resp);
10071007+ // Parse dylink.0 from the raw buffer, not via
10081008+ // WebAssembly.Module.customSections(). This should return asap
10091009+ // without waiting for rest of the wasm module binary data.
10101010+ const r = await parseDyLink0(resp2.body.getReader());
10111011+ r.modp = modp;
10121012+ r.soname = soname;
10131013+ let acc = [];
10141014+ for (const dep of r.neededSos) {
10151015+ acc.push(...(await this.#downsweep(dep)));
10161016+ }
10171017+ acc.push(r);
10181018+ return acc;
10191019+ }
10201020+10211021+ // Batch load multiple DLLs in one go.
10221022+ // Accepts a NUL-delimited string of paths to avoid array marshalling.
10231023+ // Each path can be absolute or a soname; dependency resolution is
10241024+ // performed across the full set to enable maximal parallel compile
10251025+ // while maintaining sequential instantiation order.
10261026+ async loadDLLs(packed) {
10271027+ // Normalize input to an array of strings. When called from Haskell
10281028+ // we pass a single JSString containing NUL-separated paths.
10291029+ const paths = (
10301030+ typeof packed === "string"
10311031+ ? packed.length === 0
10321032+ ? []
10331033+ : packed.split("\0")
10341034+ : [packed]
10351035+ ) // tolerate an accidental single path object
10361036+ .filter((s) => s.length > 0)
10371037+ .reverse();
10381038+10391039+ // Compute a single downsweep plan for the whole batch.
10401040+ // Note: #downsweep mutates #loadedSos to break cycles and dedup.
10411041+ const plan = [];
10421042+ for (const p of paths) {
10431043+ plan.push(...(await this.#downsweep(p)));
10441044+ }
10451045+10461046+ for (const {
10471047+ memSize,
10481048+ memP2Align,
10491049+ tableSize,
10501050+ tableP2Align,
10511051+ modp,
10521052+ soname,
10531053+ } of plan) {
10541054+ const import_obj = {
10551055+ wasi_snapshot_preview1: this.#wasi.wasiImport,
10561056+ env: {
10571057+ memory: this.#memory,
10581058+ __indirect_function_table: this.#table,
10591059+ __stack_pointer: this.#sp,
10601060+ ...this.exportFuncs,
10611061+ },
10621062+ regs: this.#regs,
10631063+ // Keep this in sync with post-link.mjs!
10641064+ ghc_wasm_jsffi: {
10651065+ newJSVal: (v) => this.#jsvalManager.newJSVal(v),
10661066+ getJSVal: (k) => this.#jsvalManager.getJSVal(k),
10671067+ freeJSVal: (k) => this.#jsvalManager.freeJSVal(k),
10681068+ scheduleWork: () => setImmediate(this.exportFuncs.rts_schedulerLoop),
10691069+ },
10701070+ "GOT.mem": this.#gotMem,
10711071+ "GOT.func": this.#gotFunc,
10721072+ };
10731073+10741074+ // __memory_base & __table_base, different for each .so
10751075+ let memory_base;
10761076+ let table_base = this.#tableGrow(tableSize);
10771077+ console.assert(tableP2Align === 0);
10781078+10791079+ // libc.so is always the first one to be ever loaded and has VIP
10801080+ // treatment. It will never be unloaded even if we support
10811081+ // unloading in the future. Nor do we support multiple libc.so
10821082+ // in the same address space.
10831083+ if (soname === "libc.so") {
10841084+ // Starting from 0x0: one page of C stack, then global data
10851085+ // segments of libc.so, then one page space between
10861086+ // __heap_base/__heap_end so that dlmalloc can initialize
10871087+ // global state. wasm-ld aligns __heap_base to page sized so
10881088+ // we follow suit.
10891089+ console.assert(memP2Align <= Math.log2(DyLD.#pageSize));
10901090+ memory_base = DyLD.#pageSize;
10911091+ const data_pages = Math.ceil(memSize / DyLD.#pageSize);
10921092+ this.#memory.grow(data_pages + 1);
10931093+10941094+ this.#gotMem.__heap_base = new WebAssembly.Global(
10951095+ { value: "i32", mutable: true },
10961096+ DyLD.#pageSize * (1 + data_pages)
10971097+ );
10981098+ this.#gotMem.__heap_end = new WebAssembly.Global(
10991099+ { value: "i32", mutable: true },
11001100+ DyLD.#pageSize * (1 + data_pages + 1)
11011101+ );
11021102+ } else {
11031103+ // TODO: this would also be __dso_handle@GOT, in case we
11041104+ // implement so unloading logic in the future.
11051105+ memory_base = this.exportFuncs.aligned_alloc(1 << memP2Align, memSize);
11061106+ }
11071107+11081108+ import_obj.env.__memory_base = new WebAssembly.Global(
11091109+ { value: "i32", mutable: false },
11101110+ memory_base
11111111+ );
11121112+ import_obj.env.__table_base = new WebAssembly.Global(
11131113+ { value: "i32", mutable: false },
11141114+ table_base
11151115+ );
11161116+11171117+ const mod = await modp;
11181118+11191119+ // Fulfill the ghc_wasm_jsffi imports. Use new Function()
11201120+ // instead of eval() to prevent bindings in this local scope to
11211121+ // be accessed by JSFFI code snippets. See Note [Variable passing in JSFFI]
11221122+ // for what's going on here.
11231123+ Object.assign(
11241124+ import_obj.ghc_wasm_jsffi,
11251125+ new Function(
11261126+ "__exports",
11271127+ "__ghc_wasm_jsffi_dyld",
11281128+ "__ghc_wasm_jsffi_finalization_registry",
11291129+ "return {".concat(
11301130+ ...parseSections(mod).map(
11311131+ (rec) => `${rec[0]}: ${parseRecord(rec)}, `
11321132+ ),
11331133+ "};"
11341134+ )
11351135+ )(this.exportFuncs, this, this.#finalizationRegistry)
11361136+ );
11371137+11381138+ // Fulfill the rest of the imports
11391139+ for (const { module, name, kind } of WebAssembly.Module.imports(mod)) {
11401140+ // Already there, no handling required
11411141+ if (import_obj[module] && import_obj[module][name]) {
11421142+ continue;
11431143+ }
11441144+11451145+ // Add a lazy function stub in env, but don't put it into
11461146+ // exportFuncs yet. This lazy binding is only effective for
11471147+ // the current so, since env is a transient object created on
11481148+ // the fly.
11491149+ if (module === "env" && kind === "function") {
11501150+ import_obj.env[name] = (...args) => {
11511151+ if (!this.exportFuncs[name]) {
11521152+ throw new WebAssembly.RuntimeError(
11531153+ `non-existent function ${name}`
11541154+ );
11551155+ }
11561156+ return this.exportFuncs[name](...args);
11571157+ };
11581158+ continue;
11591159+ }
11601160+11611161+ // Add a lazy GOT.mem entry with poison value, in the hope
11621162+ // that if they're used before being resolved with real
11631163+ // addresses, a memory trap will be triggered immediately.
11641164+ if (module === "GOT.mem" && kind === "global") {
11651165+ this.#gotMem[name] = new WebAssembly.Global(
11661166+ { value: "i32", mutable: true },
11671167+ DyLD.#poison
11681168+ );
11691169+ continue;
11701170+ }
11711171+11721172+ // Missing entry in GOT.func table, could be already defined
11731173+ // or not
11741174+ if (module === "GOT.func" && kind === "global") {
11751175+ // A dependency has exported the function, just create the
11761176+ // entry on the fly
11771177+ if (this.exportFuncs[name]) {
11781178+ this.#gotFunc[name] = new WebAssembly.Global(
11791179+ { value: "i32", mutable: true },
11801180+ this.#tableGrow(1, this.exportFuncs[name])
11811181+ );
11821182+ continue;
11831183+ }
11841184+11851185+ // Can't find this function, so poison it like GOT.mem.
11861186+ // TODO: when wasm type reflection is widely available in
11871187+ // browsers, use the WebAssembly.Function constructor to
11881188+ // dynamically create a stub function that does better error
11891189+ // reporting
11901190+ this.#gotFunc[name] = new WebAssembly.Global(
11911191+ { value: "i32", mutable: true },
11921192+ DyLD.#poison
11931193+ );
11941194+ continue;
11951195+ }
11961196+11971197+ throw new Error(
11981198+ `cannot handle import ${module}.${name} with kind ${kind}`
11991199+ );
12001200+ }
12011201+12021202+ // Fingers crossed...
12031203+ const instance = await WebAssembly.instantiate(mod, import_obj);
12041204+12051205+ // Process the exports
12061206+ for (const k in instance.exports) {
12071207+ // Boring stuff
12081208+ if (DyLD.#ldGeneratedExportNames.has(k)) {
12091209+ continue;
12101210+ }
12111211+12121212+ // Invariant: each function symbol can be defined only once.
12131213+ // This is incorrect for weak symbols which are allowed to
12141214+ // appear multiple times but this is sufficient in practice.
12151215+ console.assert(
12161216+ !this.exportFuncs[k],
12171217+ `duplicate symbol ${k} when loading ${soname}`
12181218+ );
12191219+12201220+ const v = instance.exports[k];
12211221+12221222+ if (typeof v === "function") {
12231223+ this.exportFuncs[k] = v;
12241224+ // If there's a lazy GOT.func entry, put the function in the
12251225+ // table and fulfill the entry. Otherwise no need to do
12261226+ // anything, if it's required later a GOT.func entry will be
12271227+ // created on demand.
12281228+ if (this.#gotFunc[k]) {
12291229+ const got = this.#gotFunc[k];
12301230+ if (got.value === DyLD.#poison) {
12311231+ const idx = this.#tableGrow(1, v);
12321232+ got.value = idx;
12331233+ } else {
12341234+ this.#table.set(got.value, v);
12351235+ }
12361236+ }
12371237+ continue;
12381238+ }
12391239+12401240+ // It's a GOT.mem entry
12411241+ if (v instanceof WebAssembly.Global) {
12421242+ const addr = v.value + memory_base;
12431243+ if (this.#gotMem[k]) {
12441244+ console.assert(this.#gotMem[k].value === DyLD.#poison);
12451245+ this.#gotMem[k].value = addr;
12461246+ } else {
12471247+ this.#gotMem[k] = new WebAssembly.Global(
12481248+ { value: "i32", mutable: true },
12491249+ addr
12501250+ );
12511251+ }
12521252+ continue;
12531253+ }
12541254+12551255+ throw new Error(`cannot handle export ${k} ${v}`);
12561256+ }
12571257+12581258+ // See
12591259+ // https://gitlab.haskell.org/haskell-wasm/llvm-project/-/blob/release/21.x/lld/wasm/Writer.cpp#L1451,
12601260+ // __wasm_apply_data_relocs is now optional so only call it if
12611261+ // it exists (we know for sure it exists for libc.so though).
12621262+ // There's also __wasm_init_memory (not relevant yet, we don't
12631263+ // use passive segments) & __wasm_apply_global_relocs but
12641264+ // those are included in the start function and should have
12651265+ // been called upon instantiation, see
12661266+ // Writer::createStartFunction().
12671267+ if (instance.exports.__wasm_apply_data_relocs) {
12681268+ instance.exports.__wasm_apply_data_relocs();
12691269+ }
12701270+12711271+ instance.exports._initialize();
12721272+ }
12731273+ }
12741274+12751275+ lookupSymbol(sym) {
12761276+ if (this.#gotMem[sym] && this.#gotMem[sym].value !== DyLD.#poison) {
12771277+ return this.#gotMem[sym].value;
12781278+ }
12791279+ if (this.#gotFunc[sym] && this.#gotFunc[sym].value !== DyLD.#poison) {
12801280+ return this.#gotFunc[sym].value;
12811281+ }
12821282+ // Not in GOT.func yet, create the entry on demand
12831283+ if (this.exportFuncs[sym]) {
12841284+ console.assert(!this.#gotFunc[sym]);
12851285+ const addr = this.#tableGrow(1, this.exportFuncs[sym]);
12861286+ this.#gotFunc[sym] = new WebAssembly.Global(
12871287+ { value: "i32", mutable: true },
12881288+ addr
12891289+ );
12901290+ return addr;
12911291+ }
12921292+ return 0;
12931293+ }
12941294+}
12951295+12961296+// The main entry point of dyld that may be run on node/browser, and
12971297+// may run either iserv defaultMain from the ghci library or an
12981298+// alternative entry point from another shared library
12991299+export async function main({
13001300+ rpc, // Handle the side effects of DyLD
13011301+ searchDirs, // Initial library search directories
13021302+ mainSoPath, // Could also be another shared library that's actually not ghci
13031303+ args, // WASI argv starting with the executable name. +RTS etc will be respected
13041304+ isIserv, // set to true when running iserv defaultServer
13051305+}) {
13061306+ try {
13071307+ const dyld = new DyLD({
13081308+ args,
13091309+ rpc,
13101310+ });
13111311+ for (const libdir of searchDirs) {
13121312+ await dyld.addLibrarySearchPath(libdir);
13131313+ }
13141314+ await dyld.loadDLLs(mainSoPath);
13151315+13161316+ // At this point, rts/ghc-internal are loaded, perform wasm shared
13171317+ // library specific RTS startup logic, see Note [JSFFI initialization]
13181318+ dyld.exportFuncs.__ghc_wasm_jsffi_init();
13191319+13201320+ // We're not running iserv, just return the dyld instance so user
13211321+ // could use it to invoke their exported functions, and don't
13221322+ // perform cleanup (see finally block)
13231323+ if (!isIserv) {
13241324+ return dyld;
13251325+ }
13261326+13271327+ // iserv-specific logic follows
13281328+ const reader = rpc.readStream.getReader();
13291329+ const writer = rpc.writeStream.getWriter();
13301330+13311331+ const cb_sig = (cb) => {
13321332+ rpc.installSignalHandlers(cb);
13331333+ };
13341334+13351335+ const cb_recv = async () => {
13361336+ const { done, value } = await reader.read();
13371337+ if (done) {
13381338+ throw new Error("not enough bytes");
13391339+ }
13401340+ return value;
13411341+ };
13421342+ const cb_send = (buf) => {
13431343+ writer.write(new Uint8Array(buf));
13441344+ };
13451345+13461346+ return await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send);
13471347+ } finally {
13481348+ if (isIserv) {
13491349+ rpc.close();
13501350+ }
13511351+ }
13521352+}
13531353+13541354+// node-specific iserv-specific logic
13551355+async function nodeMain({ searchDirs, mainSoPath, outFd, inFd, args }) {
13561356+ if (!process.env.GHCI_BROWSER) {
13571357+ const rpc = new DyLDHost({ outFd, inFd });
13581358+ return await main({
13591359+ rpc,
13601360+ searchDirs,
13611361+ mainSoPath,
13621362+ args,
13631363+ isIserv: true,
13641364+ });
13651365+ }
13661366+13671367+ if (!ws) {
13681368+ throw new Error(
13691369+ "Please install ws and ensure it's available via NODE_PATH"
13701370+ );
13711371+ }
13721372+13731373+ const server = new DyLDRPCServer({
13741374+ host: process.env.GHCI_BROWSER_HOST || "127.0.0.1",
13751375+ port: process.env.GHCI_BROWSER_PORT || 0,
13761376+ dyldPath: import.meta.filename,
13771377+ searchDirs,
13781378+ mainSoPath,
13791379+ outFd,
13801380+ inFd,
13811381+ args,
13821382+ redirectWasiConsole:
13831383+ process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS ||
13841384+ process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE
13851385+ ? false
13861386+ : Boolean(process.env.GHCI_BROWSER_REDIRECT_WASI_CONSOLE),
13871387+ });
13881388+ const origin = originFromServerAddress(await server.listening);
13891389+13901390+ // https://pptr.dev/api/puppeteer.consolemessage
13911391+ // https://playwright.dev/docs/api/class-consolemessage
13921392+ const on_console_msg = (msg) => {
13931393+ switch (msg.type()) {
13941394+ case "error":
13951395+ case "warn":
13961396+ case "warning":
13971397+ case "trace":
13981398+ case "assert": {
13991399+ console.error(msg.text());
14001400+ break;
14011401+ }
14021402+ default: {
14031403+ console.log(msg.text());
14041404+ break;
14051405+ }
14061406+ }
14071407+ };
14081408+14091409+ if (process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS) {
14101410+ let puppeteer;
14111411+ try {
14121412+ puppeteer = require("puppeteer");
14131413+ } catch {
14141414+ puppeteer = require("puppeteer-core");
14151415+ }
14161416+14171417+ // https://pptr.dev/api/puppeteer.puppeteernode.launch
14181418+ const browser = await puppeteer.launch(
14191419+ JSON.parse(process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS)
14201420+ );
14211421+ try {
14221422+ const page = await browser.newPage();
14231423+14241424+ // https://pptr.dev/api/puppeteer.pageevent
14251425+ page.on("console", on_console_msg);
14261426+ page.on("error", (err) => console.error(err));
14271427+ page.on("pageerror", (err) => console.error(err));
14281428+14291429+ await page.goto(`${origin}/main.html`);
14301430+ await server.closed;
14311431+ return;
14321432+ } finally {
14331433+ await browser.close();
14341434+ }
14351435+ }
14361436+14371437+ if (process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE) {
14381438+ let playwright;
14391439+ try {
14401440+ playwright = require("playwright");
14411441+ } catch {
14421442+ playwright = require("playwright-core");
14431443+ }
14441444+14451445+ // https://playwright.dev/docs/api/class-browsertype#browser-type-launch
14461446+ const browser = await playwright[
14471447+ process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE
14481448+ ].launch(
14491449+ process.env.GHCI_BROWSER_PLAYWRIGHT_LAUNCH_OPTS
14501450+ ? JSON.parse(process.env.GHCI_BROWSER_PLAYWRIGHT_LAUNCH_OPTS)
14511451+ : {}
14521452+ );
14531453+ try {
14541454+ const page = await browser.newPage();
14551455+14561456+ // https://playwright.dev/docs/api/class-page#events
14571457+ page.on("console", on_console_msg);
14581458+ page.on("pageerror", (err) => console.error(err));
14591459+14601460+ await page.goto(`${origin}/main.html`);
14611461+ await server.closed;
14621462+ return;
14631463+ } finally {
14641464+ await browser.close();
14651465+ }
14661466+ }
14671467+14681468+ console.log(
14691469+ `Open ${origin}/main.html or import("${origin}/main.js") to boot ghci`
14701470+ );
14711471+}
14721472+14731473+const isNodeMain = isNode && import.meta.filename === process.argv[1];
14741474+14751475+// node iserv as invoked by
14761476+// GHC.Runtime.Interpreter.Wasm.spawnWasmInterp
14771477+if (isNodeMain) {
14781478+ const clibdir = process.argv[2];
14791479+ const mainSoPath = process.argv[3];
14801480+ const outFd = Number.parseInt(process.argv[4]),
14811481+ inFd = Number.parseInt(process.argv[5]);
14821482+ const args = ["dyld.so", ...process.argv.slice(6)];
14831483+14841484+ await nodeMain({ searchDirs: [clibdir], mainSoPath, outFd, inFd, args });
14851485+}
···11+#!/usr/bin/env -S node
22+33+// This is the post-linker program that processes a wasm module with
44+// ghc_wasm_jsffi custom section and outputs an ESM module that
55+// exports a function to generate the ghc_wasm_jsffi wasm imports. It
66+// has a simple CLI interface: "./post-link.mjs -i foo.wasm -o
77+// foo.js", as well as an exported postLink function that takes a
88+// WebAssembly.Module object and returns the ESM module content.
99+1010+// Each record in the ghc_wasm_jsffi custom section are 3
1111+// NUL-terminated strings: name, binder, body. We try to parse the
1212+// body as an expression and fallback to statements, and return the
1313+// completely parsed arrow function source.
1414+export function parseRecord([name, binder, body]) {
1515+ for (const src of [`${binder} => (${body})`, `${binder} => {${body}}`]) {
1616+ try {
1717+ new Function(`return ${src};`);
1818+ return src;
1919+ } catch (_) {}
2020+ }
2121+ throw new Error(`parseRecord ${name} ${binder} ${body}`);
2222+}
2323+2424+// Parse ghc_wasm_jsffi custom sections in a WebAssembly.Module object
2525+// and return an array of records.
2626+export function parseSections(mod) {
2727+ const recs = [];
2828+ const dec = new TextDecoder("utf-8", { fatal: true });
2929+ const importNames = new Set(
3030+ WebAssembly.Module.imports(mod)
3131+ .filter((i) => i.module === "ghc_wasm_jsffi")
3232+ .map((i) => i.name)
3333+ );
3434+ for (const buf of WebAssembly.Module.customSections(mod, "ghc_wasm_jsffi")) {
3535+ const ba = new Uint8Array(buf);
3636+ let strs = [];
3737+ for (let l = 0, r; l < ba.length; l = r + 1) {
3838+ r = ba.indexOf(0, l);
3939+ strs.push(dec.decode(ba.subarray(l, r)));
4040+ if (strs.length === 3) {
4141+ if (importNames.has(strs[0])) {
4242+ recs.push(strs);
4343+ }
4444+ strs = [];
4545+ }
4646+ }
4747+ }
4848+ return recs;
4949+}
5050+5151+// Note [Variable passing in JSFFI]
5252+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5353+//
5454+// The JSFFI code snippets can access variables in globalThis,
5555+// arguments like $1, $2, etc, plus a few magic variables: __exports,
5656+// __ghc_wasm_jsffi_dyld and __ghc_wasm_jsffi_finalization_registry.
5757+// How are these variables passed to JSFFI code? Remember, we strive
5858+// to keep the globalThis namespace hygiene and maintain the ability
5959+// to have multiple Haskell-wasm apps coexisting in the same JS
6060+// context, so we must not pass magic variables as global variables
6161+// even though they may seem globally unique.
6262+//
6363+// The solution is simple: put them in the JS lambda binder position.
6464+// Though there are different layers of lambdas here:
6565+//
6666+// 1. User writes "$1($2, await $3)" in a JSFFI code snippet. No
6767+// explicit binder here, the snippet is either an expression or
6868+// some statements.
6969+// 2. GHC doesn't know JS syntax but it knows JS function arity from
7070+// HS type signature, as well as if the JS function is async/sync
7171+// from safe/unsafe annotation. So it infers the JS binder (like
7272+// "async ($1, $2, $3)") and emits a (name,binder,body) tuple into
7373+// the ghc_wasm_jsffi custom section.
7474+// 3. After link-time we collect these tuples to make a JS object
7575+// mapping names to binder=>body, and this JS object will be used
7676+// to fulfill the ghc_wasm_jsffi wasm imports. This JS object is
7777+// returned by an outer layer of lambda which is in charge of
7878+// passing magic variables.
7979+//
8080+// In case of post-linker for statically linked wasm modules,
8181+// __ghc_wasm_jsffi_dyld won't work so is omitted, and
8282+// __ghc_wasm_jsffi_finalization_registry can be created inside the
8383+// outer JS lambda. Only __exports is exposed as user-visible API
8484+// since it's up to the user to perform knot-tying by assigning the
8585+// instance exports back to the (initially empty) __exports object
8686+// passed to this lambda.
8787+//
8888+// In case of dyld, all magic variables are dyld-session-global
8989+// variables; dyld uses new Function() to make the outer lambda, then
9090+// immediately invokes it by passing the right magic variables.
9191+9292+export async function postLink(mod) {
9393+ const fs = (await import("node:fs/promises")).default;
9494+ const path = (await import("node:path")).default;
9595+9696+ let src = (
9797+ await fs.readFile(path.join(import.meta.dirname, "prelude.mjs"), {
9898+ encoding: "utf-8",
9999+ })
100100+ ).replaceAll("export ", ""); // we only use it as code template, don't export stuff
101101+102102+ // Keep this in sync with dyld.mjs!
103103+ src = `${src}\nexport default (__exports) => {`;
104104+ src = `${src}\nconst __ghc_wasm_jsffi_jsval_manager = new JSValManager();`;
105105+ src = `${src}\nconst __ghc_wasm_jsffi_finalization_registry = globalThis.FinalizationRegistry ? new FinalizationRegistry(sp => __exports.rts_freeStablePtr(sp)) : { register: () => {}, unregister: () => true };`;
106106+ src = `${src}\nreturn {`;
107107+ src = `${src}\nnewJSVal: (v) => __ghc_wasm_jsffi_jsval_manager.newJSVal(v),`;
108108+ src = `${src}\ngetJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.getJSVal(k),`;
109109+ src = `${src}\nfreeJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.freeJSVal(k),`;
110110+ src = `${src}\nscheduleWork: () => setImmediate(__exports.rts_schedulerLoop),`;
111111+ for (const rec of parseSections(mod)) {
112112+ src = `${src}\n${rec[0]}: ${parseRecord(rec)},`;
113113+ }
114114+ return `${src}\n};\n};\n`;
115115+}
116116+117117+function isMain() {
118118+ if (!globalThis?.process?.versions?.node) {
119119+ return false;
120120+ }
121121+122122+ return import.meta.filename === process.argv[1];
123123+}
124124+125125+async function main() {
126126+ const fs = (await import("node:fs/promises")).default;
127127+ const util = (await import("node:util")).default;
128128+129129+ const { input, output } = util.parseArgs({
130130+ options: {
131131+ input: {
132132+ type: "string",
133133+ short: "i",
134134+ },
135135+ output: {
136136+ type: "string",
137137+ short: "o",
138138+ },
139139+ },
140140+ }).values;
141141+142142+ await fs.writeFile(
143143+ output,
144144+ await postLink(await WebAssembly.compile(await fs.readFile(input)))
145145+ );
146146+}
147147+148148+if (isMain()) {
149149+ await main();
150150+}
+91
book/js/prelude.mjs
···11+// This file implements the JavaScript runtime logic for Haskell
22+// modules that use JSFFI. It is not an ESM module, but the template
33+// of one; the post-linker script will copy all contents into a new
44+// ESM module.
55+66+// Manage a mapping from 32-bit ids to actual JavaScript values.
77+export class JSValManager {
88+ #lastk = 0;
99+ #kv = new Map();
1010+1111+ newJSVal(v) {
1212+ const k = ++this.#lastk;
1313+ this.#kv.set(k, v);
1414+ return k;
1515+ }
1616+1717+ // A separate has() call to ensure we can store undefined as a value
1818+ // too. Also, unconditionally check this since the check is cheap
1919+ // anyway, if the check fails then there's a use-after-free to be
2020+ // fixed.
2121+ getJSVal(k) {
2222+ if (!this.#kv.has(k)) {
2323+ throw new WebAssembly.RuntimeError(`getJSVal(${k})`);
2424+ }
2525+ return this.#kv.get(k);
2626+ }
2727+2828+ // Check for double free as well.
2929+ freeJSVal(k) {
3030+ if (!this.#kv.delete(k)) {
3131+ throw new WebAssembly.RuntimeError(`freeJSVal(${k})`);
3232+ }
3333+ }
3434+}
3535+3636+// The actual setImmediate() to be used. This is a ESM module top
3737+// level binding and doesn't pollute the globalThis namespace.
3838+//
3939+// To benchmark different setImmediate() implementations in the
4040+// browser, use https://github.com/jphpsf/setImmediate-shim-demo as a
4141+// starting point.
4242+export const setImmediate = await (async () => {
4343+ // node, bun, or other scripts might have set this up in the browser
4444+ if (globalThis.setImmediate) {
4545+ return globalThis.setImmediate;
4646+ }
4747+4848+ // deno
4949+ if (globalThis.Deno) {
5050+ try {
5151+ return (await import("node:timers")).setImmediate;
5252+ } catch {}
5353+ }
5454+5555+ // https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask
5656+ if (globalThis.scheduler) {
5757+ return (cb, ...args) => scheduler.postTask(() => cb(...args));
5858+ }
5959+6060+ // Cloudflare workers doesn't support MessageChannel
6161+ if (globalThis.MessageChannel) {
6262+ // A simple & fast setImmediate() implementation for browsers. It's
6363+ // not a drop-in replacement for node.js setImmediate() because:
6464+ // 1. There's no clearImmediate(), and setImmediate() doesn't return
6565+ // anything
6666+ // 2. There's no guarantee that callbacks scheduled by setImmediate()
6767+ // are executed in the same order (in fact it's the opposite lol),
6868+ // but you are never supposed to rely on this assumption anyway
6969+ class SetImmediate {
7070+ #fs = [];
7171+ #mc = new MessageChannel();
7272+7373+ constructor() {
7474+ this.#mc.port1.addEventListener("message", () => {
7575+ this.#fs.pop()();
7676+ });
7777+ this.#mc.port1.start();
7878+ }
7979+8080+ setImmediate(cb, ...args) {
8181+ this.#fs.push(() => cb(...args));
8282+ this.#mc.port2.postMessage(undefined);
8383+ }
8484+ }
8585+8686+ const sm = new SetImmediate();
8787+ return (cb, ...args) => sm.setImmediate(cb, ...args);
8888+ }
8989+9090+ return (cb, ...args) => setTimeout(cb, 0, ...args);
9191+})();