You’ve likely heard about Blazor, .NET’s frontend framework for writing web applications. Blazor lets you use your existing C# skills to build full-stack applications, from client to server. One way to construct Blazor applications is to compile your frontend into a WebAssembly (WASM) file and deploy and run your app in a web browser. The Blazor WASM approach can help you build client experiences similar to native desktop applications with a native performance profile.
But what about WebAssembly on the server? In this post, we’ll see the benefits of using WASM outside the browser and its potential as a transformative technology on the server.
WebAssembly is a low-level assembly-like language with a binary format that runs in modern web browsers. As a result, WebAssembly programs can perform at near-native speeds allowing for new and exciting applications for web clients from desktop-like apps, emulators, high-performance video games, photo editors, and more. The additional benefit of WebAssembly is it can be run either as an extra add-on via service workers, enhancing or replacing the existing JavaScript experience on the client.
The designers of WebAssembly always meant it to run alongside the current web model. Developers unfamiliar with JavaScript can choose from a wide range of languages to write WebAssembly, including C, C++, C#, Go, Python and Rust, with many more languages looking at targeting WASM as an artifact of compilation. For .NET developers, you can think of the .wasm
file extension as a .dll
or .exe
format. The file format is a standard, and WebAssembly System Interface (WASI) compliant runtime will be able to consume them.
One such runtime we’ll be exploring in this post is Wasmtime.
Developed by the Bytecode Alliance, Wasmtime is a runtime for WebAssembly, allowing you to consume .wasm
files and run them on Windows, macOS, or Linux systems outside a web client. Wasmtime is a fast, secure, and standards-compliant runtime for WebAssembly, and supports the WebAssembly System Interface (WASI).
The most crucial feature of Wasmtime is its ability to interact with the native operating system, allowing for configurable access to system resources. Resources include disk access, TCP listeners, hardware input interfaces, and more.
In addition to running standalone .wasm
files, you can use Wasmtime inside your applications to consume third-party dependencies. The portability of the WASM format opens up a world where you can have native interop with a standard format across all languages.
Wasmtime also supports debugging using popular native debugging tools like GDB or LLDB, which many of the IntelliJ-family products already support. The debugging experience depends on your technology stack’s build tools.
Speaking of build tools, let’s build our first WASM server application, but first, let’s install Wasmtime.
Wasmtime is a CLI executable, so it’s straightforward to install on any environment. The first step is to visit the official Bytecode Alliance website. You can also just run the following command.
curl https://wasmtime.dev/install.sh -sSf | bash
Windows users can do the same using the Windows Subsystem for Linux (WSL), or grab the installer from the Wasmtime releases page.
You can check you’ve successfully installed Wasmtime by running the command wasmtime –version
from the command line.
> wasmtime --version wasmtime-cli 1.0.1
Note: You’ll need the latest version of .NET 7 SDK installed
Our goal in this section is to take a C# Console application and compile it to a .wasm
file. The solution will be a self-contained application that won’t need unique WASI resources like TCP listeners, file access, or environment variables.
First, let’s start with a brand new Console Application project. From JetBrains Rider’s new solution window, select the “Console Application” template and call it “HelloWasm” or whatever you’d like.
Once the solution is ready, you’ll need to add the “Wasi.Sdk” NuGet package to your console project. If you don’t see the package, be sure that you have the Prerelease checkbox checked.
You’re ready to compile the console application into a .wasm
artifact. Before doing so, change the line in Program.cs
to the following:
Console.WriteLine("Hello, Wasmtime!");
Building your project, you will see references to bundling System libraries commonly used in .NET applications. Remember that WASM is native byte code. The .NET runtime must be packaged and shipped as part of your artifact. This is not any different than Blazor WASM deployments for a web client.
Now, let’s run our WASM file using Wasmtime. In Rider, you’ll use the Run Anything dialog (Ctrl+Ctrl) to run the following command:
wasmtime ./HelloWasm/bin/Debug/net7.0/HelloWasm.wasm
When successful, you should see your Console application’s output.
Awesome! You just wrote your first WASM-targeted .NET application. Now you could deploy the .wasm
file to any host that supports WebAssmebly.
While it’s certainly possible to consume a .wasm
file in .NET, it is a bit more tedious than the plug-and-play experience of a NuGet package. Currently, there is a Wasmruntime NuGet package that contains low-level APIs for dealing with the elements of a host. These elements include an Engine
, Module
, Linker
, Store
, and more. They allow you to define your custom host, including low-level system calls and what calling them may do on a WASI-compliant system. For more information, I recommend reading excellent Mozilla documentation to understand how WASM interfaces with a host.
I would not recommend most developers attempt to write their host; instead, the community should wait for the Byte Code Alliance to create a safe-by-default host.
Let’s look at an example where we use the WebAssembly Text (WAT) format to link a C# function to WebAssembly. WAT is a human-readable and editable format representing the WASM binary format.
using Wasmtime; using var engine = new Engine(); using var module = Module.FromText( engine, "hello", "(module (func $hello (import \"\" \"hello\")) (func (export \"run\") (call $hello)))" ); using var linker = new Linker(engine); using var store = new Store(engine); linker.Define( "", "hello", Function.FromCallback(store, () => Console.WriteLine("Hello from C#!")) ); var instance = linker.Instantiate(store, module); var run = instance.GetAction("run")!; run();
The critical thing to note in this example is that our WebAssembly module uses a reference to $hello
. Since our C# application manages our WebAssembly context, we can create a definition for $hello
using the method Function.FromCallback
. In C#, we ask the linker, which now contains both our .wat
definition and our new implementation of $hello
for the run
method. All that’s left is to invoke our Action
and see the results.
Hello from C#!
While it may seem trivial, we’ll be able to run and consume WASM in our .NET applications. That opens up possibilities for solving problems using solutions from other ecosystems.
While this post has been relatively optimistic about the prospect of WASM, there are limitations to the technology that folks should be aware of, and they will vary based on the WASM host.
The first significant limitation is threading. So far, with my testing, Wasmtime only has access to a single thread for execution. Thread limits are not entirely a deal-breaker, but you’ll need to rewrite some of your solutions to reduce the need for threading. This limitation becomes apparent when using an API that involves Thread
or Task.Delay
, which will halt your running process or terminate it catastrophically. However, threading may not be an issue in the near future as .NET adds multi-threaded support to WASM. Wasmtime also enables multiple threads with an experimental flag, but your technology stack will need to take advantage of that feature.
The .NET runtime targeting WASM is the same as Blazor WASM, which is to say it has limits. APIs will not work based on a lack of support from the current WASM host. Missing APIs could limit your ability to solve specific problems. The APIs are still in active development and could change based on features added to Wasmtime and other WASI runtimes.
Another issue I’ve found is there is currently no outward socket support. Lack of socket support limits a WASM application’s ability to communicate with dependencies like a database or web service. Discussions of a Socket specification will resolve this issue, allowing for more robust WASM apps. In addition, this should enable .NET developers to use data access tools like Entity Framework Core or Dapper with few issues.
Tooling is another concern; vendors must catch up to the experiences .NET developers have gotten used to developing .NET applications. Luckily for developers, Wasmruntime uses standard debugging tools like LLDB and GDB, making integrating them into existing tools much more manageable.
Infrastructure will play an essential role in how you build your WASM solutions. The strength of WASM comes from the ability to start and dispose of executing modules quickly in the vein of “microservices” or “functions”. However, a different architectural approach will require you to rethink your existing applications’ orchestration and units of work. In addition, you and your team may not have the bandwidth to deal with the technical and conceptual challenges simultaneously.
How does a modern WASM-powered application look when deployed? Some folks think it looks like functions as a service (FaaS), while others see it as a replacement for containers in a Kubernetes cluster, and both are perfectly valid. Luckily, many new WASM hosts are springing up along with reliable cloud providers AWS, Microsoft Azure, and Cloudflare, adding options to their ever-expanding list of services.
We’ve reached the speculative part of the post, where we get to imagine the future of WASM and how it may transform your development experience, from writing code to deploying a solution. So let’s start with the process of development.
If you’ve developed any Blazor WASM application, you’ll likely realize that little feels different while writing code. After all, it’s C#! The debugging experience may be slightly different, as you now deal with a completely different host and runtime, specifically the web browser. However, writing solutions to target WASM will feel very similar to Blazor WASM, and I expect the experience to improve as tooling catches up. Switching to WASM apps will be easier for most developers than switching to a new programming paradigm.
The frameworks targeting WASM will likely change, as you will want to reconsider how and when functions are executing. For example, Fermyon released a .NET Spin SDK specifically for their hosting platform that targets WASM. Let’s look at how you might write a function given their SDK.
using System.Net; using Fermyon.Spin.Sdk; namespace Microservice; public static class Handler { [HttpHandler] public static HttpResponse HandleHttpRequest(HttpRequest request) { return new HttpResponse { StatusCode = HttpStatusCode.OK, BodyAsString = "Hello from .NET", }; } }
This example begs questions. For example, how do you transition from ASP.NET Core Minimal Endpoints or ASP.NET Core MVC to this new approach? Does ASP.NET Core need to rethink how to compile existing applications into deployable units? I expect a lot of conversations and strategies to emerge as developers adopt WASM.
We could see WASM replace most instances of containerization, especially when WASM can package both runtimes and the applications that depend on them in one portable package. Advantages you could hope to see: Reduced CPU and memory usage, decreased cold-boot times, reduced hosting bills, and more significant economies of scale.
These .wasm
files can also be considerably smaller as they don’t have the same need for layers found in images built on entire operating systems. The portability and efficiency of WASM hosts will lead to more hosting options from vendors, new and old.
Finally, the most exciting prospect of WASM on the server is the fulfillment of an agnostic cloud environment, where you, as the developer, no longer have to worry about regions. WASM can be deployed simultaneously to many global regions beyond cloud vendors’ limited and congested regions. WASM can start closest to your users and provide them with the fastest experience possible regardless of their country of origin. This democratization of user experience is most exciting for folks delivering global-scale applications.
In this post, we delved into WebAssembly and the efforts to run it on the server using Wasmtime. While it’s still relatively new, the promise of WASM on the server is exciting, and .NET is at the forefront of giving developers more hosting options. As tooling and development experience improves, so will the solutions developers deliver to customers. As you’ve seen, the experience even now is good.
There are limitations with WASM, but like all technologies, the community will push solutions forward, and progress will occur. It’s still important to be careful when choosing cutting-edge technologies as solutions, as you’ll likely be some of the first and only folks experiencing those issues.
That said, I’m cautiously optimistic about the future of WASM, and I hope you found something in this article that has sparked your curiosity to explore the topic further.