Cheng Shao pushed to branch wip/wasm-dyld-pie at Glasgow Haskell Compiler / GHC Commits: 33278bf7 by Cheng Shao at 2025-10-24T23:30:48+02:00 wasm: support running dyld fully client side in browser - - - - - 1 changed file: - utils/jsffi/dyld.mjs Changes: ===================================== utils/jsffi/dyld.mjs ===================================== @@ -285,7 +285,10 @@ function originFromServerAddress({ address, family, port }) { } // Browser/node portable code stays above this watermark. -const isNode = Boolean(globalThis?.process?.versions?.node); +let isNode = Boolean(globalThis?.process?.versions?.node); +if (globalThis.Deno) { + isNode = false; +} // Too cumbersome to only import at use sites. Too troublesome to // factor out browser-only/node-only logic into different modules. For @@ -307,18 +310,22 @@ if (isNode) { ws = require("ws"); } catch {} } else { - wasi = await import( - "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.2/dist/index.js" - ); + wasi = await import("https://esm.sh/gh/haskell-wasm/browser_wasi_shim"); } // A subset of dyld logic that can only be run in the host node // process and has full access to local filesystem -class DyLDHost { +export class DyLDHost { // Deduped absolute paths of directories where we lookup .so files #rpaths = new Set(); constructor({ out_fd, in_fd }) { + // When running a non-iserv shared library with node, the DyLDHost + // instance is created without a pair of fds, so skip creation of + // readStream/writeStream, they won't be used anyway + if (!(typeof out_fd === "number" && typeof in_fd === "number")) { + return; + } this.readStream = stream.Readable.toWeb( fs.createReadStream(undefined, { fd: in_fd }) ); @@ -373,6 +380,77 @@ class DyLDHost { } } +// Runs in the browser and uses the in-memory vfs, doesn't do any RPC +// calls +export class DyLDBrowserHost { + // Deduped absolute paths of directories where we lookup .so files + #rpaths = new Set(); + // The PreopenDirectory object of the root filesystem + rootfs; + + // Given canonicalized absolute file path, return Uint8Array of file + // content, or null if absent + #readFile(p) { + const { ret, entry } = this.rootfs.dir.get_entry_for_path({ + parts: p.split("/").filter((tok) => tok !== ""), + is_dir: false, + }); + return ret === 0 ? entry.data : null; + } + + constructor({ rootfs }) { + this.rootfs = rootfs; + } + + close() {} + + installSignalHandlers(_cb) {} + + // p must be canonicalized absolute path + async addLibrarySearchPath(p) { + this.#rpaths.add(p); + return null; + } + + async findSystemLibrary(f) { + if (f.startsWith("/")) { + if (this.#readFile(f)) { + return f; + } + throw new Error(`findSystemLibrary(${f}): not found in /`); + } + + for (const rpath of this.#rpaths) { + const r = `${rpath}/${f}`; + if (this.#readFile(r)) { + return r; + } + } + + throw new Error( + `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}` + ); + } + + async fetchWasm(p) { + let buf = this.#readFile(p); + if (buf.buffer.resizable) { + buf = new Uint8Array(buf); + } + return new Response(buf, { + headers: { "Content-Type": "application/wasm" }, + }); + } + + stdout(msg) { + console.info(msg); + } + + stderr(msg) { + console.warn(msg); + } +} + // Fulfill the same functionality as DyLDHost by doing fetch() calls // to respective RPC endpoints of a host http server. Also manages // WebSocket connections back to host. @@ -540,7 +618,7 @@ class DyLDRPCServer { res.end( ` import { DyLDRPC, main } from "./fs${dyldPath}"; -const args = ${JSON.stringify({ libdir, ghciSoPath, args })}; +const args = ${JSON.stringify({ libdirs: [libdir], ghciSoPath, args })}; args.rpc = new DyLDRPC({origin: "${origin}", redirectWasiConsole: ${redirectWasiConsole}}); args.rpc.opened.then(() => main(args)); ` @@ -832,6 +910,10 @@ class DyLD { ], { debug: false } ); + + if (this.#rpc instanceof DyLDBrowserHost) { + this.#wasi.fds[3] = this.#rpc.rootfs; + } } // Both wasi implementations we use provide @@ -1218,15 +1300,39 @@ class DyLD { } } -export async function main({ rpc, libdir, ghciSoPath, args }) { +// The main entry point of dyld that may be run on node/browser, and +// may run either iserv defaultMain from the ghci library or an +// alternative entry point from another shared library +export async function main({ + rpc, // Handle the side effects of DyLD + libdirs, // Initial library search directories + ghciSoPath, // Could also be another shared library that's actually not ghci + args, // WASI argv without the executable name. +RTS etc will be respected + altEntry, // Optional alternative entry point function name + altArgs, // Argument array to pass to the alternative entry point function +}) { try { const dyld = new DyLD({ args: ["dyld.so", ...args], rpc, }); - await dyld.addLibrarySearchPath(libdir); + for (const libdir of libdirs) { + await dyld.addLibrarySearchPath(libdir); + } await dyld.loadDLLs(ghciSoPath); + // At this point, rts/ghc-internal are loaded, perform wasm shared + // library specific RTS startup logic, see Note [JSFFI + // initialization] + dyld.exportFuncs.__ghc_wasm_jsffi_init(); + + // We're not running iserv, just invoke user-specified alternative + // entry point and pass the arguments + if (altEntry) { + return await dyld.exportFuncs[altEntry](...altArgs); + } + + // iserv-specific logic follows const reader = rpc.readStream.getReader(); const writer = rpc.writeStream.getWriter(); @@ -1245,19 +1351,19 @@ export async function main({ rpc, libdir, ghciSoPath, args }) { writer.write(new Uint8Array(buf)); }; - dyld.exportFuncs.__ghc_wasm_jsffi_init(); - await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send); + return await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send); } finally { rpc.close(); } } -export async function nodeMain({ libdir, ghciSoPath, out_fd, in_fd, args }) { +// node-specific iserv-specific logic +async function nodeMain({ libdir, ghciSoPath, out_fd, in_fd, args }) { if (!process.env.GHCI_BROWSER) { const rpc = new DyLDHost({ out_fd, in_fd }); await main({ rpc, - libdir, + libdirs: [libdir], ghciSoPath, args, }); @@ -1370,6 +1476,8 @@ export async function nodeMain({ libdir, ghciSoPath, out_fd, in_fd, args }) { ); } +// Can't be reused with isMain in post-link.mjs, import.meta.filename +// evaluates to filename of source location function isNodeMain() { if (!globalThis?.process?.versions?.node) { return false; @@ -1378,6 +1486,8 @@ function isNodeMain() { return import.meta.filename === process.argv[1]; } +// node iserv as invoked by +// GHC.Runtime.Interpreter.Wasm.spawnWasmInterp if (isNodeMain()) { const libdir = process.argv[2]; const ghciSoPath = process.argv[3]; View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/33278bf705b96a4e22bb8defd11aa8cc... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/33278bf705b96a4e22bb8defd11aa8cc... You're receiving this email because of your account on gitlab.haskell.org.