Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Importing javascript functions through third party libraries #3

Open
sanketr opened this issue Jul 3, 2017 · 7 comments
Open

Importing javascript functions through third party libraries #3

sanketr opened this issue Jul 3, 2017 · 7 comments

Comments

@sanketr
Copy link

sanketr commented Jul 3, 2017

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
Copy link
Member

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
Copy link
Author

sanketr commented Jul 4, 2017

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
Copy link
Member

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
Copy link
Author

sanketr commented Jul 6, 2017

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
Copy link
Member

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
Copy link
Author

sanketr commented Jul 7, 2017

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
Copy link

hSloan commented Jan 21, 2019

@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()?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants