Cheng Shao pushed to branch wip/wasm-dyld-pie at Glasgow Haskell Compiler / GHC

Commits:

1 changed file:

Changes:

  • utils/jsffi/dyld.mjs
    ... ... @@ -285,7 +285,7 @@ function originFromServerAddress({ address, family, port }) {
    285 285
     }
    
    286 286
     
    
    287 287
     // Browser/node portable code stays above this watermark.
    
    288
    -const isNode = Boolean(globalThis?.process?.versions?.node);
    
    288
    +const isNode = Boolean(globalThis?.process?.versions?.node && !globalThis.Deno);
    
    289 289
     
    
    290 290
     // Too cumbersome to only import at use sites. Too troublesome to
    
    291 291
     // factor out browser-only/node-only logic into different modules. For
    
    ... ... @@ -307,18 +307,22 @@ if (isNode) {
    307 307
         ws = require("ws");
    
    308 308
       } catch {}
    
    309 309
     } else {
    
    310
    -  wasi = await import(
    
    311
    -    "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.2/dist/index.js"
    
    312
    -  );
    
    310
    +  wasi = await import("https://esm.sh/gh/haskell-wasm/browser_wasi_shim");
    
    313 311
     }
    
    314 312
     
    
    315 313
     // A subset of dyld logic that can only be run in the host node
    
    316 314
     // process and has full access to local filesystem
    
    317
    -class DyLDHost {
    
    315
    +export class DyLDHost {
    
    318 316
       // Deduped absolute paths of directories where we lookup .so files
    
    319 317
       #rpaths = new Set();
    
    320 318
     
    
    321 319
       constructor({ out_fd, in_fd }) {
    
    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 out_fd === "number" && typeof in_fd === "number")) {
    
    324
    +      return;
    
    325
    +    }
    
    322 326
         this.readStream = stream.Readable.toWeb(
    
    323 327
           fs.createReadStream(undefined, { fd: in_fd })
    
    324 328
         );
    
    ... ... @@ -373,6 +377,75 @@ class DyLDHost {
    373 377
       }
    
    374 378
     }
    
    375 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
    +
    
    388
    +  // Given canonicalized absolute file path, returns the File object,
    
    389
    +  // or null if absent
    
    390
    +  #readFile(p) {
    
    391
    +    const { ret, entry } = this.rootfs.dir.get_entry_for_path({
    
    392
    +      parts: p.split("/").filter((tok) => tok !== ""),
    
    393
    +      is_dir: false,
    
    394
    +    });
    
    395
    +    return ret === 0 ? entry : null;
    
    396
    +  }
    
    397
    +
    
    398
    +  constructor({ rootfs }) {
    
    399
    +    this.rootfs = rootfs;
    
    400
    +  }
    
    401
    +
    
    402
    +  close() {}
    
    403
    +
    
    404
    +  // p must be canonicalized absolute path
    
    405
    +  async addLibrarySearchPath(p) {
    
    406
    +    this.#rpaths.add(p);
    
    407
    +    return null;
    
    408
    +  }
    
    409
    +
    
    410
    +  async findSystemLibrary(f) {
    
    411
    +    if (f.startsWith("/")) {
    
    412
    +      if (this.#readFile(f)) {
    
    413
    +        return f;
    
    414
    +      }
    
    415
    +      throw new Error(`findSystemLibrary(${f}): not found in /`);
    
    416
    +    }
    
    417
    +
    
    418
    +    for (const rpath of this.#rpaths) {
    
    419
    +      const r = `${rpath}/${f}`;
    
    420
    +      if (this.#readFile(r)) {
    
    421
    +        return r;
    
    422
    +      }
    
    423
    +    }
    
    424
    +
    
    425
    +    throw new Error(
    
    426
    +      `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}`
    
    427
    +    );
    
    428
    +  }
    
    429
    +
    
    430
    +  async fetchWasm(p) {
    
    431
    +    const entry = this.#readFile(p);
    
    432
    +    const r = new Response(entry.data, {
    
    433
    +      headers: { "Content-Type": "application/wasm" },
    
    434
    +    });
    
    435
    +    // It's only fetched once, take the chance to prune it in vfs to save memory
    
    436
    +    entry.data = new Uint8Array();
    
    437
    +    return r;
    
    438
    +  }
    
    439
    +
    
    440
    +  stdout(msg) {
    
    441
    +    console.info(msg);
    
    442
    +  }
    
    443
    +
    
    444
    +  stderr(msg) {
    
    445
    +    console.warn(msg);
    
    446
    +  }
    
    447
    +}
    
    448
    +
    
    376 449
     // Fulfill the same functionality as DyLDHost by doing fetch() calls
    
    377 450
     // to respective RPC endpoints of a host http server. Also manages
    
    378 451
     // WebSocket connections back to host.
    
    ... ... @@ -540,7 +613,7 @@ class DyLDRPCServer {
    540 613
             res.end(
    
    541 614
               `
    
    542 615
     import { DyLDRPC, main } from "./fs${dyldPath}";
    
    543
    -const args = ${JSON.stringify({ libdir, ghciSoPath, args })};
    
    616
    +const args = ${JSON.stringify({ libdirs: [libdir], ghciSoPath, args })};
    
    544 617
     args.rpc = new DyLDRPC({origin: "${origin}", redirectWasiConsole: ${redirectWasiConsole}});
    
    545 618
     args.rpc.opened.then(() => main(args));
    
    546 619
     `
    
    ... ... @@ -832,6 +905,10 @@ class DyLD {
    832 905
             ],
    
    833 906
             { debug: false }
    
    834 907
           );
    
    908
    +
    
    909
    +      if (this.#rpc instanceof DyLDBrowserHost) {
    
    910
    +        this.#wasi.fds[3] = this.#rpc.rootfs;
    
    911
    +      }
    
    835 912
         }
    
    836 913
     
    
    837 914
         // Both wasi implementations we use provide
    
    ... ... @@ -1218,15 +1295,39 @@ class DyLD {
    1218 1295
       }
    
    1219 1296
     }
    
    1220 1297
     
    
    1221
    -export async function main({ rpc, libdir, ghciSoPath, args }) {
    
    1298
    +// The main entry point of dyld that may be run on node/browser, and
    
    1299
    +// may run either iserv defaultMain from the ghci library or an
    
    1300
    +// alternative entry point from another shared library
    
    1301
    +export async function main({
    
    1302
    +  rpc, // Handle the side effects of DyLD
    
    1303
    +  libdirs, // Initial library search directories
    
    1304
    +  ghciSoPath, // Could also be another shared library that's actually not ghci
    
    1305
    +  args, // WASI argv without the executable name. +RTS etc will be respected
    
    1306
    +  altEntry, // Optional alternative entry point function name
    
    1307
    +  altArgs, // Argument array to pass to the alternative entry point function
    
    1308
    +}) {
    
    1222 1309
       try {
    
    1223 1310
         const dyld = new DyLD({
    
    1224 1311
           args: ["dyld.so", ...args],
    
    1225 1312
           rpc,
    
    1226 1313
         });
    
    1227
    -    await dyld.addLibrarySearchPath(libdir);
    
    1314
    +    for (const libdir of libdirs) {
    
    1315
    +      await dyld.addLibrarySearchPath(libdir);
    
    1316
    +    }
    
    1228 1317
         await dyld.loadDLLs(ghciSoPath);
    
    1229 1318
     
    
    1319
    +    // At this point, rts/ghc-internal are loaded, perform wasm shared
    
    1320
    +    // library specific RTS startup logic, see Note [JSFFI
    
    1321
    +    // initialization]
    
    1322
    +    dyld.exportFuncs.__ghc_wasm_jsffi_init();
    
    1323
    +
    
    1324
    +    // We're not running iserv, just invoke user-specified alternative
    
    1325
    +    // entry point and pass the arguments
    
    1326
    +    if (altEntry) {
    
    1327
    +      return await dyld.exportFuncs[altEntry](...altArgs);
    
    1328
    +    }
    
    1329
    +
    
    1330
    +    // iserv-specific logic follows
    
    1230 1331
         const reader = rpc.readStream.getReader();
    
    1231 1332
         const writer = rpc.writeStream.getWriter();
    
    1232 1333
     
    
    ... ... @@ -1245,19 +1346,19 @@ export async function main({ rpc, libdir, ghciSoPath, args }) {
    1245 1346
           writer.write(new Uint8Array(buf));
    
    1246 1347
         };
    
    1247 1348
     
    
    1248
    -    dyld.exportFuncs.__ghc_wasm_jsffi_init();
    
    1249
    -    await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send);
    
    1349
    +    return await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send);
    
    1250 1350
       } finally {
    
    1251 1351
         rpc.close();
    
    1252 1352
       }
    
    1253 1353
     }
    
    1254 1354
     
    
    1255
    -export async function nodeMain({ libdir, ghciSoPath, out_fd, in_fd, args }) {
    
    1355
    +// node-specific iserv-specific logic
    
    1356
    +async function nodeMain({ libdir, ghciSoPath, out_fd, in_fd, args }) {
    
    1256 1357
       if (!process.env.GHCI_BROWSER) {
    
    1257 1358
         const rpc = new DyLDHost({ out_fd, in_fd });
    
    1258 1359
         await main({
    
    1259 1360
           rpc,
    
    1260
    -      libdir,
    
    1361
    +      libdirs: [libdir],
    
    1261 1362
           ghciSoPath,
    
    1262 1363
           args,
    
    1263 1364
         });
    
    ... ... @@ -1370,15 +1471,11 @@ export async function nodeMain({ libdir, ghciSoPath, out_fd, in_fd, args }) {
    1370 1471
       );
    
    1371 1472
     }
    
    1372 1473
     
    
    1373
    -function isNodeMain() {
    
    1374
    -  if (!globalThis?.process?.versions?.node) {
    
    1375
    -    return false;
    
    1376
    -  }
    
    1377
    -
    
    1378
    -  return import.meta.filename === process.argv[1];
    
    1379
    -}
    
    1474
    +const isNodeMain = isNode && import.meta.filename === process.argv[1];
    
    1380 1475
     
    
    1381
    -if (isNodeMain()) {
    
    1476
    +// node iserv as invoked by
    
    1477
    +// GHC.Runtime.Interpreter.Wasm.spawnWasmInterp
    
    1478
    +if (isNodeMain) {
    
    1382 1479
       const libdir = process.argv[2];
    
    1383 1480
       const ghciSoPath = process.argv[3];
    
    1384 1481
       const out_fd = Number.parseInt(process.argv[4]),