ghcjs / jsaddle-hello

JSaddle Hello World, an example package
MIT License
8 stars 1 forks source link

Importing javascript functions through third party libraries #3

Open sanketr opened 7 years ago

sanketr commented 7 years ago

It will be helpful to show how to load the third-party libraries that are referenced in JS FFI. It can be just a simple js library/script defining, say an add function. The example can then show how to load that JS library (basically, how to link to the library referenced in FFI) and use the function referenced there.

I know how to do it through ghcjs (easy to script tag in index.html that ghcjs generates), but not how to do this through ghc. Will also appreciate pointers in comments because this will be very helpful in immediately moving forward with jsaddle, and third party JS libraries - I plan to use jsaddle, wkwebview and ghc along the lines of the hello example demonstrated here, but with third-party JS libraries also included.

hamishmack commented 7 years ago

JavaScript files can be loaded into the context using eval with something like liftIO (readFile "something.js") >>= eval.

You might want to embed the JS file into your executable with file-embed instead of using readFile (file-embed uses template haskell so it does not work yet on GHC cross compiling to iOS and Android yet).

sanketr commented 7 years ago

Thank you! One question about eval: we get JSM JSVal out of it. Let us say, we evaluated a script which defines a function testfn. How do we retrieve testfn from JSVal object we got from the eval of that script? Surely, the function needs to be pulled out using some kind of indexing (I am guessing, with a Maybe type signature in case it is not found) - so, lens indexing makes sense. I am not very familiar with conjoined type of lens indexing used here. That is why when I looked at jsaddle-hello functions like js and jsg, I couldn't figure out how to pass testfn to them. So, pointers will be very helpful.

hamishmack commented 7 years ago

You can create a single function with eval and then use call to call it like this:

f <- eval "function(x) {console.log(x)}"
call f (toJSVal "Helllo")

For a JavaScript library that defines functions in the JavaScript global scope use something like:

eval "var test = function(x) {console.log(x)}"
jsg1 "test" (toJSVal "Helllo") -- Like JavaScript test("Hello");

Note: in jsg1 the g means it is in the global scope 1 means it is a function to be called with one argument.

If the library defines the functions inside some other object you might need to do something like:

eval "var test = { f: function(x) {console.log(x)} }"
jsg "test" ^. js1 "f" (toJSVal "Helllo")  -- Like JavaScript test.f("Hello");

Without the number jsg "test" becomes a getter for the attribute. You can think of ^. in this code as being like the . in JavaScript.

sanketr commented 7 years ago

I wrote a small test along the lines you suggested, but am getting JSException - what will be a good way to debug it to find the root cause? Here is the code - I can see JSException in the debug output but can't figure out what I might be doing wrong with FFI. I did sanity check of the js code in node to make sure the js code works fine.

Test module that simulates a js function loaded through library:

{-# LANGUAGE ScopedTypeVariables #-}
module Test ( main ) where

import Control.Monad.IO.Class (MonadIO(..))
import Control.Concurrent (forkIO)
import Control.Lens ((^.))
import Language.Javascript.JSaddle
       (call,jsg1, js1, jss, fun, syncPoint, toJSVal,fromJSVal)
import Language.Javascript.JSaddle.Evaluate (eval)
import Language.Javascript.JSaddle.Run (enableLogging)

main = do

    enableLogging True
    eval "var test = function(x) {return x.length;}"
    (res :: Maybe Int) <- jsg1 "test" (toJSVal "Helllo") >>= fromJSVal
    liftIO $ print $ "String length: " ++ show res 
    return ()

WKWebView wrapper around test module:


module Main ( main ) where

import qualified Test (main) import Language.Javascript.JSaddle.WKWebView (run)

main = run Test.main

When compiled with `ghc 8.0.2` (with `-dynamic -threaded` option) on `mac sierra`, I get this when running the executable - btw, executable is named `wkwebmain` here:

Sync M ?? CB 0 (5,Right (ValueToNumber (JSValueForSend (-4)))) A JavaScript exception was thrown! (may not reach Haskell code) wkwebmain: JSException Sync M ?? CB 0 (1,Left (FreeRef "ThreadId 12" (JSValueForSend (-1)))) Sync M ?? CB 0 (4,Left (FreeRef "ThreadId 15" (JSValueForSend (-4)))) Sync M ?? CB 0 (1,Left (FreeRefs "ThreadId 15"))

hamishmack commented 7 years ago

Sorry, I forgot that eval does not run in the global scope. Try "test = function(x) {return x.length;}".

To debug this I used the jsaddle-warp runner. You can right click and get an inspector with the other runners, but often it is too late by then. With jsaddle-warp you can load a browser and have the inspector window open before you connect it. I set it to break on exceptions then connected and it stopped when calling .apply on the a undefined. A quick look for window.test showed it did not exist, but I did see test in the local scope.

sanketr commented 7 years ago

Very helpful pointers, thank you! Now, I am able to fix the error, as well as run the debugger using jsaddle-warp - this also resolves the mystery of putMVar getting stuck on askJSM (cribbed your testJSaddle function). Apparently, the browser needs to be fired up and warp server needs to be hit, for the page to be served, and hence, context to kick in. I forgot about this detail, and was waiting for the browser to pop up automatically. When you laid out debugging steps, I realized my mistake. Now, let me see if I can write a simple test, and then submit a pull request for inclusion in hello example.

hSloan commented 5 years ago

@hamishmack I'm currently trying to implement bluetooth.requestDevice using JSaddle. The goal is to structure a function that would resemble the following.

navigator.bluetooth.requestDevice({ filters: [{ services: ['battery_service'] }] })
.then(device => { /* ... */ })
.catch(error => { console.log(error); });

Here is a rough idea of what I have so far:

listBluetoothDevices = do
  nav <- liftJSM $ jsg ("navigator" :: Text)
  let handleBluetoothFunc = eval (textToStr ("(function(){device => { console.log(1); }).catch(error => { console.log(error); });" :: Text))
      bluetooth = nav
        ^. JSaddle.js ("bluetooth" :: Text)
        ^. js1 ("requestDevice" :: Text) ("{ acceptAllDevices: true }" :: Text)
  bluetoothResults <- liftJSM $ JSaddle.call bluetooth handleBluetoothFunc ()

What are the best practices for creating a Promise in JSaddle?

Lastly, what is the best way to obj.func().then().catch()?