Home / Blog Menu ↓

Learning WebAssembly, part 1

I’m having so much fun learning Elixir that I’ve decided to do a similar thing with web technologies I haven’t used yet. Similar to the 12in23 posts, these will be long and informal and more stream-of-consciousness, and I will say “interesting” and “looks like” far too often.

First up: WebAssembly. Other things I’m looking forward to trying out afterwards: WebGPU, WebRTC, WebGL (I’ve used Three.js on several projects but haven’t used WebGL directly if I recall correctly), WebSockets, service workers, web components, and a number of smaller things (primarily CSS features I haven’t yet used).

So, Wasm. I’ve read bits and pieces about it over the years but haven’t yet actually tried it on anything. I’m going to start arbitrarily with AssemblyScript. Later on I’ll use Rust, and I want to try writing the Wasm text format manually, too.

To begin, I’m going through the AssemblyScript Book.

Getting started

I generated a new AS project with asinit and have been exploring the files it produced, starting with release.js (to get a feel for how the Wasm code gets pulled in). This sample project just adds two numbers together, by the way. The actual AssemblyScript source is straightforward.

In release.js, I initially wasn’t sure what compile meant (in this context) since the example is compiling release.wasm which is already a binary. Looked up the compile page, which explained that the Wasm binary needs to be compiled into a Module, which the MDN key concepts page tells me is executable machine code. And it’s stateless. Okay, that makes sense.

I also wasn’t sure what instantiate did in this particular context. Found the instantiate page, which said it turns a Module into a stateful Instance. That also makes sense now. I imagine the reason one might want to instantiate a Wasm module more than once would be splitting the work up across workers, but I haven’t verified that. Also, looks like instantiateStreaming is now preferred, which simplifies things a little, so you don’t need both compile and instantiate. (That won’t matter for this AssemblyScript experimentation since I’ll be writing AssemblyScript and not finagling with the final JS unless I really can’t resist.)

Looking now at release.wat, because I’m intrigued and I also sometimes like to go in blind and figure things out on my own.

I completely forgot that the Wasm text format is Lispy.

Hmm, I see the $i32_i32_=>_i32 type defined. (By the way, seeing a type defined in an assembly language that looks like Lisp is kind of trippy but also quite cool.) I don’t see that type being used anywhere, though. Must be implicit. I imagine it applies to the exported function. If there’s more than one exported function, maybe they apply in order?

There’s a memory being exported alongside the add function. Interesting. I imagine this gives access to the memory the Wasm script is being executed in (the sandbox), but I’m not entirely sure why — what the host would use it for, I mean. Something to look into when I get back to the text format later.

The function definition itself looks fairly straightforward. Arguments and result type followed by the body of the function. Based on the i32.add instruction not taking any arguments, I’m guessing local.get pushes things onto a stack which i32.add then pops from.

I’m excited to dive deeper into the text format! But back to AssemblyScript proper.

Using the compiler

Okay, looks like memory can be both imported and exported, and the size can be specified as well. Interesting that this is set here (where the AssemblyScript is being compiled into a Wasm binary) rather than in the host when the Wasm is instantiated. My mental model of WebAssembly is more patchy than I realized — it’s been a while since I read an intro — but I’m having fun figuring things out archaeologically right now, so I’ll keep going like this for a bit longer.

Looks like there are several small runtimes that can be compiled in, some with garbage collection. Interesting, didn’t know that. (Looks like it’s a WIP.) And there are threads now. Wow.

Ooh, the compiler API can be used programmatically. I guess I shouldn’t be surprised.

Looks like it’s still just low-level data types that can be passed across module boundaries. Nice that AssemblyScript generates bindings for strings, arrays, and objects, though. That makes it far more usable.

Transforms are intriguing. Probably not useful for most projects, but still intriguing.

I’m going to stop here for this post, to try to keep things from going on too long in any given post. Looking forward to working through the concepts chapter next!