diff options
Diffstat (limited to 'docs/users_guide/javascript.rst')
-rw-r--r-- | docs/users_guide/javascript.rst | 175 |
1 files changed, 175 insertions, 0 deletions
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. + |