jsaddle-wasm 0.0.1.0 → 0.1.0.0
raw patch · 6 files changed
+70/−49 lines, 6 filesdep ~ghc-experimentalPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: ghc-experimental
API changes (from Hackage documentation)
Files
- CHANGELOG.md +5/−0
- README.md +23/−17
- jsaddle-wasm.cabal +4/−4
- src-native/Language/Javascript/JSaddle/Wasm/Internal.hs +2/−2
- src-wasm/Language/Javascript/JSaddle/Wasm/Internal.hs +33/−23
- src/Language/Javascript/JSaddle/Wasm.hs +3/−3
CHANGELOG.md view
@@ -1,5 +1,10 @@ # Revision history for jsaddle-wasm +## 0.1.0.0 -- 2025-03-06++ * Add support for synchronous callbacks (when using `run`, but not `runWorker`) using the new synchronous Wasm JSFFI exports feature.+ Correspondingly, jsaddle-wasm requires a GHC with support for that feature; please use older versions of this library if you can not yet upgrade.+ ## 0.0.1.0 -- 2024-10-26 * Added `runWorker` and `jsaddleScript` for running JSaddle and WASM logic in different execution environments. See the README for details on how to use this.
README.md view
@@ -3,7 +3,7 @@ [](https://hackage.haskell.org/package/jsaddle-wasm) [](https://hackage.haskell.org/package/jsaddle-wasm/docs/Language-Javascript-JSaddle-Wasm.html) -Run [JSaddle][] `JSM` actions with the [GHC WASM backend][].+Run [JSaddle][] `JSM` actions with the [GHC Wasm backend][]. This can for example be used to compile and run [Miso][] or [Reflex][] apps in the browser. @@ -12,14 +12,18 @@ ## Examples - - Several Miso examples: https://github.com/tweag/ghc-wasm-miso-examples+ - Miso examples: https://github.com/tweag/ghc-wasm-miso-examples - - Reflex TodoMVC example: https://github.com/tweag/ghc-wasm-miso-examples/pull/18+ - Reflex examples: https://github.com/tweag/ghc-wasm-reflex-examples + - Ormolu Live: https://github.com/tweag/ormolu/tree/master/ormolu-live+ (uses the web worker approach described below)+ ## How to use -Install a WASM-enabled GHC with support for the WASM JSFFI from [ghc-wasm-meta][] (GHC 9.10 or newer).+Install a Wasm-enabled GHC with support for the Wasm JSFFI (including [synchronous JSFFI exports][sync-jsffi-exports][^missing-sync-jsffi]) from [ghc-wasm-meta][] (GHC 9.10 or newer). + Assuming you built your application as an `app :: JSM ()`: ```haskell@@ -31,14 +35,14 @@ main = JSaddle.Wasm.run app ``` -Build the WASM binary with the following GHC options:+Build the Wasm binary with the following GHC options: ```cabal ghc-options: -no-hs-main -optl-mexec-model=reactor "-optl-Wl,--export=hs_start" ``` Now, run the post-linker script as described in the [GHC User's Guide][ghc-users-guide-js-api]; we will call the resulting JavaScript file `ghc_wasm_jsffi.js`. -Then, following the [GHC User's Guide][ghc-users-guide-js-api], you can run the WASM binary in the browser via e.g. [browser_wasi_shim][]:+Then, following the [GHC User's Guide][ghc-users-guide-js-api], you can run the Wasm binary in the browser via e.g. [browser_wasi_shim][]: ```javascript import { WASI, OpenFile, File, ConsoleStdout } from "@bjorn3/browser_wasi_shim"; import ghc_wasm_jsffi from "./ghc_wasm_jsffi.js";@@ -64,11 +68,11 @@ ### Separating execution environments -It is also possible to run the WASM worker in a different execution environment (e.g. a web worker) than the JSaddle JavaScript code that dispatches the JSaddle command messages.+It is also possible to run the Wasm worker in a different execution environment (e.g. a web worker) than the JSaddle JavaScript code that dispatches the JSaddle command messages. -An advantage of this approach is that computationally expensive operations in WASM do not block the UI thread. A disadvantage is that there is some overhead for copying the data back and forth, and everything relying on synchronous callbacks (e.g. `stopPropagation`/`preventDefault`) definitely no longer works.+An advantage of this approach is that computationally expensive operations in Wasm do not block the UI thread. A disadvantage is that there is some overhead for copying the data back and forth, and everything relying on synchronous callbacks (e.g. `stopPropagation`/`preventDefault`) definitely no longer works. - - Instead of the `run` function above, you need to use `runWorker`:+ - Instead of the `run` function above, you need to use `runWorker` (again assuming `app :: JSM ()`): ```haskell import Language.Javascript.JSaddle.Wasm qualified as JSaddle.Wasm@@ -76,12 +80,12 @@ foreign export javascript "hs_runWorker" runWorker :: JSVal -> IO () runWorker :: JSVal -> IO ()- runWorker = JSaddle.Wasm.runWorker Ormolu.Live.app+ runWorker = JSaddle.Wasm.runWorker app ``` The argument to `runWorker` here can be any message port in the sense of the [Channel Messaging API][]. In particular, it must provide a `postMessage` function and a `message` event. - For example, in a web worker, you can initialize the WASM module as above, and then run+ For example, in a web worker, you can initialize the Wasm module as above, and then run ```javascript await instance.exports.hs_runWorker(globalThis); ```@@ -101,15 +105,14 @@ ## Potential future work - - Take a closer look at synchronous callbacks. We have no special handling currently, but basic things like `stopPropagation`/`preventDefault` already seem to work fine with `run` (but not with `runWorker`, as expected). - Testing (e.g. via Selenium). - Add logging/stats. - Performance/benchmarking (not clear that this is actually a bottleneck for most applications). - Optimize existing command-based implementation. - Reuse buffers - Use a serialization format more efficient than JSON.- - Patch `jsaddle` to not go through commands, by using the WASM JS FFI.- - Implement `ghcjs-dom` API directly via the WASM JS FFI.+ - Patch `jsaddle` to not go through commands, by using the Wasm JS FFI.+ - Implement `ghcjs-dom` API directly via the Wasm JS FFI. This would involve creating a `ghcjs-dom-wasm` package by adapting the FFI import syntax from `ghcjs-dom-jsffi`/`ghcjs-dom-javascript` appropriately. @@ -117,14 +120,17 @@ ## Related projects - - [WebGHC/jsaddle-wasm](https://github.com/WebGHC/jsaddle-wasm) for the analogue for [WebGHC][] instead of the [GHC WASM backend][].+ - [WebGHC/jsaddle-wasm](https://github.com/WebGHC/jsaddle-wasm) for the analogue for [WebGHC][] instead of the [GHC Wasm backend][]. +[^missing-sync-jsffi]: Otherwise, you will see errors involving `unknown type name 'HsFUN'`.+ [JSaddle]: https://github.com/ghcjs/jsaddle-[GHC WASM backend]: https://www.tweag.io/blog/2022-11-22-wasm-backend-merged-in-ghc+[GHC Wasm backend]: https://www.tweag.io/blog/2022-11-22-wasm-backend-merged-in-ghc [Miso]: https://github.com/dmjio/miso [Reflex]: https://github.com/reflex-frp/reflex-[ghc-wasm-meta]: https://gitlab.haskell.org/ghc/ghc-wasm-meta+[ghc-wasm-meta]: https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta [browser_wasi_shim]: https://github.com/bjorn3/browser_wasi_shim [ghc-users-guide-js-api]: https://ghc.gitlab.haskell.org/ghc/doc/users_guide/wasm.html#the-javascript-api [WebGHC]: https://webghc.github.io [Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API+[sync-jsffi-exports]: https://gitlab.haskell.org/ghc/ghc/-/merge_requests/13994
jsaddle-wasm.cabal view
@@ -1,8 +1,8 @@ cabal-version: 3.0 name: jsaddle-wasm-version: 0.0.1.0-synopsis: Run JSaddle JSM with the GHC WASM backend-description: Run JSaddle @JSM@ with the GHC WASM backend.+version: 0.1.0.0+synopsis: Run JSaddle JSM with the GHC Wasm backend+description: Run JSaddle @JSM@ with the GHC Wasm backend. category: Web, Javascript homepage: https://github.com/amesgen/jsaddle-wasm bug-reports: https://github.com/amesgen/jsaddle-wasm/issues@@ -58,7 +58,7 @@ build-depends: aeson >=2 && <2.3, bytestring >=0.11 && <0.13,- ghc-experimental ^>=0.1,+ ghc-experimental ^>=0.1 || >=9.1000 && <9.1300, stm ^>=2.5, hs-source-dirs: src-wasm
src-native/Language/Javascript/JSaddle/Wasm/Internal.hs view
@@ -9,10 +9,10 @@ run :: JSM () -> IO () run _ =- fail "Language.Javascript.JSaddle.Wasm.run: only works on WASM backend"+ fail "Language.Javascript.JSaddle.Wasm.run: only works on Wasm backend" runWorker :: JSM () -> JSVal -> IO () runWorker _ _ =- fail "Language.Javascript.JSaddle.Wasm.runWorker: only works on WASM backend"+ fail "Language.Javascript.JSaddle.Wasm.runWorker: only works on Wasm backend" data JSVal
src-wasm/Language/Javascript/JSaddle/Wasm/Internal.hs view
@@ -21,9 +21,9 @@ import Language.Javascript.JSaddle.Types (Batch, JSM) -- Note: It is also possible to implement this succinctly on top of 'runWorker'--- and 'jsaddleScript', but then e.g. @stopPropagation@/@preventDefault@--- definitely don't work, whereas they work (at least in simple cases) with the--- implementation below.+-- and 'jsaddleScript' (using MessageChannel), but then e.g.+-- @stopPropagation@/@preventDefault@ don't work, whereas the implementation+-- below has special support via @processResultSync@. run :: JSM () -> IO () run entryPoint = do -- TODO rather use a bounded (even size 1) queue?@@ -35,8 +35,8 @@ readBatchCallback <- mkPushCallback $ atomically $ readTQueue outgoingMsgQueue - let jsaddleRunner :: JSVal -> IO ()- jsaddleRunner processResultCallback = do+ let jsaddleRunner :: JSVal -> JSVal -> IO ()+ jsaddleRunner processResultCallback processResultSyncCallback = do s <- byteStringToJSString . BLC8.toStrict . BLC8.unlines $ [ JSaddle.Files.ghcjsHelpers,@@ -46,14 +46,11 @@ " const batch = JSON.parse(await readBatch());", JSaddle.Files.runBatch (\r -> "processResult(JSON.stringify(" <> r <> "));")- -- TODO Think more about how synchronous dispatching works here- -- (see processSyncResult). For some reason, it already /seems/- -- to work fine, at least in simple cases.- Nothing,+ (Just \r -> "JSON.parse(processResultSync(JSON.stringify(" <> r <> ")))"), " }", "})();" ]- evaluate =<< js_eval s processResultCallback readBatchCallback+ evaluate =<< js_eval s processResultCallback processResultSyncCallback readBatchCallback runHelper entryPoint sendOutgoingMessage jsaddleRunner @@ -62,28 +59,39 @@ runHelper entryPoint (evaluate <=< js_postMessage worker)- (evaluate <=< js_onMessage worker)+ (\processResult _processResultSync -> evaluate =<< js_onMessage worker processResult) runHelper :: JSM () -> -- | How to send an outgoing message. (JSString -> IO ()) ->- -- | Start receiving incoming messages. For every message, invoke the given- -- 'JSVal' callback.- (JSVal -> IO ()) ->+ -- | Start receiving incoming messages. For every message, invoke one of the+ -- two given 'JSVal' callbacks (for sync/async processing, respectively).+ (JSVal -> JSVal -> IO ()) -> IO () runHelper entryPoint sendOutgoingMessage onIncomingMessage = do- (processResult, _processSyncResult, start) <-+ (processResult, processSyncResult, start) <- runJavaScript sendBatch entryPoint - processResultCallback <- mkPullCallback \s -> do- bs <- jsStringToByteString s- case A.eitherDecodeStrict bs of- Left e -> fail $ "jsaddle: received invalid JSON: " <> show e- Right r -> processResult r+ let receiveBatch :: JSString -> IO ()+ receiveBatch s = do+ bs <- jsStringToByteString s+ case A.eitherDecodeStrict bs of+ Left e -> fail $ "jsaddle: received invalid JSON: " <> show e+ Right r -> processResult r - onIncomingMessage processResultCallback+ processBatchSync :: JSString -> IO JSString+ processBatchSync s = do+ bs <- jsStringToByteString s+ case A.eitherDecodeStrict bs of+ Left e -> fail $ "jsaddle: received invalid JSON: " <> show e+ Right r -> byteStringToJSString . BLC8.toStrict . A.encode =<< processSyncResult r + processResultCallback <- mkPullCallback receiveBatch+ processResultSyncCallback <- mkSyncCallback processBatchSync++ onIncomingMessage processResultCallback processResultSyncCallback+ start where sendBatch :: Batch -> IO ()@@ -95,10 +103,12 @@ foreign import javascript "wrapper" mkPullCallback :: (JSString -> IO ()) -> IO JSVal +foreign import javascript "wrapper sync" mkSyncCallback :: (JSString -> IO JSString) -> IO JSVal+ foreign import javascript "wrapper" mkPushCallback :: IO JSString -> IO JSVal -foreign import javascript safe "new Function('processResult','readBatch',`(()=>{${$1}})()`)($2, $3)"- js_eval :: JSString -> JSVal -> JSVal -> IO ()+foreign import javascript safe "new Function('processResult','processResultSync','readBatch',`(()=>{${$1}})()`)($2, $3, $4)"+ js_eval :: JSString -> JSVal -> JSVal -> JSVal -> IO () -- Worker
src/Language/Javascript/JSaddle/Wasm.hs view
@@ -1,7 +1,7 @@ -- | See the [README](https://github.com/amesgen/jsaddle-wasm) for more details. ----- While this package also compiles on non-WASM GHCs for convenience, running--- this function will immediately fail.+-- While this package also compiles on non-Wasm GHCs for convenience, running+-- any function from this module will immediately fail in that case. module Language.Javascript.JSaddle.Wasm ( run, runWorker,@@ -17,7 +17,7 @@ import Language.Javascript.JSaddle.Wasm.Internal qualified as Internal import Language.Javascript.JSaddle.Wasm.JS (jsaddleScript) --- | Run a 'JSM' action via the WASM JavaScript FFI.+-- | Run a 'JSM' action via the Wasm JavaScript FFI. run :: JSM () -> IO () run = Internal.run