advent of code solutions aoc.oppi.li
haskell aoc

book: remove interactive bits for now

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 3406ff4c 65bbb88e

verified
+1 -1861
+1 -3
book/build.sh
··· 17 17 --css style.css \ 18 18 --lua-filter "$SCRIPTDIR"/filter.lua \ 19 19 --highlight-style "$SCRIPTDIR"/highlight.theme \ 20 - --template "$SCRIPTDIR"/template.html \ 21 - -H "$SCRIPTDIR"/js/index.js; 20 + --template "$SCRIPTDIR"/template.html 22 21 23 22 # setup the out directory 24 23 cp "$SCRIPTDIR"/style.css "$OUTDIR"/style.css 25 - cp "$SCRIPTDIR"/js/* "$OUTDIR"/ 26 24 27 25 echo "book generated in ./$OUTDIR"
-1485
book/js/dyld.mjs
··· 1 - #!/usr/bin/env -S node --disable-warning=ExperimentalWarning --max-old-space-size=65536 --wasm-lazy-validation 2 - 3 - // Note [The Wasm Dynamic Linker] 4 - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 - // 6 - // This script mainly has two roles: 7 - // 8 - // 1. Message broker: relay iserv messages between host GHC and wasm 9 - // iserv (GHCi.Server.defaultServer). This part only runs in 10 - // nodejs. 11 - // 2. Dynamic linker: provide RTS linker interfaces like 12 - // loadDLLs/lookupSymbol etc which are imported by wasm iserv. This 13 - // part can run in browsers as well. 14 - // 15 - // When GHC starts external interpreter for the wasm target, it starts 16 - // this script and passes a pair of pipe fds for iserv messages, 17 - // libHSghci.so path, and command line arguments for wasm iserv. By 18 - // default, the wasm iserv runs in the same node process, so the 19 - // message broker logic is simple: wrap the pipe fds as 20 - // ReadableStream/WritableStream, pass reader/writer callbacks to wasm 21 - // iserv and run it to completion. It doesn't need to intercept or 22 - // parse any message, unlike iserv-proxy. 23 - // 24 - // Things are a bit more interesting with ghci browser mode. All the 25 - // Haskell code and all the runtime runs in the browser, including the 26 - // dynamic linker parts of this script. The host GHC process doeesn't 27 - // need to know about "browser mode" at all as long as iserv messages 28 - // are handled as usual, though obviously we can't pass fds to 29 - // browsers like before! So this script starts an HTTP 1.1 server with 30 - // WebSockets support. The browser side can import a startup script 31 - // served by the server, which will import this script and invoke main 32 - // with the right arguments, hooray isomorphic JavaScript! The browser 33 - // side will proceed to bootstrap wasm iserv, and the iserv messages 34 - // are relayed over the WebSockets. (also ^C signals over a different 35 - // connection) 36 - // 37 - // Under the browser mode, there's more traffic than just the iserv 38 - // message WebSockets. The browser side can fulfill most of the RTS 39 - // linker functionality alone, but it still needs to do stuff like 40 - // searching for a shared library in a bunch of search paths or 41 - // fetching a shared library blob; these side effects require access 42 - // to the same host filesystem that runs GHC, so the HTTP server also 43 - // exposes some rpc endpoints that the browser side can perform 44 - // requests. The server binds to 127.0.0.1 by default for a good 45 - // reason, it doesn't (and shouldn't) have extra logic to try to guard 46 - // against potential malicious requests to scrape your home directory. 47 - // 48 - // So much intro to the message broker part, below are Q/As regarding 49 - // the dynamic linker part: 50 - // 51 - // *** What works right now and what doesn't work yet? 52 - // 53 - // loadDLLs & bytecode interpreter work. Template Haskell & ghci work. 54 - // Profiled dynamic code works. Compiled code and bytecode can all be 55 - // loaded, though the side effects are constrained to what's supported 56 - // by wasi preview1: we map the full host filesystem into wasm cause 57 - // yolo, but things like processes and sockets don't work. 58 - // 59 - // loadArchive/loadObj etc are unsupported and will stay that way. The 60 - // only form of compiled code that can be loaded is wasm shared 61 - // library. There's no code unloading logic. The retain_cafs flag is 62 - // ignored and revertCAFs is a no-op. 63 - // 64 - // JSFFI works. ghci debugger works. 65 - // 66 - // *** What are implications to end users? 67 - // 68 - // Even if you intend to compile fully static wasm modules, you must 69 - // compile everything with -dynamic-too to ensure shared libraries are 70 - // present, otherwise TH doesn't work. In cabal, this is achieved by 71 - // setting `shared: True` in the global cabal config (or under a 72 - // `package *` stanza in your `cabal.project`). You also need to set 73 - // `library-for-ghci: False` since that's unsupported. 74 - // 75 - // *** Why not extend the RTS linker in C like every other new 76 - // platform? 77 - // 78 - // Aside from all the pain of binary manipulation in C, what you can 79 - // do in C on wasm is fairly limited: for instance, you can't manage 80 - // executable memory regions at all. So you need a lot of back and 81 - // forth between C and JS host, totally not worth the extra effort 82 - // just for the sake of making the original C RTS linker interface 83 - // partially work. 84 - // 85 - // *** What kind of wasm shared library can be loaded? What features 86 - // work to what extent? 87 - // 88 - // We support .so files produced by wasm-ld --shared which conforms to 89 - // https://github.com/WebAssembly/tool-conventions/blob/f44d6c526a06a19eec59003a924e475f57f5a6a1/DynamicLinking.md. 90 - // All .so files in the wasm32-wasi sysroot as well as those produced 91 - // by ghc can be loaded. 92 - // 93 - // For simplicity, we don't have any special treatment for weak 94 - // symbols. Any unresolved symbol at link-time will not produce an 95 - // error, they will only trigger an error when they're used at 96 - // run-time and the data/function definition has not been realized by 97 - // then. 98 - // 99 - // There's no dlopen/dlclose etc exposed to the C/C++ world, the 100 - // interfaces here are directly called by JSFFI imports in ghci. 101 - // There's no so unloading logic yet, but it would be fairly easy to 102 - // add once we need it. 103 - // 104 - // No fancy stuff like LD_PRELOAD, LD_LIBRARY_PATH etc. 105 - 106 - import { JSValManager, setImmediate } from "./prelude.mjs"; 107 - import { parseRecord, parseSections } from "./post-link.mjs"; 108 - 109 - // Make a consumer callback from a buffer. See Parser class 110 - // constructor comments for what a consumer is. 111 - function makeBufferConsumer(buf) { 112 - return (len) => { 113 - if (len > buf.length) { 114 - throw new Error("not enough bytes"); 115 - } 116 - 117 - const r = buf.subarray(0, len); 118 - buf = buf.subarray(len); 119 - return r; 120 - }; 121 - } 122 - 123 - // Make a consumer callback from a ReadableStreamDefaultReader. 124 - function makeStreamConsumer(reader) { 125 - let buf = new Uint8Array(); 126 - 127 - return async (len) => { 128 - while (buf.length < len) { 129 - const { done, value } = await reader.read(); 130 - if (done) { 131 - throw new Error("not enough bytes"); 132 - } 133 - if (buf.length === 0) { 134 - buf = value; 135 - continue; 136 - } 137 - const tmp = new Uint8Array(buf.length + value.length); 138 - tmp.set(buf, 0); 139 - tmp.set(value, buf.length); 140 - buf = tmp; 141 - } 142 - 143 - const r = buf.subarray(0, len); 144 - buf = buf.subarray(len); 145 - return r; 146 - }; 147 - } 148 - 149 - // A simple binary parser 150 - class Parser { 151 - #cb; 152 - #consumed = 0; 153 - #limit; 154 - 155 - // cb is a consumer callback that returns a buffer with exact N 156 - // bytes for await cb(N). limit indicates how many bytes the Parser 157 - // may consume at most; it's optional and only used by eof(). 158 - constructor(cb, limit) { 159 - this.#cb = cb; 160 - this.#limit = limit; 161 - } 162 - 163 - eof() { 164 - return this.#consumed >= this.#limit; 165 - } 166 - 167 - async skip(len) { 168 - await this.#cb(len); 169 - this.#consumed += len; 170 - } 171 - 172 - async readUInt8() { 173 - const r = (await this.#cb(1))[0]; 174 - this.#consumed += 1; 175 - return r; 176 - } 177 - 178 - async readULEB128() { 179 - let acc = 0n, 180 - shift = 0n; 181 - while (true) { 182 - const byte = await this.readUInt8(); 183 - acc |= BigInt(byte & 0x7f) << shift; 184 - shift += 7n; 185 - if (byte >> 7 === 0) { 186 - break; 187 - } 188 - } 189 - return Number(acc); 190 - } 191 - 192 - async readBuffer() { 193 - const len = await this.readULEB128(); 194 - const r = await this.#cb(len); 195 - this.#consumed += len; 196 - return r; 197 - } 198 - 199 - async readString() { 200 - return new TextDecoder("utf-8", { fatal: true }).decode( 201 - await this.readBuffer() 202 - ); 203 - } 204 - } 205 - 206 - // Parse the dylink.0 section of a wasm module 207 - async function parseDyLink0(reader) { 208 - const p0 = new Parser(makeStreamConsumer(reader)); 209 - // magic, version 210 - await p0.skip(8); 211 - // section id 212 - console.assert((await p0.readUInt8()) === 0); 213 - const p1_buf = await p0.readBuffer(); 214 - const p1 = new Parser(makeBufferConsumer(p1_buf), p1_buf.length); 215 - // custom section name 216 - console.assert((await p1.readString()) === "dylink.0"); 217 - 218 - const r = { neededSos: [], exportInfo: [], importInfo: [] }; 219 - while (!p1.eof()) { 220 - const subsection_type = await p1.readUInt8(); 221 - const p2_buf = await p1.readBuffer(); 222 - const p2 = new Parser(makeBufferConsumer(p2_buf), p2_buf.length); 223 - switch (subsection_type) { 224 - case 1: { 225 - // WASM_DYLINK_MEM_INFO 226 - r.memSize = await p2.readULEB128(); 227 - r.memP2Align = await p2.readULEB128(); 228 - r.tableSize = await p2.readULEB128(); 229 - r.tableP2Align = await p2.readULEB128(); 230 - break; 231 - } 232 - case 2: { 233 - // WASM_DYLINK_NEEDED 234 - // 235 - // There may be duplicate entries. Not a big deal to not 236 - // dedupe, but why not. 237 - const n = await p2.readULEB128(); 238 - const acc = new Set(); 239 - for (let i = 0; i < n; ++i) { 240 - acc.add(await p2.readString()); 241 - } 242 - r.neededSos = [...acc]; 243 - break; 244 - } 245 - case 3: { 246 - // WASM_DYLINK_EXPORT_INFO 247 - // 248 - // Not actually used yet, kept for completeness in case of 249 - // future usage. 250 - const n = await p2.readULEB128(); 251 - for (let i = 0; i < n; ++i) { 252 - const name = await p2.readString(); 253 - const flags = await p2.readULEB128(); 254 - r.exportInfo.push({ name, flags }); 255 - } 256 - break; 257 - } 258 - case 4: { 259 - // WASM_DYLINK_IMPORT_INFO 260 - // 261 - // Same. 262 - const n = await p2.readULEB128(); 263 - for (let i = 0; i < n; ++i) { 264 - const module = await p2.readString(); 265 - const name = await p2.readString(); 266 - const flags = await p2.readULEB128(); 267 - r.importInfo.push({ module, name, flags }); 268 - } 269 - break; 270 - } 271 - default: { 272 - throw new Error(`unknown subsection type ${subsection_type}`); 273 - } 274 - } 275 - } 276 - 277 - return r; 278 - } 279 - 280 - // Formats a server.address() result to a URL origin with correct 281 - // handling for IPv6 hostname 282 - function originFromServerAddress({ address, family, port }) { 283 - const hostname = family === "IPv6" ? `[${address}]` : address; 284 - return `http://${hostname}:${port}`; 285 - } 286 - 287 - // Browser/node portable code stays above this watermark. 288 - const isNode = Boolean(globalThis?.process?.versions?.node && !globalThis.Deno); 289 - 290 - // Too cumbersome to only import at use sites. Too troublesome to 291 - // factor out browser-only/node-only logic into different modules. For 292 - // now, just make these global let bindings optionally initialized if 293 - // isNode and be careful to not use them in browser-only logic. 294 - let fs, http, path, require, stream, wasi, ws; 295 - 296 - if (isNode) { 297 - require = (await import("node:module")).createRequire(import.meta.url); 298 - 299 - fs = require("fs"); 300 - http = require("http"); 301 - path = require("path"); 302 - stream = require("stream"); 303 - wasi = require("wasi"); 304 - 305 - // Optional npm dependencies loaded via NODE_PATH 306 - try { 307 - ws = require("ws"); 308 - } catch {} 309 - } else { 310 - wasi = await import("https://esm.sh/gh/haskell-wasm/browser_wasi_shim"); 311 - } 312 - 313 - // A subset of dyld logic that can only be run in the host node 314 - // process and has full access to local filesystem 315 - export class DyLDHost { 316 - // Deduped absolute paths of directories where we lookup .so files 317 - #rpaths = new Set(); 318 - 319 - constructor({ outFd, inFd }) { 320 - // When running a non-iserv shared library with node, the DyLDHost 321 - // instance is created without a pair of fds, so skip creation of 322 - // readStream/writeStream, they won't be used anyway 323 - if (!(typeof outFd === "number" && typeof inFd === "number")) { 324 - return; 325 - } 326 - this.readStream = stream.Readable.toWeb( 327 - fs.createReadStream(undefined, { fd: inFd }) 328 - ); 329 - this.writeStream = stream.Writable.toWeb( 330 - fs.createWriteStream(undefined, { fd: outFd }) 331 - ); 332 - } 333 - 334 - close() {} 335 - 336 - installSignalHandlers(cb) { 337 - process.on("SIGINT", cb); 338 - process.on("SIGQUIT", cb); 339 - } 340 - 341 - // removeLibrarySearchPath is a no-op in ghci. If you have a use 342 - // case where it's actually needed, I would like to hear.. 343 - async addLibrarySearchPath(p) { 344 - this.#rpaths.add(path.resolve(p)); 345 - return null; 346 - } 347 - 348 - // f can be either just soname or an absolute path, will be 349 - // canonicalized and checked for file existence here. Throws if 350 - // non-existent. 351 - async findSystemLibrary(f) { 352 - if (path.isAbsolute(f)) { 353 - await fs.promises.access(f, fs.promises.constants.R_OK); 354 - return f; 355 - } 356 - const r = ( 357 - await Promise.allSettled( 358 - [...this.#rpaths].map(async (p) => { 359 - const r = path.resolve(p, f); 360 - await fs.promises.access(r, fs.promises.constants.R_OK); 361 - return r; 362 - }) 363 - ) 364 - ).find(({ status }) => status === "fulfilled"); 365 - console.assert( 366 - r, 367 - `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}` 368 - ); 369 - return r.value; 370 - } 371 - 372 - // returns a Response for a .so absolute path 373 - async fetchWasm(p) { 374 - return new Response(stream.Readable.toWeb(fs.createReadStream(p)), { 375 - headers: { "Content-Type": "application/wasm" }, 376 - }); 377 - } 378 - } 379 - 380 - // Runs in the browser and uses the in-memory vfs, doesn't do any RPC 381 - // calls 382 - export class DyLDBrowserHost { 383 - // Deduped absolute paths of directories where we lookup .so files 384 - #rpaths = new Set(); 385 - // The PreopenDirectory object of the root filesystem 386 - rootfs; 387 - // Continuations to output a single line to stdout/stderr 388 - stdout; 389 - stderr; 390 - 391 - // Given canonicalized absolute file path, returns the File object, 392 - // or null if absent 393 - #readFile(p) { 394 - const { ret, entry } = this.rootfs.dir.get_entry_for_path({ 395 - parts: p.split("/").filter((tok) => tok !== ""), 396 - is_dir: false, 397 - }); 398 - return ret === 0 ? entry : null; 399 - } 400 - 401 - constructor({ rootfs, stdout, stderr }) { 402 - this.rootfs = rootfs 403 - ? rootfs 404 - : new wasi.PreopenDirectory("/", [["tmp", new wasi.Directory([])]]); 405 - this.stdout = stdout ? stdout : (msg) => console.info(msg); 406 - this.stderr = stderr ? stderr : (msg) => console.warn(msg); 407 - } 408 - 409 - // p must be canonicalized absolute path 410 - async addLibrarySearchPath(p) { 411 - this.#rpaths.add(p); 412 - return null; 413 - } 414 - 415 - async findSystemLibrary(f) { 416 - if (f.startsWith("/")) { 417 - if (this.#readFile(f)) { 418 - return f; 419 - } 420 - throw new Error(`findSystemLibrary(${f}): not found in /`); 421 - } 422 - 423 - for (const rpath of this.#rpaths) { 424 - const r = `${rpath}/${f}`; 425 - if (this.#readFile(r)) { 426 - return r; 427 - } 428 - } 429 - 430 - throw new Error( 431 - `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}` 432 - ); 433 - } 434 - 435 - async fetchWasm(p) { 436 - const entry = this.#readFile(p); 437 - const r = new Response(entry.data, { 438 - headers: { "Content-Type": "application/wasm" }, 439 - }); 440 - // It's only fetched once, take the chance to prune it in vfs to save memory 441 - entry.data = new Uint8Array(); 442 - return r; 443 - } 444 - } 445 - 446 - // Fulfill the same functionality as DyLDHost by doing fetch() calls 447 - // to respective RPC endpoints of a host http server. Also manages 448 - // WebSocket connections back to host. 449 - export class DyLDRPC { 450 - #origin; 451 - #wsPipe; 452 - #wsSig; 453 - #redirectWasiConsole; 454 - #wsStdout; 455 - #wsStderr; 456 - 457 - constructor({ origin, redirectWasiConsole }) { 458 - this.#origin = origin; 459 - 460 - const ws_url = this.#origin.replace("http://", "ws://"); 461 - 462 - this.#wsPipe = new WebSocket(ws_url, "pipe"); 463 - this.#wsPipe.binaryType = "arraybuffer"; 464 - 465 - this.readStream = new ReadableStream({ 466 - start: (controller) => { 467 - this.#wsPipe.addEventListener("message", (ev) => 468 - controller.enqueue(new Uint8Array(ev.data)) 469 - ); 470 - this.#wsPipe.addEventListener("error", (ev) => controller.error(ev)); 471 - this.#wsPipe.addEventListener("close", () => controller.close()); 472 - }, 473 - }); 474 - 475 - this.writeStream = new WritableStream({ 476 - start: (controller) => { 477 - this.#wsPipe.addEventListener("error", (ev) => controller.error(ev)); 478 - }, 479 - write: (buf) => this.#wsPipe.send(buf), 480 - }); 481 - 482 - this.#wsSig = new WebSocket(ws_url, "sig"); 483 - this.#wsSig.binaryType = "arraybuffer"; 484 - 485 - this.#redirectWasiConsole = redirectWasiConsole; 486 - if (redirectWasiConsole) { 487 - this.#wsStdout = new WebSocket(ws_url, "stdout"); 488 - this.#wsStderr = new WebSocket(ws_url, "stderr"); 489 - } 490 - 491 - this.opened = Promise.all( 492 - (redirectWasiConsole 493 - ? [this.#wsPipe, this.#wsSig, this.#wsStdout, this.#wsStderr] 494 - : [this.#wsPipe, this.#wsSig] 495 - ).map( 496 - (ws) => 497 - new Promise((res, rej) => { 498 - ws.addEventListener("open", res); 499 - ws.addEventListener("error", rej); 500 - }) 501 - ) 502 - ); 503 - } 504 - 505 - close() { 506 - this.#wsPipe.close(); 507 - this.#wsSig.close(); 508 - if (this.#redirectWasiConsole) { 509 - this.#wsStdout.close(); 510 - this.#wsStderr.close(); 511 - } 512 - } 513 - 514 - async #rpc(endpoint, ...args) { 515 - const r = await fetch(`${this.#origin}/rpc/${endpoint}`, { 516 - method: "POST", 517 - headers: { 518 - "Content-Type": "application/json", 519 - }, 520 - body: JSON.stringify(args), 521 - }); 522 - if (!r.ok) { 523 - throw new Error(await r.text()); 524 - } 525 - return r.json(); 526 - } 527 - 528 - installSignalHandlers(cb) { 529 - this.#wsSig.addEventListener("message", cb); 530 - } 531 - 532 - async addLibrarySearchPath(p) { 533 - return this.#rpc("addLibrarySearchPath", p); 534 - } 535 - 536 - async findSystemLibrary(f) { 537 - return this.#rpc("findSystemLibrary", f); 538 - } 539 - 540 - async fetchWasm(p) { 541 - return fetch(`${this.#origin}/fs${p}`); 542 - } 543 - 544 - stdout(msg) { 545 - if (this.#redirectWasiConsole) { 546 - this.#wsStdout.send(msg); 547 - } else { 548 - console.info(msg); 549 - } 550 - } 551 - 552 - stderr(msg) { 553 - if (this.#redirectWasiConsole) { 554 - this.#wsStderr.send(msg); 555 - } else { 556 - console.warn(msg); 557 - } 558 - } 559 - } 560 - 561 - // Actual implementation of endpoints used by DyLDRPC 562 - class DyLDRPCServer { 563 - #dyldHost; 564 - #server; 565 - #wss; 566 - 567 - constructor({ 568 - host, 569 - port, 570 - dyldPath, 571 - searchDirs, 572 - mainSoPath, 573 - outFd, 574 - inFd, 575 - args, 576 - redirectWasiConsole, 577 - }) { 578 - this.#dyldHost = new DyLDHost({ outFd, inFd }); 579 - 580 - this.#server = http.createServer(async (req, res) => { 581 - const origin = originFromServerAddress(await this.listening); 582 - 583 - res.setHeader("Access-Control-Allow-Origin", "*"); 584 - res.setHeader("Access-Control-Allow-Headers", "*"); 585 - 586 - if (req.method === "OPTIONS") { 587 - res.writeHead(204); 588 - res.end(); 589 - return; 590 - } 591 - 592 - if (req.url === "/main.html") { 593 - res.writeHead(200, { 594 - "Content-Type": "text/html", 595 - }); 596 - res.end( 597 - ` 598 - <!DOCTYPE html> 599 - <title>wasm ghci</title> 600 - <script type="module" src="./main.js"></script> 601 - ` 602 - ); 603 - return; 604 - } 605 - 606 - if (req.url === "/main.js") { 607 - res.writeHead(200, { 608 - "Content-Type": "application/javascript", 609 - }); 610 - res.end( 611 - ` 612 - import { DyLDRPC, main } from "./fs${dyldPath}"; 613 - const args = ${JSON.stringify({ searchDirs, mainSoPath, args, isIserv: true })}; 614 - args.rpc = new DyLDRPC({origin: "${origin}", redirectWasiConsole: ${redirectWasiConsole}}); 615 - args.rpc.opened.then(() => main(args)); 616 - ` 617 - ); 618 - return; 619 - } 620 - 621 - if (req.url.startsWith("/fs")) { 622 - const p = req.url.replace("/fs", ""); 623 - 624 - res.setHeader( 625 - "Content-Type", 626 - { 627 - ".mjs": "application/javascript", 628 - ".so": "application/wasm", 629 - }[path.extname(p)] || "application/octet-stream" 630 - ); 631 - 632 - res.writeHead(200); 633 - fs.createReadStream(p).pipe(res); 634 - return; 635 - } 636 - 637 - if (req.url.startsWith("/rpc")) { 638 - const endpoint = req.url.replace("/rpc/", ""); 639 - 640 - let body = ""; 641 - for await (const chunk of req) { 642 - body += chunk; 643 - } 644 - 645 - res.writeHead(200, { 646 - "Content-Type": "application/json", 647 - }); 648 - res.end( 649 - JSON.stringify(await this.#dyldHost[endpoint](...JSON.parse(body))) 650 - ); 651 - return; 652 - } 653 - 654 - res.writeHead(404, { 655 - "Content-Type": "text/plain", 656 - }); 657 - res.end("not found"); 658 - }); 659 - 660 - this.closed = new Promise((res) => this.#server.on("close", res)); 661 - 662 - this.#wss = new ws.WebSocketServer({ server: this.#server }); 663 - this.#wss.on("connection", (ws) => { 664 - ws.addEventListener("error", () => { 665 - this.#wss.close(); 666 - this.#server.close(); 667 - }); 668 - 669 - ws.addEventListener("close", () => { 670 - this.#wss.close(); 671 - this.#server.close(); 672 - }); 673 - 674 - if (ws.protocol === "pipe") { 675 - (async () => { 676 - for await (const buf of this.#dyldHost.readStream) { 677 - ws.send(buf); 678 - } 679 - })(); 680 - const writer = this.#dyldHost.writeStream.getWriter(); 681 - ws.addEventListener("message", (ev) => 682 - writer.write(new Uint8Array(ev.data)) 683 - ); 684 - return; 685 - } 686 - 687 - if (ws.protocol === "sig") { 688 - this.#dyldHost.installSignalHandlers(() => ws.send(new Uint8Array(0))); 689 - return; 690 - } 691 - 692 - if (ws.protocol === "stdout") { 693 - ws.addEventListener("message", (ev) => console.info(ev.data)); 694 - return; 695 - } 696 - 697 - if (ws.protocol === "stderr") { 698 - ws.addEventListener("message", (ev) => console.warn(ev.data)); 699 - return; 700 - } 701 - 702 - throw new Error(`unknown protocol ${ws.protocol}`); 703 - }); 704 - 705 - this.listening = new Promise((res) => 706 - this.#server.listen({ host, port }, () => res(this.#server.address())) 707 - ); 708 - } 709 - } 710 - 711 - // The real stuff 712 - class DyLD { 713 - // Wasm page size. 714 - static #pageSize = 0x10000; 715 - 716 - // Placeholder value of GOT.mem addresses that must be imported 717 - // first and later modified to be the correct relocated pointer. 718 - // This value is 0xffffffff subtracts one page, so hopefully any 719 - // memory access near this address will trap immediately. 720 - // 721 - // In JS API i32 is signed, hence this layer of redirection. 722 - static #poison = (0xffffffff - DyLD.#pageSize) | 0; 723 - 724 - // When processing exports, skip the following ones since they're 725 - // generated by wasm-ld. 726 - static #ldGeneratedExportNames = new Set([ 727 - "_initialize", 728 - "__wasm_apply_data_relocs", 729 - "__wasm_apply_global_relocs", 730 - "__wasm_call_ctors", 731 - ]); 732 - 733 - // Handles RPC logic back to host in a browser, or just do plain 734 - // function calls in node 735 - #rpc; 736 - 737 - // The WASI instance to provide wasi imports, shared across all wasm 738 - // instances 739 - #wasi; 740 - 741 - // Wasm memory & table 742 - #memory = new WebAssembly.Memory({ initial: 1 }); 743 - 744 - #table = new WebAssembly.Table({ element: "anyfunc", initial: 1 }); 745 - // First free slot, might be invalid when it advances to #table.length 746 - #tableFree = 1; 747 - // See Note [The evil wasm table grower] 748 - #tableGrowInstance = new WebAssembly.Instance( 749 - new WebAssembly.Module( 750 - new Uint8Array([ 751 - 0, 97, 115, 109, 1, 0, 0, 0, 1, 6, 1, 96, 1, 127, 1, 127, 2, 35, 1, 3, 752 - 101, 110, 118, 25, 95, 95, 105, 110, 100, 105, 114, 101, 99, 116, 95, 753 - 102, 117, 110, 99, 116, 105, 111, 110, 95, 116, 97, 98, 108, 101, 1, 754 - 112, 0, 0, 3, 2, 1, 0, 7, 31, 1, 27, 95, 95, 103, 104, 99, 95, 119, 97, 755 - 115, 109, 95, 106, 115, 102, 102, 105, 95, 116, 97, 98, 108, 101, 95, 756 - 103, 114, 111, 119, 0, 0, 10, 11, 1, 9, 0, 208, 112, 32, 0, 252, 15, 0, 757 - 11, 758 - ]) 759 - ), 760 - { env: { __indirect_function_table: this.#table } } 761 - ); 762 - 763 - // __stack_pointer 764 - #sp = new WebAssembly.Global( 765 - { 766 - value: "i32", 767 - mutable: true, 768 - }, 769 - DyLD.#pageSize 770 - ); 771 - 772 - // The JSVal manager 773 - #jsvalManager = new JSValManager(); 774 - 775 - // sonames of loaded sos. 776 - // 777 - // Note that "soname" is just xxx.so as in file path, not actually 778 - // parsed from a section in .so file. wasm-ld does accept 779 - // --soname=<value>, but it just writes the module name to the name 780 - // section, which can be stripped by wasm-opt and such. We do not 781 - // rely on the name section at all. 782 - // 783 - // Invariant: soname is globally unique! 784 - #loadedSos = new Set(); 785 - 786 - // Mapping from export names to export funcs. It's also passed as 787 - // __exports in JSFFI code, hence the "memory" special field. 788 - exportFuncs = { memory: this.#memory }; 789 - 790 - // The FinalizationRegistry used by JSFFI. 791 - #finalizationRegistry = new FinalizationRegistry((sp) => 792 - this.exportFuncs.rts_freeStablePtr(sp) 793 - ); 794 - 795 - // The GOT.func table. 796 - #gotFunc = {}; 797 - 798 - // The GOT.mem table. By wasm dylink convention, a wasm global 799 - // exported by .so is always assumed to be a GOT.mem entry, not a 800 - // re-exported actual wasm global. 801 - #gotMem = {}; 802 - 803 - // Global STG registers 804 - #regs = {}; 805 - 806 - // Note [The evil wasm table grower] 807 - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 808 - // We need to grow the wasm table as we load shared libraries in 809 - // wasm dyld. We used to directly call the table.grow() JS API, 810 - // which works as expected in Firefox/Chrome, but unfortunately, 811 - // WebKit's implementation of the table.grow() JS API is broken: 812 - // https://bugs.webkit.org/show_bug.cgi?id=290681, which means that 813 - // the wasm dyld simply does not work in WebKit-based browsers like 814 - // Safari. 815 - // 816 - // Now, one simple workaround would be to avoid growing the table at 817 - // all: just allocate a huge table upfront (current limitation 818 - // agreed by all vendors is 10000000). To avoid unnecessary space 819 - // waste on non-WebKit platforms, we could additionally check 820 - // navigator.userAgent against some regexes and only allocate 821 - // fixed-length table when there's no blink/gecko mention. But this 822 - // is fragile and gross, and it's better to stick to a uniform code 823 - // path for all browsers. 824 - // 825 - // Fortunately, it turns out the table.grow wasm instruction work as 826 - // expected in WebKit! So we can invoke a wasm function that grows 827 - // the table for us. But don't open a champagne yet, where would 828 - // that wasm function come from? It can't be put into RTS, or even 829 - // libc.so, because loading those libraries would require growing 830 - // the table in the first place! Or perhaps, reserve a table upfront 831 - // that's just large enough to load RTS and then we can access that 832 - // function for subsequent table grows? But then we need to 833 - // experiment for a reasonable initial size, and add a magic number 834 - // here, which is also fragile and gross and not future-proof! 835 - // 836 - // So this special wasm function needs to live in a single wasm 837 - // module, which is loaded before we load anything else. The full 838 - // source code for this module is: 839 - // 840 - // (module 841 - // (type (func (param i32) (result i32))) 842 - // (import "env" "__indirect_function_table" (table 0 funcref)) 843 - // (export "__ghc_wasm_jsffi_table_grow" (func 0)) 844 - // (func (type 0) (param i32) (result i32) 845 - // ref.null func 846 - // local.get 0 847 - // table.grow 0 848 - // ) 849 - // ) 850 - // 851 - // This module is 103 bytes so that we can inline its blob in dyld, 852 - // and use the usually discouraged synchronous 853 - // WebAssembly.Instance/WebAssembly.Module constructors to load it. 854 - // On non-WebKit platforms, growing tables this way would introduce 855 - // a bit of extra JS/Wasm interop overhead, which can be amplified 856 - // as we used to call table.grow(1, foo) for every GOT.func item. 857 - // Therefore, unless we're about to exceed the hard limit of table 858 - // size, we now grow the table exponentially, and use bump 859 - // allocation to calculate the table index to be returned. 860 - // Exponential growth is only implemented to minimize the JS/Wasm 861 - // interop overhead when calling __ghc_wasm_jsffi_table_grow; 862 - // V8/SpiderMonkey/WebKit already do their own exponential growth of 863 - // the table's backing buffer in their table growth logic. 864 - // 865 - // Invariants: n >= 0; when v is non-null, n === 1 866 - #tableGrow(n, v) { 867 - const prev_free = this.#tableFree; 868 - if (prev_free + n > this.#table.length) { 869 - const min_delta = prev_free + n - this.#table.length; 870 - const delta = Math.max(min_delta, this.#table.length); 871 - this.#tableGrowInstance.exports.__ghc_wasm_jsffi_table_grow( 872 - this.#table.length + delta <= 10000000 ? delta : min_delta 873 - ); 874 - } 875 - if (v) { 876 - this.#table.set(prev_free, v); 877 - } 878 - this.#tableFree += n; 879 - return prev_free; 880 - } 881 - 882 - constructor({ args, rpc }) { 883 - this.#rpc = rpc; 884 - 885 - if (isNode) { 886 - this.#wasi = new wasi.WASI({ 887 - version: "preview1", 888 - args, 889 - env: { PATH: "", PWD: process.cwd() }, 890 - preopens: { "/": "/" }, 891 - }); 892 - } else { 893 - this.#wasi = new wasi.WASI( 894 - args, 895 - [], 896 - [ 897 - new wasi.OpenFile( 898 - new wasi.File(new Uint8Array(), { readonly: true }) 899 - ), 900 - wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stdout(msg)), 901 - wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stderr(msg)), 902 - // for ghci browser mode, default to an empty rootfs with 903 - // /tmp 904 - this.#rpc instanceof DyLDBrowserHost 905 - ? this.#rpc.rootfs 906 - : new wasi.PreopenDirectory("/", [["tmp", new wasi.Directory([])]]), 907 - ], 908 - { debug: false } 909 - ); 910 - } 911 - 912 - // Both wasi implementations we use provide 913 - // wasi.initialize(instance) to initialize a wasip1 reactor 914 - // module. However, instance does not really need to be a 915 - // WebAssembly.Instance object; the wasi implementations only need 916 - // to access instance.exports.memory for the wasi syscalls to 917 - // work. 918 - // 919 - // Given we'll reuse the same wasi object across different 920 - // WebAssembly.Instance objects anyway and 921 - // wasi.initialize(instance) can't be called more than once, we 922 - // use this simple trick and pass a fake instance object that 923 - // contains just enough info for the wasi implementation to 924 - // initialize its internal state. Later when we load each wasm 925 - // shared library, we can just manually invoke their 926 - // initialization functions. 927 - this.#wasi.initialize({ 928 - exports: { 929 - memory: this.#memory, 930 - }, 931 - }); 932 - 933 - // Keep this in sync with rts/wasm/Wasm.S! 934 - for (let i = 1; i <= 10; ++i) { 935 - this.#regs[`__R${i}`] = new WebAssembly.Global({ 936 - value: "i32", 937 - mutable: true, 938 - }); 939 - } 940 - 941 - for (let i = 1; i <= 6; ++i) { 942 - this.#regs[`__F${i}`] = new WebAssembly.Global({ 943 - value: "f32", 944 - mutable: true, 945 - }); 946 - } 947 - 948 - for (let i = 1; i <= 6; ++i) { 949 - this.#regs[`__D${i}`] = new WebAssembly.Global({ 950 - value: "f64", 951 - mutable: true, 952 - }); 953 - } 954 - 955 - this.#regs.__L1 = new WebAssembly.Global({ value: "i64", mutable: true }); 956 - 957 - for (const k of ["__Sp", "__SpLim", "__Hp", "__HpLim"]) { 958 - this.#regs[k] = new WebAssembly.Global({ value: "i32", mutable: true }); 959 - } 960 - } 961 - 962 - async addLibrarySearchPath(p) { 963 - return this.#rpc.addLibrarySearchPath(p); 964 - } 965 - 966 - async findSystemLibrary(f) { 967 - return this.#rpc.findSystemLibrary(f); 968 - } 969 - 970 - // When we do loadDLLs, we first perform "downsweep" which return a 971 - // toposorted array of dependencies up to itself, then sequentially 972 - // load the downsweep result. 973 - // 974 - // The rationale of a separate downsweep phase, instead of a simple 975 - // recursive loadDLLs function is: V8 delegates async 976 - // WebAssembly.compile to a background worker thread pool. To 977 - // maintain consistent internal linker state, we *must* load each so 978 - // file sequentially, but it's okay to kick off compilation asap, 979 - // store the Promise in downsweep result and await for the actual 980 - // WebAssembly.Module in loadDLLs logic. This way we can harness some 981 - // background parallelism. 982 - async #downsweep(p) { 983 - const toks = p.split("/"); 984 - 985 - const soname = toks[toks.length - 1]; 986 - 987 - if (this.#loadedSos.has(soname)) { 988 - return []; 989 - } 990 - 991 - // Do this before loading dependencies to break potential cycles. 992 - this.#loadedSos.add(soname); 993 - 994 - if (p.startsWith("/")) { 995 - // GHC may attempt to load libghc_tmp_2.so that needs 996 - // libghc_tmp_1.so in a temporary directory without adding that 997 - // directory via addLibrarySearchPath 998 - toks.pop(); 999 - await this.addLibrarySearchPath(toks.join("/")); 1000 - } else { 1001 - p = await this.findSystemLibrary(p); 1002 - } 1003 - 1004 - const resp = await this.#rpc.fetchWasm(p); 1005 - const resp2 = resp.clone(); 1006 - const modp = WebAssembly.compileStreaming(resp); 1007 - // Parse dylink.0 from the raw buffer, not via 1008 - // WebAssembly.Module.customSections(). This should return asap 1009 - // without waiting for rest of the wasm module binary data. 1010 - const r = await parseDyLink0(resp2.body.getReader()); 1011 - r.modp = modp; 1012 - r.soname = soname; 1013 - let acc = []; 1014 - for (const dep of r.neededSos) { 1015 - acc.push(...(await this.#downsweep(dep))); 1016 - } 1017 - acc.push(r); 1018 - return acc; 1019 - } 1020 - 1021 - // Batch load multiple DLLs in one go. 1022 - // Accepts a NUL-delimited string of paths to avoid array marshalling. 1023 - // Each path can be absolute or a soname; dependency resolution is 1024 - // performed across the full set to enable maximal parallel compile 1025 - // while maintaining sequential instantiation order. 1026 - async loadDLLs(packed) { 1027 - // Normalize input to an array of strings. When called from Haskell 1028 - // we pass a single JSString containing NUL-separated paths. 1029 - const paths = ( 1030 - typeof packed === "string" 1031 - ? packed.length === 0 1032 - ? [] 1033 - : packed.split("\0") 1034 - : [packed] 1035 - ) // tolerate an accidental single path object 1036 - .filter((s) => s.length > 0) 1037 - .reverse(); 1038 - 1039 - // Compute a single downsweep plan for the whole batch. 1040 - // Note: #downsweep mutates #loadedSos to break cycles and dedup. 1041 - const plan = []; 1042 - for (const p of paths) { 1043 - plan.push(...(await this.#downsweep(p))); 1044 - } 1045 - 1046 - for (const { 1047 - memSize, 1048 - memP2Align, 1049 - tableSize, 1050 - tableP2Align, 1051 - modp, 1052 - soname, 1053 - } of plan) { 1054 - const import_obj = { 1055 - wasi_snapshot_preview1: this.#wasi.wasiImport, 1056 - env: { 1057 - memory: this.#memory, 1058 - __indirect_function_table: this.#table, 1059 - __stack_pointer: this.#sp, 1060 - ...this.exportFuncs, 1061 - }, 1062 - regs: this.#regs, 1063 - // Keep this in sync with post-link.mjs! 1064 - ghc_wasm_jsffi: { 1065 - newJSVal: (v) => this.#jsvalManager.newJSVal(v), 1066 - getJSVal: (k) => this.#jsvalManager.getJSVal(k), 1067 - freeJSVal: (k) => this.#jsvalManager.freeJSVal(k), 1068 - scheduleWork: () => setImmediate(this.exportFuncs.rts_schedulerLoop), 1069 - }, 1070 - "GOT.mem": this.#gotMem, 1071 - "GOT.func": this.#gotFunc, 1072 - }; 1073 - 1074 - // __memory_base & __table_base, different for each .so 1075 - let memory_base; 1076 - let table_base = this.#tableGrow(tableSize); 1077 - console.assert(tableP2Align === 0); 1078 - 1079 - // libc.so is always the first one to be ever loaded and has VIP 1080 - // treatment. It will never be unloaded even if we support 1081 - // unloading in the future. Nor do we support multiple libc.so 1082 - // in the same address space. 1083 - if (soname === "libc.so") { 1084 - // Starting from 0x0: one page of C stack, then global data 1085 - // segments of libc.so, then one page space between 1086 - // __heap_base/__heap_end so that dlmalloc can initialize 1087 - // global state. wasm-ld aligns __heap_base to page sized so 1088 - // we follow suit. 1089 - console.assert(memP2Align <= Math.log2(DyLD.#pageSize)); 1090 - memory_base = DyLD.#pageSize; 1091 - const data_pages = Math.ceil(memSize / DyLD.#pageSize); 1092 - this.#memory.grow(data_pages + 1); 1093 - 1094 - this.#gotMem.__heap_base = new WebAssembly.Global( 1095 - { value: "i32", mutable: true }, 1096 - DyLD.#pageSize * (1 + data_pages) 1097 - ); 1098 - this.#gotMem.__heap_end = new WebAssembly.Global( 1099 - { value: "i32", mutable: true }, 1100 - DyLD.#pageSize * (1 + data_pages + 1) 1101 - ); 1102 - } else { 1103 - // TODO: this would also be __dso_handle@GOT, in case we 1104 - // implement so unloading logic in the future. 1105 - memory_base = this.exportFuncs.aligned_alloc(1 << memP2Align, memSize); 1106 - } 1107 - 1108 - import_obj.env.__memory_base = new WebAssembly.Global( 1109 - { value: "i32", mutable: false }, 1110 - memory_base 1111 - ); 1112 - import_obj.env.__table_base = new WebAssembly.Global( 1113 - { value: "i32", mutable: false }, 1114 - table_base 1115 - ); 1116 - 1117 - const mod = await modp; 1118 - 1119 - // Fulfill the ghc_wasm_jsffi imports. Use new Function() 1120 - // instead of eval() to prevent bindings in this local scope to 1121 - // be accessed by JSFFI code snippets. See Note [Variable passing in JSFFI] 1122 - // for what's going on here. 1123 - Object.assign( 1124 - import_obj.ghc_wasm_jsffi, 1125 - new Function( 1126 - "__exports", 1127 - "__ghc_wasm_jsffi_dyld", 1128 - "__ghc_wasm_jsffi_finalization_registry", 1129 - "return {".concat( 1130 - ...parseSections(mod).map( 1131 - (rec) => `${rec[0]}: ${parseRecord(rec)}, ` 1132 - ), 1133 - "};" 1134 - ) 1135 - )(this.exportFuncs, this, this.#finalizationRegistry) 1136 - ); 1137 - 1138 - // Fulfill the rest of the imports 1139 - for (const { module, name, kind } of WebAssembly.Module.imports(mod)) { 1140 - // Already there, no handling required 1141 - if (import_obj[module] && import_obj[module][name]) { 1142 - continue; 1143 - } 1144 - 1145 - // Add a lazy function stub in env, but don't put it into 1146 - // exportFuncs yet. This lazy binding is only effective for 1147 - // the current so, since env is a transient object created on 1148 - // the fly. 1149 - if (module === "env" && kind === "function") { 1150 - import_obj.env[name] = (...args) => { 1151 - if (!this.exportFuncs[name]) { 1152 - throw new WebAssembly.RuntimeError( 1153 - `non-existent function ${name}` 1154 - ); 1155 - } 1156 - return this.exportFuncs[name](...args); 1157 - }; 1158 - continue; 1159 - } 1160 - 1161 - // Add a lazy GOT.mem entry with poison value, in the hope 1162 - // that if they're used before being resolved with real 1163 - // addresses, a memory trap will be triggered immediately. 1164 - if (module === "GOT.mem" && kind === "global") { 1165 - this.#gotMem[name] = new WebAssembly.Global( 1166 - { value: "i32", mutable: true }, 1167 - DyLD.#poison 1168 - ); 1169 - continue; 1170 - } 1171 - 1172 - // Missing entry in GOT.func table, could be already defined 1173 - // or not 1174 - if (module === "GOT.func" && kind === "global") { 1175 - // A dependency has exported the function, just create the 1176 - // entry on the fly 1177 - if (this.exportFuncs[name]) { 1178 - this.#gotFunc[name] = new WebAssembly.Global( 1179 - { value: "i32", mutable: true }, 1180 - this.#tableGrow(1, this.exportFuncs[name]) 1181 - ); 1182 - continue; 1183 - } 1184 - 1185 - // Can't find this function, so poison it like GOT.mem. 1186 - // TODO: when wasm type reflection is widely available in 1187 - // browsers, use the WebAssembly.Function constructor to 1188 - // dynamically create a stub function that does better error 1189 - // reporting 1190 - this.#gotFunc[name] = new WebAssembly.Global( 1191 - { value: "i32", mutable: true }, 1192 - DyLD.#poison 1193 - ); 1194 - continue; 1195 - } 1196 - 1197 - throw new Error( 1198 - `cannot handle import ${module}.${name} with kind ${kind}` 1199 - ); 1200 - } 1201 - 1202 - // Fingers crossed... 1203 - const instance = await WebAssembly.instantiate(mod, import_obj); 1204 - 1205 - // Process the exports 1206 - for (const k in instance.exports) { 1207 - // Boring stuff 1208 - if (DyLD.#ldGeneratedExportNames.has(k)) { 1209 - continue; 1210 - } 1211 - 1212 - // Invariant: each function symbol can be defined only once. 1213 - // This is incorrect for weak symbols which are allowed to 1214 - // appear multiple times but this is sufficient in practice. 1215 - console.assert( 1216 - !this.exportFuncs[k], 1217 - `duplicate symbol ${k} when loading ${soname}` 1218 - ); 1219 - 1220 - const v = instance.exports[k]; 1221 - 1222 - if (typeof v === "function") { 1223 - this.exportFuncs[k] = v; 1224 - // If there's a lazy GOT.func entry, put the function in the 1225 - // table and fulfill the entry. Otherwise no need to do 1226 - // anything, if it's required later a GOT.func entry will be 1227 - // created on demand. 1228 - if (this.#gotFunc[k]) { 1229 - const got = this.#gotFunc[k]; 1230 - if (got.value === DyLD.#poison) { 1231 - const idx = this.#tableGrow(1, v); 1232 - got.value = idx; 1233 - } else { 1234 - this.#table.set(got.value, v); 1235 - } 1236 - } 1237 - continue; 1238 - } 1239 - 1240 - // It's a GOT.mem entry 1241 - if (v instanceof WebAssembly.Global) { 1242 - const addr = v.value + memory_base; 1243 - if (this.#gotMem[k]) { 1244 - console.assert(this.#gotMem[k].value === DyLD.#poison); 1245 - this.#gotMem[k].value = addr; 1246 - } else { 1247 - this.#gotMem[k] = new WebAssembly.Global( 1248 - { value: "i32", mutable: true }, 1249 - addr 1250 - ); 1251 - } 1252 - continue; 1253 - } 1254 - 1255 - throw new Error(`cannot handle export ${k} ${v}`); 1256 - } 1257 - 1258 - // See 1259 - // https://gitlab.haskell.org/haskell-wasm/llvm-project/-/blob/release/21.x/lld/wasm/Writer.cpp#L1451, 1260 - // __wasm_apply_data_relocs is now optional so only call it if 1261 - // it exists (we know for sure it exists for libc.so though). 1262 - // There's also __wasm_init_memory (not relevant yet, we don't 1263 - // use passive segments) & __wasm_apply_global_relocs but 1264 - // those are included in the start function and should have 1265 - // been called upon instantiation, see 1266 - // Writer::createStartFunction(). 1267 - if (instance.exports.__wasm_apply_data_relocs) { 1268 - instance.exports.__wasm_apply_data_relocs(); 1269 - } 1270 - 1271 - instance.exports._initialize(); 1272 - } 1273 - } 1274 - 1275 - lookupSymbol(sym) { 1276 - if (this.#gotMem[sym] && this.#gotMem[sym].value !== DyLD.#poison) { 1277 - return this.#gotMem[sym].value; 1278 - } 1279 - if (this.#gotFunc[sym] && this.#gotFunc[sym].value !== DyLD.#poison) { 1280 - return this.#gotFunc[sym].value; 1281 - } 1282 - // Not in GOT.func yet, create the entry on demand 1283 - if (this.exportFuncs[sym]) { 1284 - console.assert(!this.#gotFunc[sym]); 1285 - const addr = this.#tableGrow(1, this.exportFuncs[sym]); 1286 - this.#gotFunc[sym] = new WebAssembly.Global( 1287 - { value: "i32", mutable: true }, 1288 - addr 1289 - ); 1290 - return addr; 1291 - } 1292 - return 0; 1293 - } 1294 - } 1295 - 1296 - // The main entry point of dyld that may be run on node/browser, and 1297 - // may run either iserv defaultMain from the ghci library or an 1298 - // alternative entry point from another shared library 1299 - export async function main({ 1300 - rpc, // Handle the side effects of DyLD 1301 - searchDirs, // Initial library search directories 1302 - mainSoPath, // Could also be another shared library that's actually not ghci 1303 - args, // WASI argv starting with the executable name. +RTS etc will be respected 1304 - isIserv, // set to true when running iserv defaultServer 1305 - }) { 1306 - try { 1307 - const dyld = new DyLD({ 1308 - args, 1309 - rpc, 1310 - }); 1311 - for (const libdir of searchDirs) { 1312 - await dyld.addLibrarySearchPath(libdir); 1313 - } 1314 - await dyld.loadDLLs(mainSoPath); 1315 - 1316 - // At this point, rts/ghc-internal are loaded, perform wasm shared 1317 - // library specific RTS startup logic, see Note [JSFFI initialization] 1318 - dyld.exportFuncs.__ghc_wasm_jsffi_init(); 1319 - 1320 - // We're not running iserv, just return the dyld instance so user 1321 - // could use it to invoke their exported functions, and don't 1322 - // perform cleanup (see finally block) 1323 - if (!isIserv) { 1324 - return dyld; 1325 - } 1326 - 1327 - // iserv-specific logic follows 1328 - const reader = rpc.readStream.getReader(); 1329 - const writer = rpc.writeStream.getWriter(); 1330 - 1331 - const cb_sig = (cb) => { 1332 - rpc.installSignalHandlers(cb); 1333 - }; 1334 - 1335 - const cb_recv = async () => { 1336 - const { done, value } = await reader.read(); 1337 - if (done) { 1338 - throw new Error("not enough bytes"); 1339 - } 1340 - return value; 1341 - }; 1342 - const cb_send = (buf) => { 1343 - writer.write(new Uint8Array(buf)); 1344 - }; 1345 - 1346 - return await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send); 1347 - } finally { 1348 - if (isIserv) { 1349 - rpc.close(); 1350 - } 1351 - } 1352 - } 1353 - 1354 - // node-specific iserv-specific logic 1355 - async function nodeMain({ searchDirs, mainSoPath, outFd, inFd, args }) { 1356 - if (!process.env.GHCI_BROWSER) { 1357 - const rpc = new DyLDHost({ outFd, inFd }); 1358 - return await main({ 1359 - rpc, 1360 - searchDirs, 1361 - mainSoPath, 1362 - args, 1363 - isIserv: true, 1364 - }); 1365 - } 1366 - 1367 - if (!ws) { 1368 - throw new Error( 1369 - "Please install ws and ensure it's available via NODE_PATH" 1370 - ); 1371 - } 1372 - 1373 - const server = new DyLDRPCServer({ 1374 - host: process.env.GHCI_BROWSER_HOST || "127.0.0.1", 1375 - port: process.env.GHCI_BROWSER_PORT || 0, 1376 - dyldPath: import.meta.filename, 1377 - searchDirs, 1378 - mainSoPath, 1379 - outFd, 1380 - inFd, 1381 - args, 1382 - redirectWasiConsole: 1383 - process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS || 1384 - process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE 1385 - ? false 1386 - : Boolean(process.env.GHCI_BROWSER_REDIRECT_WASI_CONSOLE), 1387 - }); 1388 - const origin = originFromServerAddress(await server.listening); 1389 - 1390 - // https://pptr.dev/api/puppeteer.consolemessage 1391 - // https://playwright.dev/docs/api/class-consolemessage 1392 - const on_console_msg = (msg) => { 1393 - switch (msg.type()) { 1394 - case "error": 1395 - case "warn": 1396 - case "warning": 1397 - case "trace": 1398 - case "assert": { 1399 - console.error(msg.text()); 1400 - break; 1401 - } 1402 - default: { 1403 - console.log(msg.text()); 1404 - break; 1405 - } 1406 - } 1407 - }; 1408 - 1409 - if (process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS) { 1410 - let puppeteer; 1411 - try { 1412 - puppeteer = require("puppeteer"); 1413 - } catch { 1414 - puppeteer = require("puppeteer-core"); 1415 - } 1416 - 1417 - // https://pptr.dev/api/puppeteer.puppeteernode.launch 1418 - const browser = await puppeteer.launch( 1419 - JSON.parse(process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS) 1420 - ); 1421 - try { 1422 - const page = await browser.newPage(); 1423 - 1424 - // https://pptr.dev/api/puppeteer.pageevent 1425 - page.on("console", on_console_msg); 1426 - page.on("error", (err) => console.error(err)); 1427 - page.on("pageerror", (err) => console.error(err)); 1428 - 1429 - await page.goto(`${origin}/main.html`); 1430 - await server.closed; 1431 - return; 1432 - } finally { 1433 - await browser.close(); 1434 - } 1435 - } 1436 - 1437 - if (process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE) { 1438 - let playwright; 1439 - try { 1440 - playwright = require("playwright"); 1441 - } catch { 1442 - playwright = require("playwright-core"); 1443 - } 1444 - 1445 - // https://playwright.dev/docs/api/class-browsertype#browser-type-launch 1446 - const browser = await playwright[ 1447 - process.env.GHCI_BROWSER_PLAYWRIGHT_BROWSER_TYPE 1448 - ].launch( 1449 - process.env.GHCI_BROWSER_PLAYWRIGHT_LAUNCH_OPTS 1450 - ? JSON.parse(process.env.GHCI_BROWSER_PLAYWRIGHT_LAUNCH_OPTS) 1451 - : {} 1452 - ); 1453 - try { 1454 - const page = await browser.newPage(); 1455 - 1456 - // https://playwright.dev/docs/api/class-page#events 1457 - page.on("console", on_console_msg); 1458 - page.on("pageerror", (err) => console.error(err)); 1459 - 1460 - await page.goto(`${origin}/main.html`); 1461 - await server.closed; 1462 - return; 1463 - } finally { 1464 - await browser.close(); 1465 - } 1466 - } 1467 - 1468 - console.log( 1469 - `Open ${origin}/main.html or import("${origin}/main.js") to boot ghci` 1470 - ); 1471 - } 1472 - 1473 - const isNodeMain = isNode && import.meta.filename === process.argv[1]; 1474 - 1475 - // node iserv as invoked by 1476 - // GHC.Runtime.Interpreter.Wasm.spawnWasmInterp 1477 - if (isNodeMain) { 1478 - const clibdir = process.argv[2]; 1479 - const mainSoPath = process.argv[3]; 1480 - const outFd = Number.parseInt(process.argv[4]), 1481 - inFd = Number.parseInt(process.argv[5]); 1482 - const args = ["dyld.so", ...process.argv.slice(6)]; 1483 - 1484 - await nodeMain({ searchDirs: [clibdir], mainSoPath, outFd, inFd, args }); 1485 - }
-121
book/js/index.js
··· 1 - <script async type="module"> 2 - import { 3 - ConsoleStdout, 4 - File, 5 - OpenFile, 6 - PreopenDirectory, 7 - WASI, 8 - } from "https://esm.sh/gh/haskell-wasm/browser_wasi_shim"; 9 - import { DyLDBrowserHost, main } from "./dyld.mjs"; 10 - 11 - const outputPanel = document.getElementById('output-panel'); 12 - const outputContent = document.getElementById('output-content'); 13 - const runButton = document.getElementById('run-button'); 14 - const clearButton = document.getElementById('clear-button'); 15 - 16 - // Initialize filesystem 17 - const rootfs = new PreopenDirectory("/", []); 18 - const bsdtar_wasi = new WASI( 19 - ["bsdtar.wasm", "-x"], 20 - [], 21 - [ 22 - new OpenFile(new File(new Uint8Array(), { readonly: true })), 23 - ConsoleStdout.lineBuffered((msg) => addOutput(msg, 'output-info')), 24 - ConsoleStdout.lineBuffered((msg) => addOutput(msg, 'output-error')), 25 - rootfs, 26 - ], 27 - { debug: false } 28 - ); 29 - 30 - const [{ instance }, rootfs_bytes] = await Promise.all([ 31 - WebAssembly.instantiateStreaming( 32 - fetch("https://haskell-wasm.github.io/bsdtar-wasm/bsdtar.wasm"), 33 - { wasi_snapshot_preview1: bsdtar_wasi.wasiImport } 34 - ), 35 - fetch("./rootfs.tar.zst").then((r) => r.bytes()), 36 - ]); 37 - 38 - bsdtar_wasi.fds[0] = new OpenFile( 39 - new File(rootfs_bytes, { readonly: true }) 40 - ); 41 - bsdtar_wasi.start(instance); 42 - 43 - if (document.readyState === "loading") { 44 - await new Promise((res) => 45 - document.addEventListener("DOMContentLoaded", res, { once: true }) 46 - ); 47 - } 48 - 49 - // Initialize dyld runtime 50 - const dyld = await main({ 51 - rpc: new DyLDBrowserHost({ 52 - rootfs, 53 - stdout: (msg) => addOutput(msg, 'output-success'), 54 - stderr: (msg) => addOutput(msg, 'output-error'), 55 - }), 56 - searchDirs: [ 57 - "/tmp/clib", 58 - "/tmp/hslib/lib/wasm32-wasi-ghc-9.14.0.20251031-inplace", 59 - ], 60 - mainSoPath: "/tmp/libplayground001.so", 61 - args: ["libplayground001.so", "+RTS", "-c", "-RTS"], 62 - isIserv: false, 63 - }); 64 - 65 - const main_func = await dyld.exportFuncs.myMain("/tmp/hslib/lib"); 66 - 67 - addOutput('runtime initialized successfully', 'output-success'); 68 - 69 - // extract all haskell code blocks from the page 70 - function extract() { 71 - const codeBlocks = document.querySelectorAll('pre code.language-haskell, pre code.haskell'); 72 - const codes = []; 73 - 74 - codeBlocks.forEach(block => { 75 - const code = block.textContent.trim(); 76 - if (code) { 77 - codes.push(code); 78 - } 79 - }); 80 - 81 - return codes.join('\n\n'); 82 - } 83 - 84 - async function runHaskell() { 85 - const haskellCode = extract(); 86 - console.log("=== extracted code ===") 87 - console.log(haskellCode) 88 - if (!haskellCode) { 89 - return; 90 - } 91 - 92 - runButton.disabled = true; 93 - runButton.textContent = "Running"; 94 - outputPanel.open = true; 95 - 96 - try { 97 - await main_func("", haskellCode); 98 - } catch (error) { 99 - addOutput(`Execution error: ${error}`, 'output-error'); 100 - } finally { 101 - runButton.disabled = false; 102 - runButton.textContent = 'Run'; 103 - } 104 - } 105 - 106 - function addOutput(message, className = 'output-info') { 107 - const line = document.createElement('div'); 108 - line.className = `output-line ${className}`; 109 - line.textContent = message; 110 - outputContent.appendChild(line); 111 - outputContent.scrollTop = outputContent.scrollHeight; 112 - } 113 - 114 - function clearOutput() { 115 - outputContent.innerHTML = ''; 116 - outputPanel.open = false; 117 - } 118 - 119 - runButton.addEventListener('click', runHaskell); 120 - clearButton.addEventListener('click', clearOutput); 121 - </script>
-150
book/js/post-link.mjs
··· 1 - #!/usr/bin/env -S node 2 - 3 - // This is the post-linker program that processes a wasm module with 4 - // ghc_wasm_jsffi custom section and outputs an ESM module that 5 - // exports a function to generate the ghc_wasm_jsffi wasm imports. It 6 - // has a simple CLI interface: "./post-link.mjs -i foo.wasm -o 7 - // foo.js", as well as an exported postLink function that takes a 8 - // WebAssembly.Module object and returns the ESM module content. 9 - 10 - // Each record in the ghc_wasm_jsffi custom section are 3 11 - // NUL-terminated strings: name, binder, body. We try to parse the 12 - // body as an expression and fallback to statements, and return the 13 - // completely parsed arrow function source. 14 - export function parseRecord([name, binder, body]) { 15 - for (const src of [`${binder} => (${body})`, `${binder} => {${body}}`]) { 16 - try { 17 - new Function(`return ${src};`); 18 - return src; 19 - } catch (_) {} 20 - } 21 - throw new Error(`parseRecord ${name} ${binder} ${body}`); 22 - } 23 - 24 - // Parse ghc_wasm_jsffi custom sections in a WebAssembly.Module object 25 - // and return an array of records. 26 - export function parseSections(mod) { 27 - const recs = []; 28 - const dec = new TextDecoder("utf-8", { fatal: true }); 29 - const importNames = new Set( 30 - WebAssembly.Module.imports(mod) 31 - .filter((i) => i.module === "ghc_wasm_jsffi") 32 - .map((i) => i.name) 33 - ); 34 - for (const buf of WebAssembly.Module.customSections(mod, "ghc_wasm_jsffi")) { 35 - const ba = new Uint8Array(buf); 36 - let strs = []; 37 - for (let l = 0, r; l < ba.length; l = r + 1) { 38 - r = ba.indexOf(0, l); 39 - strs.push(dec.decode(ba.subarray(l, r))); 40 - if (strs.length === 3) { 41 - if (importNames.has(strs[0])) { 42 - recs.push(strs); 43 - } 44 - strs = []; 45 - } 46 - } 47 - } 48 - return recs; 49 - } 50 - 51 - // Note [Variable passing in JSFFI] 52 - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 53 - // 54 - // The JSFFI code snippets can access variables in globalThis, 55 - // arguments like $1, $2, etc, plus a few magic variables: __exports, 56 - // __ghc_wasm_jsffi_dyld and __ghc_wasm_jsffi_finalization_registry. 57 - // How are these variables passed to JSFFI code? Remember, we strive 58 - // to keep the globalThis namespace hygiene and maintain the ability 59 - // to have multiple Haskell-wasm apps coexisting in the same JS 60 - // context, so we must not pass magic variables as global variables 61 - // even though they may seem globally unique. 62 - // 63 - // The solution is simple: put them in the JS lambda binder position. 64 - // Though there are different layers of lambdas here: 65 - // 66 - // 1. User writes "$1($2, await $3)" in a JSFFI code snippet. No 67 - // explicit binder here, the snippet is either an expression or 68 - // some statements. 69 - // 2. GHC doesn't know JS syntax but it knows JS function arity from 70 - // HS type signature, as well as if the JS function is async/sync 71 - // from safe/unsafe annotation. So it infers the JS binder (like 72 - // "async ($1, $2, $3)") and emits a (name,binder,body) tuple into 73 - // the ghc_wasm_jsffi custom section. 74 - // 3. After link-time we collect these tuples to make a JS object 75 - // mapping names to binder=>body, and this JS object will be used 76 - // to fulfill the ghc_wasm_jsffi wasm imports. This JS object is 77 - // returned by an outer layer of lambda which is in charge of 78 - // passing magic variables. 79 - // 80 - // In case of post-linker for statically linked wasm modules, 81 - // __ghc_wasm_jsffi_dyld won't work so is omitted, and 82 - // __ghc_wasm_jsffi_finalization_registry can be created inside the 83 - // outer JS lambda. Only __exports is exposed as user-visible API 84 - // since it's up to the user to perform knot-tying by assigning the 85 - // instance exports back to the (initially empty) __exports object 86 - // passed to this lambda. 87 - // 88 - // In case of dyld, all magic variables are dyld-session-global 89 - // variables; dyld uses new Function() to make the outer lambda, then 90 - // immediately invokes it by passing the right magic variables. 91 - 92 - export async function postLink(mod) { 93 - const fs = (await import("node:fs/promises")).default; 94 - const path = (await import("node:path")).default; 95 - 96 - let src = ( 97 - await fs.readFile(path.join(import.meta.dirname, "prelude.mjs"), { 98 - encoding: "utf-8", 99 - }) 100 - ).replaceAll("export ", ""); // we only use it as code template, don't export stuff 101 - 102 - // Keep this in sync with dyld.mjs! 103 - src = `${src}\nexport default (__exports) => {`; 104 - src = `${src}\nconst __ghc_wasm_jsffi_jsval_manager = new JSValManager();`; 105 - src = `${src}\nconst __ghc_wasm_jsffi_finalization_registry = globalThis.FinalizationRegistry ? new FinalizationRegistry(sp => __exports.rts_freeStablePtr(sp)) : { register: () => {}, unregister: () => true };`; 106 - src = `${src}\nreturn {`; 107 - src = `${src}\nnewJSVal: (v) => __ghc_wasm_jsffi_jsval_manager.newJSVal(v),`; 108 - src = `${src}\ngetJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.getJSVal(k),`; 109 - src = `${src}\nfreeJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.freeJSVal(k),`; 110 - src = `${src}\nscheduleWork: () => setImmediate(__exports.rts_schedulerLoop),`; 111 - for (const rec of parseSections(mod)) { 112 - src = `${src}\n${rec[0]}: ${parseRecord(rec)},`; 113 - } 114 - return `${src}\n};\n};\n`; 115 - } 116 - 117 - function isMain() { 118 - if (!globalThis?.process?.versions?.node) { 119 - return false; 120 - } 121 - 122 - return import.meta.filename === process.argv[1]; 123 - } 124 - 125 - async function main() { 126 - const fs = (await import("node:fs/promises")).default; 127 - const util = (await import("node:util")).default; 128 - 129 - const { input, output } = util.parseArgs({ 130 - options: { 131 - input: { 132 - type: "string", 133 - short: "i", 134 - }, 135 - output: { 136 - type: "string", 137 - short: "o", 138 - }, 139 - }, 140 - }).values; 141 - 142 - await fs.writeFile( 143 - output, 144 - await postLink(await WebAssembly.compile(await fs.readFile(input))) 145 - ); 146 - } 147 - 148 - if (isMain()) { 149 - await main(); 150 - }
-91
book/js/prelude.mjs
··· 1 - // This file implements the JavaScript runtime logic for Haskell 2 - // modules that use JSFFI. It is not an ESM module, but the template 3 - // of one; the post-linker script will copy all contents into a new 4 - // ESM module. 5 - 6 - // Manage a mapping from 32-bit ids to actual JavaScript values. 7 - export class JSValManager { 8 - #lastk = 0; 9 - #kv = new Map(); 10 - 11 - newJSVal(v) { 12 - const k = ++this.#lastk; 13 - this.#kv.set(k, v); 14 - return k; 15 - } 16 - 17 - // A separate has() call to ensure we can store undefined as a value 18 - // too. Also, unconditionally check this since the check is cheap 19 - // anyway, if the check fails then there's a use-after-free to be 20 - // fixed. 21 - getJSVal(k) { 22 - if (!this.#kv.has(k)) { 23 - throw new WebAssembly.RuntimeError(`getJSVal(${k})`); 24 - } 25 - return this.#kv.get(k); 26 - } 27 - 28 - // Check for double free as well. 29 - freeJSVal(k) { 30 - if (!this.#kv.delete(k)) { 31 - throw new WebAssembly.RuntimeError(`freeJSVal(${k})`); 32 - } 33 - } 34 - } 35 - 36 - // The actual setImmediate() to be used. This is a ESM module top 37 - // level binding and doesn't pollute the globalThis namespace. 38 - // 39 - // To benchmark different setImmediate() implementations in the 40 - // browser, use https://github.com/jphpsf/setImmediate-shim-demo as a 41 - // starting point. 42 - export const setImmediate = await (async () => { 43 - // node, bun, or other scripts might have set this up in the browser 44 - if (globalThis.setImmediate) { 45 - return globalThis.setImmediate; 46 - } 47 - 48 - // deno 49 - if (globalThis.Deno) { 50 - try { 51 - return (await import("node:timers")).setImmediate; 52 - } catch {} 53 - } 54 - 55 - // https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask 56 - if (globalThis.scheduler) { 57 - return (cb, ...args) => scheduler.postTask(() => cb(...args)); 58 - } 59 - 60 - // Cloudflare workers doesn't support MessageChannel 61 - if (globalThis.MessageChannel) { 62 - // A simple & fast setImmediate() implementation for browsers. It's 63 - // not a drop-in replacement for node.js setImmediate() because: 64 - // 1. There's no clearImmediate(), and setImmediate() doesn't return 65 - // anything 66 - // 2. There's no guarantee that callbacks scheduled by setImmediate() 67 - // are executed in the same order (in fact it's the opposite lol), 68 - // but you are never supposed to rely on this assumption anyway 69 - class SetImmediate { 70 - #fs = []; 71 - #mc = new MessageChannel(); 72 - 73 - constructor() { 74 - this.#mc.port1.addEventListener("message", () => { 75 - this.#fs.pop()(); 76 - }); 77 - this.#mc.port1.start(); 78 - } 79 - 80 - setImmediate(cb, ...args) { 81 - this.#fs.push(() => cb(...args)); 82 - this.#mc.port2.postMessage(undefined); 83 - } 84 - } 85 - 86 - const sm = new SetImmediate(); 87 - return (cb, ...args) => sm.setImmediate(cb, ...args); 88 - } 89 - 90 - return (cb, ...args) => setTimeout(cb, 0, ...args); 91 - })();
-11
book/template.html
··· 96 96 $for(include-after)$ 97 97 $include-after$ 98 98 $endfor$ 99 - 100 - <details id="output-panel"> 101 - <summary id="panel-actions"> 102 - <span> 103 - <button id="run-button">Run</button> 104 - <button id="clear-button">Clear</button> 105 - </span> 106 - </summary> 107 - <div id="output-content"></div> 108 - </details> 109 - 110 99 </body> 111 100 </html> 112 101