on master help sign in
jsaddle/README.md
222 lines 11 kB history raw
1JSaddle
2=======
4JSaddle is an interface for JavaScript that works with GHCJS or GHC. It is used by [ghcjs-dom](https://github.com/ghcjs/ghcjs-dom)
5when compiled with GHC and is compatible with [ghcjs-dom](https://github.com/ghcjs/ghcjs-dom) when compiled with GHCJS.
7You can use JSaddle directly as follows:
9``` Haskell
10module Main ( main ) where
12import Control.Monad.IO.Class (MonadIO(..))
13import Control.Concurrent.MVar (takeMVar, putMVar, newEmptyMVar)
14import Control.Lens ((^.))
15import Language.Javascript.JSaddle
16 (jsg, js, js1, jss, fun, valToNumber, syncPoint)
17import Language.Javascript.JSaddle.Warp (run)
19main = run 3709 $ do
20 doc <- jsg "document"
21 doc ^. js "body" ^. jss "innerHTML" "<h1>Kia ora (Hi)</h1>"
23 -- Create a haskell function call back for the onclick event
24 doc ^. jss "onclick" (fun $ \ _ _ [e] -> do
25 x <- e ^. js "clientX" >>= valToNumber
26 y <- e ^. js "clientY" >>= valToNumber
27 newParagraph <- doc ^. js1 "createElement" "p"
28 newParagraph ^. js1 "appendChild" (
29 doc ^. js1 "createTextNode" ("Click " ++ show (x, y)))
30 doc ^. js "body" ^. js1 "appendChild" newParagraph
31 return ())
33 -- Make an exit button
34 exitMVar <- liftIO newEmptyMVar
35 exit <- doc ^. js1 "createElement" "span"
36 exit ^. js1 "appendChild" (
37 doc ^. js1 "createTextNode" "Click here to exit")
38 doc ^. js "body" ^. js1 "appendChild" exit
39 exit ^. jss "onclick" (fun $ \ _ _ _ -> liftIO $ putMVar exitMVar ())
41 -- Force all all the lazy evaluation to be executed
42 syncPoint
44 -- In GHC compiled version the WebSocket connection will end when this
45 -- thread ends. So we will wait until the user clicks exit.
46 liftIO $ takeMVar exitMVar
47 doc ^. js "body" ^. jss "innerHTML" "<h1>Ka kite ano (See you later)</h1>"
48 return ()
49```
51When compiled with GHC this code will run a Warp web server you can connect to at `http:\\localhost:3709\`.
53Here is the same program using ghcjs-dom to call JSaddle. As well as better type safety this version uses
54JS FFI directly (rather than via JSaddle) when compiled with GHCJS:
56``` Haskell
57module Main (
58 main
59) where
61import Control.Monad.IO.Class (MonadIO(..))
62import Control.Concurrent.MVar (takeMVar, putMVar, newEmptyMVar)
64import GHCJS.DOM (run, syncPoint, currentDocument)
65import GHCJS.DOM.Document (getBody, createElement, createTextNode)
66import GHCJS.DOM.Element (setInnerHTML)
67import GHCJS.DOM.Node (appendChild)
68import GHCJS.DOM.EventM (on, mouseClientXY)
69import qualified GHCJS.DOM.Document as D (click)
70import qualified GHCJS.DOM.Element as E (click)
72main = run 3708 $ do
73 Just doc <- currentDocument
74 Just body <- getBody doc
75 setInnerHTML body (Just "<h1>Kia ora (Hi)</h1>")
76 on doc D.click $ do
77 (x, y) <- mouseClientXY
78 Just newParagraph <- createElement doc (Just "p")
79 text <- createTextNode doc $ "Click " ++ show (x, y)
80 appendChild newParagraph text
81 appendChild body (Just newParagraph)
82 return ()
84 -- Make an exit button
85 exitMVar <- liftIO newEmptyMVar
86 Just exit <- createElement doc (Just "span")
87 text <- createTextNode doc "Click here to exit"
88 appendChild exit text
89 appendChild body (Just exit)
90 on exit E.click $ liftIO $ putMVar exitMVar ()
92 -- Force all all the lazy evaluation to be executed
93 syncPoint
95 -- In GHC compiled version the WebSocket connection will end when this
96 -- thread ends. So we will wait until the user clicks exit.
97 liftIO $ takeMVar exitMVar
98 setInnerHTML body (Just "<h1>Ka kite ano (See you later)</h1>")
99 return ()
100```
102When compiled with GHC this code will run a Warp web server you can connect to at `http:\\localhost:3708\`.
104## How does it work on GHC
106There are a number of different JSaddle runners to choose from
108* [jsaddle-warp](https://hackage.haskell.org/package/jsaddle-warp) - runs JSaddle in a warp server with a web browser connected to it.
109* [jsaddle-webkit2gtk](https://hackage.haskell.org/package/jsaddle-webkit2gtk) - runs JSaddle in a WebKitGTK window.
110* [jsaddle-wkwebview](https://hackage.haskell.org/package/jsaddle-wkwebview) - runs JSaddle in a WKWebView on iOS or macOS.
111* [jsaddle-clib](https://hackage.haskell.org/package/jsaddle-clib) - C interface used to run JSaddle on Android using JNI.
113In all of these cases a web control or browser of some sort is used. An
114[HTML file](./jsaddle/src/Language/Javascript/JSaddle/Run/Files.hs#L24)
115and a [small JavaScript command interpreter](./jsaddle/src/Language/Javascript/JSaddle/Run/Files.hs#L340)
116are loaded into it. Then the native GHC compiled JSaddle code runs. In order to interact with the browser
117it sends commands to the JavaScript command interpreter. The mechanism for sending the commands differs
118for the different runners, but they are always combined into batches and encoded in JSON.
120Haskell callbacks from JavaScript are supported by sending JSON back from the command interpreter. These can be synchronous (blocking the
121JavaScript thread until the Haskell callback completes) or asynchronous.
123### Why use a JavaScript command interpreter?
125Older versions of JSaddle relied on the WebKit1 interface to WebKitGTK and JavaScriptCore that is only supported in older versions of
126WebKitGTK. With the newer WebKit2 interface this level of access is only available to WebKit Extensions.
127The general advice for people migrating from WebKit1 to WebKit2 seems to be to use JavaScript.
129As a bonus we have been able to support a number of different platforms with the JavaScript command interpreter and it should
130be easy to support more in the future.
132### JSVal
134When compiling with GHC a `JSVal` is represented by a integer key for a JavaScript `Map` in `jsaddle.js` code.
135The first five keys `[0..4]` are
136`null`, `undefined`, `true`, `false` and `window`. This means the Haskell code can quickly create a JSVal with one of
137these values. Both the Haskell code and `jsaddle.js` can allocate new `JSVal` keys. The Haskell code allocates negative
138keys and `jsaddle.js` allocates positive keys to avoid clashing.
140The haskell code will not know the value to go with the key, but it will include it in commands to the server
141that are sort of "hey call this function and put the result in the map with this key"). This allows for the
142lazy execution of the JSaddle code.
144A finalizer is added on the Haskell code and an asynchronous `FreeJSVal` command is automatically sent to `jsaddle.js`
145when the JSVal is no longer reachable. The `jsaddle.js` code then deletes the key from the Map.
147### JSStrings
149These 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`.
151### Lazy Execution
153To 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:
155| Synchronous Command | What will trigger it |
156| ------------------- | ------------------------------------------------------------ |
157| **ValueToString** | converting a `JSVal` to a `JSString` (`Text` when using GHC) |
158| **ValueToBool** | converting a `JSVal` to a `Bool` |
159| **ValueToNumber** | converting a `JSVal` to a `Double` |
160| **ValueToJSON** | converting a `JSVal` to a `JSON` |
161| **DeRefVal** | converting a `JSVal` to a `Value` |
162| **IsNull** | testing to see if a `JSVal` is `null` |
163| **IsUndefined** | testing to see if a `JSVal` is `undefined` |
164| **InstanceOf** | testing to see if a `JSVal` is an `instanceOf` a given type |
165| **StrictEqual** | testing too `JSVal` values for equality (`===`) |
166| **PropertyNames** | getting the list of properties in an JS object |
168So basically if you don't look inside a `JSVal` to find out something the state of the JavaScript context you will not produce any
169synchronous 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
170send the results back and your code will block.
172In some cases you may wish to make sure all the JSaddle commands are executed. You can use the `syncPoint` and `syncAfter` functions to
173force all the pending asynchronous commands to be executed.
176## How does it work on GHCJS
178It 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.
180For the best performance you may want to write both JS FFI and JSaddle wrappers
181for your JavaScript code. This is the approach taken by `ghcjs-dom`.
183For instance here is `getElementById` from the `ghcjs-dom-jsffi` (used by `ghcjs-dom` when compiled with GHCJS)
185``` Haskell
186foreign import javascript unsafe "$1[\"getElementById\"]($2)"
187 js_getElementById :: Document -> JSString -> IO (Nullable Element)
189getElementById ::
190 (MonadIO m, IsDocument self, ToJSString elementId) =>
191 self -> elementId -> m (Maybe Element)
192getElementById self elementId
193 = liftIO
194 (nullableToMaybe <$>
195 (js_getElementById (toDocument self) (toJSString elementId)))
196```
198Here is `getElementById` from `jsaddle-dom` (used by `ghcjs-dom` when compiled with GHC)
200``` Haskell
201getElementById ::
202 (MonadDOM m, IsDocument self, ToJSString elementId) =>
203 self -> elementId -> m (Maybe Element)
204getElementById self elementId
205 = liftDOM
206 (((toDocument self) ^. jsf "getElementById" [toJSVal elementId])
207 >>= fromJSVal)
208```
210## Exceptions
212JSaddle does not support exceptions well. When compiled with GHCJS an exception will result in termination of the thread
213at the point the exception is thrown.
215When using GHC the Haskell executions will probably continue for a while before the exception is received at all by the Haskell code
216(because the lazy execution of the JS code). The exception will be thrown when the next synchronous command is executed.
217When it reaches the synchronous command a haskell type JSException will be thrown (this may not be the thread that initiated the
218command that caused the exception). You can use `syncPoint` and `syncAfter` to force the exception to be thrown.
219There are `JSM` versions of `catch` and `bracket` that also include a `syncPoint` call.
221## Building with Stack
222The `jsaddle-webkit2gtk` runner can be difficult to build with stack. See [this issue](https://github.com/ghcjs/jsaddle/issues/38#issuecomment-331392995) if you get stuck.