diff --git a/libraries/ghci/GHCi/ObjLink.hs b/libraries/ghci/GHCi/ObjLink.hs index 1875918a9210ea5ac126a810fe7266eb5e37ecc6..4f63aa2f9b5a6a598b2db40fbb247975fcb10906 100644 --- a/libraries/ghci/GHCi/ObjLink.hs +++ b/libraries/ghci/GHCi/ObjLink.hs @@ -76,6 +76,9 @@ loadDLL f = evaluate =<< js_loadDLL (toJSString f) pure $ Right nullPtr +-- See Note [Variable passing in JSFFI] for where +-- __ghc_wasm_jsffi_dyld comes from + foreign import javascript safe "__ghc_wasm_jsffi_dyld.loadDLL($1)" js_loadDLL :: JSString -> IO () diff --git a/utils/jsffi/dyld.mjs b/utils/jsffi/dyld.mjs index c6cfedbece4f60075ebf4cb66929e1c2ed68ce29..db9c8b382d6c09e38d7bf6be252e05cf09c13bb0 100755 --- a/utils/jsffi/dyld.mjs +++ b/utils/jsffi/dyld.mjs @@ -629,7 +629,8 @@ class DyLD { // Fulfill the ghc_wasm_jsffi imports. Use new Function() // instead of eval() to prevent bindings in this local scope to - // be accessed by JSFFI code snippets. + // be accessed by JSFFI code snippets. See Note [Variable passing in JSFFI] + // for what's going on here. Object.assign( import_obj.ghc_wasm_jsffi, new Function( diff --git a/utils/jsffi/post-link.mjs b/utils/jsffi/post-link.mjs index 9e3e06435127ffc99f2bf01eb622c569053ba0af..b5493f35817d447b388f1fd3e4f04082171a070e 100755 --- a/utils/jsffi/post-link.mjs +++ b/utils/jsffi/post-link.mjs @@ -52,6 +52,47 @@ export function parseSections(mod) { return recs; } +// Note [Variable passing in JSFFI] +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// The JSFFI code snippets can access variables in globalThis, +// arguments like $1, $2, etc, plus a few magic variables: __exports, +// __ghc_wasm_jsffi_dyld and __ghc_wasm_jsffi_finalization_registry. +// How are these variables passed to JSFFI code? Remember, we strive +// to keep the globalThis namespace hygiene and maintain the ability +// to have multiple Haskell-wasm apps coexisting in the same JS +// context, so we must not pass magic variables as global variables +// even though they may seem globally unique. +// +// The solution is simple: put them in the JS lambda binder position. +// Though there are different layers of lambdas here: +// +// 1. User writes "$1($2, await $3)" in a JSFFI code snippet. No +// explicit binder here, the snippet is either an expression or +// some statements. +// 2. GHC doesn't know JS syntax but it knows JS function arity from +// HS type signature, as well as if the JS function is async/sync +// from safe/unsafe annotation. So it infers the JS binder (like +// "async ($1, $2, $3)") and emits a (name,binder,body) tuple into +// the ghc_wasm_jsffi custom section. +// 3. After link-time we collect these tuples to make a JS object +// mapping names to binder=>body, and this JS object will be used +// to fulfill the ghc_wasm_jsffi wasm imports. This JS object is +// returned by an outer layer of lambda which is in charge of +// passing magic variables. +// +// In case of post-linker for statically linked wasm modules, +// __ghc_wasm_jsffi_dyld won't work so is omitted, and +// __ghc_wasm_jsffi_finalization_registry can be created inside the +// outer JS lambda. Only __exports is exposed as user-visible API +// since it's up to the user to perform knot-tying by assigning the +// instance exports back to the (initially empty) __exports object +// passed to this lambda. +// +// In case of dyld, all magic variables are dyld-session-global +// variables; dyld uses new Function() to make the outer lambda, then +// immediately invokes it by passing the right magic variables. + export async function postLink(mod) { let src = ( await fs.readFile(path.join(import.meta.dirname, "prelude.mjs"), {