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:
const argument = i.exports.rts_mkInt(5);
const thunk = i.exports.rts_apply(i.staticsSymbolMap.Main_fact_closure, argument);
const tid = i.exports.rts_eval(thunk);
console.log(i.exports.rts_getInt(i.exports.getTSOret(tid)));
A line-by-line explanation follows:
-
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.