ActuallyUsingWasm
Last updated: March 2020
What is wasm?
WebAssembly, aka wasm, is a portable bytecode intended for fast and secure execution of programs across different systems.
Let’s be 100% clear: this is not a new idea. Most notably Java and .NET both have done this idea before, pretty successfully, and build on a long history of other tech before them. Why bother doing this again? Well, the JVM is about 25 years old, from an age where, for instance, it really wasn’t a sure thing whether garbage collection even could get widespread acceptance in modestly performance-oriented code. It was also designed before the Internet became what it is today, and is full of ideas about security and distribution of programs that are at odds with current methods. As for the .NET CLI, it just never seems to have caught on for realsies much outside of the Microsoft world. I’m really not sure why; it’s a technological improvement to the JVM in basically every way that I am aware of. Maybe it’s just the Stigma Of Microsoft, the until-recently closed-source nature and threat of patents being able to crush Mono whenever MS really wanted to that kept people from buying into it. Sad but reasonable.
So, why reinvent the wheel? Well, to take another shot and see if we can make something better, something that doesn’t have a single company pushing it and which uses a bit of the knowledge gained from a few decades of people trying to write really fast bytecode systems and JIT compilers. Do we really need to do this? Up to you. But in my opinion, wasm looks like enough of an improvement to be worth it. Webassembly has its own share of warts, of course: I dislike that it starts with a 32-bit version instead of 64-bit, some kinda fundamental things like pointer and interface types are still WIP, and there’s no versioning system attached since apparently web browser devs think “if you try it and it doesn’t work, then you know it’s not supported” is perfectly fine. Despite that sort of stuff, the tech in general seems generally reasonable, and people are actually Using It In The Real World by now.
One of the interesting prospects of WebAssembly is that it can be used for safe and portable sandboxing on systems that aren’t web browsers. This is somewhat undersold by at least some parts of the wasm ecosystem, in my opinion, but has some real value. Being able to distribute executables and libraries in a portable bytecode that gets locally compiled seems to me like a strict improvement on distributing platform-specific binaries, even if the programs are open source and you can just compile them from source if you really want to. Given that we’re increasingly living less in an x86_64 monoculture, with diverse good non-x86 hardware widely available, this idea seems more and more useful. So, as someone who mostly wants to write portable programs that run on desktop-style hardware, let’s try to actually use webassembly for this and see how it goes.
What does the ecosystem look like?
Well, it’s a bunch of acronyms, and they all start with W. Let’s try to look at how the parts fit together:
wasm
– “machine code”. A low-level bytecode designed for portable, fast and easy execution. An open standard being built by the WebAssembly WG of the W3C.wasi
– “system calls”. An API for doing basic system stuff, mainly I/O. emscripten does this too but I dislike using emscripten’s tooling. Open standard being built by the WebAssembly WG of the W3C.- Compiler – Bring your own. rustc supports
wasm32-unknown-emscripten
andwasm32-wasi
. It also supportswasm32-unknown-unknown
, which is basically “as if you were writing for a microcontroller”, with no OS, system interface or such; you have to write your own, or use an existing one like theweb-sys
crate. You can also use clang, emscripten and probably others, but that would involve using a language less cool than Rust so I’m not interested right now. That said, it looks like Zig and some other languages are starting to support webassembly as well, so we’ll probably see more of it as time goes on.
Webassembly implementations and tools:
wasmer
– Interpreter/JIT made by the folks at wasmer.io. Based on Cranelift but has other compiler backends too (LLVM, standalone)wapm
– A package manager similar to npm. Whyyyyyy do we have Yet Another One Of These. For programming languages it’s useful for it to be part of the build system, but wasm modules are more like DLL’s than anything else, so I don’t see what this is supposed to gain me. We already have ways to distribute executable and library binaries. Maybewapm
will end up being a better way, but I’m not convinced yet.wasmtime
– Interpreter/JIT. Does the same jobwasmer
but made by different people. Based on Cranelift. Doesn’t seem to be involved withwapm
at all.- Many others are listed at Awesome Wasm
Runtimes, which seems more or less up to date at the moment. However
it is not my goal here to compare every possible
implementation, just the ones that seem (to me) to have the most push
behind them. Other reasonably mature implementations appear to be Perlin
Networks’
life
(in Go), Parity Tech’swasmi
(in Rust), and probably others I’m missing. Both are interpreters and neither support WASI, so they are less interesting to me, thoughlife
’s experimental JIT looks pretty sweet.
Misc other terminology:
- WASI – The WebAssembly System Interface, a somewhat POSIX-y API intended for giving wasm programs on non-web platforms a useful system interface.
- Bytecode Alliance – A loose association for developing WASI that seems to mainly consist of Mozilla, Fastly and optimism. Intel and Red Hat are listed as members, but from rummaging through github group owners (as of March 2020) there’s only one person who lists themselves as an Intel employee and no Red Hat ones, so. I really hope it grows a lot!
- Cranelift – General-purpose compiler and JIT backend written in Rust. Similar to LLVM in concept.
The delta between wasmer
and wasmtime
is
interesting. Who’s paying for wasmer
? From the Github org the
most likely candidate is Syrus Akbary, the lead dev of the
Graphene-Python GraphQL lib and owner of Wasmer Inc. Looks like
officially Wasmer Inc maintains
wasmer
as well as
wapm
. Wasmer Inc was started in 2018 and the Bytecode
Alliance in 2019, so that’s an easy explination for the question of “why
do both these exist”. Wasmer Inc started first, as a for-profit company,
then Bytecode Alliance formed later as a non-profit for various
companies to cooperate through.
Both wasmer
and wasmtime
are written in
Rust, which is part of why I know about them. Why is Rust often attached
to Webassembly stuff? As far as I can tell, three reasons:
rustc
, Cranelift, and wasm itself. In reverse order:
- Unlike the JVM or .NET CLI, wasm doesn’t require a garbage collector. This means it’s an attractive compilation target for languages like Rust, C and C++, which also don’t require a garbage collector. wasm is also, in my limited-but-real experience, smaller and lower level and thus easier to target with a new compiler or runtime. It has no concept of classes or even structures, very simple namespaces and linking/loading model, no complex types such as generics, etc. It is much more like machine code than JVM or CLI bytecode is. So if you’re starting a green-fields low-level programming project, and your lowest-level possible languages are C, C++ and Rust, then Rust is a pretty strong contender.
rustc
treats cross-compilation as a first class citizen.rustc
is always a cross-compiler, andcargo
andrustup
let you target a new architecture with just a command or two. Far cry fromgcc
, where cross-compiling starts with “install these specific versions of binutils etc, then buildgcc
from scratch with these magic options and install it into a particular place and then you can call it with the right magic command line incantation.”clang
apparently is better, being morerustc
-like in always being a cross compiler, but building and installing a cross-compiled libc and such is still wacky and fragile. Between them,rustup
,cargo
andrustc
control pretty much the whole stack, including the system lib interface, linker and all library dependencies, and so can easily cooperate to make cross-compilation easy.rustc
treats Webassembly like any other target and has good support for generating it, so, creating Webassembly code from Rust is extremely easy.- And lastly, you have Cranelift just sitting there, being a
still-in-development-but-nice compiler backend library just waiting to
be used. LLVM is better, but LLVM is also a lot more work to deal with.
If you’re looking at language+compiler backend combinations, the obvious
first one is C++ and LLVM, and if you then ask “can we use something
like LLVM in Rust easily” the most mature thing you find is Cranelift,
and Cranelift can target Webassembly. Binding to LLVM from Rust is
certainly possible,
rustc
itself does it, but it’s not much fun.
So part of Rust’s prevalence in this space is coincidence, part of it is good design, and part of it is people recognizing that these two technologies that are growing at the same time should be able to go well together. It’s also probably not much coincidence that Mozilla is a player in both wasm and Rust, though far from the largest or only player. That said, wasm == Rust is hardly a law. There’s plenty of wasm runtimes written in other languages, it’s just surprising to find so many different projects in the same space using Rust.
Using wasmer
wasmer
’s official tutorial is here
and there’s nothing I can really show you that it doesn’t do better.
Long story short, writing a program to call a wasm function using
wasmer
looks like this:
use wasmer_runtime::{error, imports, instantiate, Func};
fn main() {
let wasm_bytes = include_bytes!("add.wasm");
let import_object = imports! {};
let instance = instantiate(wasm_bytes, &import_object)?;
let add_one: Func<u32, u32> = instance.func("add_one")?;
let result = add_one.call(42)?;
println!("Result: {}", result);
}
To make it clear, this produces a native program that loads
and executes wasm bytecode, essentially as if it were calling
out to a DLL. (A wasm module looks and functions a lot like a DLL.)
add.wasm
is a program from the wasmer
tutorial
that they provide for you to download. …Also it’s a Rust program, and a
kinda jank one, ’cause it only exports one function,
add_one()
, but includes all of the Rust executable fluff
like panic handler, memory allocation functions, etc. It’s 1.7 megabytes
FFS. Let’s fix that, hmm?
# Install wasm tools if necessary: on Debian 10, this is `apt install wabt`
wasm2wat add.wasm > add_hacked.wat
# edit the assembly and remove everything but the `add_one` function, the export decl for it, and the type definition it needs
wat2wasm add_hacked.wat
wasm-validate add_hacked.wasm
# Huzzah, we managed not to break anything
Much better, my add_hacked.wasm
is 143 bytes and frankly
is still oversized, and loading it with the same hello-world program
still works and produces the same result. Wonder how much work that
would take with a native code DLL? It’s totally doable but I feel like
it probably would have taken more than ten minutes to get right. Now, it
would be nicer to make the Rust program that the wasm came from produce
something similarly minimal in the first place, but that’s beyond the
scope of this for now. IIRC all you have to do is treat it like you’re
making an embedded
program: use the abort
panic handler, possibly compile
it with no_std
, and strip out debugging symbols. Doing that
is left as an exercise to the reader.
(Heck, I can’t resist…. In practice, when I used
crate-type = ["cdylib"]
and lto = "thin"
in my
Cargo.toml
it produced a 49 KB wasm file… consisting of
99.7% debugging symbols for backtraces, even with
panic = "abort"
and debug = false
. Irritating,
I’m sure there’s a way to get it to only output bare code, just gotta
find it. There’s a wasm-strip
program that takes out
debugging stuff though, if you don’t want it, and leaves just the 119
bytes of actual code.)
The wasmer
page has more tutorials about various other
embedding use cases, which don’t touch advanced stuff in much
depth but do seem to do a good job of getting the basics down.
Using wasmtime
The wasmtime
docs are not oriented towards embedding it
as a wasm runtime in a native program, and example code for doing so is
present but
less sophisticated. But, figuring it out for basic things is still
pretty easy. The wasmtime
equivalent of the above program
is this:
#[wasmtime_rust::wasmtime]
trait WasmAdd {
fn add_one(&mut self, input: i32) -> i32;
}
fn main() {
let wasm_bytes = include_bytes!("add.wasm");
let mut add = WasmAdd::load_bytes(wasm_bytes.as_ref()).unwrap();
let result = add.add_one(42);
println!("Result: {}", result);
}
There is actually an interesting subtle difference here: the arg and
return type of add_one()
must be i32
here, not
u32
. Webassembly itself does not specify whether integers
are signed or unsigned, just whether operations treat them as one or the
other. Looks like wasmer
and wasmtime
have
different opinions about how to interpret that. Our actual WebAssembly
program uses the i32.add
instruction, which is identical
for signed and unsigned numbers, so either is a valid interpretation and
either will produce correct results. There is a WebAssembly
proposal for being able to more strictly define these things in the
wasm module itself. In the mean time, Rust’s stubbornness about signed
and unsigned integers saves us yet again, or at least alerts us to a
potential edge case. Implementers beware!
Standalone programs in Webassembly
What I don’t seem to find is any documentation on making standalone
programs for execution on wasmer
. That seems a little
ingenuous. Well, wasmer
claims it supports the WASI API,
and rustc
offers WASI as a target, so let’s just try that I
guess?
cargo init hello
cd hello
cargo run
# prints 'Hello world!'
cargo build --target wasm32-wasi
wasmer target/wasm32-wasi/debug/hello.wasm
# prints 'Hello world!'
…well that was easy. What about wasmtime
?
Not much to complain about here, I suppose! wasmer
doesn’t have much documentation on writing programs using the WASI API,
while wasmtime
has enough
of a tutorial to get you started. Just based on what they
demonstrate, wasmer
is more focused on embedding wasm in
your native program, while wasmtime
is more focused on
executing standalone wasm programs using WASI. Both are capable of both,
it just seems a matter of emphasis.
So, for basic stuff, to use WASI from Rust you don’t actually need to
do anything special. Rust’s standard library compiles to WASI just like
it compiles to POSIX and Windows API’s, and handles all the stuff
necessary to present a reasonable API using it. Some things like threads
that WASI doesn’t support yet are presumably unimplemented and will
either refuse to compile or panic at runtime. (This is not a great way
of doing it, but nobody in the Rust libs team been able to come up with
a consistently better way, and it’s not because they’re not trying.) The
main difficulty with targeting WASI is system-specific functionality;
for example I tried to build termion
for it but it choked
on ioctl interface stuff. Pure computation, memory allocation and I/O
seems to mostly Just Work.
Performance
This is a complicated topic, so I’m going to simplify it way too much. Part of the promise of wasm is that it can be JIT compiled and executed Fast, at least broadly on par with JVM and CLI code. To a first approximation, those commonly come well within an order of magnitude of native code, more or less, so I’d hope for wasm programs to perform somewhere in the same range: within 10x the performance of the same program in native code.
I’m going to use my favorite stupid benchmark, Fibonacci The Dumb
Way. To me it’s a good not bad easy way to take a
look at the performance of a language implementation in terms
of function calls, branches and integer math.
fn fib(x: u32) -> u32 {
if x < 2 {
1
} else {
fib(x - 1) + fib(x - 2)
}
}
fn main() {
println!("{}", fib(40));
}
fib(40)
is traditional, as it’s the largest round number
a slowish computer can calculate without me getting too impatient. So,
let’s try it out:
# Build native code
cargo build --release
time ./target/release/fib
# 165580141
# 0.35user 0.00system 0:00.35elapsed 100%CPU (0avgtext+0avgdata 1836maxresident)k
# 0inputs+0outputs (0major+100minor)pagefaults 0swaps
# Run a few more times, each time we get 0.35 seconds +/- 0.01ish
# Great, now in wasm
cargo build --release --target wasm32-wasi
time wasmer target/wasm32-wasi/debug/fib.wasm
# 165580141
# 3.67user 0.00system 0:03.67elapsed 99%CPU (0avgtext+0avgdata 16832maxresident)k
# 0inputs+0outputs (0major+2092minor)pagefaults 0swaps
# Run a few more times, each time we get about 3.7 seconds +/- 0.1ish
wasmtime target/wasm32-wasi/debug/fib.wasm
# 165580141
# 2.40user 0.00system 0:02.40elapsed 100%CPU (0avgtext+0avgdata 11392maxresident)k
# 0inputs+8outputs (0major+1709minor)pagefaults 0swaps
# Run a few more times, each time we get about 2.4 seconds +/- 0.1ish
Considering they both use Cranelift for a backend, I’m actually
surprised the performance for wasmtime
and
wasmer
is noticably different. wasmtime
looks
a little faster, but frankly this is so crude I’m willing to call them
the same. They both fall pretty much at the high end of my “JIT’ed
static language” mental field of <= 10x slower than native code, and
considering wasm and Cranelift are both only a couple years old this
performance seems really pretty decent. Also note memory use: native
code, max 1.8 mb max resident memory. The wasm versions have a memory
overhead of 10-15 megabytes. Not trivial, but also not giant. Java runs
the same “benchmark” in 0.66 seconds, for example, but maxes out at 35
mb of memory. Meanwhile the black magic that is LuaJIT does the same
calculation in about 1 second on that machine, using 2.2 mb of max
resident memory.
Start-up overhead is also potentially a thing! JIT’s traditionally
start up slower than native code because they need extra time to, you
know, load and compile the code before running it. This is honestly the
main reason I never want to use bloody Clojure. Sad, but true. I omitted
the first runs from the previous time and memory calculations to try to
remove any cache warm-up cost. wasmer
and
wasmtime
both cache the results of the JIT compilation
automatically. For both runtimes the cached files are opaque blobs, and
they’re named only by hash, so there’s not (currently) much you can do
with them. For wasmer
you can clear the cache with
wasmer cache clean
. wasmtime
doesn’t seem to
have a way to tell it to clean the cache, but does have some settings for
how the cache is managed, and deleting the files by hand just makes
it shrug and re-create them. So, let’s run this program a few more times
and clear the cache between each run and check if we can even see any
overhead at all on something this small:
wasmer
, about 3.85 seconds and 26 MB max resident size, so 0.2 sec and 10 MB JIT overheadwasmtime
, about 2.5 seconds and 15 MB max resident size, so 0.1 sec and 4 MB JIT overhead
So yes, there is measurable overhead to JIT compilation, but it doesn’t seem huge for this tiny program.
One last time though, these are literally the stupidest performance benchmarks possible, so don’t actually make any decisions based off of them. It’s purely to get a vague feel for scale. The only real conclusion worth taking away at the end is “slower than C” and “faster than Python”.
Security
A non-obvious but, in my mind, fundamental part of webassembly is sandboxing. If you execute a program, what is that program is allowed to do? Originally on non-mainframe computers the answer was generally “anything”, which was fine when the internet wasn’t a thing or your system was run by a dedicated sysadmin. If you ran something nasty, the worst thing you could do is hose that one system, and then you either get a young child to fix it or get kicked off the machine by an irate sysadmin and not get let back on. As shared computing systems became more common and sophisticated, various permissions got put in place to limit the amount of damage people could do to others, and these mostly worked okay. Then desktops and the internet happened and suddenly running an unknown program, or even a known-but-compromised program, can and will delete your cat photos, launder money for the Russian mob, and steal credit card numbers from little old ladies in Iowa. Sometimes all at once. And, unless you have more money than time, you do not have a dedicated sysadmin to help you out.
The permissions that modern OS’s use to try to deal with this are all
based on the minicomputer-style shared computer systems these OS’s grew
up on: users, groups, access control lists, and blacklists. User
adam
cannot delete the files for user eve
unless explicitly allowed, and you can set it so that he can’t even see
what those files are. But these are still fundamentally lists of things
that a particular program can not do, and are generally tied to
particular users. Nothing is protecting adam
from running a
malicious program that steals his own credit card number, snoops through
his private info, or talks to other people in his name. By now it’s
obvious that we need more compartmentalized control and that
blacklisting is not the answer; as someone who has worked desktop tech
support before, I think the only feasible solution is to permit
nothing and whitelist the required capabilities.
Make it so you must always specify all things a program is allowed to
do, including “execute code” and “allocate memory”, per program, per
user. This can still have problems, and is more work to manage, but is
far easier to control and far harder to subvert. Android and iOS do
this, crudely, by asking for user permissions to do specific things on a
per-program basis and hiding a lot of the details of the system from
random programs. It’s not perfect, but it’s totally doable.
More broadly, if we’re not building our machines to expect humans to fail then we’re doing it wrong. This is not just a matter of computing, this is a system design problem for any machine that can hurt people. High gantries have handrails, and are made out of materials that are hard to slip on. Internal combustion engines with moving parts are packed away into steel boxes where they can’t casually mangle fingers. Doors that lock themselves must be unlocked from the inside, so that people can escape the building in case of a fire. And programs that run on the multi-billion-transistor supercomputers we all carry around in our pockets every day must only that have access to the data and capabilities that they absolutely require, and that access must be auditable and revocable. However, even OpenBSD’s security systems are fundamentally opt-in, because the basic API was designed in the 70’s and isn’t going to change, and throwing away 50 years of software development to change it all is Not Easy. Wasm and WASI has the potential to fix this by making a clean break that doesn’t require OS support, that works on any OS, and can be adopted incrementally.
This is useful for far more than a layer of protection against
malicious programs though. It’s a way to make programs that literally do
not know or care what computer they are running on. If you explicitly
have to provide a program with a list of all resources it can access,
from memory to files to network sockets, then that changes how you run
multiple programs on a system. The current paradigm is that each program
has random assumptions and dependencies built into it that a sysadmin or
devops engineer needs to understand to make it cooperate with other
programs, even in just such simple ways as “where are config files” and
“what TCP port do I use”. If you have every program explicitly
list these requirements and have them fulfilled from an outside source,
it becomes something that can be automated. You can and must write
programs that don’t CARE what TCP port they use, they use the port the
system hands them. They don’t care whether their config file is
/etc/foo.conf
or /local/etc/foo.conf
, they use
the the file handle the system hands them. You then don’t have to
configure individual programs, you configure systems, and these
systems can be inspected and managed via code. This is a capability that
has only actually become common the last few years, with “serverless”
computing systems like AWS Lambda. Docker tries to emulate this sort of
functionality using existing API’s, and Kubernetes tries to manage it,
but this SHOULD be the operating system’s job. OS functionality like
Linux’s capabilities and OpenBSD’s pledge
are steps in the
right direction that make this possible, and are the primitives upon
which things like Docker are built. But you still have to opt
in to this sort of sandboxing. It is not the
default.
Anyway! Lecture over. So how do we tell these runtimes to sandbox
things? Can’t find any docs on it for wasmer
, so let’s look
at wasmtime
.
wasmtime
has a minimal but functional guide to this here.
Basically, we have a program that copies a file:
Now we try to run it with wasmtime
:
wasmtime copy.wasm src/main.rs copy2.rs
# error opening input: failed to find a preopened file descriptor through which "src/main.rs" could be opened
It says ‘no u’ because we haven’t explicitly told it that it is
allowed to access files anywhere. We can do that using the
--dir
flag:
wasmtime --dir=. copy.wasm src/main.rs copy2.rs
diff src/main.rs copy2.rs
# No output, files are identical
So, that works. Now if we try to read a file outside the given dir,
say using an absolute path, then it tells us to get lost again. There’s
similar functionality for environment variables, and you can also rename
directories. This capability is… not much, to be honest. Upon digging
more, it appears that wasmer
has the exact same
capabilities, and… in fact, produces the exact same error messages when
it tells you permissions are denied. They probably use the same WASI
library under the hood.
What functionality does WASI provide at all, then? What does this high-level, sandboxed pseudo-OS look like? A high-level overview is here, and the API reference is here. Seems like the general list is:
- Command line args
- Environment variables
- Basic clock time stuff
- File stuff
- Secure RNG
- Very simple send-data-through-socket functions
- Very simple process control like
yield()
andexit()
So yeah, not a lot. Looks much like very basic POSIX. Work in progress, I suppose. It’s totally enough to implement, say, a serverless HTTP API service though.
Can we run wasmer
in
wasmer
?
…or wasmer
in wasmtime
or vice versa?
Nope, both require system interface stuff that the WASI API doesn’t provide. JIT compilers be like that, I guess. Someday, hopefully!
Conclusions
You can probably compile and run nethack
for wasm using
WASI, if you work at it a bit and are willing to get your hands dirty
with terminal nonsense. You could definitely run a web
service backend at least partially on it, though some stuff like
threading and database interface might get a little Exciting. More
desktop-y capabilities, media libs, and hardware interfaces are not
there yet, but hopefully will be someday.
Both wasmer
and wasmtime
are perfectly good
runtimes that are both easy to embed (in Rust programs) or easy to use
standalone. They use broadly similar codebases written in Rust.
wasmer
is run by a corporation that also runs the package
manager wapm
, while wasmtime
is run by a
non-profit backed mainly by Mozilla and Fastly. If I had to choose one,
I’d choose wasmtime
; the presentation is much rougher, but
I’d rather have it run by a non-profit than a company, and it’s closer
to the root of the technology with some Notable Names from the Rust
compiler and std lib team on it. From a technical point of view, I’d
prefer a compiler with one backend and some experienced compiler
developers on the team to one that has three backends (while from a
business view, it might make more sense to hedge your bets more). While
it works just fine, from my view wapm
is mostly worth
mentioning in how useful it isn’t, given I never needed or wanted to
actually touch it while writing this, though I’m mainly thinking about
writing standalone programs in Rust so it might have use cases for other
types of systems. Embedding either of these runtimes to have a program
that loads wasm, or expose native functions to a wasm program, both work
fine but might have some Interesting edge cases just as much as any
other FFI system. Wasm has various
other implementations
with various goals, and maybe I’ll investigate them more someday, but
those don’t seem to support WASI (yet?).
Now that I think of it, Google seems conspicuously absent in non-web-browser uses of wasm: they have no standalone WASI runtime, Go does not compile to wasm, there is no serverless hosting platform for it, and I haven’t heard anyone talking about using it for Android even though it might be an interesting replacement for Dalvik, even if only as a research project.
WASI is usable but basic, and needs more momentum behind it. It’s an idea with a ton of potential, but for now it’s still mostly potential. On the other hand, if you’re looking for a good may-become-big-someday project to put your name on, it seems like a great candidate to me.
So, yeah. Webassembly is still a somewhat crude tool, but it has some applications I’m really interested in, and it’s officially usable outside a web browser.