Skip to content

Port ghcjs-base.GHCJS.Foreign.Callback as base.GHC.JS.Foreign.Callback for the GHC JavaScript Backend #150

@JoshMeredith

Description

@JoshMeredith

Code available at https://gitlab.haskell.org/ghc/ghc/-/merge_requests/10128

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    approvedApproved by CLC votebase-4.19Implemented in base-4.19 (GHC 9.8)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions