···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-})();