JavaScript interface that works with GHCJS or GHC
Clone or download
Ericson2314 Merge pull request #75 from matthewbauer/patch-4
jsaddle-webkit2gtk: bump aeson upper bound
Latest commit b423436 Aug 14, 2018

README.md

JSaddle

JSaddle is an interface for JavaScript that works with GHCJS or GHC. It is used by ghcjs-dom when compiled with GHC and is compatible with ghcjs-dom when compiled with GHCJS.

You can use JSaddle directly as follows:

module Main ( main ) where

import Control.Monad.IO.Class (MonadIO(..))
import Control.Concurrent.MVar (takeMVar, putMVar, newEmptyMVar)
import Control.Lens ((^.))
import Language.Javascript.JSaddle
       (jsg, js, js1, jss, fun, valToNumber, syncPoint)
import Language.Javascript.JSaddle.Warp (run)

main = run 3709 $ do
    doc <- jsg "document"
    doc ^. js "body" ^. jss "innerHTML" "<h1>Kia ora (Hi)</h1>"

    -- Create a haskell function call back for the onclick event
    doc ^. jss "onclick" (fun $ \ _ _ [e] -> do
        x <- e ^. js "clientX" >>= valToNumber
        y <- e ^. js "clientY" >>= valToNumber
        newParagraph <- doc ^. js1 "createElement" "p"
        newParagraph ^. js1 "appendChild" (
            doc ^. js1 "createTextNode" ("Click " ++ show (x, y)))
        doc ^. js "body" ^. js1 "appendChild" newParagraph
        return ())

    -- Make an exit button
    exitMVar <- liftIO newEmptyMVar
    exit <- doc ^. js1 "createElement" "span"
    exit ^. js1 "appendChild" (
        doc ^. js1 "createTextNode" "Click here to exit")
    doc ^. js "body" ^. js1 "appendChild" exit
    exit ^. jss "onclick" (fun $ \ _ _ _ -> liftIO $ putMVar exitMVar ())

    -- Force all all the lazy evaluation to be executed
    syncPoint

    -- In GHC compiled version the WebSocket connection will end when this
    -- thread ends.  So we will wait until the user clicks exit.
    liftIO $ takeMVar exitMVar
    doc ^. js "body" ^. jss "innerHTML" "<h1>Ka kite ano (See you later)</h1>"
    return ()

When compiled with GHC this code will run a Warp web server you can connect to at http:\\localhost:3709\.

Here is the same program using ghcjs-dom to call JSaddle. As well as better type safety this version uses JS FFI directly (rather than via JSaddle) when compiled with GHCJS:

module Main (
    main
) where

import Control.Monad.IO.Class (MonadIO(..))
import Control.Concurrent.MVar (takeMVar, putMVar, newEmptyMVar)

import GHCJS.DOM (run, syncPoint, currentDocument)
import GHCJS.DOM.Document (getBody, createElement, createTextNode)
import GHCJS.DOM.Element (setInnerHTML)
import GHCJS.DOM.Node (appendChild)
import GHCJS.DOM.EventM (on, mouseClientXY)
import qualified GHCJS.DOM.Document as D (click)
import qualified GHCJS.DOM.Element as E (click)

main = run 3708 $ do
    Just doc <- currentDocument
    Just body <- getBody doc
    setInnerHTML body (Just "<h1>Kia ora (Hi)</h1>")
    on doc D.click $ do
        (x, y) <- mouseClientXY
        Just newParagraph <- createElement doc (Just "p")
        text <- createTextNode doc $ "Click " ++ show (x, y)
        appendChild newParagraph text
        appendChild body (Just newParagraph)
        return ()
    
    -- Make an exit button
    exitMVar <- liftIO newEmptyMVar
    Just exit <- createElement doc (Just "span")
    text <- createTextNode doc "Click here to exit"
    appendChild exit text
    appendChild body (Just exit)
    on exit E.click $ liftIO $ putMVar exitMVar ()

    -- Force all all the lazy evaluation to be executed
    syncPoint

    -- In GHC compiled version the WebSocket connection will end when this
    -- thread ends.  So we will wait until the user clicks exit.
    liftIO $ takeMVar exitMVar
    setInnerHTML body (Just "<h1>Ka kite ano (See you later)</h1>")
    return ()

When compiled with GHC this code will run a Warp web server you can connect to at http:\\localhost:3708\.

How does it work on GHC

There are a number of different JSaddle runners to choose from

In all of these cases a web control or browser of some sort is used. An HTML file and a small JavaScript command interpreter are loaded into it. Then the native GHC compiled JSaddle code runs. In order to interact with the browser it sends commands to the JavaScript command interpreter. The mechanism for sending the commands differs for the different runners, but they are always combined into batches and encoded in JSON.

Haskell callbacks from JavaScript are supported by sending JSON back from the command interpreter. These can be synchronous (blocking the JavaScript thread until the Haskell callback completes) or asynchronous.

Why use a JavaScript command interpreter?

Older versions of JSaddle relied on the WebKit1 interface to WebKitGTK and JavaScriptCore that is only supported in older versions of WebKitGTK. With the newer WekKit2 interface this level of access is only available to WebKit Extensions. The general advice for people migrating from WebKit1 to WebKit2 seems to be to use JavaScript.

As a bonus we have been able to support a number of different platforms with the JavaScript command interpreter and it should be easy to support more in the future.

JSVal

When compiling with GHC a JSVal is represented by a integer key for a JavaScript Map in jsaddle.js code. The first five keys [0..4] are null, undefined, true, false and window. This means the Haskell code can quickly create a JSVal with one of these values. Both the Haskell code and jsaddle.js can allocate new JSVal keys. The Haskell code allocates negative keys and jsaddle.js allocates positive keys to avoid clashing.

The haskell code will not know the value of the to go with the key, but it will include it in commands to the server that are sort of "hey call this function and and put the result in the map with this key"). This allows for the lazy execution of the JSaddle code.

A finalizer is added on the Haskell code and an asynchronous FreeJSVal command is automatically sent to jsaddle.js when the JSVal is no longer reachable. The jsaddle.js code then deletes the key from the Map.

JSStrings

These are haskell Text type and so reside on the native side of the WebSocket. If you want to make avoid transferring large string values over the WebSocket use toJSVal to conevt it to a JSVal.

Lazy Execution

To improve performance commands are sent to jsaddle.js in batches that contain any number of asynchronous commands followed by one synchronous one. To get the best performance you should avoid synchronous commands, these are:

Synchronous Command What will trigger it
ValueToString converting a JSVal to a JSString (Text when using GHC)
ValueToBool converting a JSVal to a Bool
ValueToNumber converting a JSVal to a Double
ValueToJSON converting a JSVal to a JSON
DeRefVal converting a JSVal to a Value
IsNull testing to see if a JSVal is null
IsUndefined testing to see if a JSVal is undefined
InstanceOf testing to see if a JSVal is an instanceOf a given type
StrictEqual testing too JSVal values for equality (===)
PropertyNames getting the list of properties in an JS object

So basically if you don't look inside a JSVal to find out something the state of the JavaScript context you will not produce any synchronous commands and your code will run fast. As soon as you look at what is in a JSVal we have to wait for jsaddle.js to send the results back and your code will block.

In some cases you may wish to make sure all the JSaddle commands are executed. You can use the syncPoint and syncAfter functions to force all the pending asynchronous commands to be executed.

How does it work on GHCJS

It uses a handful of JS FFI calls to execute JavaScript functions indirectly. This indirection will be small compared to the overhead of the WebSockets approach (used when JSaddle is compiled with GHC), but it will be significant compared to hand crafted JS FFI calls.

For the best performance you may want to write both JS FFI and JSaddle wrappers For your JavaScript code. This is the approach taken by ghcjs-dom.

For instance here is getElementById from the ghcjs-dom-jsffi (used by ghcjs-dom when compiled with GHCJS)

foreign import javascript unsafe "$1[\"getElementById\"]($2)"
        js_getElementById :: Document -> JSString -> IO (Nullable Element)

getElementById ::
               (MonadIO m, IsDocument self, ToJSString elementId) =>
                 self -> elementId -> m (Maybe Element)
getElementById self elementId
  = liftIO
      (nullableToMaybe <$>
         (js_getElementById (toDocument self) (toJSString elementId)))

Here is getElementById from jsaddle-dom (used by ghcjs-dom when compiled with GHC)

getElementById ::
               (MonadDOM m, IsDocument self, ToJSString elementId) =>
                 self -> elementId -> m (Maybe Element)
getElementById self elementId
  = liftDOM
      (((toDocument self) ^. jsf "getElementById" [toJSVal elementId])
         >>= fromJSVal)

Exceptions

JSaddle does not support exceptions well. When compiled with GHCJS an exception will result in termination of the thread at the point the exception is thrown.

When using GHC the Haskell executions will probably continue for a while before the exception is received at all by the Haskell code (because the lazy execution of the JS code). The exception will be thrown when the the next synchronous command is executed. When it reaches the synchronous command a haskell type JSException will be thrown (this may not be the thread that initiated the command that caused the exception). You can use syncPoint and syncAfter to force the exception to be thrown. There are JSM versions of catch and bracket that also include a syncPoint call.

Building with Stack

The jsaddle-webkit2gtk runner can be difficult to build with stack. See this issue if you get stuck.