1
1
mirror of https://github.com/tweag/asterius.git synced 2024-11-10 14:52:44 +03:00
asterius/docs/jsffi.md
2020-04-03 05:43:04 +02:00

9.2 KiB

JavaScript FFI

Asterius implements JSFFI, which enables importing sync/async JavaScript code, and exporting static/dynamic Haskell functions. The JSFFI syntax and semantics is inspired by JSFFI in GHCJS, but there differ in certain ways.

Marshaling data between Haskell and JavaScript

Directly marshalable value types

There are mainly 3 kinds of marshalable value types which can be directly used as function arguments and return values in either JSFFI imports or exports:

  • Regular Haskell value types like Int, Ptr, StablePtr, etc. When the MagicHash and UnliftedFFITypes extensions are enabled, some unboxed types like Int# are also supported.
  • The JSVal type and its newtypes.
  • The Any type.

The JSVal type is exported by Asterius.Types. It represents an opaque JavaScript value in the Haskell world; one can use JSFFI imports to obtain JSVal values, pass them across Haskell/JavaScript, store them in Haskell data structures like ordinary Haskell values. JSVals are garbage collected, but it's also possible to call freeJSVal to explicitly free them in the runtime.

The Any type in GHC.Exts represents a boxed Haskell value, which is a managed pointer into the heap. This is only intended to be used by power users.

Just like regular ccall imports/exports, the result type of javascript imports/exports can be wrapped in IO or not.

The JSVal family of types

Other than JSVal, Asterius.Types additionally exports these types:

  • JSArray
  • JSFunction
  • JSObject
  • JSString
  • JSUint8Array

They are newtypes of JSVal and can be directly used as argument or result types as well. The runtime doesn't perform type-checking at the JavaScript side, e.g. it won't check if typeof $1 === "string" when $1 is declared as a JSString. It's up to the users to guarantee the runtime invariants about such JSVal wrapper types.

User-defined newtypes of JSVal can also be used as marshalable value types, as long as the newtype constructor is available in scope.

Marshaling structured data

Given the ability of passing simple value types, one can implement their own utilities for passing a piece of structured data either from JavaScript to Haskell, or vice versa.

To build a Haskell data structure from a JavaScript value, usually we write a builder function which recursively traverses the substructure of the JavaScript value (sequence, tree, etc) and build up the Haskell structure, passing one cell at a time. Similarly, to pass a Haskell data structure to JavaScript, we traverse the Haskell data structure and build up the JavaScript value.

The Asterius standard library provides functions for common marshaling purposes:

import Asterius.Aeson
import Asterius.ByteString
import Asterius.Text
import Asterius.Types

fromJSArray :: JSArray -> [JSVal]
toJSArray :: [JSVal] -> JSArray
fromJSString :: JSString -> String
toJSString :: String -> JSString
byteStringFromJSUint8Array :: JSUint8Array -> IO ByteString
byteStringToJSUint8Array :: ByteString -> IO JSUint8Array
textFromJSString :: JSString -> Text
textToJSString :: Text -> JSString
jsonToJSVal :: ToJSON a => a -> JSVal
jsonFromJSVal :: FromJSON a => JSVal -> Either String a
jsonFromJSVal' :: FromJSON a => JSVal -> a

The 64-bit integer precision problem

Keep in mind that when passing 64-bit integers via Int, Word, etc, precision can be lost, since they're represented by numbers on the JavaScript side. In the future, we may consider using bigints instead of numbers as the JavaScript representations of 64-bit integers to solve this issue.

JSFFI imports

JSFFI import syntax

import Asterius.Types

foreign import javascript unsafe "new Date()" current_time :: IO JSVal

foreign import javascript interruptible "fetch($1)" fetch :: JSString -> IO JSVal

The source text of foreign import javascript should be a single valid JavaScript expression, using $n to refer to the n-th argument (starting from 1). It's possible to use IIFE(Immediately Invoked Function Expression) in the source text, so more advanced JavaScript constructs can be used.

Sync/async JSFFI imports

The safety level in a foreign import javascript declaration indicates whether the JavaScript logic is asynchronous. When omitted, the default is unsafe, which means the JavaScript code will return the result synchronously. When calling an unsafe import, the whole runtime blocks until the result is returned from JavaScript.

The safe and interruptible levels mean the JavaScript code should return a Promise which later resolves with the result. The current thread will be suspended when such an import function is called, and resumed when the Promise resolves or rejects. Other threads may continue execution when a thread is blocked by a call to an async import.

Error handling in JSFFI imports

When calling a JSFFI import function, The JavaScript code may synchronously throw exceptions or reject the Promise with errors. They are wrapped as JSExceptions and thrown in the calling thread, and the JSExceptions can be handled like regular synchronous exceptions in Haskell. JSException is also exported by Asterius.Types; it contains both a JSVal reference to the original JavaScript exception/rejection value, and a String representation of the error, possibly including a JavaScript stack trace.

Accessing the asterius instance object

In the source text of a foreign import javascript declaration, one can access everything in the global scope and the function arguments. Additionally, there is an __asterius_jsffi binding which represents the asterius instance object. __asterius_jsffi exposes certain interfaces for power users, e.g. __asterius_jsffi.exposeMemory() which exposes a memory region as a JavaScript typed array. The interfaces are largely undocumented and not likely to be useful to regular users.

There is one usage of __asterius_jsffi which may be useful to regular users though. Say that we'd like the JSFFI import code to call some 3rd-party library code, but we don't want to pollute the global scope; we can assign the library functions as additional fields of the asterius instance object after it's returned by newAsteriusInstance(), then access them using __asterius_jsffi in the JSFFI import code.

JSFFI exports

JSFFI static exports

foreign export javascript "mult_hs" (*) :: Int -> Int -> Int

The foreign export javascript syntax can be used for exporting a static top-level Haskell function to JavaScript. The source text is the export function name, which must be globally unique. The supported export function types are the same with JSFFI imports.

For the exported functions we need to call in JavaScript, at link-time, each exported function needs an additional --export-function flag to be passed to ahc-link/ahc-dist, e.g. --export-function=mult_hs.

In JavaScript, after newAsteriusInstance() returns the asterius instance object, one can access the exported functions in the exports field:

i.exports.hs_init();
const r = await i.exports.mult_hs(6, 7);

Note that all exported Haskell functions are async JavaScript functions. The returned Promise resolves with the result when the thread successfully returns; otherwise it may reject with a JavaScript string, which is the serialized form of the Haskell exception if present.

It's safe to call a JSFFI export function multiple times, or call another JSFFI export function before a previous call resolves/rejects. The export functions can be passed around as first-class JavaScript values, called as ordinary JavaScript functions or indirectly as JavaScript callbacks. They can even be imported back to Haskell as JSVals and called in Haskell.

JSFFI dynamic exports

import Asterius.Types

foreign import javascript "wrapper" makeCallback :: (JSVal -> IO ()) -> IO JSFunction
foreign import javascript "wrapper oneshot" makeOneshotCallback :: (JSVal -> IO ()) -> IO JSFunction

freeHaskellCallback :: JSFunction -> IO ()

The foreign import javascript "wrapper" syntax can be used for exporting a Haskell function closure to a JavaScript function dynamically. The type signature must be of the form Fun -> IO JSVal, where Fun represents a marshalable JSFFI function type in either JSFFI imports or static exports, and the result can be JSVal or its newtype.

After declaring the "wrapper" function, one can pass a Haskell function closure to it and obtain the JSVal reference of the exported JavaScript function. The exported function can be used in the same way as the JSFFI static exports.

When a JSFFI dynamic export is no longer useful, call freeHaskellCallback to free it. The JSVal reference of the JavaScript callback as well as the StablePtr of the Haskell closure will be freed.

Sometimes, we expect a JSFFI dynamic export to be one-shot, being called for only once. For such one-shot exports, use foreign import javascript "wrapper oneshot". The runtime will automatically free the resources once the exported JavaScript is invoked, and there'll be no need to manually call freeHaskellCallback for one-shot exports.