JavaScript: implement callbacks in the FFI
Introduction
GHCJS previously implemented a Callback a
data type, which is currently missing from GHC's JavaScript backend. This type represents functions passable in the FFI that use a more standard JavaScript calling convention - rather than the calling conventions used in the JS generated code.
GHCJS supported functions up to 3 arity, and it supported various synchronicities of callbacks. Arguments are passed as an untyped JSVal
- essentially a representation of a plain JavaScript value.
Usage
To construct callbacks, the following functions are used depending on synchronisation and the existence (or lack thereof) of a return value:
data OnBlocked = ContinueAsync | ThrowWouldBlock
asyncCallback1 :: (JSVal -> IO ()) -> IO (Callback (JSVal -> IO ()))
syncCallback1 :: OnBlocked -> (JSVal -> IO ()) -> IO (Callback (JSVal -> IO ()))
syncCallback1' :: (JSVal -> IO JSVal) -> IO (Callback (JSVal -> IO JSVal))
Only the 1-arity signatures are demonstrated here, but sizes up to 3 - including 0 - are available.
These are expected to be used as a way to call into Haskell-generated code from regular JavaScript, via FFI imports. A simple example demonstrating this is as follows:
foreign import javascript "((f,x) -> { f(x); })"
js_apply :: Callback (JSVal -> IO ()) -> JSVal -> IO ()
fn x = if isNull x then putStrLn "isNull" else fromJSString x
main = do
f <- syncCallback1 ThrowWouldBlock fn
js_apply f (toJSString "example!")
releaseCallback f -- Memory can't be automatically freed because we don't know if the function is being held on the JavaScript side
Callbacks as FFI "exports"
Callbacks enable a form of FFI "exports", through setting global variables:
foreign import javascript "((f) => { globalF = f; })"
setF :: Callback (JSVal -> IO ()) -> IO ()
fn x = if isNull x then putStrLn "isNull" else fromJSString x
main = setF =<< syncCallback1 ThrowWouldBlock fn
Then, if our generated Haskell-main is called in the HTML head, globalF
will be available as a callback for e.g. HTML buttons.
Implementation
The GHCJS.Foreign.Callback
module from the ghcjs-base
package ports with minimal changes to the module and without changes to the current JavaScript backend (except for a few bugfixes that have already been merged).