[Git][ghc/ghc][wip/js-exports] JS/FFI/Callbacks: add user guide documentation

Josh Meredith (@JoshMeredith) gitlab at gitlab.haskell.org
Mon Mar 20 15:10:48 UTC 2023



Josh Meredith pushed to branch wip/js-exports at Glasgow Haskell Compiler / GHC


Commits:
ef9a48e5 by Josh Meredith at 2023-03-20T15:10:32+00:00
JS/FFI/Callbacks: add user guide documentation

- - - - -


1 changed file:

- docs/users_guide/exts/ffi.rst


Changes:

=====================================
docs/users_guide/exts/ffi.rst
=====================================
@@ -1137,3 +1137,173 @@ byte array can be pinned as a result of three possible causes:
   ``GHC.Exts.readWord8Array#`` for this.
 .. [3] As in [2]_, the FFI is not actually needed for this. ``GHC.Exts``
   includes primitives for reading from on ``ArrayArray#``.
+
+.. _ffi-javascript
+
+FFI and the JavaScript Backend
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. index::
+   single: FFI and the JavaScript Backend
+
+GHC's JavaScript backend supports its own calling convention for
+JavaScript-specific foreign imports. Instead of a function name,
+the import string expected to be an unapplied JavaScript `arrow
+function <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions>`_.
+
+Arrow functions enable the use of arbitrary JavaScript in import
+strings, so a simple example might look like:
+
+.. code-block:: haskell
+
+  foreign import javascript "((x,y) => { return x + y; })"
+    js_add :: Int -> Int -> Int
+
+JSVal
+^^^^^
+
+The JavaScript backend has a concept of an untyped 'plain' JavaScript
+value, under the guise of the type ``JSVal``. While many Haskell data
+types are represented in JavaScript-incompatible ways under-the-hood,
+``JSVal`` is represented as a real JavaScript object.
+
+The module ``GHC.JS.Prim`` from ``base`` contains functions for working
+with foreign ``JSVal`` objects. Currently, it can contains the following
+conversions:
+
+* ``Int`` <-> ``JSVal`` (``toJSInt``, ``fromJSInt``)
+* ``String`` <-> ``JSVal`` (``toJSString``, ``fromJSString``)
+* ``[JSVal]`` <-> ``JSVal`` (``toJSArray``, ``fromJSArray``)
+
+It also contains functions for working with objects:
+
+* ``jsNull :: JSVal`` - the JavaScript ``null``
+* ``isNull :: JSVal -> Bool`` - test for the JavaScript ``null``
+* ``isUndefined :: JSVal -> Bool`` - test for the JavaScript ``undefined``
+* ``getProp :: JSVal -> String -> JSVal`` - object field access
+
+JavaScript FFI Types
+^^^^^^^^^^^^^^^^^^^^
+
+Some types are able to be used directly in the type signatures of foreign
+exports, without conversion to a ``JSVal``. We saw in the first example
+that ``Int`` is one of these.
+
+The supported types are those with primitive JavaScript representations
+that match the Haskell type. This means types such as the Haskell ``String``
+type aren't supported directly, because they're lists - which don't have
+a primitive JavaScript representation, and so are incompatible with each
+other.
+
+The following types are supported in this way:
+
+* ``Int``
+* ``Bool``
+* ``Char``
+
+As in the C FFI, types in the JavaScript FFI can't be type checked, so
+the following example would compile successfully - despite the type
+error:
+
+.. code-block:: haskell
+
+  foreign import javascript "((x) => { return 5; })"
+    type_error :: Bool -> Bool
+
+JavaScript Callbacks
+^^^^^^^^^^^^^^^^^^^^
+
+The JavaScript execution model is based around callback functions, and
+GHC's JavaScript backend implements these as a type in order to support
+useful browser programs, and programs interacting with JavaScript libraries.
+
+The module ``GHC.JS.Foreign.Callback`` in ``base`` defines the type ``Callback a``,
+as well as several functions to construct callbacks from Haskell functions
+of up to three ``JSVal`` arguments. Unlike a regular function, a ``Callback``
+function is passed in the FFI as a plain JavaScript function - enabling us to call
+these functions from within JavaScript:
+
+.. code-block:: haskell
+
+  foreign import javascript "((f) => { f('Example!'); })"
+    callback_example :: Callback (JSVal -> IO ()) -> IO ()
+
+  printJSValAsString :: JSVal -> IO ()
+  printJSValAsString = putStrLn . fromJSString
+
+  main :: IO ()
+  main = do
+    printJS <- syncCallback1 ThrowWouldBlock printJSValAsString
+    callback_example printJS
+    releaseCallback printJS
+
+This example will call our ``printJSValAsString`` function, via JavaScript,
+with the JavaScript string ``Example!`` as an argument. On the last line,
+the callback memory is freed. Since there's no way for the Haskell JS runtime
+to know if a function is still being referenced by JavaScript code, the memory
+must be manually released when no longer needed.
+
+On the first line of ``main``, we see where the ``Callback`` is actually
+created, by ``syncCallback1``. ``syncCallback`` has versions up to three,
+including a zero-argument version with no suffix.
+
+There are three categories of functions that create callbacks, with the
+arity-1 type signatures shown here for demonstration:
+
+* ``syncCallback1 :: (JSVal -> IO ()) -> OnBlocked -> IO (Callback (JSVal -> IO ()))``:
+  Synchronous callbacks that don't return a value. These take an additional
+  ``data OnBlocked = ThrowWouldBlock | ContinueAsync`` argument for use in the
+  case that the thread becomes blocked on e.g. an ``MVar`` transaction.
+
+* ``syncCallback' :: (JSVal -> IO JSVal) -> IO (Callback (JSVal -> IO ()))``:
+  Synchronous callbacks that return a value. Because of the return value, there
+  is no possibility of continuing asynchronously, so no ``OnBlocked`` argument
+  is taken.
+
+* ``asyncCallback :: (JSVal -> IO ()) -> IO (Callback (JSVal -> IO ()))``:
+  Asynchronous callbacks that immediately start in a new thread. Cannot return a
+  value.
+
+There is no checking that the passed arguments match the callback, so the
+following example compiles and correctly prints 10, despite the argument being
+passed as an ``Int`` to a ``Callback`` that accepts a ``JSVal``:
+
+.. code-block:: haskell
+
+  foreign import javascript "((f,x) => { return f(x); })"
+    apply_int :: Callback (JSVal -> IO JSVal) -> Int -> IO Int
+
+  main :: IO ()
+  main = do
+    add3 <- syncCallback1' (return . (+3))
+    print =<< apply_int add3 7
+    releaseCallback add3
+
+Callbacks as Foreign Exports
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+JavaScript callbacks allow for a sort of FFI exports via FFI imports. To do
+this, a global JavaScript variable is set, and that global variable can then
+be called from use cases that access plain JavaScript functions - such as
+interactive HTML elements. This would look like:
+
+.. code-block:: haskell
+
+  foreign import javascript "((f) => { globalF = f })"
+    setF :: Callback (JSVal -> IO ()) -> IO ()
+
+  main :: IO ()
+  main = do
+    log <- syncCallback1 ThrowWouldBlock (print . fromJSString)
+    setF log
+    -- don't releaseCallback log
+
+
+.. code-block:: html
+
+  <button onClick="globalF('Button pressed!")>Example</button>
+
+We have to make sure not to use ``releaseCallback`` on any functions that
+are to be available in HTML, because we want these functions to be in
+memory indefinitely.
+



View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/ef9a48e565d126b514aeb710dcb816cce4734097

-- 
View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/ef9a48e565d126b514aeb710dcb816cce4734097
You're receiving this email because of your account on gitlab.haskell.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/ghc-commits/attachments/20230320/9a21ab2a/attachment-0001.html>


More information about the ghc-commits mailing list