• Stars
    star
    116
  • Rank 303,894 (Top 6 %)
  • Language
    Haskell
  • Created almost 12 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

JavaScript interface that works with GHCJS or GHC

JSaddle

Build status

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 WebKit2 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 to go with the key, but it will include it in commands to the server that are sort of "hey call this function 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 avoid transferring large string values over the WebSocket use toJSVal to convert 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 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.

More Repositories

1

ghcjs

Haskell to JavaScript compiler, based on GHC
Haskell
2,602
star
2

ghcjs-examples

Haskell
89
star
3

ghcjs-dom

Make Document Object Model (DOM) apps that run in any browser and natively using WebKitGtk
Haskell
74
star
4

ghcjs-base

base library for GHCJS for JavaScript interaction and marshalling, used by higher level libraries like JSC
Haskell
45
star
5

ghcjs-vdom

bindings for virtual-dom
JavaScript
34
star
6

diagrams-ghcjs

diagrams backend that renders directly to an HTML5 canvas
Haskell
23
star
7

shims

GHCJS runtime system and JavaScript support code for packages - deprecated as of GHC 8.2
JavaScript
21
star
8

ghcjs-ffiqq

QuasiQuoter for GHCJS JavaScript FFI
Haskell
20
star
9

jsaddle-dom

DOM library that uses jsaddle to support both GHCJS and WebKitGTK
Haskell
14
star
10

ghcjs-jquery

jQuery bindings
Haskell
12
star
11

ghcjs-build

A Vagrant configuration to build ghcjs
Ruby
10
star
12

ghcjs-dom-hello

GHCJS DOM Hello World, an example package
Haskell
10
star
13

ghcjs-canvas

bindings for the canvas API - deprecated, part of the ghcjs-base library now
Haskell
9
star
14

jsaddle-hello

JSaddle Hello World, an example package
Haskell
8
star
15

ghcjs-sodium

Reactive GUI library for GHCJS using Sodium
Haskell
6
star
16

domconv-webkit

Tool for generating bindings based on the WebKit IDL files
Haskell
6
star
17

ghcjs-boot

boot libraries for ghcjs - deprecated as of GHC 8.2
Haskell
5
star
18

ghcjs-prim

deprecated (install only for old, pre improved-base GHCJS)
Haskell
5
star
19

ghcjs-hterm

Chromium assets, which we are using for hterm
JavaScript
5
star
20

source-map

Library for working with JSON Source Maps
Haskell
4
star
21

ghcjs-closure

Google Closure Library and Compiler dependencies for GHCJS
Haskell
4
star
22

ghcjs-node

library and tools for GHCJS on the node.js platform
Shell
2
star
23

haddock-internal

the haddock library with every module exposed
Haskell
1
star
24

ghcjs.github.com

JavaScript
1
star
25

ghcjs-integration

Integration project for ghcjs-base, jsaddle, jsaddle-dom and ghcjs-dom
Nix
1
star
26

gloss-ghcjs

backend for the gloss library that draws on a canvas
1
star