ghcjs / ghcjs-base

base library for GHCJS for JavaScript interaction and marshalling, used by higher level libraries like JSC
MIT License
45 stars 67 forks source link

Introduction to ghcjs-base

It is a minimal low-level base library for GHCJS, used by higher level libraries like JSC

It contains modules for

Marshalling from and to Javascript

This section assumes you read GHCJS foreign function interface which you can find on ghcjs/ghcjs.

Representation of Javascript Types in GHCJS

Arbitrary Javascript Value

Arbitrary javascript values are represented by GHCJS.Types.JSVal

import GHCJS.Types (JSVal)

foreign import javascript unsafe
  "$1 + $2" add :: JSVal -> Int -> JSVal

foreign import javascript unsafe
  "require($1)" require :: JSVal -> JSVal

Internally, JSVal is defined as

data JSVal = JSVal ByteArray#

But, it is an implementation detail you should not have to care about in most cases.

Javascript String

It is defined in Data.JSString as

newtype JSString = JSString JSVal

JSString is also available from GHCJS.Types.

TypedArray

If you already knew how to use TypedArray, understanding JavaScript.TypedArray would not be difficult.

ArrayBuffer

It is represented by JavaScript.TypedArray.ArrayBuffer. GHCJS.Buffer is an obsolete implementation of ArrayBuffer.

Conversion among Javascript Types

It seems people use unsafeCoerce from Unsafe.Coerce to convert JSVal to other Javascript types and vice versa. There is JavaScript.Cast, but it is considered incomplete.

Conversion between Haskell String and Javascript String

Data.JSString contains

pack :: String -> JSString
unpack :: JSString -> String

Use them as in the following example.

import Data.JSString as S

foreign import javascript unsafe
  "require('console').log($1)" console_log :: S.JSString -> IO ()

main :: IO ()
main = do
  let message = "yo" :: String
  console_log (S.pack message)

Pass Haskell String Literals as Javascript String

Data.JSString contains instance IsString JSString, so it's possible to write

import Data.JSString as S

foreign import javascript unsafe
  "require('console').log($1)" console_log :: S.JSString -> IO ()

main :: IO ()
main = do
  console_log "yo"

Marshalling Arbitrary Types from and to Javascript

GHCJS FFI by itself cannot marshal a lot of types from and to JSVal. If you want to convert JSVal into such types, you need to make wrapper functions that call foreign functions, convert the result to a value of the desired type, and return the converted value. Here is a minimal example.

foreign import javascript unsafe
  "$1 === 0" js_isEqualToZero :: Int -> Bool

data NewBool = Yes | No

isEqualToZero :: Int -> NewBool
isEqualToZero n = if js_isEqualToZero n then Yes else No

Pure Marshalling

If the conversion from and to JSVal doesn't involve side effects, you can use pure marshalling API. GHCJS.Marshal.Pure exports

class PToJSVal a where
  pToJSVal :: a -> JSVal

class PFromJSVal a where
  pFromJSVal :: JSVal -> a

GHCJS.Marshal.Pure has PFromJSVal and PToJSVal instances for various basic types.

Impure Marshalling

If the conversion from and to JSVal involves side effects or doesn't return the same output every time for the same input, you may want to use GHCJS.Marshal. GHCJS.Marshal exports

import qualified Data.Aeson as AE

toJSVal_aeson :: AE.ToJSON a => a -> IO JSVal
toJSVal_pure :: PToJSVal a => a -> IO JSVal

class ToJSVal a where
  toJSVal :: a -> IO JSVal
  -- other functions are omitted for simplicity

class FromJSVal a where
  fromJSVal :: JSVal -> IO (Maybe a)
  -- other functions are omitted for simplicity

As far as I know, since FromJSVal and ToJSVal are generic typeclasses, you can use Generics to derive instances for FromJSVal and ToJSVal without boiler plates if you know how to use Generics.

Maybe in Javascript Functions

If you want to express Maybe in imported foreign functions, use GHCJS.Nullable. Nullable is defined in GHCJS.Nullable as

newtype Nullable = Nullable JSVal

It is simply a newtype wrapper around JSVal. The following functions turn Nullable into something more than a mere newtype wrapper around JSVal. GHCJS.Nullable exports

nullableToMaybe :: PFromJSVal a => Nullable a -> Maybe a
maybeToNullable :: PToJSVal a => Maybe a -> Nullable a

The type signatures of those function make it clear that you need to implement pure marshalling typeclasses if you want to marshal Maybe.

You can use Nullable as below.

import GHCJS.Nullable

foreign import javascript unsafe
  "if($1 === 0) { $r = null; } else { $r=$1; }"
  js_nullIfZero :: Int -> Nullable Int

maybeZero :: Int -> Maybe Int
maybeZero n = nullableToMaybe (js_nullIfZero n)

In the above example, I didn't need instance PFromJSVal Int because it is already implemented in GHCJS.Marshal.Pure. As stated before, GHCJS.Marshal.Pure has PFromJSVal and PToJSVal instances for many basic types.

Passing Haskell Callbacks to Javascript

With GHCJS.Foreign.Callback, you can create callbacks that you can pass to imported javascript functions. Callback is defined as

newtype Callback a = Callback JSVal

It's just a newtype wrapper around JSVal. There are currently two kinds of callbacks, synchronous callbacks and asynchronous callbacks.

Asynchronous Callbacks

Asynchronous callbacks are simpler than synchronous callbacks. If an asynchronous callback is passed to a javascript function and the function calls the callback, the callback launches an asynchronous haskell thread. Let's look at the functions that generate asynchronous callbacks.

asyncCallback :: IO () -> IO (Callback (IO ()))
asyncCallback1 :: (JSVal -> IO ()) -> IO (Callback (JSVal -> IO ()))
asyncCallback2 :: (JSVal -> JSVal -> IO ())
               -> IO (Callback (JSVal -> JSVal -> IO ()))

-- There is also asyncCallback3

asyncCallback accepts an IO action and returns a callback in IO. asyncCallback1 returns a callback that accepts one argument. asyncCallback2 for a callback of 2 arguments. You can guess what asyncCallback3 is for.

Synchronous Callbacks

When a synchronous callback is called in javascript, it launches a new synchronous thread. There are two kinds of synchronous callback.

Let's look at the functions that generate synchronous callbacks.

syncCallback :: OnBlocked -- ^ what to do when the thread blocks
             -> IO () -- ^ the Haskell action
             -> IO (Callback (IO ())) -- ^ the callback

It's almost the same as asyncCallback except the argument for OnBlocked. OnBlocked is defined in GHCJS.Concurrent as

data OnBlocked = ContinueAsync -- ^ continue the thread asynchronously if blocked
               | ThrowWouldBlock -- ^ throw 'WouldBlockException' if blocked
               deriving (Data, Typeable, Enum, Show, Eq, Ord)

You can guess what syncCallback1, syncCallback2, and syncCallback3 do.

syncCallback', syncCallback1', and so on generate synchronous callbacks that throw WouldBlockException when their threads block. It's the same as syncCallback ThrowWouldBlock, syncCallback1 ThrowWouldBlock, and so on.

An Example of Using Callback in NodeJs

import GHCJS.Foreign.Callback
import Data.JSString -- This includes an IsString instance for JSString
import GHCJS.Types (JSVal)

foreign import javascript unsafe
  "require('console').log($1)" js_consoleLog :: JSVal -> IO ()

foreign import javascript unsafe
  "require('fs').stat($1, $2)"
  js_fsStat :: JSString -> Callback (JSVal -> JSVal -> IO ()) -> IO ()

main :: IO ()
main = do
  cb <- asyncCallback2 $ \err stat -> js_consoleLog stat
  js_fsStat "/home" cb
  releaseCallback cb

Caveats on Callbacks

How can Arbitrary Haskell Value be stored in and retrieved from Javascript Data Structures?

For example, if you wanted arbitrary haskell data structures to be stored in and retrieved from javascript hashmap, GHCJS.Foreign.Export should be used. GHCJS.Foreign.Export exports

newtype Export a = Export JSVal

export :: Typeable a => a -> IO (Export a)
withExport :: Typeable a => a -> (Export a -> IO b) -> IO b
derefExport :: forall a. Typeable a => Export a -> IO (Maybe a)
releaseExport :: Export a -> IO ()

Various Utilities for Javascript

GHCJS.Foreign exports jsTrue, jsFalse, jsNull, isTruthy, isString, isBoolean, etc, ... You can inspect those functions and understand them easily.

Javascript API

If you already knew javascript APIs, it wouldn't be difficult to inspect and understand JavaScript.Object, JavaScript.Array, JavaScript.String, JavaScript.RegExp, etc, ...