Marge Bot pushed to branch master at Glasgow Haskell Compiler / GHC

Commits:

10 changed files:

Changes:

  • testsuite/tests/ghc-api-browser/README.md
    1
    +# The Haskell playground browser test
    
    2
    +
    
    3
    +This directory contains the `playground001` test, which builds a fully
    
    4
    +client side Haskell playground in the browser, then runs a
    
    5
    +puppeteer-based test to actually interpret a Haskell program in a
    
    6
    +headless browser.
    
    7
    +
    
    8
    +## Headless testing
    
    9
    +
    
    10
    +`playground001` is tested in GHC CI. To test it locally, first ensure
    
    11
    +you've set up the latest
    
    12
    +[`ghc-wasm-meta`](https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta)
    
    13
    +toolchain and sourced the `~/.ghc-wasm/env` script, so the right
    
    14
    +`node` with the right pre-installed libraries are used. Additionally,
    
    15
    +you need to install latest Firefox and:
    
    16
    +
    
    17
    +```sh
    
    18
    +export FIREFOX_LAUNCH_OPTS='{"browser":"firefox","executablePath":"/usr/bin/firefox"}'`
    
    19
    +```
    
    20
    +
    
    21
    +Or on macOS:
    
    22
    +
    
    23
    +```sh
    
    24
    +export FIREFOX_LAUNCH_OPTS='{"browser":"firefox","executablePath":"/Applications/Firefox.app/Contents/MacOS/firefox"}'
    
    25
    +```
    
    26
    +
    
    27
    +Without `FIREFOX_LAUNCH_OPTS`, `playground001` is skipped.
    
    28
    +
    
    29
    +It's possible to test against Chrome as well, the
    
    30
    +[`playground001.js`](./playground001.js) test driver doesn't assume
    
    31
    +anything Firefox-specific, it just takes the
    
    32
    +[`puppeteer.launch`](https://pptr.dev/api/puppeteer.puppeteernode.launch)
    
    33
    +options as JSON passed via command line.
    
    34
    +
    
    35
    +`playground001` works on latest versions of Firefox/Chrome/Safari.
    
    36
    +
    
    37
    +## Manual testing
    
    38
    +
    
    39
    +The simplest way to build the playground manually and run it in a
    
    40
    +browser tab is to test it once with `--only=playground001
    
    41
    +--keep-test-files` passed to Hadrian, then you can find the temporary
    
    42
    +directory containing [`index.html`](./index.html), `rootfs.tar.zst`
    
    43
    +etc, then fire up a dev web server and load it.
    
    44
    +
    
    45
    +Additionally, you can build the playground in tree without invoking
    
    46
    +the GHC testsuite. Just build GHC with the wasm target first, then
    
    47
    +copy `utils/jsffi/*.mjs` here and run
    
    48
    +[`./playground001.sh`](./playground001.sh) script. You need to set
    
    49
    +`TEST_CC` to the path of `wasm32-wasi-clang` and `TEST_HC` to the path
    
    50
    +of `wasm32-wasi-ghc`, that's it.
    
    51
    +
    
    52
    +## Customized Haskell playground
    
    53
    +
    
    54
    +You may want to build a customized Haskell playground that uses GHC
    
    55
    +API to interpret Haskell code with custom packages, here are some tips
    
    56
    +to get started:
    
    57
    +
    
    58
    +- Read the code in this directory and figure out how `playground001`
    
    59
    +  itself works.
    
    60
    +- [`./playground001.sh`](./playground001.sh) can be used as a basis to
    
    61
    +  write your own build/test script.
    
    62
    +
    
    63
    +You don't need to read the full `dyld.mjs` script. The user-facing
    
    64
    +things that are relevant to the playground use case are:
    
    65
    +
    
    66
    +- `export class DyLDBrowserHost`: it is the `rpc` object required when
    
    67
    +  calling `main`. You need to pass `stdout`/`stderr` callbacks to
    
    68
    +  write each line of stdout/stderr, as well as a `rootfs` object that
    
    69
    +  represents an in-memory vfs containing the shared libraries to load.
    
    70
    +- `export async function main`: it eventually returns a `DyLD` object
    
    71
    +  that can be used like `await
    
    72
    +  dyld.exportFuncs.myExportedHaskellFunc(js_foo, js_bar)` to invoke
    
    73
    +  your exported Haskell function.
    
    74
    +
    
    75
    +Check the source code of [`index.html`](./index.html) and cross
    
    76
    +reference [`playground001.hs`](./playground001.hs) for the example of
    
    77
    +how they are used.
    
    78
    +
    
    79
    +The `rootfs` object is a
    
    80
    +[`PreopenDirectory`](https://github.com/haskell-wasm/browser_wasi_shim/blob/master/src/fs_mem.ts)
    
    81
    +object in the
    
    82
    +[`browser_wasi_shim`](https://github.com/haskell-wasm/browser_wasi_shim)
    
    83
    +library. The Haskell playground needs a complex vfs containing many
    
    84
    +files (shared libraries, interface files, package databases, etc), so
    
    85
    +to speed things up, the whole vfs is compressed into a
    
    86
    +`rootfs.tar.zst` archive, then that archive is extracted using
    
    87
    +[`bsdtar-wasm`](https://github.com/haskell-wasm/bsdtar-wasm).
    
    88
    +
    
    89
    +You don't need to read the source code of `browser_wasi_shim`; you can
    
    90
    +simply paste and adapt the relevant code snippet in
    
    91
    +[`index.html`](./index.html) to create the right `rootfs` object from
    
    92
    +a tarball.
    
    93
    +
    
    94
    +The main concern is what do you need to pack into `rootfs.tar.zst`.
    
    95
    +For `playground001`, it contains:
    
    96
    +
    
    97
    +- `/tmp/clib`: the C/C++ shared libraries
    
    98
    +- `/tmp/hslib/lib`: the GHC libdir
    
    99
    +- `/tmp/libplayground001.so`: the main shared library to start loading
    
    100
    +  that exports `myMain`
    
    101
    +
    
    102
    +You can read [`./playground001.sh`](./playground001.sh) to figure out
    
    103
    +the details of how I prepare `rootfs.tar.zst` and trim unneeded files
    
    104
    +to minimize the tarball size.
    
    105
    +
    
    106
    +There are multiple possible ways to install third-party packages in
    
    107
    +the playground:
    
    108
    +
    
    109
    +- Start from a `wasm32-wasi-ghc` installation, use `wasm32-wasi-cabal
    
    110
    +  v1-install --global` to install everything to the global package
    
    111
    +  database. In theory this is the simplest way, though I haven't tried
    
    112
    +  it myself and it's unclear to what extent do `v1` commands work
    
    113
    +  these days.
    
    114
    +- Use default nix-style installation, then package the cabal store and
    
    115
    +  `dist-newstyle` directories into `rootfs.tar.zst`, and pass the
    
    116
    +  right package database flags when calling GHC API.
    
    117
    +
    
    118
    +Note that cabal built packages are not relocatable! So things will
    
    119
    +break if you build them at a host location and then package into a
    
    120
    +different absolute path into the rootfs, keep this in mind.
    
    121
    +
    
    122
    +If you have any difficulties, you're welcome to the [Haskell
    
    123
    +Wasm](https://matrix.to/#/#haskell.wasm:matrix.org) matrix room for
    
    124
    +community support.

  • testsuite/tests/ghc-api-browser/all.T
    1
    +# makefile_test/run_command is skipped when config.target_wrapper is
    
    2
    +# not None, see test_common_work in testsuite/driver/testlib.py. for
    
    3
    +# now just use this workaround to run custom test script here; ideally
    
    4
    +# we'd fix test failures elsewhere and enable
    
    5
    +# makefile_test/run_command for cross targets some day.
    
    6
    +async def stub_run_command(name, way, cmd):
    
    7
    +  return await run_command(name, way, cmd)
    
    8
    +
    
    9
    +
    
    10
    +# config.target_wrapper is prepended when running any command when
    
    11
    +# testing a cross target, see simple_run in
    
    12
    +# testsuite/driver/testlib.py. this is problematic when running a host
    
    13
    +# test script. for now do this override; ideally we'd have clear
    
    14
    +# host/target distinction for command invocations in the testsuite
    
    15
    +# driver instead of just a command string.
    
    16
    +def override_target_wrapper(name, opts):
    
    17
    +  opts.target_wrapper = ""
    
    18
    +
    
    19
    +
    
    20
    +setTestOpts(
    
    21
    +  [
    
    22
    +    unless(arch("wasm32"), skip),
    
    23
    +    override_target_wrapper,
    
    24
    +    high_memory_usage,
    
    25
    +    ignore_stderr,
    
    26
    +    only_ways(["dyn"]),
    
    27
    +    extra_ways(["dyn"]),
    
    28
    +  ]
    
    29
    +)
    
    30
    +
    
    31
    +
    
    32
    +test(
    
    33
    +  "playground001",
    
    34
    +  [
    
    35
    +    # pretty heavyweight, just test one browser for now.
    
    36
    +    unless("FIREFOX_LAUNCH_OPTS" in ghc_env, skip),
    
    37
    +    extra_files(
    
    38
    +      [
    
    39
    +        "../../../.gitlab/hello.hs",
    
    40
    +        "../../../utils/jsffi/dyld.mjs",
    
    41
    +        "../../../utils/jsffi/post-link.mjs",
    
    42
    +        "../../../utils/jsffi/prelude.mjs",
    
    43
    +        "index.html",
    
    44
    +        "playground001.hs",
    
    45
    +        "playground001.js",
    
    46
    +        "playground001.sh",
    
    47
    +      ]
    
    48
    +    ),
    
    49
    +  ],
    
    50
    +  stub_run_command,
    
    51
    +  ['./playground001.sh "$FIREFOX_LAUNCH_OPTS"'],
    
    52
    +)

  • testsuite/tests/ghc-api-browser/index.html
    1
    +<!DOCTYPE html>
    
    2
    +<html lang="en">
    
    3
    +  <head>
    
    4
    +    <meta charset="utf-8" />
    
    5
    +    <meta name="viewport" content="width=device-width, initial-scale=1" />
    
    6
    +    <title>ghc-in-browser</title>
    
    7
    +    <link
    
    8
    +      rel="stylesheet"
    
    9
    +      href="https://cdn.jsdelivr.net/npm/modern-normalize/modern-normalize.min.css"
    
    10
    +    />
    
    11
    +    <style>
    
    12
    +      html,
    
    13
    +      body {
    
    14
    +        height: 100%;
    
    15
    +      }
    
    16
    +      body {
    
    17
    +        margin: 0;
    
    18
    +        font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
    
    19
    +        background: #0f172a;
    
    20
    +        color: #e5e7eb;
    
    21
    +      }
    
    22
    +      .app {
    
    23
    +        height: 100vh;
    
    24
    +        display: grid;
    
    25
    +        gap: 0.5rem;
    
    26
    +        padding: 0.5rem;
    
    27
    +      }
    
    28
    +      @media (min-width: 800px) {
    
    29
    +        .app {
    
    30
    +          grid-template-columns: 1fr 1fr;
    
    31
    +        }
    
    32
    +      }
    
    33
    +      @media (max-width: 799.98px) {
    
    34
    +        .app {
    
    35
    +          grid-template-rows: 1fr 1fr;
    
    36
    +        }
    
    37
    +      }
    
    38
    +      .pane {
    
    39
    +        background: #111827;
    
    40
    +        border: 1px solid #1f2937;
    
    41
    +        border-radius: 12px;
    
    42
    +        display: flex;
    
    43
    +        flex-direction: column;
    
    44
    +        min-height: 0;
    
    45
    +      }
    
    46
    +      header {
    
    47
    +        padding: 0.5rem 0.75rem;
    
    48
    +        border-bottom: 1px solid #1f2937;
    
    49
    +        font-weight: 600;
    
    50
    +      }
    
    51
    +      #editor {
    
    52
    +        flex: 1;
    
    53
    +        min-height: 0;
    
    54
    +      }
    
    55
    +      .right {
    
    56
    +        padding: 0.6rem;
    
    57
    +        gap: 0.6rem;
    
    58
    +      }
    
    59
    +      .controls {
    
    60
    +        display: flex;
    
    61
    +        gap: 0.5rem;
    
    62
    +        flex-wrap: wrap;
    
    63
    +        margin-bottom: 0.4rem;
    
    64
    +      }
    
    65
    +      .controls input[type="text"] {
    
    66
    +        flex: 1;
    
    67
    +        min-width: 200px;
    
    68
    +        background: #0b1020;
    
    69
    +        color: #e5e7eb;
    
    70
    +        border: 1px solid #223;
    
    71
    +        border-radius: 8px;
    
    72
    +        padding: 0.55rem;
    
    73
    +      }
    
    74
    +      .controls button {
    
    75
    +        background: #22c55e;
    
    76
    +        border: none;
    
    77
    +        border-radius: 8px;
    
    78
    +        padding: 0.55rem 0.85rem;
    
    79
    +        font-weight: 600;
    
    80
    +        cursor: pointer;
    
    81
    +      }
    
    82
    +      .outputs {
    
    83
    +        display: block;
    
    84
    +      }
    
    85
    +      .outputs .label {
    
    86
    +        font-size: 0.85rem;
    
    87
    +        opacity: 0.8;
    
    88
    +        margin: 0.35rem 0;
    
    89
    +      }
    
    90
    +      .outputs textarea {
    
    91
    +        display: block;
    
    92
    +        width: 100%;
    
    93
    +        min-height: 30vh;
    
    94
    +        background: #0b1020;
    
    95
    +        color: #d1fae5;
    
    96
    +        border: 1px solid #223;
    
    97
    +        border-radius: 8px;
    
    98
    +        padding: 0.6rem;
    
    99
    +        resize: vertical;
    
    100
    +      }
    
    101
    +      .stderr {
    
    102
    +        color: #fee2e2;
    
    103
    +      }
    
    104
    +    </style>
    
    105
    +
    
    106
    +    <script async type="module">
    
    107
    +      import * as monaco from "https://cdn.jsdelivr.net/npm/monaco-editor/+esm";
    
    108
    +      import {
    
    109
    +        ConsoleStdout,
    
    110
    +        File,
    
    111
    +        OpenFile,
    
    112
    +        PreopenDirectory,
    
    113
    +        WASI,
    
    114
    +      } from "https://esm.sh/gh/haskell-wasm/browser_wasi_shim";
    
    115
    +      import { DyLDBrowserHost, main } from "./dyld.mjs";
    
    116
    +
    
    117
    +      const rootfs = new PreopenDirectory("/", []);
    
    118
    +
    
    119
    +      const bsdtar_wasi = new WASI(
    
    120
    +        ["bsdtar.wasm", "-x"],
    
    121
    +        [],
    
    122
    +        [
    
    123
    +          new OpenFile(new File(new Uint8Array(), { readonly: true })),
    
    124
    +          ConsoleStdout.lineBuffered((msg) => console.info(msg)),
    
    125
    +          ConsoleStdout.lineBuffered((msg) => console.warn(msg)),
    
    126
    +          rootfs,
    
    127
    +        ],
    
    128
    +        { debug: false }
    
    129
    +      );
    
    130
    +
    
    131
    +      const [{ instance }, rootfs_bytes] = await Promise.all([
    
    132
    +        WebAssembly.instantiateStreaming(
    
    133
    +          fetch("https://haskell-wasm.github.io/bsdtar-wasm/bsdtar.wasm"),
    
    134
    +          { wasi_snapshot_preview1: bsdtar_wasi.wasiImport }
    
    135
    +        ),
    
    136
    +        fetch("./rootfs.tar.zst").then((r) => r.bytes()),
    
    137
    +      ]);
    
    138
    +
    
    139
    +      bsdtar_wasi.fds[0] = new OpenFile(
    
    140
    +        new File(rootfs_bytes, { readonly: true })
    
    141
    +      );
    
    142
    +      bsdtar_wasi.start(instance);
    
    143
    +
    
    144
    +      if (document.readyState === "loading") {
    
    145
    +        await new Promise((res) =>
    
    146
    +          document.addEventListener("DOMContentLoaded", res, { once: true })
    
    147
    +        );
    
    148
    +      }
    
    149
    +
    
    150
    +      window.editor = monaco.editor.create(document.getElementById("editor"), {
    
    151
    +        value: 'main :: IO ()\nmain = putStrLn "Hello, Haskell!"\n',
    
    152
    +        language: "haskell",
    
    153
    +        automaticLayout: true,
    
    154
    +        minimap: { enabled: false },
    
    155
    +        theme: "vs-dark",
    
    156
    +        fontSize: 14,
    
    157
    +      });
    
    158
    +
    
    159
    +      const dyld = await main({
    
    160
    +        rpc: new DyLDBrowserHost({
    
    161
    +          rootfs,
    
    162
    +          stdout: (msg) => {
    
    163
    +            document.getElementById("stdout").value += `${msg}\n`;
    
    164
    +          },
    
    165
    +          stderr: (msg) => {
    
    166
    +            document.getElementById("stderr").value += `${msg}\n`;
    
    167
    +          },
    
    168
    +        }),
    
    169
    +        searchDirs: [
    
    170
    +          "/tmp/clib",
    
    171
    +          "/tmp/hslib/lib/wasm32-wasi-ghc-9.15.20251024",
    
    172
    +        ],
    
    173
    +        mainSoPath: "/tmp/libplayground001.so",
    
    174
    +        args: ["libplayground001.so", "+RTS", "-c", "-RTS"],
    
    175
    +        isIserv: false,
    
    176
    +      });
    
    177
    +      const main_func = await dyld.exportFuncs.myMain("/tmp/hslib/lib");
    
    178
    +
    
    179
    +      document.getElementById("runBtn").addEventListener("click", async () => {
    
    180
    +        document.getElementById("runBtn").disabled = true;
    
    181
    +
    
    182
    +        try {
    
    183
    +          document.getElementById("stdout").value = "";
    
    184
    +          document.getElementById("stderr").value = "";
    
    185
    +
    
    186
    +          await main_func(
    
    187
    +            document.getElementById("ghcArgs").value,
    
    188
    +            editor.getValue()
    
    189
    +          );
    
    190
    +        } finally {
    
    191
    +          document.getElementById("runBtn").disabled = false;
    
    192
    +        }
    
    193
    +      });
    
    194
    +
    
    195
    +      document.getElementById("runBtn").disabled = false;
    
    196
    +    </script>
    
    197
    +  </head>
    
    198
    +  <body>
    
    199
    +    <div class="app">
    
    200
    +      <section class="pane">
    
    201
    +        <header>Haskell Source</header>
    
    202
    +        <div id="editor"></div>
    
    203
    +      </section>
    
    204
    +
    
    205
    +      <section class="pane right">
    
    206
    +        <header>Controls / Output</header>
    
    207
    +        <div class="controls">
    
    208
    +          <input
    
    209
    +            id="ghcArgs"
    
    210
    +            type="text"
    
    211
    +            placeholder="GHC args"
    
    212
    +            style="font-family: ui-monospace, Menlo, Consolas, monospace"
    
    213
    +          />
    
    214
    +          <button id="runBtn" disabled="true">Run</button>
    
    215
    +        </div>
    
    216
    +        <div class="outputs">
    
    217
    +          <div class="label">stdout</div>
    
    218
    +          <textarea
    
    219
    +            id="stdout"
    
    220
    +            readonly
    
    221
    +            style="font-family: ui-monospace, Menlo, Consolas, monospace"
    
    222
    +          ></textarea>
    
    223
    +          <div class="label">stderr</div>
    
    224
    +          <textarea
    
    225
    +            id="stderr"
    
    226
    +            class="stderr"
    
    227
    +            readonly
    
    228
    +            style="font-family: ui-monospace, Menlo, Consolas, monospace"
    
    229
    +          ></textarea>
    
    230
    +        </div>
    
    231
    +      </section>
    
    232
    +    </div>
    
    233
    +  </body>
    
    234
    +</html>

  • testsuite/tests/ghc-api-browser/playground001.hs
    1
    +module Playground
    
    2
    +  ( myMain,
    
    3
    +  )
    
    4
    +where
    
    5
    +
    
    6
    +import Control.Monad
    
    7
    +import Data.Coerce
    
    8
    +import Data.IORef
    
    9
    +import GHC
    
    10
    +import GHC.Driver.Config.Diagnostic
    
    11
    +import GHC.Driver.Errors
    
    12
    +import GHC.Driver.Errors.Types
    
    13
    +import GHC.Driver.Monad
    
    14
    +import GHC.Plugins
    
    15
    +import GHC.Runtime.Interpreter
    
    16
    +import GHC.Utils.Exception
    
    17
    +import GHC.Wasm.Prim
    
    18
    +
    
    19
    +newtype JSFunction t = JSFunction JSVal
    
    20
    +
    
    21
    +type ExportedMainFunction = JSString -> JSString -> IO ()
    
    22
    +
    
    23
    +-- main entry point of playground001, returns a js async function that
    
    24
    +-- takes ghc args and Main.hs content, interprets Main.hs and runs
    
    25
    +-- Main.main.
    
    26
    +myMain :: JSString -> IO (JSFunction ExportedMainFunction)
    
    27
    +myMain js_libdir =
    
    28
    +  defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
    
    29
    +    libdir <- evaluate $ fromJSString js_libdir
    
    30
    +    freeJSVal $ coerce js_libdir
    
    31
    +    -- we don't use runGhc since we want to share a session to be
    
    32
    +    -- reused.
    
    33
    +    session <- Session <$> newIORef undefined
    
    34
    +    -- save a fresh default dflags, otherwise user input ghc args are
    
    35
    +    -- not properly reset.
    
    36
    +    dflags0 <- flip reflectGhc session $ do
    
    37
    +      initGhcMonad (Just libdir)
    
    38
    +      dflags0 <- getSessionDynFlags
    
    39
    +      setSessionDynFlags $
    
    40
    +        dflags0
    
    41
    +          { ghcMode = CompManager,
    
    42
    +            backend = bytecodeBackend,
    
    43
    +            ghcLink = LinkInMemory,
    
    44
    +            verbosity = 1
    
    45
    +          }
    
    46
    +      getSessionDynFlags
    
    47
    +    -- this is always run in a forked thread. which is fine as long as
    
    48
    +    -- the sesssion is not reused concurrently, but it's up to the
    
    49
    +    -- caller in js to ensure that. we simply disable the run button
    
    50
    +    -- until each run completes in the playground ui logic.
    
    51
    +    toMainFunc $ \js_args js_src ->
    
    52
    +      defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
    
    53
    +        args <- evaluate $ words $ fromJSString js_args
    
    54
    +        freeJSVal $ coerce js_args
    
    55
    +        writeFile f $ fromJSString js_src
    
    56
    +        freeJSVal $ coerce js_src
    
    57
    +        -- it's fine to call withCleanupSession since it just cleans up
    
    58
    +        -- tmpfs for now. in the future if it does more cleanup that
    
    59
    +        -- makes the session state invalid for reuse, just remove it;
    
    60
    +        -- everything will be cleaned up anyway when the browser tab is
    
    61
    +        -- closed
    
    62
    +        flip reflectGhc session $ withCleanupSession $ do
    
    63
    +          setSessionDynFlags dflags0
    
    64
    +          logger0 <- getLogger
    
    65
    +          (dflags1, _, dynamicFlagWarnings) <-
    
    66
    +            parseDynamicFlags logger0 dflags0 $ map noLoc args
    
    67
    +          setSessionDynFlags dflags1
    
    68
    +          logger1 <- getLogger
    
    69
    +          liftIO
    
    70
    +            $ printOrThrowDiagnostics
    
    71
    +              logger1
    
    72
    +              (initPrintConfig dflags1)
    
    73
    +              (initDiagOpts dflags1)
    
    74
    +            $ GhcDriverMessage
    
    75
    +              <$> dynamicFlagWarnings
    
    76
    +          setTargets =<< (: []) <$> guessTarget f Nothing Nothing
    
    77
    +          r <- load LoadAllTargets
    
    78
    +          when (failed r) $ fail "load returned Failed"
    
    79
    +          setContext [IIDecl $ simpleImportDecl $ mkModuleName "Main"]
    
    80
    +          fhv <- compileExprRemote "Main.main"
    
    81
    +          hsc_env <- getSession
    
    82
    +          liftIO $ evalIO (hscInterp hsc_env) fhv
    
    83
    +  where
    
    84
    +    f = "/tmp/Main.hs"
    
    85
    +
    
    86
    +foreign import javascript "wrapper"
    
    87
    +  toMainFunc ::
    
    88
    +    ExportedMainFunction ->
    
    89
    +    IO (JSFunction ExportedMainFunction)
    
    90
    +
    
    91
    +foreign export javascript "myMain"
    
    92
    +  myMain ::
    
    93
    +    JSString ->
    
    94
    +    IO
    
    95
    +      (JSFunction ExportedMainFunction)

  • testsuite/tests/ghc-api-browser/playground001.js
    1
    +#!/usr/bin/env -S node
    
    2
    +
    
    3
    +const puppeteer = require("puppeteer-core");
    
    4
    +const fs = require("node:fs");
    
    5
    +const path = require("node:path");
    
    6
    +
    
    7
    +class Playground {
    
    8
    +  static #token = Symbol("Playground");
    
    9
    +  #browser;
    
    10
    +  #page;
    
    11
    +
    
    12
    +  static async create({ launchOpts, artifactDir }) {
    
    13
    +    const playground = new Playground(Playground.#token);
    
    14
    +    playground.#browser = await puppeteer.launch(launchOpts);
    
    15
    +
    
    16
    +    playground.#page = await playground.#browser.newPage();
    
    17
    +    await playground.#page.setRequestInterception(true);
    
    18
    +    playground.#page.on("request", async (req) => {
    
    19
    +      if (!req.url().startsWith("http://localhost")) {
    
    20
    +        return req.continue();
    
    21
    +      }
    
    22
    +
    
    23
    +      try {
    
    24
    +        const f = req.url().replace("http://localhost", artifactDir);
    
    25
    +        return req.respond({
    
    26
    +          status: 200,
    
    27
    +          contentType:
    
    28
    +            {
    
    29
    +              ".html": "text/html",
    
    30
    +              ".mjs": "application/javascript",
    
    31
    +            }[path.extname(f)] || "application/octet-stream",
    
    32
    +          body: await fs.promises.readFile(f),
    
    33
    +        });
    
    34
    +      } catch {
    
    35
    +        return req.abort();
    
    36
    +      }
    
    37
    +    });
    
    38
    +
    
    39
    +    await playground.#page.goto("http://localhost/index.html");
    
    40
    +    await playground.#page.locator("#runBtn:enabled").wait();
    
    41
    +    return playground;
    
    42
    +  }
    
    43
    +
    
    44
    +  async close() {
    
    45
    +    await this.#browser.close();
    
    46
    +  }
    
    47
    +
    
    48
    +  async runMain({ mainSrc, ghcArgs }) {
    
    49
    +    await Promise.all([
    
    50
    +      this.#page.evaluate((mainSrc) => editor.setValue(mainSrc), mainSrc),
    
    51
    +      this.#page.locator("#ghcArgs").fill(ghcArgs),
    
    52
    +    ]);
    
    53
    +    await this.#page.locator("#runBtn:enabled").click();
    
    54
    +    await this.#page.locator("#runBtn:enabled").wait();
    
    55
    +
    
    56
    +    const [stdout, stderr] = await Promise.all(
    
    57
    +      ["#stdout", "#stderr"].map((el) =>
    
    58
    +        this.#page
    
    59
    +          .locator(el)
    
    60
    +          .map((t) => t.value)
    
    61
    +          .wait()
    
    62
    +      )
    
    63
    +    );
    
    64
    +
    
    65
    +    return { stdout, stderr };
    
    66
    +  }
    
    67
    +
    
    68
    +  constructor(token) {
    
    69
    +    if (token !== Playground.#token) {
    
    70
    +      throw new Error("new Playground() is forbidden, use Playground.create()");
    
    71
    +    }
    
    72
    +  }
    
    73
    +}
    
    74
    +
    
    75
    +(async () => {
    
    76
    +  const playground = await Playground.create({
    
    77
    +    launchOpts: JSON.parse(process.argv[2]),
    
    78
    +    artifactDir: process.cwd(),
    
    79
    +  });
    
    80
    +
    
    81
    +  try {
    
    82
    +    const { stdout, stderr } = await playground.runMain({
    
    83
    +      mainSrc: await fs.promises.readFile("./hello.hs", { encoding: "utf-8" }),
    
    84
    +      ghcArgs: "-package ghc -v0",
    
    85
    +    });
    
    86
    +    process.stdout.write(stdout);
    
    87
    +    process.stderr.write(stderr);
    
    88
    +  } finally {
    
    89
    +    await playground.close();
    
    90
    +  }
    
    91
    +})();

  • testsuite/tests/ghc-api-browser/playground001.sh
    1
    +#!/usr/bin/env bash
    
    2
    +
    
    3
    +set -euo pipefail
    
    4
    +
    
    5
    +# also set this when building wasm32-wasi-ghc for production
    
    6
    +# deployment of haskell playground, so all the .so files are
    
    7
    +# optimized.
    
    8
    +export WASM_SO_OPT="--debuginfo --low-memory-unused --strip-dwarf -Oz"
    
    9
    +
    
    10
    +# we'll build a rootfs tarball that contains everything in tmp and
    
    11
    +# extracts to /tmp, mapped from here
    
    12
    +mkdir ./tmp
    
    13
    +
    
    14
    +$TEST_HC \
    
    15
    +  -v0 \
    
    16
    +  -package ghc \
    
    17
    +  -shared -dynamic \
    
    18
    +  -no-keep-hi-files -no-keep-o-files \
    
    19
    +  -O2 \
    
    20
    +  playground001.hs -o ./tmp/libplayground001.so
    
    21
    +rm -f ./*_stub.h ./playground001.hs
    
    22
    +
    
    23
    +# /tmp/clib contains libc/libc++ .so files
    
    24
    +cp -r "$(dirname "$TEST_CC")/../share/wasi-sysroot/lib/wasm32-wasi" ./tmp/clib
    
    25
    +# trim unneeded stuff in c libdir
    
    26
    +find ./tmp/clib -type f ! -name "*.so" -delete
    
    27
    +rm -f \
    
    28
    +  ./tmp/clib/libsetjmp.so \
    
    29
    +  ./tmp/clib/libwasi-emulated-*.so
    
    30
    +
    
    31
    +# /tmp/hslib/lib is the ghc libdir
    
    32
    +mkdir ./tmp/hslib
    
    33
    +cp -r "$($TEST_HC --print-libdir)" ./tmp/hslib/lib
    
    34
    +# unregister Cabal/Cabal-syntax, too big
    
    35
    +$GHC_PKG --no-user-package-db --global-package-db=./tmp/hslib/lib/package.conf.d unregister Cabal Cabal-syntax
    
    36
    +$GHC_PKG --no-user-package-db --global-package-db=./tmp/hslib/lib/package.conf.d recache
    
    37
    +# we only need non-profiling .dyn_hi/.so, trim as much as we can
    
    38
    +find ./tmp/hslib/lib "(" \
    
    39
    +  -name "*.hi" \
    
    40
    +  -o -name "*.a" \
    
    41
    +  -o -name "*.p_hi" \
    
    42
    +  -o -name "libHS*_p.a" \
    
    43
    +  -o -name "*.p_dyn_hi" \
    
    44
    +  -o -name "libHS*_p*.so" \
    
    45
    +  -o -name "libHSrts*_debug*.so" \
    
    46
    +  ")" -delete
    
    47
    +rm -rf \
    
    48
    +  ./tmp/hslib/lib/doc \
    
    49
    +  ./tmp/hslib/lib/html \
    
    50
    +  ./tmp/hslib/lib/latex \
    
    51
    +  ./tmp/hslib/lib/*.mjs \
    
    52
    +  ./tmp/hslib/lib/*.js \
    
    53
    +  ./tmp/hslib/lib/*.txt
    
    54
    +# HS_SEARCHDIR is something like
    
    55
    +# /tmp/hslib/lib/wasm32-wasi-ghc-9.15.20251024 which is the
    
    56
    +# dynamic-library-dirs that contains all libHS*.so in one place, and
    
    57
    +# also static libraries in per-unit directories
    
    58
    +HS_SEARCHDIR=$(find ./tmp/hslib/lib -type f -name "*.so" -print0 | xargs -0 -n1 dirname | sort -u | sed "s|^\./|/|")
    
    59
    +# hunt down the remaining bits of Cabal/Cabal-syntax. too bad there's
    
    60
    +# no ghc-pkg uninstall.
    
    61
    +rm -rf ."$HS_SEARCHDIR"/*Cabal*
    
    62
    +
    
    63
    +# fix the hard coded search dir in index.html
    
    64
    +SED_IS_GNU=$(sed --version &> /dev/null && echo 1 || echo 0)
    
    65
    +if [[ $SED_IS_GNU == "1" ]]; then
    
    66
    +  sed -i "s|/tmp/hslib/lib/wasm32-wasi-ghc-9.15.20251024|$HS_SEARCHDIR|" ./index.html
    
    67
    +else
    
    68
    +  sed -i "" "s|/tmp/hslib/lib/wasm32-wasi-ghc-9.15.20251024|$HS_SEARCHDIR|" ./index.html
    
    69
    +fi
    
    70
    +
    
    71
    +# also set ZSTD_NBTHREADS/ZSTD_CLEVEL when building for production
    
    72
    +tar -cf ./rootfs.tar.zst --zstd tmp
    
    73
    +rm -rf ./tmp
    
    74
    +
    
    75
    +# pass puppeteer.launch() opts as json
    
    76
    +exec ./playground001.js "$1"

  • testsuite/tests/ghci-wasm/T26431.stdout โ†’ testsuite/tests/ghc-api-browser/playground001.stdout

  • testsuite/tests/ghci-wasm/T26431.hs deleted
    1
    -import Control.Exception
    
    2
    -import Control.Monad.IO.Class
    
    3
    -import Data.Maybe
    
    4
    -import GHC
    
    5
    -import GHC.Plugins
    
    6
    -import GHC.Runtime.Interpreter
    
    7
    -import System.Environment.Blank
    
    8
    -
    
    9
    -main :: IO ()
    
    10
    -main = do
    
    11
    -  [libdir] <- getArgs
    
    12
    -  defaultErrorHandler defaultFatalMessager defaultFlushOut $
    
    13
    -    runGhc (Just libdir) $
    
    14
    -      do
    
    15
    -        dflags0 <- getSessionDynFlags
    
    16
    -        let dflags1 =
    
    17
    -              dflags0
    
    18
    -                { ghcMode = CompManager,
    
    19
    -                  backend = bytecodeBackend,
    
    20
    -                  ghcLink = LinkInMemory
    
    21
    -                }
    
    22
    -        logger <- getLogger
    
    23
    -        (dflags2, _, _) <-
    
    24
    -          parseDynamicFlags logger dflags1 $
    
    25
    -            map noLoc ["-package", "ghc"]
    
    26
    -        _ <- setSessionDynFlags dflags2
    
    27
    -        addTarget =<< guessTarget "hello.hs" Nothing Nothing
    
    28
    -        _ <- load LoadAllTargets
    
    29
    -        setContext
    
    30
    -          [ IIDecl $ simpleImportDecl $ mkModuleName "Prelude",
    
    31
    -            IIDecl $ simpleImportDecl $ mkModuleName "Main"
    
    32
    -          ]
    
    33
    -        hsc_env <- getSession
    
    34
    -        fhv <- compileExprRemote "main"
    
    35
    -        liftIO $ evalIO (fromJust $ hsc_interp hsc_env) fhv

  • testsuite/tests/ghci-wasm/all.T
    ... ... @@ -10,11 +10,3 @@ test('T26430', [
    10 10
       extra_hc_opts('-L. -lT26430B')]
    
    11 11
     , compile_and_run, ['']
    
    12 12
     )
    13
    -
    
    14
    -test('T26431', [
    
    15
    -  extra_files(['../../../.gitlab/hello.hs']),
    
    16
    -  extra_hc_opts('-package ghc'),
    
    17
    -  extra_run_opts(f'"{config.libdir}"'),
    
    18
    -  ignore_stderr]
    
    19
    -, compile_and_run, ['']
    
    20
    -)

  • 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,27 +307,27 @@ 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
    -  constructor() {
    
    322
    -    // Inherited pipe file descriptors from GHC
    
    323
    -    const out_fd = Number.parseInt(process.argv[4]),
    
    324
    -      in_fd = Number.parseInt(process.argv[5]);
    
    325
    -
    
    319
    +  constructor({ outFd, inFd }) {
    
    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 outFd === "number" && typeof inFd === "number")) {
    
    324
    +      return;
    
    325
    +    }
    
    326 326
         this.readStream = stream.Readable.toWeb(
    
    327
    -      fs.createReadStream(undefined, { fd: in_fd })
    
    327
    +      fs.createReadStream(undefined, { fd: inFd })
    
    328 328
         );
    
    329 329
         this.writeStream = stream.Writable.toWeb(
    
    330
    -      fs.createWriteStream(undefined, { fd: out_fd })
    
    330
    +      fs.createWriteStream(undefined, { fd: outFd })
    
    331 331
         );
    
    332 332
       }
    
    333 333
     
    
    ... ... @@ -377,6 +377,72 @@ class DyLDHost {
    377 377
       }
    
    378 378
     }
    
    379 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
    +  // Continuations to output a single line to stdout/stderr
    
    388
    +  stdout;
    
    389
    +  stderr;
    
    390
    +
    
    391
    +  // Given canonicalized absolute file path, returns the File object,
    
    392
    +  // or null if absent
    
    393
    +  #readFile(p) {
    
    394
    +    const { ret, entry } = this.rootfs.dir.get_entry_for_path({
    
    395
    +      parts: p.split("/").filter((tok) => tok !== ""),
    
    396
    +      is_dir: false,
    
    397
    +    });
    
    398
    +    return ret === 0 ? entry : null;
    
    399
    +  }
    
    400
    +
    
    401
    +  constructor({ rootfs, stdout, stderr }) {
    
    402
    +    this.rootfs = rootfs
    
    403
    +      ? rootfs
    
    404
    +      : new wasi.PreopenDirectory("/", [["tmp", new wasi.Directory([])]]);
    
    405
    +    this.stdout = stdout ? stdout : (msg) => console.info(msg);
    
    406
    +    this.stderr = stderr ? stderr : (msg) => console.warn(msg);
    
    407
    +  }
    
    408
    +
    
    409
    +  // p must be canonicalized absolute path
    
    410
    +  async addLibrarySearchPath(p) {
    
    411
    +    this.#rpaths.add(p);
    
    412
    +    return null;
    
    413
    +  }
    
    414
    +
    
    415
    +  async findSystemLibrary(f) {
    
    416
    +    if (f.startsWith("/")) {
    
    417
    +      if (this.#readFile(f)) {
    
    418
    +        return f;
    
    419
    +      }
    
    420
    +      throw new Error(`findSystemLibrary(${f}): not found in /`);
    
    421
    +    }
    
    422
    +
    
    423
    +    for (const rpath of this.#rpaths) {
    
    424
    +      const r = `${rpath}/${f}`;
    
    425
    +      if (this.#readFile(r)) {
    
    426
    +        return r;
    
    427
    +      }
    
    428
    +    }
    
    429
    +
    
    430
    +    throw new Error(
    
    431
    +      `findSystemLibrary(${f}): not found in ${[...this.#rpaths]}`
    
    432
    +    );
    
    433
    +  }
    
    434
    +
    
    435
    +  async fetchWasm(p) {
    
    436
    +    const entry = this.#readFile(p);
    
    437
    +    const r = new Response(entry.data, {
    
    438
    +      headers: { "Content-Type": "application/wasm" },
    
    439
    +    });
    
    440
    +    // It's only fetched once, take the chance to prune it in vfs to save memory
    
    441
    +    entry.data = new Uint8Array();
    
    442
    +    return r;
    
    443
    +  }
    
    444
    +}
    
    445
    +
    
    380 446
     // Fulfill the same functionality as DyLDHost by doing fetch() calls
    
    381 447
     // to respective RPC endpoints of a host http server. Also manages
    
    382 448
     // WebSocket connections back to host.
    
    ... ... @@ -494,7 +560,7 @@ export class DyLDRPC {
    494 560
     
    
    495 561
     // Actual implementation of endpoints used by DyLDRPC
    
    496 562
     class DyLDRPCServer {
    
    497
    -  #dyldHost = new DyLDHost();
    
    563
    +  #dyldHost;
    
    498 564
       #server;
    
    499 565
       #wss;
    
    500 566
     
    
    ... ... @@ -502,11 +568,15 @@ class DyLDRPCServer {
    502 568
         host,
    
    503 569
         port,
    
    504 570
         dyldPath,
    
    505
    -    libdir,
    
    506
    -    ghciSoPath,
    
    571
    +    searchDirs,
    
    572
    +    mainSoPath,
    
    573
    +    outFd,
    
    574
    +    inFd,
    
    507 575
         args,
    
    508 576
         redirectWasiConsole,
    
    509 577
       }) {
    
    578
    +    this.#dyldHost = new DyLDHost({ outFd, inFd });
    
    579
    +
    
    510 580
         this.#server = http.createServer(async (req, res) => {
    
    511 581
           const origin = originFromServerAddress(await this.listening);
    
    512 582
     
    
    ... ... @@ -540,7 +610,7 @@ class DyLDRPCServer {
    540 610
             res.end(
    
    541 611
               `
    
    542 612
     import { DyLDRPC, main } from "./fs${dyldPath}";
    
    543
    -const args = ${JSON.stringify({ libdir, ghciSoPath, args })};
    
    613
    +const args = ${JSON.stringify({ searchDirs, mainSoPath, args, isIserv: true })};
    
    544 614
     args.rpc = new DyLDRPC({origin: "${origin}", redirectWasiConsole: ${redirectWasiConsole}});
    
    545 615
     args.rpc.opened.then(() => main(args));
    
    546 616
     `
    
    ... ... @@ -829,11 +899,37 @@ class DyLD {
    829 899
               ),
    
    830 900
               wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stdout(msg)),
    
    831 901
               wasi.ConsoleStdout.lineBuffered((msg) => this.#rpc.stderr(msg)),
    
    902
    +          // for ghci browser mode, default to an empty rootfs with
    
    903
    +          // /tmp
    
    904
    +          this.#rpc instanceof DyLDBrowserHost
    
    905
    +            ? this.#rpc.rootfs
    
    906
    +            : new wasi.PreopenDirectory("/", [["tmp", new wasi.Directory([])]]),
    
    832 907
             ],
    
    833 908
             { debug: false }
    
    834 909
           );
    
    835 910
         }
    
    836 911
     
    
    912
    +    // Both wasi implementations we use provide
    
    913
    +    // wasi.initialize(instance) to initialize a wasip1 reactor
    
    914
    +    // module. However, instance does not really need to be a
    
    915
    +    // WebAssembly.Instance object; the wasi implementations only need
    
    916
    +    // to access instance.exports.memory for the wasi syscalls to
    
    917
    +    // work.
    
    918
    +    //
    
    919
    +    // Given we'll reuse the same wasi object across different
    
    920
    +    // WebAssembly.Instance objects anyway and
    
    921
    +    // wasi.initialize(instance) can't be called more than once, we
    
    922
    +    // use this simple trick and pass a fake instance object that
    
    923
    +    // contains just enough info for the wasi implementation to
    
    924
    +    // initialize its internal state. Later when we load each wasm
    
    925
    +    // shared library, we can just manually invoke their
    
    926
    +    // initialization functions.
    
    927
    +    this.#wasi.initialize({
    
    928
    +      exports: {
    
    929
    +        memory: this.#memory,
    
    930
    +      },
    
    931
    +    });
    
    932
    +
    
    837 933
         // Keep this in sync with rts/wasm/Wasm.S!
    
    838 934
         for (let i = 1; i <= 10; ++i) {
    
    839 935
           this.#regs[`__R${i}`] = new WebAssembly.Global({
    
    ... ... @@ -930,10 +1026,15 @@ class DyLD {
    930 1026
       async loadDLLs(packed) {
    
    931 1027
         // Normalize input to an array of strings. When called from Haskell
    
    932 1028
         // we pass a single JSString containing NUL-separated paths.
    
    933
    -    const paths = (typeof packed === "string"
    
    934
    -      ? (packed.length === 0 ? [] : packed.split("\0"))
    
    935
    -      : [packed] // tolerate an accidental single path object
    
    936
    -    ).filter((s) => s.length > 0).reverse();
    
    1029
    +    const paths = (
    
    1030
    +      typeof packed === "string"
    
    1031
    +        ? packed.length === 0
    
    1032
    +          ? []
    
    1033
    +          : packed.split("\0")
    
    1034
    +        : [packed]
    
    1035
    +    ) // tolerate an accidental single path object
    
    1036
    +      .filter((s) => s.length > 0)
    
    1037
    +      .reverse();
    
    937 1038
     
    
    938 1039
         // Compute a single downsweep plan for the whole batch.
    
    939 1040
         // Note: #downsweep mutates #loadedSos to break cycles and dedup.
    
    ... ... @@ -1154,22 +1255,6 @@ class DyLD {
    1154 1255
             throw new Error(`cannot handle export ${k} ${v}`);
    
    1155 1256
           }
    
    1156 1257
     
    
    1157
    -      // We call wasi.initialize when loading libc.so, then reuse the
    
    1158
    -      // wasi instance globally. When loading later .so files, just
    
    1159
    -      // manually invoke _initialize().
    
    1160
    -      if (soname === "libc.so") {
    
    1161
    -        instance.exports.__wasm_apply_data_relocs();
    
    1162
    -        // wasm-ld forbits --export-memory with --shared, I don't know
    
    1163
    -        // why but this is sufficient to make things work
    
    1164
    -        this.#wasi.initialize({
    
    1165
    -          exports: {
    
    1166
    -            memory: this.#memory,
    
    1167
    -            _initialize: instance.exports._initialize,
    
    1168
    -          },
    
    1169
    -        });
    
    1170
    -        continue;
    
    1171
    -      }
    
    1172
    -
    
    1173 1258
           // See
    
    1174 1259
           // https://gitlab.haskell.org/haskell-wasm/llvm-project/-/blob/release/21.x/lld/wasm/Writer.cpp#L1451,
    
    1175 1260
           // __wasm_apply_data_relocs is now optional so only call it if
    
    ... ... @@ -1180,7 +1265,7 @@ class DyLD {
    1180 1265
           // been called upon instantiation, see
    
    1181 1266
           // Writer::createStartFunction().
    
    1182 1267
           if (instance.exports.__wasm_apply_data_relocs) {
    
    1183
    -          instance.exports.__wasm_apply_data_relocs();
    
    1268
    +        instance.exports.__wasm_apply_data_relocs();
    
    1184 1269
           }
    
    1185 1270
     
    
    1186 1271
           instance.exports._initialize();
    
    ... ... @@ -1208,15 +1293,38 @@ class DyLD {
    1208 1293
       }
    
    1209 1294
     }
    
    1210 1295
     
    
    1211
    -export async function main({ rpc, libdir, ghciSoPath, args }) {
    
    1296
    +// The main entry point of dyld that may be run on node/browser, and
    
    1297
    +// may run either iserv defaultMain from the ghci library or an
    
    1298
    +// alternative entry point from another shared library
    
    1299
    +export async function main({
    
    1300
    +  rpc, // Handle the side effects of DyLD
    
    1301
    +  searchDirs, // Initial library search directories
    
    1302
    +  mainSoPath, // Could also be another shared library that's actually not ghci
    
    1303
    +  args, // WASI argv starting with the executable name. +RTS etc will be respected
    
    1304
    +  isIserv, // set to true when running iserv defaultServer
    
    1305
    +}) {
    
    1212 1306
       try {
    
    1213 1307
         const dyld = new DyLD({
    
    1214
    -      args: ["dyld.so", ...args],
    
    1308
    +      args,
    
    1215 1309
           rpc,
    
    1216 1310
         });
    
    1217
    -    await dyld.addLibrarySearchPath(libdir);
    
    1218
    -    await dyld.loadDLLs(ghciSoPath);
    
    1311
    +    for (const libdir of searchDirs) {
    
    1312
    +      await dyld.addLibrarySearchPath(libdir);
    
    1313
    +    }
    
    1314
    +    await dyld.loadDLLs(mainSoPath);
    
    1315
    +
    
    1316
    +    // At this point, rts/ghc-internal are loaded, perform wasm shared
    
    1317
    +    // library specific RTS startup logic, see Note [JSFFI initialization]
    
    1318
    +    dyld.exportFuncs.__ghc_wasm_jsffi_init();
    
    1319
    +
    
    1320
    +    // We're not running iserv, just return the dyld instance so user
    
    1321
    +    // could use it to invoke their exported functions, and don't
    
    1322
    +    // perform cleanup (see finally block)
    
    1323
    +    if (!isIserv) {
    
    1324
    +      return dyld;
    
    1325
    +    }
    
    1219 1326
     
    
    1327
    +    // iserv-specific logic follows
    
    1220 1328
         const reader = rpc.readStream.getReader();
    
    1221 1329
         const writer = rpc.writeStream.getWriter();
    
    1222 1330
     
    
    ... ... @@ -1235,31 +1343,25 @@ export async function main({ rpc, libdir, ghciSoPath, args }) {
    1235 1343
           writer.write(new Uint8Array(buf));
    
    1236 1344
         };
    
    1237 1345
     
    
    1238
    -    dyld.exportFuncs.__ghc_wasm_jsffi_init();
    
    1239
    -    await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send);
    
    1346
    +    return await dyld.exportFuncs.defaultServer(cb_sig, cb_recv, cb_send);
    
    1240 1347
       } finally {
    
    1241
    -    rpc.close();
    
    1348
    +    if (isIserv) {
    
    1349
    +      rpc.close();
    
    1350
    +    }
    
    1242 1351
       }
    
    1243 1352
     }
    
    1244 1353
     
    
    1245
    -(async () => {
    
    1246
    -  if (!isNode) {
    
    1247
    -    return;
    
    1248
    -  }
    
    1249
    -
    
    1250
    -  const libdir = process.argv[2];
    
    1251
    -  const ghciSoPath = process.argv[3];
    
    1252
    -  const args = process.argv.slice(6);
    
    1253
    -
    
    1354
    +// node-specific iserv-specific logic
    
    1355
    +async function nodeMain({ searchDirs, mainSoPath, outFd, inFd, args }) {
    
    1254 1356
       if (!process.env.GHCI_BROWSER) {
    
    1255
    -    const rpc = new DyLDHost();
    
    1256
    -    await main({
    
    1357
    +    const rpc = new DyLDHost({ outFd, inFd });
    
    1358
    +    return await main({
    
    1257 1359
           rpc,
    
    1258
    -      libdir,
    
    1259
    -      ghciSoPath,
    
    1360
    +      searchDirs,
    
    1361
    +      mainSoPath,
    
    1260 1362
           args,
    
    1363
    +      isIserv: true,
    
    1261 1364
         });
    
    1262
    -    return;
    
    1263 1365
       }
    
    1264 1366
     
    
    1265 1367
       if (!ws) {
    
    ... ... @@ -1272,8 +1374,10 @@ export async function main({ rpc, libdir, ghciSoPath, args }) {
    1272 1374
         host: process.env.GHCI_BROWSER_HOST || "127.0.0.1",
    
    1273 1375
         port: process.env.GHCI_BROWSER_PORT || 0,
    
    1274 1376
         dyldPath: import.meta.filename,
    
    1275
    -    libdir,
    
    1276
    -    ghciSoPath,
    
    1377
    +    searchDirs,
    
    1378
    +    mainSoPath,
    
    1379
    +    outFd,
    
    1380
    +    inFd,
    
    1277 1381
         args,
    
    1278 1382
         redirectWasiConsole:
    
    1279 1383
           process.env.GHCI_BROWSER_PUPPETEER_LAUNCH_OPTS ||
    
    ... ... @@ -1362,6 +1466,20 @@ export async function main({ rpc, libdir, ghciSoPath, args }) {
    1362 1466
       }
    
    1363 1467
     
    
    1364 1468
       console.log(
    
    1365
    -    `Open ${origin}/main.html or import ${origin}/main.js to boot ghci`
    
    1469
    +    `Open ${origin}/main.html or import("${origin}/main.js") to boot ghci`
    
    1366 1470
       );
    
    1367
    -})();
    1471
    +}
    
    1472
    +
    
    1473
    +const isNodeMain = isNode && import.meta.filename === process.argv[1];
    
    1474
    +
    
    1475
    +// node iserv as invoked by
    
    1476
    +// GHC.Runtime.Interpreter.Wasm.spawnWasmInterp
    
    1477
    +if (isNodeMain) {
    
    1478
    +  const clibdir = process.argv[2];
    
    1479
    +  const mainSoPath = process.argv[3];
    
    1480
    +  const outFd = Number.parseInt(process.argv[4]),
    
    1481
    +    inFd = Number.parseInt(process.argv[5]);
    
    1482
    +  const args = ["dyld.so", ...process.argv.slice(6)];
    
    1483
    +
    
    1484
    +  await nodeMain({ searchDirs: [clibdir], mainSoPath, outFd, inFd, args });
    
    1485
    +}