From 37381bcff385e871da7a24e78ba9540d14944a7d Mon Sep 17 00:00:00 2001
From: Cheng Shao <terrorjack@type.dance>
Date: Fri, 21 Mar 2025 06:05:25 +0000
Subject: [PATCH] docs: update Note [The Wasm Dynamic Linker]

This commit updates Note [The Wasm Dynamic Linker] to reflect recent
developments, in particular the wasm ghci browser mode.
---
 utils/jsffi/dyld.mjs | 76 ++++++++++++++++++++++++++------------------
 1 file changed, 45 insertions(+), 31 deletions(-)

diff --git a/utils/jsffi/dyld.mjs b/utils/jsffi/dyld.mjs
index 7c6f7688d4f..285cc8b7201 100755
--- a/utils/jsffi/dyld.mjs
+++ b/utils/jsffi/dyld.mjs
@@ -3,11 +3,50 @@
 // Note [The Wasm Dynamic Linker]
 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 //
-// This nodejs script implements the wasm dynamic linker to support
-// Template Haskell & ghci in the GHC wasm backend. It loads wasm
-// shared libraries, resolves symbols and runs code on demand,
-// communicating with the host GHC via the standard iserv protocol.
-// Below are questions/answers to elaborate the introduction:
+// This script mainly has two roles:
+//
+// 1. Message broker: relay iserv messages between host GHC and wasm
+//    iserv (GHCi.Server.defaultServer). This part only runs in
+//    nodejs.
+// 2. Dynamic linker: provide RTS linker interfaces like
+//    loadDLL/lookupSymbol etc which are imported by wasm iserv. This
+//    part can run in browsers as well.
+//
+// When GHC starts external interpreter for the wasm target, it starts
+// this script and passes a pair of pipe fds for iserv messages,
+// libHSghci.so path, and command line arguments for wasm iserv. By
+// default, the wasm iserv runs in the same node process, so the
+// message broker logic is simple: wrap the pipe fds as
+// ReadableStream/WritableStream, pass reader/writer callbacks to wasm
+// iserv and run it to completion. It doesn't need to intercept or
+// parse any message, unlike iserv-proxy.
+//
+// Things are a bit more interesting with ghci browser mode. All the
+// Haskell code and all the runtime runs in the browser, including the
+// dynamic linker parts of this script. The host GHC process doeesn't
+// need to know about "browser mode" at all as long as iserv messages
+// are handled as usual, though obviously we can't pass fds to
+// browsers like before! So this script starts an HTTP 1.1 server with
+// WebSockets support. The browser side can import a startup script
+// served by the server, which will import this script and invoke main
+// with the right arguments, hooray isomorphic JavaScript! The browser
+// side will proceed to bootstrap wasm iserv, and the iserv messages
+// are relayed over the WebSockets. (also ^C signals over a different
+// connection)
+//
+// Under the browser mode, there's more traffic than just the iserv
+// message WebSockets. The browser side can fulfill most of the RTS
+// linker functionality alone, but it still needs to do stuff like
+// searching for a shared library in a bunch of search paths or
+// fetching a shared library blob; these side effects require access
+// to the same host filesystem that runs GHC, so the HTTP server also
+// exposes some rpc endpoints that the browser side can perform
+// requests. The server binds to 127.0.0.1 by default for a good
+// reason, it doesn't (and shouldn't) have extra logic to try to guard
+// against potential malicious requests to scrape your home directory.
+//
+// So much intro to the message broker part, below are Q/As regarding
+// the dynamic linker part:
 //
 // *** What works right now and what doesn't work yet?
 //
@@ -17,16 +56,12 @@
 // by wasi preview1: we map the full host filesystem into wasm cause
 // yolo, but things like processes and sockets don't work.
 //
-// JSFFI is unsupported in bytecode yet. So in ghci you can't type in
-// code that contains JSFFI declarations, though you can invoke
-// compiled code that uses JSFFI.
-//
 // loadArchive/loadObj etc are unsupported and will stay that way. The
 // only form of compiled code that can be loaded is wasm shared
 // library. There's no code unloading logic. The retain_cafs flag is
 // ignored and revertCAFs is a no-op.
 //
-// ghc -j doesn't work yet (#25285).
+// JSFFI works. ghci debugger works.
 //
 // *** What are implications to end users?
 //
@@ -67,27 +102,6 @@
 // add once we need it.
 //
 // No fancy stuff like LD_PRELOAD, LD_LIBRARY_PATH etc.
-//
-// *** How does GHC interact with the wasm dynamic linker?
-//
-// dyld.mjs is tracked as a build dependency and installed in GHC
-// libdir with executable perms. When GHC targets wasm and needs to
-// start iserv, it starts dyld.mjs and manage the process lifecycle
-// through the entire GHC session. nodejs location is not tracked and
-// must be present in PATH.
-//
-// GHC passes the libHSghci*.so location via command line, so dyld.mjs
-// will load it as well as all dependent so files, then start the
-// default iserv implementation in the ghci library and read/write
-// binary messages. nodejs receives pipe file descriptors from GHC,
-// though uvwasi doesn't support preopening them as wasi virtual file
-// descriptors, therefore we hook a few wasi syscalls and designate
-// our own preopen file descriptors for IPC logic.
-//
-// dyld.mjs inherits default stdin/stdout/stderr from GHC and that's
-// how ghci works. Like native external interpreter, you can use the
-// -opti GHC flag to pass process arguments, like RTS flags or -opti-v
-// to dump the iserv messages.
 
 import { JSValManager, setImmediate } from "./prelude.mjs";
 import { parseRecord, parseSections } from "./post-link.mjs";
-- 
GitLab