4.6 KiB
Invoking RTS API in JavaScript
For the brave souls who prefer to play with raw pointers instead of syntactic sugar, it's possible to invoke RTS API directly in JavaScript. This grants us the ability to:
- Allocate memory, create and inspect Haskell closures on the heap.
- Trigger Haskell evaluation, then retrieve the results back into JavaScript.
- Use raw Cmm symbols to summon any function, not limited to the "foreign exported" ones.
Here is a simple example. Suppose we have a Main.fact
function:
fact :: Int -> Int
fact 0 = 1
fact n = n * fact (n - 1)
The first step is ensuring fact
is actually contained in the final WebAssembly binary produced by ahc-link
. ahc-link
performs aggressive dead-code elimination (or more precisely, live-code discovery) by starting from a set of "root symbols" (usually Main_main_closure
which corresponds to Main.main
), repeatedly traversing ASTs and including any discovered symbols. So if Main.main
does not have a transitive dependency on fact
, fact
won't be included into the binary. In order to include fact
, either use it in some way in main
, or supply --extra-root-symbol=Main_fact_closure
flag to ahc-link
when compiling.
The next step is locating the pointer of fact
. The "asterius instance" type we mentioned before contains two "symbol map" fields: staticsSymbolMap
maps static data symbols to linear memory absolute addresses, and functionSymbolMap
maps function symbols to WebAssembly function table indices. In this case, we can use i.staticsSymbolMap.Main_fact_closure
as the pointer value of Main_fact_closure
. For a Haskell top-level function, there're also pointers to the info table/entry function, but we don't need those two in this example.
Since we'd like to call fact
, we need to apply it to an argument, build a thunk representing the result, then evaluate the thunk to WHNF and retrieve the result. Assuming we're passing --asterius-instance-callback=i=>{ ... }
to ahc-link
, in the callback body, we can use RTS API like this:
i.wasmInstance.exports.hs_init();
const argument = i.wasmInstance.exports.rts_mkInt(5);
const thunk = i.wasmInstance.exports.rts_apply(i.staticsSymbolMap.Main_fact_closure, argument);
const tid = i.wasmInstance.exports.rts_eval(thunk);
console.log(i.wasmInstance.exports.rts_getInt(i.wasmInstance.exports.getTSOret(tid)));
A line-by-line explanation follows:
- As usual, the first step is calling
hs_init
to initialize the runtime. - Assuming we'd like to calculate
fact 5
, we need to build anInt
object which value is5
. We can't directly pass the JavaScript5
, instead we should callrts_mkInt
, which properly allocates a heap object and sets up the info pointer of anInt
value. When we need to pass a value of basic type (e.g.Int
,StablePtr
, etc), we should always callrts_mk*
and use the returned pointers to the allocated heap object. - Then we can apply
fact
to5
by usingrts_apply
. It builds a thunk without triggering evaluation. If we are dealing with a curried multiple-arguments function, we should chainrts_apply
repeatedly until we get a thunk representing the final result. - Finally, we call
rts_eval
, which enters the runtime and perform all the evaluation for us. There are different types of evaluation functions:rts_eval
evaluates a thunk of typea
to WHNF.rts_evalIO
evaluates the result ofIO a
to WHNF.rts_evalLazyIO
evaluatesIO a
, without forcing the result to WHNF. It is also the default evaluator used by the runtime to runMain.main
.
- All
rts_eval*
functions initiate a new Haskell thread for evaluation, and they return a thread ID. The thread ID is useful for inspecting whether or not evaluation succeeded and what the result is. - If we need to retrieve the result back to JavaScript, we must pick an evaluator function which forces the result to WHNF. The
rts_get*
functions assume the objects are evaluated and won't trigger evaluation. - Assuming we stored the thread ID to
tid
, we can usegetTSOret(tid)
to retrieve the result. The result is always a pointer to the Haskell heap, so additionally we need to userts_getInt
to retrieve the unboxedInt
content to JavaScript.
Most users probably don't need to use RTS API manually, since the foreign import
/export
syntactic sugar and the makeHaskellCallback
interface should be sufficient for typical use cases of Haskell/JavaScript interaction. Though it won't hurt to know what is hidden beneath the syntactic sugar, foreign import
/export
is implemented by automatically generating stub WebAssembly functions which calls RTS API for you.