WASM could just be magic

WebAssembly (WASM), is a bytecode format that originally derived from asm.js (a subset of JavaScript that is extremely strictly typed with a very small subset of instructions). The point? It’s fast, near-native fast because it can do away with the overhead of a massive JavaScript interpreter to run the code.

Because WASM is so small, it can be made the target for compilation from a variety of other languages (most notably, Rust, Go, AssemblyScript, C and C++). Much like other byte code-interpreted languages like Java or .Net, just this time in the browser (and server-side).

That’s caused some pretty cool proof-of-concepts where folks have ported Quake and other software straight into the browser through compiling the C-source code into WASM.

One last thing worth noting: WebAssembly does not compile to machine code the same way that other languages do, compilers produce WASM binaries, and these binaries are still loaded and run by a runtime environment.

This is where it gets cool.

Because of this trait, WASM can run anywhere the runtime can run, without the need for re-compiling anything. To whit: If I wanted to make my Go program (e.g. Tyk), work on an ARM CPU (e.g. a Raspberry Pi), then I would need to recompile the source code to target that architecture.

With WASM, like Python or JavaScript, you don’t need that, you just have the WASM binary file that you push through an interpreter to get the functionality you need. It’s portable like python and JS, but fast like C and C++. Cool huh?

Secure By Default

The last thing that’s made WASM such an interesting bit of technology is that it is sand-boxed, that means that when you run WASM code, it runs in a virtual machine that is completely cut off fro the underlying OS. Kind of like a docker container, but much more strict.

Think of it this way: you need to explicitly allow it to access the File System, or the Standard Input/Output in order for it to do anything. It doesn’t even share memory with the rest of the computer, it has it’s own linear memory assigned to each WASM module that cannot access anything outside of it’s boundary.

That means WASM is really safe, in particular for third-party code running in a controlled environment. Unlike Docker Containers, or Virtual Machines, there’s no clear way to escape the sandbox and escalate privilege to the hypervisor because the code runs in a black box.

That’s the promise at least – what’s the reality?

Let’s go write some WASM!

When we started experimenting with WASM at Tyk (the API Management Platform), we felt WASM was best suited for a plugin architecture to add functionality to our Open Source API Gateway since we already support Python and Go amongst others.

As we started looking into it in more detail, we saw that there’s quite a few issues that need to be solved (and have been to some extent by third-party libraries), but mainly, we found working with WASM currently – especially for go devs – presents quite a few hoops to jump through…

Here’s a few things that need to be solved first:

  • WASM functions only support number types (integers and floats) of various sizes, no strings, characters, or complex structures as their input or output parameters
  • WASM functions only support two or more input arguments and one output.
  • Not all WASM compilers are made the same, especially in the world of Go (I’m focusing on this because this is the programming language I am most comfortable in)

The biggest question that comes up here, is: how do I get meaningful data into a WASM function? I can’t just pass a few integers. I need to pass strings, objects, arrays. Real value only comes when I can easily move data into and out of a WASM function as if I were working with any other high-level language.

It is possible, it’s just a real PITA, because you now need to work with WASM’s linear memory model directly and start messing around with memory pointers. Yikes.

Function I/O – The Basics

Here’s how you are expected to work with WASM linear memory:

  1. In your WASM module, you create a fixed-size array
  2. In your WASM module, you create an exported function, that lets your host (your runtime), know the real memory start location of your array.
  3. Your host writes data directly into this array
  4. Your guest reads the data back and works with it
  5. Reverse this process for output

In all of the guides for WASM, there is a great deal of boilerplate and shared assumptions between host and guest to make modules work.

The reality is that in WASM all your code runs without input OR output to the underlying system resources unless you explicitly grant access, to get any data into and out of the module, you need to dance with the linear memory model. That makes things like HTTP Request processing, networking, file system operations etc. really quite complex and burdensome.

Wait, I can’t make HTTP requests!? What the WASI?

Yes, and you can’t write to the file system either. Remember that WASM runs in a sandbox, and that means capabilities like network and file system interfaces are unavailable without imported support.

This is going to change with something known s WebAssembly System Interface (WASI), which will make available system call capabilities like network and file-system access through native interfaces in your guest language. However it’s very early days and only file-system access is available (TCP/IP is well on it’s way though!).

Once WASI is more mature, I expect WASM to explode in terms of adoption.

More Gotcha’s

So, there’s a couple of things I haven’t talked about here, but I need to mention:

  1. WASM support in Go is poor, and to get a WASI-compatible module compiled you actually need to use TinyGo, which produces properly formatted WASM module binaries
  2. TinyGo does not fully implement the Reflect() interfaces of traditional Go, which means using JSON, XML or any other type encoding is literally impossible
  3. Thankfully there is a lightweight version of msgpack available that we can use, however it means we need to use prescriptive I/O types as it relies on per-compilation code generation to generate the encorders and decoders (not ideal).
  4. The lightweight version of msgpack doesn’t support nested map[string]interface{} types, so there’s a limit to what you can and cannot send into and out of a WASM function.
  5. The good news: TinyGo team are busy implementing more Reflect capabilities (so, for example base64 encoding now works).

That’s a massive set of caveats, however it’s important to remember this only really matters in the i/o of the WASM boundary, not within the module itself, you are only limited by what TinyGo can do.

Conclusion

WASM has huge potential, specifically in terms of cloud-native apps, the high-performance combined with the high-portability of a binary once compiled makes it an excellent way to distribute “normalised” applications within managed framework applications such as service mesh or FaaS

What I mean by that is, in Kubernetes the predominant mechanic is to distribute containers that run apps (which could be any *nix distribution, but limited by architecture), and these containers need to be as slim as possible (least amount of possible dependencies / junk in the underlying container layers), in order to ensure a small attack surface and overall size.

What folks are trying to do with slim Linux distributions like Alpine for K8s is to get closer to Uni-kernels – which are specially compiled apps which bundle only the core required dependencies with the app itself – super lightweight and efficient, extremely secure, but technically hard to pull off for a wide range of languages.

What makes containers so appealing is the fact you can bundle apps written in any language, and encapsulate that. Ultimately the “right tool for the job” mentality is what drives micro-service architecture adoption and creates fantastic flexibility.

A possible future…

Now imagine if K8s only managed a single instance of a WASM runtime and then loaded in WASM binaries instead of containers, instead of routing HTTP or TCP connections, it simply pushed messages across the cluster using an in-memory queue (perhaps intercepted at ingress and converted), then this system only needs to scale the WASM modules, and all activity is in-memory. This would enable a much greater process-per-core-per-node ratio than containers, and would massively simplify writing apps because they would be normalised to a message-passing pattern.

Wait a sec, I just described the Beam runtime and Erlang OTP. Oops.

I guess the main difference is that with Erlang you need to use, well,Erlang. Whereas with WASM you can use any guest language.

We can take this application network even further, what if when cutting your code in your preferred language, you didn’t even need to worry about the convert-to-WASM step? This step could actually be handled at the CI/CD stage when pushing to production, and the only thing the developer will need to ensure is to adhere to some syntactic sugar (perhaps using decorators or semi-structured comments).

In this utopian future:

  • Developers don’t need to think about architecture anymore
  • Structuring code becomes mostly templated (arguably, it already is for API-first apps) – this actually could make training and up-skilling back-end developers much faster and cheaper
  • Mixed-tenancy architectures become tenable even for applications that traditionally need more secure siloed approaches
  • Cost-per-cycle of running a highly-scaled application becomes cheaper
  • Overall architecture becomes simpler

Admittedly, this style of architecture is essentially Functions-as-a-Service (FaaS), and I guess where AWS, Azure and Google want folks to head with their development patterns.

What do you think – Will WASM replace containers? Will WASM stifle or increase innovation? How do you think WASM will be used in the near future?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s