diff --git a/utils/jsffi/dyld.mjs b/utils/jsffi/dyld.mjs index e9cffa4f74c8b9897268feb67ed57d646d487178..1129c6b34a1a717547046f55368e44d0b4427070 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 () => {