Blog

World Wide Wasm: Low Hanging Fruit

20 Jul, 2023
Xebia Background Header Wave

It seems as though Wasm, or WebAssembly if you are so inclined, is starting to assert its presence beyond the browser more and more each day. The inaugural Wasm I/O conference was hosted in Barcelona, it has become significant enough for the Cloud Native Computing Foundation to dedicate a day to it at CloudNativeCon, and Docker has released their second technical preview for the holy union of containers and Wasm. Wasm even received an honourable mention at Google I/O. All of this has happened within the first half of this year alone.

It is a seemingly good omen then to those who have been keeping an eye on its development. Some may have begun entertaining the notion that Wasm could indeed be ushered in as part of a next wave of computing paradigms. I, for one, am certainly convinced that Wasm has something to offer, and that it is likely to succeed.

Avoiding Magpie Driven Development

That’s great and all, but is it really indicative of anything substantial or worth our limited attention span? In the tech industry we tend to blindly focus on shiny new technologies with fleeting regard for its implications, well at first anyway. So then, the onus falls on us to attempt to explore and surface the supposed benefits proffered by Wasm. After all, if Wasm does not provide any compelling value, it is just another shiny bauble to add to an ever-growing pile.

Naturally, this is easier said than done since Wasm is the hub of a sprawling ecosystem. The Wasm ecosystem runs the gammut of computing, from client and server applications to embedded devices and edge computing. In fact, that is sort of the point. Wasm really does try to get in the middle of everything – where it makes sense – on purpose.

What exacerbates the sense of chaos is the fact that everything is in flux and only a few foundational constructs are considered stable. This may be immediately offputting for some, but even in its current state there is a case to be made that Wasm is a practical choice in certain scenarios. This is especially true considering that server-side Wasm has started seeing more and more less-than-tentative declarations of its use in production.

All of this makes it quite daunting to explore. My aim is to be a guide of sorts through a growing series of use cases that places the spotlight on Wasm.

However, an obligatory disclaimer is in order. To say that the current state of Wasm (not pertaining to the browser) is rough around the edges, is quite an understatement. It would serve the reader well not to lose traction on the specifics, but rather to focus on the elements of Wasm that are put forth as being beneficial or solving a problem.

The Low Hanging Fruit

To start off with, I have opted to take a look at the low hanging fruit, or in other words, a use case where Wasm is a no-brainer. Of course, the real low hanging fruit would be the original reason for Wasm: code execution in a browser environment. However, despite the relative lack of attention, Wasm in the browser has already proven itself and I consider it to be sufficiently covered as a topic.

No, instead we shall turn to Wasm on the server, since that has been the reason for an uptick in recent buzz and development activity. The no-brainer equivalent on the server is (ironically) serverless computing, specifically FaaS.

What Makes It a No-Brainer?

Well, if we consider the stated best practices for serverless code execution by most vendors, it typically boils down to the following:

  • Reduce the size of your deployment package
  • Reduce the amount of unnecessary code execution
  • Carefully consider the dependencies which you include in your package

The reasons for the above points are fairly transparent upon inspection. These points are there to benefit both the platform providers as well as the platform consumers.

Reducing the package size and the amount of unnecessary code execution both result in the same benefit. A smaller package (often enforced by hard limits) allows for a diminished operational overhead for providers. Similarly, a reduction in unecessary code execution means that the operational resources are being efficiently utilised and not tied up executing code that yields little value. This is especially true if you consider the scale at which the serverless platform providers typically operate.

For platform consumers, a smaller package translates into a better end-user experience and cost savings, since a smaller package implies less code and therefore correlates with faster execution. The same is true for unnecessary code execution. Less time spent executing means a faster and less costly result.

Lastly, careful consideration of package dependencies also translates into a reduction of package size, and by extension unnecessary code execution. This is not all, however, since another aspect to consider is security. Platform providers often go to great lengths to ensure that the code provided by the platform consumers is run in a strictly contained environment, otherwise there are real security concerns for both parties.

Ok, let’s dispense with the setup. With the above in-mind we now take a look at what Wasm is. Wasm can be summarised as a low-level code format that is:

  • Fast
  • Secure
  • Compact
  • Portable

Like I said, a no-brainer.

What Does Serverless Wasm Look Like?

The platform provider has a few ways that they could go about hosting consumer code, which I intend to explore in a later post. For now, what seems to be the sanctioned way to host a unit of consumer Wasm code, known as a module, is by utilising WASI. WASI is essentially analogous to the syscalls made available by an opertating system (OS) kernel. However, since a Wasm module can run conceptually anywhere, such calls to the “OS” hosting the Wasm module have to keep portability in-mind. This is where the Component Model comes in. In short, the designers behind WASI opted to deviate from the POSIX method of using ABIs and instead use an Interface Definition Language (IDL) to define the interface between a Wasm module and its host. It is known as the Wasm Interface Type (WIT) format.

All of this is only mentioned to satisfy curiosity, and in reality developers that would like to run their Wasm module on a serverless platform would only have to concern themselves with the following:

  1. Obtain the WIT files for the target platform.
  2. Use tooling to generate the relevant bindings for your supported programming language of choice.
  3. Add the relevant business logic.
  4. Compile the Wasm module.
  5. Push the Wasm module to the target platform.

For those interested in seeing a working example written in Rust, feel free to continue reading through the subsequent section. If you get the gist or are, perhaps, sick of Rust shilling, feel free to skip the working example.

A Working Example

1. Obtain the WIT files for the target platform

This is a somewhat arbitrary example, but a WIT file for a hypothetical serverless platform could look akin to the following:

// ./wit/example-corp-faas-platform.wit

// Package identifier
package example-corp:faas-platform@0.0.1

// A 'world' encloses a particular scope of imports and exports
world example-corp-faas-platform {
  // Import and use a WASI interface type
  // In this case use the WASI `datetime` type
  use wasi:clocks/wall-clock.{datetime}

  // Type aliasing
  type event-version = string
  type response-status = u16

  // Enumerated type representing all possible types of events
  enum event-type {
    object-added,
    object-removed,
    object-updated,
    // any other event types...
  }

  // A struct equivalent type representing a single event record
  record handler-event {
    event-type: event-type,
    event-timestamp: datetime,
    event-version: event-version,
    // any other relevant event attributes...
  }

  // Similar to an enumerated type, but may carry additional data
  variant handler-error {
    timeout,
    unexpected(string),
    unsupported(event-type),
    // any other errors that make sense...
  }

  // Exports refer to items made available to a Wasm module host.
  // In this case it is the function to invoke when an event comes in on the platform.
  export handle: func(event: handler-event) -> result<_, handler-error>
}

After obtaining the WIT definitions, ensure that the relevant dependency WITs are installed. Given the following deps.toml file:

# ./wit/deps.toml

clocks = "https://github.com/WebAssembly/wasi-clocks/archive/main.tar.gz"

Install the WIT dependecies using wit-deps-cli:

wit-deps update

2. Generate bindings for your supported programming language of choice

The example will be written in Rust due to its greater (although still unstable and limited) support for Wasm at this particular point in time. C, C++ and Go are all viable options as well.

Using the first-class Rust support from wit-bindgen, generate the bindings for the platform types defined in the WIT file:

// ./src/lib.rs

// Use a procedural macro to generate bindings for the `world` specified in
// the WIT file (`./wit/example-corp-faas-platform.wit`)
wit_bindgen::generate!("example-corp-faas-platform");

// Code implementing business logic using bindings to follow...

3. Add the relevant business logic

Next, implement the desired business logic after some boilerplate code:

wit_bindgen::generate!("example-corp-faas-platform");

// Define a custom type and implement the generated `ExampleCorpFaasPlatform` trait
// for it, which represents implementing all the necessary exported interfaces
struct Component;

impl ExampleCorpFaasPlatform for Component {
    fn handle(event: HandlerEvent) -> Result<(), HandlerError> {
        match &event.event_type {
            EventType::ObjectAdded => {
                // Handle `object-added` event
                // ...
                Ok(())
            },
            EventType::ObjectRemoved => {
                // Handle `object-removed` event
                // ...
                Ok(())
            },
            _ => Err(HandlerError::Unsupported(event.event_type)),
        }
    }
}

// Export the custom type for the Wasm module host to hook into
export_example_corp_faas_platform!(Component);

4. Compile the Wasm module

Compiling a Wasm module with Rust is fairly simple. The first step is to ensure that the wasm32-wasi target toolchain is added to the Rust compiler:

rustup target add wasm32-wasi

Next, ensure that the Rust project is to be compiled as a dynamic library by adding the following to the project Cargo.toml:

# ./Cargo.toml

[lib]
crate-type = ["cdylib"]

Then, ensure that wit-bindgen has been added as a dependency:

cargo add --git https://github.com/bytecodealliance/wit-bindgen wit-bindgen

All of the above is once-off configuration and once it has been done, compiling a Component Model compatible Wasm module is as straightforward as running the following commands for all subsequent builds (one of which requires the installation of wasm-tools):

cargo build --target wasm32-wasi --release
wasm-tools component new ./target/wasm32-wasi/release/my_project.wasm -o ./my_component.wasm

5. Push the Wasm module to the target platform

If all went well, then a Component Model compatible Wasm module should have been created. In the above case it may be found under ./my_component.wasm for upload.

A Few Remarks

It is worth noting a few things on what the development process currently looks like.

First, it really bares mentioning again that the tooling is not yet finalised, and that more work can and ought to be done. For instance, wit-bindgen provides first-class support for Rust projects, as is shown in step 2 of the working example with the use of the wit_bindgen::generate! macro. What it means is that some languages have it better than others until the relevant communities devlop idiomatic tooling for each language. Until then, one may have to use tools like wit-bindgen via the CLI to generate the bindings in an out-of-band fashion.

Even with better support, the process itself is not quite frictionless. There are too many things to configure before one can get going. The WIT dependency management, binding generation and component creation (via wasm-tools) should ideally be folded into a toolchain. But again, it is still early days. It is likely to improve if the current rate of development and interest persists.

While the specifics are still in flux, I believe that the overall process will still resemble the above working example. I am also fairly confident that the tooling and support will improve to reduce the amount of friction.

In my opinion, in which I am sure I am not alone, the Component Model is the cornerstone of the entire Wasm enterprise, since it can make or break its perception and adoption. For better, or worse, this approach will likely form the foundation for the vast majority of all potential future Wasm modules. Needless to say, we will be seeing more of the Component Model as it develops further.

Ok, So What?

That may, or may not, have been quite a lot to take in. In the spirit of the introduction to this post, the question one ought to ask is: what is it that has been gained and/or lost here?

Less Baggage

Starting with on a positive note, the claim is that the Wasm unit of execution (module) is smaller in size compared to a traditional containerised image. This does indeed appear to be the case. A typical container image can be about tens of thousands of megabytes while a Wasm module is substantially smaller. While one can, and certainly should, optimise containers to be downsized where possible, it still gets held back by some overhead, such as the filesystem. This remains true even when going so far as to compile a native executable for a scratch container. Wasm does not have such baggage, which is rather fortunate for serverless use cases.

The downside, however, is that as far as developers are concerned, Wasm is not quite abstracted away from them. I was tempted to point out that compared to simply offloading an application into a container image and mostly living in blissful ignorance, developers are directly confronted with Wasm and related concepts. However, this is true for practically all serverless applications as they tend to require some framework or depenedency to serve as an entrypoint, so Wasm cannot lose too many points here. Yet, it still implies learning a new set of technologies and the nuances thereof.

Off to the Races

It is claimed that Wasm startup times are an order of magnitude faster than that of containers. The comparison is on the order of milliseconds versus seconds. This is not even considering some of the optimisations that could be performed. For instance, since Wasm modules are executed as conceptual virtual machines, one could “pre-bake” a module by spinning it up, doing some initialisation and then snapshotting it as the starting point for all future invocations. Think AWS SnapStart for Java, but with a potentially far simpler model and smaller overall snapshot size. Honestly, there is little reason to doubt the claim of faster Wasm startup times and if it holds up, it would indeed be a boon for serverless applications.

The startup times do seem quite snappy, yet all that relative speed is partially due to Wasm’s quite simplistic model. Depending on who one asks, such simplicity is a good thing and I would tend to agree. However, I fear that as Wasm grows it will lose some of that simplicity, negating the performance gains that come along with it. Wasm is not yet mature and is pretty much guaranteed to grow, hopefully just not in complexity.

The Lowest Common Denominator

Wasm was designed to be portable. It is a task at which Wasm excels. A few years ago one might not have concerned oneself much with portability, since on the server side one could rely on containerization. However, since fairly recently, ARM-based architectures have become more popular and containerisation has become less straight-forward as a result. In the world of Wasm, only the custodians of the Wasm runtimes need truly concern themselves with the underlying architecture. The average developer targetting Wasm is, once again, liberated from such responsibilities.

Naturally, nothing comes for free and portability is no exception. In order to achieve a great degree of portability, one typically has to cater for the lowest common denominator. The result of this is that Wasm is unlikely to behave precisely the same on every combination of architecture and stacks. It remains to be seen whether any major problems arise due to this, however, Wasm can consequentially be deemed a leaky abstraction. Like with the case of making itself known, developers will need to be mindful of the fact that Wasm is being utilised somewhere in the stack, especially on serverless platforms where there is little to no control.

Access Denied

Then there is the sandboxing aspect. Each Wasm module invocation is isolated by design and is intended to explicitly declare what actions it would like to perform to interact with the world outside of the sandbox, known as capabilities. What this effectively means is that some of the current heavy-handed methods of sandboxing can, theoretically, be forgone. This would drastically simplify processes in the serverless space.

As an early introduction to Wasm has mentioned, sandboxing is (unsurprisingly) not a panacea. Wasm hosts could still introduce vulnerabilities through the functions they export. Moreover, Wasm has yet to be thoroughly put through its paces in terms of security, so only time will tell whether it really is secure. That is not to say that it is being taken for granted, as Wasm runtimes often attempt to rigorously ensure that security is upheld wherever possible.

Of course, security is pretty complex to say the least and there are always multiple angles to consider. Most of the security effort of Wasm is concerned with malicious code breaking out beyond the confines of the sandbox. However, consider the case where the host is compromised. While the hosted module is unable to see beyond its sandbox, the host certainly can peer into each sandbox. This is certainly not the reponsibility of platform consumers, but it should not breed complacency.

Donde Esta La Bibleoteca

Coincidentally, the above point of potentially getting rid of some rather cumbersome methods of sandboxing ties in with another benefit of Wasm. Wasm is polyglot. This means that in addition to being able to use any language that can target Wasm, which in an of itself is a welcome prospect, one could also use any version of a language as well. Gone would be the days where the likes of AWS, Azure and Google Cloud have a strict list of supported versions of NodeJS, Python, Go, etc. due to seemingly arbitrary reasons.

Support for specific languages, however, becomes the responsibility of the broader Wasm community. In theory all versions of a language can be used, but in reality all nuances have to be addressed when crossing the Wasm boundary. This will most likely stress the Wasm Component Model the most, since generating bindings for various versions of NodeJS, for instance, may well be different. Granted, this may not seem too aggregious, but it is yet another thing developers may need to be aware of.

Waste Not, Want Not

Considering all of the above, I believe there is another important benefit to address. Wasm is kinder to the environment. Platform providers have to commit less energy and fewer resources – especially if the use of ARM continues to take off – all while being sandboxed for easier bin-packing. This should result in a substantially reduced operational overhead.

Of course, if we really did wish to fully commit to this we would compile native binaries for less power-hungry architectures, like ARM. Wasm is not really intended to compete with the speed and size of a native, ahead-of-time (AOT) compiled binary. Yet, Wasm can be AOT compiled while keeping the sandboxing intact. Such a compilation may be quite lossy. It should be noted that if this technique were to be employed opaquely, such as, for the sake of this argument, on serverless platforms, then debugging would be rather unpleasant if not outright hair-pullingly frustrasting.

Conclusion

Wasm is a really good fit for serverless FaaS use cases. It checks all the right boxes. Wasm is inherently sandboxed, small and can be started rapidly. It also comes with the added bonus of being portable, polyglot and an overall improvement regarding our impact on the environment.

It does, however, come at some cost. Wasm is relatively obtrusive and has to cater for the lowest common denominator. As of now, Wasm owes some of its speed to its simplistic model, yet it still requires further development and may introduce performance dampening complexity. Moreover, there exist the obvious downsides due to an immature technology. There is limited support, a lack of tooling and instability.

Putting aside the immaturity (certainly a big deal for some), the pros do decisively outweigh the cons. The benefits translate into tangible gains for all parties, while the caveats are primarily about a degraded developer experience that is extremely likely to improve.

The one exception, however, is security. It may seem that the virtual machine sandbox is completely safe, but only given enough time will we be able to assess this claim thoroughly.

PS

I would like to make it clear that Wasm + serverless makes obvious sense and is (in my humble opinion) rather uninteresting. It is a narrow application of Wasm, since it is mainly about optimising for serverless environments. That is to say it is ultimately about reducing cost. Much of what I have said has already been said numerous times elsewhere. As the title of the post implies, that is the point.

I am hoping, however, that this post inspired some level of critical thinking. If one was to take the stated aims of Wasm at face value, then I am relatively certain any truly invested reader would have piereced the “shiny tech” armor. The discernible reader would have thought up fairly interesting use cases before even reaching the end of this post. I believe that, as others will undoubtedly demonstrate, Wasm has more potential for other applications and that is, ultimately, what evokes a greater degree of genuine ethusiasm.

Jan-Justin van Tonder
Cool, calm, contemplative and a constant tinkerer. I am always on the lookout for new, strange and wonderful challenges to overcome. After chasing ever-expanding horizons, I unwind by seeking out truly compelling stories, be it between the pages of a book or rendered on a screen.
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts