From 9d22e71d26589467db23db2ca111d8d6cae8afc1 Mon Sep 17 00:00:00 2001 From: Cheng Shao <terrorjack@type.dance> Date: Sun, 16 Mar 2025 23:32:59 +0000 Subject: [PATCH] wasm: isolate dyld side effects that might require IPC This commit spins out a DyLDHost class from DyLD that handles side effects that must be run in the same host environment that runs wasm32-wasi-ghc. When the dyld script runs in the browser, it'll need to do IPC to find libraries, fetch wasm library, etc, and the other side of dyld that runs on nodejs would simply expose the DyLDHost methods as endpoints for WebSockets/HTTP. (cherry picked from commit 22ba2a787ccd1c5fdf2ff5bfd0ed0bc67b67289f) --- utils/jsffi/dyld.mjs | 131 +++++++++++++++++++++++++++---------------- 1 file changed, 82 insertions(+), 49 deletions(-) diff --git a/utils/jsffi/dyld.mjs b/utils/jsffi/dyld.mjs index e9cffa4f74c..1129c6b34a1 100755 --- a/utils/jsffi/dyld.mjs +++ b/utils/jsffi/dyld.mjs @@ -285,6 +285,69 @@ if (isNode) { ); } +// A subset of dyld logic that can only be run in the host node +// process and has full access to local filesystem +class DyLDHost { + // Deduped absolute paths of directories where we lookup .so files + #rpaths = new Set(); + + constructor() { + // Inherited pipe file descriptors from GHC + const out_fd = Number.parseInt(process.argv[4]), + in_fd = Number.parseInt(process.argv[5]); + + this.readStream = stream.Readable.toWeb( + fs.createReadStream(undefined, { fd: in_fd }) + ); + this.writeStream = stream.Writable.toWeb( + fs.createWriteStream(undefined, { fd: out_fd }) + ); + } + + installSignalHandlers(cb) { + process.on("SIGINT", cb); + process.on("SIGQUIT", cb); + } + + // removeLibrarySearchPath is a no-op in ghci. If you have a use + // case where it's actually needed, I would like to hear.. + async addLibrarySearchPath(p) { + this.#rpaths.add(path.resolve(p)); + return null; + } + + // f can be either just soname or an absolute path, will be + // canonicalized and checked for file existence here. Throws if + // non-existent. + async findSystemLibrary(f) { + if (path.isAbsolute(f)) { + await fs.promises.access(f, fs.promises.constants.R_OK); + return f; + } + const r = ( + await Promise.allSettled( + [...this.#rpaths].map(async (p) => { + const r = path.resolve(p, f); + await fs.promises.access(r, fs.promises.constants.R_OK); + return r; + }) + ) + ).find(({ status }) => status === "fulfilled"); + console.assert( + r, + `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}` + ); + return r.value; + } + + // returns a Response for a .so absolute path + async fetchWasm(p) { + return new Response(stream.Readable.toWeb(fs.createReadStream(p)), { + headers: { "Content-Type": "application/wasm" }, + }); + } +} + // The real stuff class DyLD { // Wasm page size. @@ -307,6 +370,10 @@ class DyLD { "__wasm_call_ctors", ]); + // Handles RPC logic back to host in a browser, or just do plain + // function calls in node + #rpc; + // The WASI instance to provide wasi imports, shared across all wasm // instances #wasi; @@ -327,9 +394,6 @@ class DyLD { // The JSVal manager #jsvalManager = new JSValManager(); - // Deduped absolute paths of directories where we lookup .so files - #rpaths = new Set(); - // sonames of loaded sos. // // Note that "soname" is just xxx.so as in file path, not actually @@ -361,7 +425,9 @@ class DyLD { // Global STG registers #regs = {}; - constructor({ args }) { + constructor({ args, rpc }) { + this.#rpc = rpc; + if (isNode) { this.#wasi = new wasi.WASI({ version: "preview1", @@ -417,34 +483,12 @@ class DyLD { } } - // removeLibrarySearchPath is a no-op in ghci. If you have a use - // case where it's actually needed, I would like to hear.. - addLibrarySearchPath(p) { - this.#rpaths.add(path.resolve(p)); + async addLibrarySearchPath(p) { + return this.#rpc.addLibrarySearchPath(p); } - // f can be either just soname or an absolute path, will be - // canonicalized and checked for file existence here. Throws if - // non-existent. async findSystemLibrary(f) { - if (path.isAbsolute(f)) { - await fs.promises.access(f, fs.promises.constants.R_OK); - return f; - } - const r = ( - await Promise.allSettled( - [...this.#rpaths].map(async (p) => { - const r = path.resolve(p, f); - await fs.promises.access(r, fs.promises.constants.R_OK); - return r; - }) - ) - ).find(({ status }) => status === "fulfilled"); - console.assert( - r, - `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}` - ); - return r.value; + return this.#rpc.findSystemLibrary(f); } // When we do loadDLL, we first perform "downsweep" which return a @@ -476,14 +520,12 @@ class DyLD { // libghc_tmp_1.so in a temporary directory without adding that // directory via addLibrarySearchPath toks.pop(); - this.addLibrarySearchPath(toks.join("/")); + await this.addLibrarySearchPath(toks.join("/")); } else { p = await this.findSystemLibrary(p); } - const resp = new Response(stream.Readable.toWeb(fs.createReadStream(p)), { - headers: { "Content-Type": "application/wasm" }, - }); + const resp = await this.#rpc.fetchWasm(p); const resp2 = resp.clone(); const modp = WebAssembly.compileStreaming(resp); // Parse dylink.0 from the raw buffer, not via @@ -779,6 +821,8 @@ class DyLD { } } +const rpc = new DyLDHost(); + if (isNode) { // sysroot libdir that contains libc.so etc const libdir = process.argv[2], @@ -786,27 +830,16 @@ if (isNode) { const dyld = new DyLD({ args: ["dyld.so", ...process.argv.slice(6)], + rpc, }); - dyld.addLibrarySearchPath(libdir); + await dyld.addLibrarySearchPath(libdir); await dyld.loadDLL(ghci_so_path); - // Inherited pipe file descriptors from GHC - const out_fd = Number.parseInt(process.argv[4]), - in_fd = Number.parseInt(process.argv[5]); - - const in_stream = stream.Readable.toWeb( - fs.createReadStream(undefined, { fd: in_fd }) - ); - const out_stream = stream.Writable.toWeb( - fs.createWriteStream(undefined, { fd: out_fd }) - ); - - const reader = in_stream.getReader(); - const writer = out_stream.getWriter(); + const reader = rpc.readStream.getReader(); + const writer = rpc.writeStream.getWriter(); const cb_sig = (cb) => { - process.on("SIGINT", cb); - process.on("SIGQUIT", cb); + rpc.installSignalHandlers(cb); }; const cb_recv = async () => { -- GitLab