Skip to content
Snippets Groups Projects
Commit fd40eaa1 authored by Cheng Shao's avatar Cheng Shao :beach: Committed by Marge Bot
Browse files

wasm: revamp JSFFI internal implementation and documentation

This patch revamps the wasm backend's JSFFI internal implementation
and documentation:

- `JSValManager` logic to allocate a key is simplified to simple
  bumping. According to experiments with all major browsers, the
  internal `Map` would overflow the heap much earlier before we really
  exhaust the 32-bit key space, so there's no point in the extra
  complexity.
- `freeJSVal` is now idempotent and safe to call more than once. This
  is achieved by attaching the `StablePtr#` to the `JSVal#` closure
  and nullifying it when calling `freeJSVal`, so the same stable
  pointer cannot be double freed.
- `mkWeakJSVal` no longer exposes the internal `Weak#` pointer and
  always creates a new `Weak#` on the fly. Otherwise by finalizing
  that `Weak#`, user could accidentally drop the `JSVal`, but
  `mkWeakJSVal` is only supposed to create a `Weak` that observes the
  `JSVal`'s liveliness without actually interfering it.
- `PromisePendingException` is no longer exported since it's never
  meant to be caught by user code; it's a severe bug if it's actually
  raised at runtime.
- Everything exported by user-facing `GHC.Wasm.Prim` now has proper
  haddock documentation.
- Note [JSVal representation for wasm] has been updated to reflect the
  new JSVal# memory layout.
parent 75fcc5c9
No related branches found
No related tags found
No related merge requests found
......@@ -189,9 +189,9 @@ use of ``freeJSVal`` when you’re sure about a ``JSVal``\ ’s lifetime,
especially for the temporary ``JSVal``\ s. This will help reducing the
memory footprint at runtime.
Note that ``freeJSVal`` is not idempotent and it’s only safe to call it
exactly once or not at all. Once it’s called, any subsequent usage of
that ``JSVal`` results in a runtime panic.
Note that ``freeJSVal`` is idempotent and it’s safe to call it more
than once. After it’s called, any subsequent usage of that ``JSVal``
by passing to the JavaScript side results in a runtime panic.
.. _wasm-jsffi-import:
......@@ -390,7 +390,7 @@ callback and intends to call it later, so the Haskell function closure
is still retained by default.
Still, the runtime can gradually drop these retainers by using
``FinalizerRegistry`` to invoke the finalizers to free the underlying
``FinalizationRegistry`` to invoke the finalizers to free the underlying
stable pointers once the JavaScript callbacks are recycled.
One last corner case is cyclic reference between the two heaps: if a
......
{-# LANGUAGE NoImplicitPrelude #-}
module GHC.Wasm.Prim (
-- User-facing JSVal type and freeJSVal
-- * User-facing 'JSVal' and related utilities
JSVal,
freeJSVal,
mkWeakJSVal,
-- The JSString type and conversion from/to Haskell String
-- * 'JSString' and conversion from/to Haskell 'String'
JSString (..),
fromJSString,
toJSString,
-- Exception types related to JSFFI
-- * Exception types related to JSFFI
JSException (..),
WouldBlockException (..),
PromisePendingException (..),
-- Is JSFFI used in the current wasm module?
-- * Is JSFFI used in the current wasm module?
isJSFFIUsed
) where
......
{-# LANGUAGE NoImplicitPrelude #-}
module GHC.Internal.Wasm.Prim (
-- User-facing JSVal type and freeJSVal
-- * User-facing 'JSVal' and related utilities
JSVal (..),
freeJSVal,
mkWeakJSVal,
-- The JSString type and conversion from/to Haskell String
-- * 'JSString' and conversion from/to Haskell 'String'
JSString (..),
fromJSString,
toJSString,
-- Exception types related to JSFFI
-- * Exception types related to JSFFI
JSException (..),
WouldBlockException (..),
PromisePendingException (..),
-- Is JSFFI used in the current wasm module?
-- * Is JSFFI used in the current wasm module?
isJSFFIUsed
) where
......
......@@ -43,10 +43,14 @@ import GHC.Internal.Word
mkJSCallback :: (StablePtr a -> IO JSVal) -> a -> IO JSVal
mkJSCallback adjustor f = do
sp@(StablePtr sp#) <- newStablePtr f
JSVal v w _ <- adjustor sp
let r = JSVal v w sp#
js_callback_register r sp
pure r
v@(JSVal p) <- adjustor sp
IO $ \s0 -> case stg_setJSVALsp p sp# s0 of
(# s1 #) -> (# s1, () #)
js_callback_register v sp
pure v
foreign import prim "stg_setJSVALsp"
stg_setJSVALsp :: JSVal# -> StablePtr# a -> State# RealWorld -> (# State# RealWorld #)
foreign import javascript unsafe "__ghc_wasm_jsffi_finalization_registry.register($1, $2, $1)"
js_callback_register :: JSVal -> StablePtr a -> IO ()
......
......@@ -7,4 +7,8 @@ where
import GHC.Internal.Base
-- | If the current wasm module has any JSFFI functionality linked in,
-- this would be 'True' at runtime and 'False' otherwise. If this is
-- 'False', the wasm module would be a self-contained wasm32-wasi
-- module that can be run by non-web runtimes as well.
foreign import ccall unsafe "rts_JSFFI_used" isJSFFIUsed :: Bool
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE GHCForeignImportPrim #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE UnliftedFFITypes #-}
{-# LANGUAGE UnliftedNewtypes #-}
module GHC.Internal.Wasm.Prim.Types (
......@@ -26,7 +28,6 @@ import GHC.Internal.IO
import GHC.Internal.IO.Encoding
import GHC.Internal.Num
import GHC.Internal.Show
import GHC.Internal.Stable
import GHC.Internal.Weak
{-
......@@ -38,76 +39,150 @@ On wasm, the Haskell heap lives in the linear memory space, and it can
only contain bit patterns, not opaque references of the host
JavaScript heap. As long as we have two heaps that coexist in this
way, the best we can do is representing JavaScript references as
unique ids in the Haskell heap.
In JavaScript, we have a JSValManager which exposes some interfaces as
wasm imports. The JSValManager is in charge of allocating unique ids
and managing the mapping from ids to the actual JavaScript values. In
fact we can implement the entire JSValManager in wasm, using a wasm
table with externref elements to hold the JavaScript values and a
special allocator to manage free slots in the table. That'll take more
work to implement though, with one more caveat: browsers typically
limit max wasm table size to 10000000 which may not be large enough
for some use cases. We can workaround the table size restriction by
managing a pool or tree of wasm tables, but at this point we really
should ditch the idea of doing everything in wasm just because we can.
Next, we have the unlifted JSVal# type, defined in jsval.cmm and
contains one non-pointer word which is the id allocated by
JSValManager. On top of JSVal#, we have the user-facing lifted JSVal
type, which carries the JSVal#, as well as a weak pointer and a stable
pointer.
The weak pointer is used to garbage collect JSVals. Its key is the
JSVal# closure, and it has a C finalizer that tells the JSValManager
to drop the mapping when the JSVal# closure is collected. Since we
want to provide freeJSVal to allow eager freeing of JSVals, we need to
carry it as a field of JSVal.
The stable pointer field is NULL for normal JSVals created via foreign
import results or foreign export arguments. But for JSFFI dynamic
exports that wraps a Haskell function closure as a JavaScript callback
and returns that callback's JSVal, it is a stable pointer that pins
that Haskell function closure. If this JSVal is garbage collected,
then we can only rely on a JavaScript FinalizerRegistry to free the
stable pointer in the future, but if we eagerly free the callback with
freeJSVal, then we can eagerly free this stable pointer as well.
The lifted JSVal type is meant to be an abstract type. Its creation
and consumption is mainly handled by the RTS API functions rts_mkJSVal
and rts_getJSVal, which are used in C stub files generated when
desugaring JSFFI foreign imports/exports.
ids in the Haskell heap.
First, we have the unlifted JSVal# type, defined in jsval.cmm with the
following memory layout:
+--------------+-----+----+----------+
|stg_JSVAL_info|Weak#|Int#|StablePtr#|
+--------------+-----+----+----------+
The first non-pointer Int# field is a 32-bit id allocated and
returned by the JSValManager on the JavaScript side. The JSValManager
maintains a Map from ids to actual JavaScript values. This field is
immutable throughout a JSVal# closure's lifetime and is unique for
each JSVal# ever created.
The Weak# poiner sets the JSVal# closure as key and has a C finalizer
that drops the mapping in JSValManager. When the JSVal# closure is
garbage collected, the finalizer is invoked, but it can also be
eagerly invoked by freeJSVal, that's why we carry the Weak# in JSVal#
as a pointer field.
Normally, one JSVal# manage one kind of resource: the JavaScript value
retained in JSValManager. However, in case of JSFFI exports where we
convert Haskell functions to JavaScript callbacks, the JSVal# manages
not only the callback on the JavaScript side, but also a stable
pointer that pins the exported function on the Haskell side. That
StablePtr# is recorded in the JSVal# closure.
Even if the JSVal# closure is garbage collected, we don't know if the
JavaScript side still retains the callback somewhere other than
JSValManager, so the stable pointer will continue to pin the Haskell
function closure. We do a best effort cleanup on the JavaScript side
by using a FinalizationRegistry: if the JSVal# is automatically
collected, the callback is dropped in JSValManager and also not used
elsewhere, the FinalizationRegistry calls into the RTS to drop the
stable pointer as well.
However, JSVal# can be eagerly freed by freeJSVal. It'll deregister
the callback in the FinalizationRegistry, finalize the Weak# pointer
and also free the stable pointer. In order to make freeJSVal
idempotent, we must not free the stable pointer twice; therefore the
StablePtr# field is mutable and will be overwritten with NULL upon
first freeJSVal invocation; it's also NULL upon creation by
rts_mkJSVal and later overwritten with the StablePtr# upon the
callback creation.
On top of JSVal#, we have the user-facing lifted JSVal type, which
wraps the JSVal#. The lifted JSVal type is meant to be an abstract
type. Its creation and consumption is mainly handled by the RTS API
functions rts_mkJSVal and rts_getJSVal, which are used in C stub files
generated when desugaring JSFFI foreign imports/exports.
-}
newtype JSVal#
= JSVal# (Any :: UnliftedType)
-- | A 'JSVal' is a first-class Haskell value on the Haskell heap that
-- represents a JavaScript value. You can use 'JSVal' or its @newtype@
-- as a supported argument or result type in JSFFI import & export
-- declarations, in addition to those lifted FFI types like 'Int' or
-- 'Ptr' that's already supported by C FFI. It is garbage collected by
-- the GHC RTS:
--
-- * There can be different 'JSVal's that point to the same JavaScript
-- value. As long as there's at least one 'JSVal' still alive on the
-- Haskell heap, that JavaScript value will still be alive on the
-- JavaScript heap.
-- * If there's no longer any live 'JSVal' that points to the
-- JavaScript value, after Haskell garbage collection, the
-- JavaScript runtime will be able to eventually garbage collect
-- that JavaScript value as well.
--
-- There's a special kind of 'JSVal' that represents a JavaScript
-- callback exported from a Haskell function like this:
--
-- > foreign import javascript "wrapper"
-- > exportFibAsAsyncJSCallback :: (Int -> Int) -> IO JSVal
--
-- Such a 'JSVal' manages an additional kind of resource: the exported
-- Haskell function closure. Even if it is automatically garbage
-- collected, the Haskell function closure would still be retained
-- since the JavaScript callback might be retained elsewhere. We do a
-- best-effort collection here using JavaScript
-- [@FinalizationRegistry@](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry),
-- so the Haskell function closure might be eventually dropped if the
-- JavaScript callback is garbage collected.
--
-- Note that even the @FinalizationRegistry@ logic can't break cyclic
-- references between the Haskell/JavaScript heap: when an exported
-- Haskell function closure retains a 'JSVal' that represents a
-- JavaScript callback. Though this can be solved by explicit
-- 'freeJSVal' calls.
data JSVal
= forall a . JSVal JSVal# (Weak# JSVal) (StablePtr# a)
= JSVal JSVal#
-- | 'freeJSVal' eagerly frees a 'JSVal' in the runtime. It drops the
-- retained JavaScript value on the JavaScript side, and in case of a
-- 'JSVal' that represents a callback, also drops the retained Haskell
-- function closure. Once a 'JSVal' is freed by 'freeJSVal', later
-- attempts to pass it to the JavaScript side would result in runtime
-- crashes, so you should only call 'freeJSVal' when you're confident
-- that 'JSVal' won't be used again (and in case of callbacks, that
-- callback won't be invoked again).
--
-- 'freeJSVal' is idempotent: it's safe to call it more than once on
-- the same 'JSVal', subsequent invocations are no-ops. You are
-- strongly recommended to call 'freeJSVal' on short-lived
-- intermediate 'JSVal' values for timely release of resources!
freeJSVal :: JSVal -> IO ()
freeJSVal v@(JSVal _ w sp) = do
case sp `eqStablePtr#` unsafeCoerce# nullAddr# of
0# -> do
js_callback_unregister v
freeStablePtr $ StablePtr sp
_ -> pure ()
IO $ \s0 -> case finalizeWeak# w s0 of
freeJSVal v@(JSVal p) = do
js_callback_unregister v
IO $ \s0 -> case stg_freeJSVal# p s0 of
(# s1, _, _ #) -> (# s1, () #)
-- | 'mkWeakJSVal' allows you to create a 'Weak' pointer that observes
-- the liveliness of a 'JSVal' closure on the Haskell heap and
-- optionally attach a finalizer.
--
-- Note that this liveliness is not affected by 'freeJSVal': even if
-- 'freeJSVal' is called, the 'JSVal' might still be alive on the
-- Haskell heap as a dangling reference and 'deRefWeak' might still be
-- able to retrieve the 'JSVal' before it is garbage collected.
mkWeakJSVal :: JSVal -> Maybe (IO ()) -> IO (Weak JSVal)
mkWeakJSVal v@(JSVal k _ _) (Just (IO fin)) = IO $ \s0 ->
case mkWeak# k v fin s0 of
mkWeakJSVal v@(JSVal p) (Just (IO fin)) = IO $ \s0 ->
case mkWeak# p v fin s0 of
(# s1, w #) -> (# s1, Weak w #)
mkWeakJSVal (JSVal _ w _) Nothing = pure $ Weak w
mkWeakJSVal v@(JSVal p) Nothing = IO $ \s0 ->
case mkWeakNoFinalizer# p v s0 of
(# s1, w #) -> (# s1, Weak w #)
foreign import prim "stg_freeJSVAL"
stg_freeJSVal# :: JSVal# -> State# RealWorld -> (# State# RealWorld, Int#, State# RealWorld -> (# State# RealWorld, b #) #)
foreign import javascript unsafe "if (!__ghc_wasm_jsffi_finalization_registry.unregister($1)) { throw new WebAssembly.RuntimeError('js_callback_unregister'); }"
foreign import javascript unsafe "try { __ghc_wasm_jsffi_finalization_registry.unregister($1); } catch {}"
js_callback_unregister :: JSVal -> IO ()
-- | A 'JSString' represents a JavaScript string.
newtype JSString
= JSString JSVal
-- | Converts a 'JSString' to a Haskell 'String'. Conversion is done
-- eagerly once the resulting 'String' is forced, and the argument
-- 'JSString' may be explicitly freed if no longer used.
fromJSString :: JSString -> String
fromJSString s = unsafeDupablePerformIO $ do
l <- js_stringLength s
......@@ -122,15 +197,25 @@ foreign import javascript unsafe "$1.length"
foreign import javascript unsafe "(new TextEncoder()).encodeInto($1, new Uint8Array(__exports.memory.buffer, $2, $3)).written"
js_encodeInto :: JSString -> Ptr a -> Int -> IO Int
-- | Converts a Haskell 'String' to a 'JSString'.
toJSString :: String -> JSString
toJSString s = unsafeDupablePerformIO $ withCStringLen utf8 s $ \(buf, len) -> js_toJSString buf len
foreign import javascript unsafe "(new TextDecoder('utf-8', {fatal: true})).decode(new Uint8Array(__exports.memory.buffer, $1, $2))"
js_toJSString :: Ptr a -> Int -> IO JSString
-- | A 'JSException' represents a JavaScript exception. It is likely
-- but not guaranteed to be an instance of the @Error@ class. When you
-- call an async JSFFI import and the result @Promise@ rejected, the
-- rejection value will be wrapped in a 'JSException' and re-thrown in
-- Haskell once you force the result.
newtype JSException
= JSException JSVal
-- | If the
-- [@error.stack@](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack)
-- property is present, it will be used to render the 'Show' instance
-- output so you can see the JavaScript stack trace.
instance Show JSException where
showsPrec p e =
showParen (p >= 11) $ showString "JSException " . showsPrec 11 (jsErrorString e)
......@@ -147,6 +232,24 @@ foreign import javascript unsafe "`${$1.stack ? $1.stack : $1}`"
instance Exception JSException
-- | An async JSFFI import returns a thunk that represents a pending
-- JavaScript @Promise@:
--
-- > foreign import javascript "(await fetch($1)).text()"
-- > js_fetch :: JSString -> IO JSString
--
-- Forcing that thunk blocks the current Haskell thread until the
-- @Promise@ is fulfilled, but that cannot happen if the Haskell
-- thread is a bound thread created by a JSFFI sync export or a C FFI
-- export! Those Haskell computations are meant to return
-- synchronously, but JavaScript asynchronocity is contagious and
-- there's no escape hatch like @unsafeAwaitPromise@.
--
-- In such cases, a 'WouldBlockException' exception would be thrown.
-- The 'WouldBlockException' is attached with a diagnostic message
-- generated at compile-time (currently just the JSFFI source snippet
-- of the corresponding async import) to help debugging the
-- exception's cause.
newtype WouldBlockException
= WouldBlockException String
deriving (Show)
......
......@@ -70,7 +70,7 @@ __attribute__((constructor(102))) static void __ghc_wasm_jsffi_init(void) {
}
typedef __externref_t HsJSVal;
typedef StgWord JSValKey;
typedef StgInt JSValKey;
extern const StgInfoTable stg_JSVAL_info;
extern const StgInfoTable ghczminternal_GHCziInternalziWasmziPrimziTypes_JSVal_con_info;
......@@ -91,9 +91,10 @@ HaskellObj rts_mkJSVal(Capability*, HsJSVal);
HaskellObj rts_mkJSVal(Capability *cap, HsJSVal v) {
JSValKey k = __imported_newJSVal(v);
HaskellObj p = (HaskellObj)allocate(cap, CONSTR_sizeW(0, 1));
HaskellObj p = (HaskellObj)allocate(cap, CONSTR_sizeW(1, 2));
SET_HDR(p, &stg_JSVAL_info, CCS_SYSTEM);
p->payload[0] = (HaskellObj)k;
p->payload[1] = (HaskellObj)k;
p->payload[2] = NULL;
StgCFinalizerList *cfin =
(StgCFinalizerList *)allocate(cap, sizeofW(StgCFinalizerList));
......@@ -107,6 +108,7 @@ HaskellObj rts_mkJSVal(Capability *cap, HsJSVal v) {
SET_HDR(w, &stg_WEAK_info, CCS_SYSTEM);
w->cfinalizers = (StgClosure *)cfin;
w->key = p;
w->value = Unit_closure;
w->finalizer = &stg_NO_FINALIZER_closure;
w->link = cap->weak_ptr_list_hd;
cap->weak_ptr_list_hd = w;
......@@ -114,14 +116,13 @@ HaskellObj rts_mkJSVal(Capability *cap, HsJSVal v) {
cap->weak_ptr_list_tl = w;
}
HaskellObj box = (HaskellObj)allocate(cap, CONSTR_sizeW(3, 0));
p->payload[0] = (HaskellObj)w;
HaskellObj box = (HaskellObj)allocate(cap, CONSTR_sizeW(1, 0));
SET_HDR(box, &ghczminternal_GHCziInternalziWasmziPrimziTypes_JSVal_con_info, CCS_SYSTEM);
box->payload[0] = p;
box->payload[1] = (HaskellObj)w;
box->payload[2] = NULL;
w->value = TAG_CLOSURE(1, box);
return w->value;
return TAG_CLOSURE(1, box);
}
__attribute__((import_module("ghc_wasm_jsffi"), import_name("getJSVal")))
......@@ -129,7 +130,7 @@ HsJSVal __imported_getJSVal(JSValKey);
STATIC_INLINE HsJSVal rts_getJSValzh(HaskellObj p) {
ASSERT(p->header.info == &stg_JSVAL_info);
return __imported_getJSVal((JSValKey)p->payload[0]);
return __imported_getJSVal((JSValKey)p->payload[1]);
}
HsJSVal rts_getJSVal(HaskellObj);
......
#include "Cmm.h"
// This defines the unlifted JSVal# type. See Note [JSVal
// representation for wasm] for detailed explanation.
// This defines the unlifted JSVal# type. See
// Note [JSVal representation for wasm] for
// detailed explanation.
INFO_TABLE(stg_JSVAL, 0, 1, PRIM, "JSVAL", "JSVAL")
INFO_TABLE(stg_JSVAL, 1, 2, PRIM, "JSVAL", "JSVAL")
(P_ node)
{
return (node);
}
stg_setJSVALsp (P_ p, W_ sp)
{
W_[p + SIZEOF_StgHeader + WDS(2)] = sp;
return ();
}
stg_freeJSVAL (P_ p)
{
P_ w;
W_ sp;
w = P_[p + SIZEOF_StgHeader];
sp = W_[p + SIZEOF_StgHeader + WDS(2)];
if (sp != NULL) {
ccall freeStablePtr(sp);
W_[p + SIZEOF_StgHeader + WDS(2)] = NULL;
}
jump stg_finalizzeWeakzh (w);
}
......@@ -68,7 +68,7 @@ testDynExportGC x y z = do
-- Return a continuation to be called after the JavaScript side
-- finishes garbage collection.
js_mk_cont $ do
-- The JavaScript FinalizerRegistry logic only frees the stable
-- The JavaScript FinalizationRegistry logic only frees the stable
-- pointer that pins fn. So we need to invoke Haskell garbage
-- collection again.
performGC
......
......@@ -3,29 +3,13 @@
// of one; the post-linker script will copy all contents into a new
// ESM module.
// Manage a mapping from unique 32-bit ids to actual JavaScript
// values.
// Manage a mapping from 32-bit ids to actual JavaScript values.
export class JSValManager {
#lastk = 0;
#kv = new Map();
constructor() {}
// Maybe just bump this.#lastk? For 64-bit ids that's sufficient,
// but better safe than sorry in the 32-bit case.
#allocKey() {
let k = this.#lastk;
while (true) {
if (!this.#kv.has(k)) {
this.#lastk = k;
return k;
}
k = (k + 1) | 0;
}
}
newJSVal(v) {
const k = this.#allocKey();
const k = ++this.#lastk;
this.#kv.set(k, v);
return k;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment