summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorJosh Meredith <joshmeredith2008@gmail.com>2023-03-06 09:18:03 +0000
committerMarge Bot <ben+marge-bot@smart-cactus.org>2023-04-13 08:51:09 -0400
commit27d2978e5412f2bef4448e208182a03137dd5ee8 (patch)
tree0e98a70f487c9c06061d92338de6fd9da593d3f6 /docs
parent8af401ccfbe28d7bbfc493c0097834e9c66a36b0 (diff)
downloadhaskell-27d2978e5412f2bef4448e208182a03137dd5ee8.tar.gz
Base/JS: GHC.JS.Foreign.Callback module (issue 23126)
* Add the Callback module for "exporting" Haskell functions to be available to plain JavaScript code * Fix some primitives defined in GHC.JS.Prim * Add a JavaScript section to the user guide with instructions on how to use the JavaScript FFI, building up to using Callbacks to interact with the browser * Add tests for the JavaScript FFI and Callbacks
Diffstat (limited to 'docs')
-rw-r--r--docs/users_guide/index.rst1
-rw-r--r--docs/users_guide/javascript.rst175
2 files changed, 176 insertions, 0 deletions
diff --git a/docs/users_guide/index.rst b/docs/users_guide/index.rst
index 56354e36bb..15274dcc71 100644
--- a/docs/users_guide/index.rst
+++ b/docs/users_guide/index.rst
@@ -23,6 +23,7 @@ Contents:
hints
utils
win32-dlls
+ javascript
wasm
bugs
eventlog-formats
diff --git a/docs/users_guide/javascript.rst b/docs/users_guide/javascript.rst
new file mode 100644
index 0000000000..f33c146f87
--- /dev/null
+++ b/docs/users_guide/javascript.rst
@@ -0,0 +1,175 @@
+.. _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. Any unapplied function is
+supported, including function names. Commonly, JavaScript foreign
+imports are written as an unapplied JavaScript `arrow function
+<https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions>`_,
+but ``function`` keyword anonymous functions are also supported.
+
+By treating an import string as an unapplied function, arbitrary
+JavaScript can be included in an import, 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``. Values having this type
+are mostly opaque to Haskell codes: you can think of `JSVal` as a data type whose
+data constructors aren't exposed. Its main use case is to pass opaque
+JavaScript values from one FFI call to another.
+
+Nevertheless the module ``GHC.JS.Prim`` from ``base`` contains functions for
+working with foreign ``JSVal`` objects. Currently, it provides 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 against the foreign code, so
+the following example would compile successfully - despite `5` not being a valid JavaScript value
+for the Haskell `Bool` type:
+
+.. 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. To use callbacks with more
+than three pieces of data, it's recommended to package data into JavaScript
+objects or arrays as required.
+
+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.
+