Cheng Shao pushed to branch wip/wasm-dyld-pie at Glasgow Haskell Compiler / GHC Commits: 3d7985e3 by Cheng Shao at 2025-10-26T17:53:34+01: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,7 @@ function originFromServerAddress({ address, family, port }) { } // Browser/node portable code stays above this watermark. -const isNode = Boolean(globalThis?.process?.versions?.node); +const isNode = Boolean(globalThis?.process?.versions?.node && !globalThis.Deno); // Too cumbersome to only import at use sites. Too troublesome to // factor out browser-only/node-only logic into different modules. For @@ -307,18 +307,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 +377,75 @@ 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, returns the File object, + // 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 : null; + } + + constructor({ rootfs }) { + this.rootfs = rootfs; + } + + close() {} + + // 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) { + const entry = this.#readFile(p); + const r = new Response(entry.data, { + headers: { "Content-Type": "application/wasm" }, + }); + // It's only fetched once, take the chance to prune it in vfs to save memory + entry.data = new Uint8Array(); + return r; + } + + 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 +613,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 +905,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 +1295,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 +1346,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,15 +1471,11 @@ export async function nodeMain({ libdir, ghciSoPath, out_fd, in_fd, args }) { ); } -function isNodeMain() { - if (!globalThis?.process?.versions?.node) { - return false; - } - - return import.meta.filename === process.argv[1]; -} +const isNodeMain = isNode && import.meta.filename === process.argv[1]; -if (isNodeMain()) { +// node iserv as invoked by +// GHC.Runtime.Interpreter.Wasm.spawnWasmInterp +if (isNodeMain) { const libdir = process.argv[2]; const ghciSoPath = process.argv[3]; const out_fd = Number.parseInt(process.argv[4]), View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/3d7985e376935e3583370f85a83bbcf6... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/3d7985e376935e3583370f85a83bbcf6... You're receiving this email because of your account on gitlab.haskell.org.
participants (1)
-
Cheng Shao (@TerrorJack)