Utilize WebAssembly in .NET
- Panot Thaiuppathum
- Programming , /thcategories/programming Web Programming /thcategories/web-programming
- 27 Dec, 2022
We heard the WebAssembly quite a while ago but the use case, especially for .NET developers, was still limited. As of the time writing this post, in the last quarter of 2022, there are many new things around the WebAssembly itself yet the arrival of .NET 7 that comes with the interesting feature to work with the WebAssembly.
Due to most of the toolchains being open source, some are in the experiment phase hence the development is fast and very likely to break changes as it is getting reshaped to be more mature through time. So, I am writing this post to be more like a reference for myself and a seed for someone that is interested to explore the possibility of WebAssembly in the current and future for further research.
Use the WebAssembly on the browser
The WebAssembly was originally designed to be used on the web browser (as its name state). There are many languages supported to implement and build the .wasm file with the help of extensions — Rust, AssemblyScript (Typescript-like), Emscripten (C/C++), .NET 7 (C#) and many more.
Once we have .wasm file then we can call it using the built-in WebAssembly library in most of the browsers using either WebAssembly.instantiateStreaming() or WebAssembly.instantiate().
The WebAssembly is function-based, and you can use it either.
- Export functions from the .wasm file to the javascript. Then javascript can call those functions as if they are the regular javascript function.
- Import functions of the javascript to the .wasm. Then the WebAssembly can call those functions inside. The same concept as function delegation.
You can find many useful hands-on samples in Wasm By Example. For .NET, further detail and some promising samples can be found in:
- Run .NET from JavaScript | Microsoft Learn
- Use .NET from any JavaScript app in .NET 7 — .NET Blog (microsoft.com)
- Consuming .NET WebAssembly From JavaScript in the Browser | Khalid Abuhakmeh.
Support Types and Type Mapping
The WebAssembly, at this point, has still some limitations and the major problem for me is the type passing. Due to the WebAssembly being a binary format and can be compiled from various languages, passing complex types like a class can be a challenge. It depends on the .wasm compile tool to handle type mapping. Like in Rust, here are the supported types in wasm-bindgen. While .NET supported types can be found here, the types are automatically detected and mapped between JavaScript type and .NET type. You can also manually define JSMarshalAs
attribute.
For passing the complex types like a class in .NET, there are two options. Run .NET from JavaScript | Microsoft Learn
- Pass json object from JavaScript and map to JSObject in .NET. JSObject type has
GetPropertyAs[Type](“propertyname”)
functions to retrieve property value. - Serialize json object from one side and pass to the function as a string and deserialize it on the other side. I would recommend this option in most cases esp. for complex types that have nested.
Use the WebAssembly out of the browser
WebAssembly is originally known to be used on the web browser. But due to the WebAssembly being a binary format that can be compiled from various languages and with the runtime that is available in nearly all OS and platforms, it could become a significant material for software development.
To run a .wasm file out of the browser, you need a WebAssembly runtime — Wasmtime, Wasmer, WasmEdge, Wasm3. All have the basic functionality to run .wasm, import-export functions, etc. but functionalities in depth could be different. Most of the existing libraries for .NET are used or made by Wasmtime.
wasmtime-dotnet — A .NET library to run .wasm within the .NET application using Wasmtime. Looking into its readme may not show all options to load and run the WebAssembly. It shows only how to run WebAssembly from the text as below code snippet (see the definition and toolkit for wat in WebAssembly Toolkit section below).
using var module = Module.FromText(
engine,
"hello",
"(module (func $hello (import \"\" \"hello\")) (func (export \"run\") (call $hello)))"
);
It actually also supports running from .wasm file using Module.FromFile() and several more options. See sample and step-by-step how to use wasmtime-dotnet in this post — WebAssembly Beyond The Browser: Running Wasm In .NET Core Applications With WASI & Wasmtime — Thinktecture AG.
Access host resources like I/O, network and more
Similar to JavaScript, WebAssembly itself cannot access the outside world aka Sandboxed. The arrival of WebAssembly/WASI: WebAssembly System Interface (github.com) enhances WebAssembly to another level. It is an API standard for the runtime to be implemented so running a .wasm that uses WASI to access host resources with different runtime should get the same behavior.
The WebAssembly System Interface is not a monolithic standard system interface, but is instead a modular collection of standardized APIs. None of the APIs are required to be implemented to have a compliant runtime. Instead, host environments can choose which APIs make sense for their use cases.
See the entire list of the proposal in WASI/Proposals.md at main · WebAssembly/WASI (github.com).
There is an SDK to turn .NET project compiling to WASI-compliant .wasm. It could turn the entire ASP.NET web into a WebAssembly module. dotnet/dotnet-wasi-sdk: An SDK for building .NET projects as standalone WASI-compliant modules (github.com)
With the magic of the below extension function.
var builder = WebApplication.CreateBuilder(args).UseWasiConnectionListener();
// or
var builder = WebApplication.CreateBuilder(args).UseWasiCustomHostServer();
Caution: Don’t actually do this! It’s purely to demonstrate that ASP.NET Core on WebAssembly can run anywhere. It’s not a sensible way to deliver a normal web app.
Use the WebAssembly as a Cloud Serverless Function
The core concept of using WebAssembly is by import and export functions so we can consider it a stateless function. The same as the serverless function on the cloud. So, there are quite many experiments trying to use WebAssembly as a cloud serverless function.
- Manually proxy API controller to WebAssembly function using wasmtime-dotnet as this sample does.
- Use dotnet/dotnet-wasi-sdk package to turn the entire asp.net project output to a WebAssembly as exposed in the above section. Docker recently announced the support of Wasm with the new dedicated containerd shim runtime from WasmEdge — Introducing the Docker+Wasm Technical Preview | Docker. They probably be the perfect fit.
- Some cloud providers already support WebAssembly as the Serverless function — Fermyon Spin seems to be the most mature on the market at present.
Use the WebAssembly as a package and plugin
WASM package manager
The WAPM is the package manager of WebAssembly similar to NPM. It is a product of the Wasmer. You can find both library and standalone package type. Using wapmcommand to install and run a package.
wapm install cowsay wapm run cowsay hello wapm
WASM plugin
Some products already support using wasm as its extension or plugin. With the benefit of no boundary of language and platform to run a wasm file, it is very convenient for the developer to implement or use the custom extension or plugin in wasm.
In the past if you need to create a custom extension of Envoy, you have to write in Lua. Envoy now supports extension in wasm meaning that we can write the extension in any language that can compile to wasm. Below are a few to checkout.
- Istio / Wasm Plugin
- wasmerio/wasmer-postgres: 💽🕸 Postgres library to run WebAssembly binaries. (github.com)
- WASM — envoy 1.25.0-dev-23eacb documentation (envoyproxy.io)
If you are developing a product that aim to support importable custom module or plugin then supporting the wasm might be a good idea.
Write once run many
With the portability of the WebAssembly, we can cross the boundary of language and platform using the same codebase to use with any platform.
- The same functions written in C# can be used in Go, Rust, or PHP and vice versa.
- The same functions written in C# can be used in both the backend and the frontend (browser via JavaScript).
Imagine we are implementing Single Page Application website and need the business logic/validation apply at the client side before sending the payload to proceed on the server side/API. If we choose NodeJS as the backend that would not be too difficult but share the same codebase for both the client and the server side though that is not the case for .NET in the past. The WebAssembly and System.Runtime.InteropServices.JavaScript in .NET 7 made it possible.
Simply create three separate projects.
- A business logic project in .NET 7 or .NET Standard if aims to be using it with the older .NET version like .NET Core 3, .NET 5, or .NET 6.
- A backend/API project in any recent .NET framework. No need to be .NET 7 if the business logic project is a .NET Standard.
- A .NET 7 project with System.Runtime.InteropServices.JavaScript to proxy the business logic functions and compile to WebAssembly as well as producing the
dotnet.js
file. Thedotnet.js
file is used to create and start the .NET WebAssembly runtime.
See a very simple sample project I created panot-hong/wasm-csharp-browser (github.com).
WebAssembly Toolkit
As bonus track, the WebAssembly has various toolkit for developer to use, see the list in WebAssembly/wabt: The WebAssembly Binary Toolkit (github.com). One important toolkit is ability to convert .wasm file to .wat (WebAssembly text format) and vice versa.
Wrap Up
You may have a question since .NET Core it can run cross-platform — Windows, Linux, ARM, etc. what is the benefit to compile .NET into a wasm file, so it becomes a platform independent as long as there is WebAssembly runtime support that platform. From what I understand it is a pretty similar concept except for the wasm together with WASI, it is sandboxed and access to the resources is limited.
The mainstream of the WebAssembly runtime, toolkit, and SDK are all written in Rust. Thus, Rust has the most feature-rich and always the most recent. It is though nice to see that Microsoft is one of the Bytecode Alliance members and has the intention to support WebAssembly since the arrival of Blazor.
Many stuffs in this post are in the experimental stage and WASI is still nearly half of the entire proposal list. I would recommend using it with caution and bear in mind that things could be (breaking) changed pretty quickly. Proxy and decouple the wasm pieces as much as possible.