[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