WebAssembly Beyond The Browser: Running Wasm In .NET Core Applications With WASI & Wasmtime

When people talk about WebAssembly and .NET Core these days, they immediately think about Blazor WebAssembly. But there is more to it. Actually, Blazor WebAssembly currently does not run our code as a WebAssembly (Wasm) module, but rather runs the .NET CLR (or a Mono version of it) as a Wasm module and our .NET code is run as plain old .NET assemblies on top of it. But there is indeed the need for executing (native) code as WebAssembly modules in the browser, and in .NET Core applications as well. In this article, I show how you can achieve running Wasm code in .NET Core, with a most likely future-proof approach.

In this article:

Christian Weyer
Christian Weyer is co-founder and CTO of Thinktecture. He's been creating software for more than two decades.

Version information

  • Azure Functions SDK: 3.0.3
  • Wasmtime: 0.19.0-preview1

The WebAssembly Universe: a Code Execution Platform

WebAssembly has been introduced into major browsers some years ago (beginning at the end of 2017). With it, you can take more or less arbitrary source code and languages and compile it into a binary bytecode, usually ending in a .wasm file. People soon realized that it would be a great idea to not interpret WebAssembly as WebBrowswerAssembly :-). The notion of a common, very lightweight runtime that can uniformly run any code on any platform, is tempting. So, under the lead of Mozilla, the WebAssembly experts started to work on a specification that enables a uniform hosting, execution, and usage of WebAssembly also outside of the browser.

WASI

The WebAssembly system interface (WASI) was born. The perfect intro to WASI can be found in the GitHub repo of the Wasmtime project:

It is an API […] that provides access to several operating-system-like features, including files and filesystems, Berkeley sockets, clocks, and random numbers, that we will be proposing for standardization. It is designed to be independent of browsers, so it does not depend on Web APIs or JS, and is not limited by the need to be compatible with JS. And it has integrated capability-based security, so it extends WebAssembly’s characteristic sandboxing to include I/O.

That really sounds good and very promising to me. With WASI we can have a uniform interface for the browser and any other execution environments to run WebAssembly code. This indeed sounds a little like a potential base for having very lightweight sandboxes safely executing code – something that may be an interesting alternative to Docker, someday.

Wasmtime

The Wasmtime project from the Bytecode Alliance is an effort to create a WASI-compliant stand-alone runtime for WebAssembly: https://wasmtime.dev/. This means that we can run Wasm modules that adhere to WASI everywhere, either via a CLI or via embedding it into other applications.

Currently, Wasmtime supports

  • Rust
  • C
  • Python
  • .NET
  • Go
  • Bash

There we have it: .NET (Core)!

A Simple Sample: Fibonacci, All the Things!

In order to get a clear picture of what is possible and also of what is needed to run WebAssembly code in a .NET Core application, let us create a sample application. Suppose, we have some really sophisticated C-based algorithms. They are proven, they are mature, they are blazingly fast. Indeed, we are dealing with this situation in customer projects from time to time.

However, I cannot expose those intellectual property assets in an online article. Therefore, let us use a C implementation of finding the n-th Fibonacci number for demonstration purposes.

Please have a look at the following quite simplistic implementation:

				
					int fib(int n) {
  if(n <= 0){
    return 0;
  }

  int i, t, a = 0, b = 1;

  for (i = 1; i < n; i++) {
    t = a + b;
    a = b;
    b = t;
  }
  
  return b;
}
				
			

The beginning of the Fibonacci sequence is: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, … If we pass a value of 4 as n into the function, the result will be 2. You get it 🙂

Compiling C/C++ Code to WebAssembly With Emscripten

There are a number of compiler tools able to compile C or C++ code to WebAssembly. One of the most commonly used is Emscripten. Our C code above is simple enough for explaining the WebAssembly use case, and the only thing we need to do is decorate the function with the EMSCRIPTEN_KEEPALIVE macro.

				
					#include <emscripten.h>

EMSCRIPTEN_KEEPALIVE
int fib(int n) {
  if(n <= 0){
    return 0;
  }

  int i, t, a = 0, b = 1;

  for (i = 1; i < n; i++) {
    t = a + b;
    a = b;
    b = t;
  }
  
  return b;
}
				
			

This macro takes care that the function will not be inlined and will be exported for external usage.

If you want to avoid to repetitively install the latest Emscripten toolchain, you can save some time by using the official Docker image.

The following command runs emcc in Docker, passes the wasm/fibonacci.c file as an input parameter and generates the WebAssembly file in wasm/fibonacci.wasm:

				
					docker run \
  --rm \
  -v `pwd`:`pwd` \
  -w `pwd` \
  -u $(id -u):$(id -g) \
  emscripten/emsdk \
  emcc native/fibonacci.c -o wasm/fibonacci.wasm --no-entry
				
			

The important detail here is to explicitly specify a .wasm file as the output and the –no-entry flag. With both in place, emcc generates a WASI-compliant interface and does not use Emscripten’s proprietary ABI and system interface.

We can put this command into a shell script to re-use it in the build process. Let us use this now to create the needed .wasm file and host it in a .NET Core application – specifically in an Azure Function with C#.

Using WebAssembly Modules in .NET Core

One of the currently simplest and most straightforward ways of running Wasm code in .NET is to use the wasmtime-dotnet .NET embedding of Wasmtime.

We can add it to our project simply by adding the Nuget package: dotnet add package --version 0.19.0-preview1 wasmtime (preview version at the time of writing)

With this in place, we can write a simple Azure function.

Creating an Azure Function to Execute WebAssembly Code

Our Azure Functions project from the demo repository looks like this:

The essential files are:

  • the source C file in the native folder
  • the compile/build shell script containing the Docker emcc command
  • the resulting .wasm file in the wasm folder

All that is left is to use wasmtime-dotnet’s API in the Azure Function to load the Wasm module and invoke the desired C function via an HTTP trigger – fib(number) in our case:

				
					public static class RunWasm
{
    [FunctionName("FibonacciWasm")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous,
           "GET", Route = "fibonacci/{number}")]
        HttpRequest request,
        int number,
        ILogger log)
    {
        using var engine = new Engine();
        using var module = Module.FromFile(engine, "wasm/fibonacci.wasm");

        using var host = new Host(engine);
        using dynamic instance = host.Instantiate(module);

        var result = instance.fib(number);

        return new OkObjectResult(result);
    }
}
				
			

wasmtime-dotnet uses dynamics which makes the API very readable.

If we finally call the Azure Function with a number parameter of, for instance, 12 (e.g. by simply calling the URL via a browser), it returns 144 as the result from invoking the WebAssembly code (remember: which was a C algorithm in the first place).

Voila!

Conclusion - Running WebAssembly Beyond the Browser

Having the ability to use WebAssembly as common byte code and WASI as a common interface to run WebAssembly code literally everywhere – and beyond the web browser – is very tempting. In this article, you have seen how to take WASI-compliant WebAssembly code, like existing C/C++ code and run it in your modern .NET Core application. A number of inspiring use cases are opening up – at least in the heads of some customers.

The entire sample application can be found on Github here.

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
.NET
KP-round
.NET 8 introduces a new Garbage Collector feature called DATAS for Server GC mode - let's make some benchmarks and check how it fits into the big picture.
09.10.2023