Here's a little mini-tutorial on using the latest WASM support in nlvm to call a Nim function from a web page - it's actually surprisingly easy :)
WASM is a simple IR / instruction format that comes with a virtual machine specification, similar - well - to any other virtual machine out there. What's particular about it is that it enjoys broad browser support, giving developers an easy way to deploy high-performance code. It can also be used standalone - using Wasmer (https://wasmer.io/) for example.
Let's start with a function we want to use from the web:
proc fib2(term, val, prev: int): int =
if term == 0: prev
else: fib2(term - 1, val + prev, val)
proc fib(term: int): int {.exportc.} = fib2(term, 1, 0)
WASM works by compiling code to a binary format, a .wasm file. This file is loaded in the browser, and the code therein can be accessed as if it were JavaScript.
Compiling to WASM can be done in many ways - a popular way until now has been to compile to C, then use either Emscripten or clang to compile to WASM, sometimes with the help of tooling from the Binaryen project.
Recently, I threw in LLVM v8 support in nlvm which comes with a library for linking - lld. In the future, this library will be used to make nlvm completely standalone - no gcc or msvc needed - but for now, only WASM is enabled. Linking in the C world is surprisingly onerous and full of thorny legacy / compatibility issues.
Fortunately, compiling fib to wasm is a simple single-step process:
nlvm c -d:release --nlvm.target=wasm32 --gc:none -l:--no-entry fib
wasm32 mode in nlvm implies --os:standalone - the normal C functions like printf are not available here. Depending on where the wasm code runs, it will have a sandboxed environment available, such as the browser or WASI - but accessing this will require a little bit more work on both the nlvm and Nim standard library sides.
--os:standalone means that a panicoverride file is needed for the Nim standard library - we'll start with a bare-bones version of it - name it panicoverride.nim and drop it next to fib.nim:
proc rawoutput(s: string) = discard
proc panic(s: string) = rawoutput(s)
The final piece of the puzzle is to load the wasm file in the browser:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Fib</title>
<script type='text/javascript'>
//<![CDATA[
var v = 10
fetch('fib.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes)
).then(obj => {
document.getElementById("btn").addEventListener("click", function() {
document.getElementById("here").textContent = obj.instance.exports.fib(document.getElementById("x").value);
}, false);
});
//]]>
</script>
</head>
<body>
<form>
<input id="x" /> <input type="button" id="btn" value="!" />
</form>
<p>Answer:</p>
<p id="here"></p>
</body>
</html>
Type in 10, and hopefully you get 55 back - type in 50, and a surprise is waiting for you - bonus points if you can figure it out :) You can tell I stopped writing HTML when blink was still around.
Behind the scenes, nlvm will translate the Nim code to LLVM IR that looks like this:
; ModuleID = 'fib'
source_filename = "fib"
target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128"
target triple = "wasm32"
@nim_program_result = local_unnamed_addr global i32 0
; Function Attrs: norecurse nounwind readnone
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
secArgs:
ret i32 0
}
; Function Attrs: nounwind readnone
define i32 @fib(i32 %n) local_unnamed_addr #1 {
secAlloca:
switch i32 %n, label %ifx.false.fib.2.13 [
i32 0, label %ifx.end.fib.2.1
i32 1, label %ifx.true.fib.2.12
]
ifx.true.fib.2.12: ; preds = %secAlloca
br label %ifx.end.fib.2.1
ifx.false.fib.2.13: ; preds = %secAlloca
%binop.Sub.fib.4.12 = add i32 %n, -1
%call.res.fib.4.10 = tail call i32 @fib(i32 %binop.Sub.fib.4.12)
%binop.Sub.fib.4.24 = add i32 %n, -2
%call.res.fib.4.21 = tail call i32 @fib(i32 %binop.Sub.fib.4.24)
%binop.Add.fib.4.16 = add i32 %call.res.fib.4.21, %call.res.fib.4.10
ret i32 %binop.Add.fib.4.16
ifx.end.fib.2.1: ; preds = %secAlloca, %ifx.true.fib.2.12
%ifx.res.fib.2.1.0 = phi i32 [ 1, %ifx.true.fib.2.12 ], [ %n, %secAlloca ]
ret i32 %ifx.res.fib.2.1.0
}
attributes #0 = { norecurse nounwind readnone }
attributes #1 = { nounwind readnone "no-frame-pointer-elim"="true" }
Notice how there's some cruft in there - a main function and the nim_program_result global. What's nice however is that llvm has been able to transform the recursive function call into a nice and tight loop.
Next, the IR is passed to the WASM machine code generator, and finally the linker. WASM has a text representation, and you can use a handy online tool to get it if you're too lazy to compile it yourself: https://webassembly.github.io/wabt/demo/wasm2wat/
(module
(type $t0 (func))
(type $t1 (func (param i32 i32) (result i32)))
(type $t2 (func (param i32) (result i32)))
(type $t3 (func (param i32 i32 i32) (result i32)))
(func $__wasm_call_ctors (type $t0))
(func $main (export "main") (type $t1) (param $p0 i32) (param $p1 i32) (result i32)
(i32.const 0))
(func $fib (export "fib") (type $t2) (param $p0 i32) (result i32)
(call $fib2_1ukXXM9bbrtY8QseZE5J9cUw
(local.get $p0)
(i32.const 1)
(i32.const 0)))
(func $fib2_1ukXXM9bbrtY8QseZE5J9cUw (type $t3) (param $p0 i32) (param $p1 i32) (param $p2 i32) (result i32)
(local $l3 i32)
(block $B0
(br_if $B0
(i32.eqz
(local.get $p0)))
(loop $L1
(local.set $p1
(i32.add
(local.get $p2)
(local.tee $l3
(local.get $p1))))
(local.set $p2
(local.get $l3))
(br_if $L1
(local.tee $p0
(i32.add
(local.get $p0)
(i32.const -1)))))
(return
(local.get $l3)))
(local.get $p2))
(table $T0 1 1 anyfunc)
(memory $memory (export "memory") 2)
(global $g0 (mut i32) (i32.const 66576))
(global $__heap_base (export "__heap_base") i32 (i32.const 66576))
(global $__data_end (export "__data_end") i32 (i32.const 1028))
(global $nim_program_result (export "nim_program_result") i32 (i32.const 1024))
(data $d0 (i32.const 1024) "\00\00\00\00"))
Of course, there's plenty of room for improvement, this is just a first cut - but I was pleasantly surprised at how easy it was to add this stuff. Have a go, and let me know how it goes :) On linux, you can download a precompiled nlvm easily:
curl -L https://github.com/arnetheduck/nlvm/releases/download/continuous/nlvm-x86_64.AppImage -o nlvm; chmod +x nlvm
That would go in the Nim std lib - an --os:wasi or some bindgen macros like here: https://hacks.mozilla.org/2018/04/javascript-to-rust-and-back-again-a-wasm-bindgen-tale/ - wasm-bindgen is apparently pluggable so it's possible to marshal other languages than rust.
Clearly upstream Nim stuff and not part of nlvm :)
Ok, nice ... but ... how can I produce output or call into the DOM? :-)
I've got some proof of concept of that in here: https://github.com/yglukhov/nimwasmrt. It's not that hard to upgrade, say, jsbind to support that.
The goal of nimwasmrt is is to let nim generate a standalone wasm that can be bootstrapped with a one-liner JS, and have all the powers (api access) that the JS would have.