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 anIntobject 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 anIntvalue. 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
factto5by usingrts_apply. It builds a thunk without triggering evaluation. If we are dealing with a curried multiple-arguments function, we should chainrts_applyrepeatedly 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_evalevaluates a thunk of typeato WHNF.rts_evalIOevaluates the result ofIO ato WHNF.rts_evalLazyIOevaluatesIO 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_getIntto retrieve the unboxedIntcontent 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.