Exploring .NET WebAssembly with WASI
Overview
WebAssembly (Wasm) is something that the Cloud Native Advocacy team has been exploring. It has been around for a few years and has mostly been used within browser-based applications. There are many blog posts on what makes WebAssembly an ideal target for running applications (e.g., smaller footprint with .wasm files compared to containers, code isolation, and sandboxing). My colleague Steven Murawski wrote a blog series on getting started with hosting Wasm apps on an emerging PaaS platform called Hippo which is developed by folks at Fermyon. In Part 1 of the series, he introduces topics and define some of the acronyms like “Wagi” and “WASI”. He also introduced a runtime called Wasmtime which implements the WebAssembly System Interface (WASI) standard. This article will walk you through how Steven and I went about getting a .NET console app running as a Wasm app on the Wasmtime runtime in a Dev Container. The .NET console app produced in this article has also been contributed as a csharp
template in the yo-wasm repo which is also maintained by Fermyon; so you can quickly test it out for yourself later.
Stop… Wasmtime
Why is the emergence of standards like WASI and runtimes like Wasmtime exciting? Well, now we can run more than just web applications inside of a web browser. We can start looking at running Wasm server apps in places that implements the WASI standard. This means we can run any app, anywhere as long as the app has been compiled down into a single .wasm binary and the host has WASI-based runtime installed.
This is great, but what does it mean for .NET developers? We do know that Blazor WebAssembly has been going down this path for a few years, but the Blazor WebAssembly primarily targets web browsers as its runtime and the compilation of a Blazor WebAssembly project does not produce the single .wasm binary that is needed to run using Wasmtime. If only .NET developers had a toolchain that can compile .NET apps into single .wasm binary (much like how the dotnet build
command can compile a project into a single .dll) and compile these .NET apps to target WASI so that these .NET app can run outside of browsers. Well, it turns out that Steve Sanderson and the .NET team have been building an experimental SDK that will allow you to do just that.
The experimental SDK is published here and the SDK is really simple to use. It is published as a NuGet package and all you need to do is add the package to your .NET project. That’s it… the SDK hooks into the build pipeline and produces the single .wasm we need. It simply compiles the .NET app to Wasm using the dotnet build
command.
This SDK is experimental and may be moved to a proper Microsoft GitHub repo in the near future.
Let’s walk through the steps we took to put this together and give you a quick overview of the yo-wasm project so you can use it to get started with your Wasm projects.
Walkthrough
Open your favorite terminal, create a folder to work in, cd
into the folder, and open it using VS Code.
mkdir MyFirstWasiApp
cd MyFirstWasiApp
code .
Why Dev Container?
This experimental Wasi.Sdk
package requires .NET 7 which at the time of this writing, is in preview. You can choose to install the preview version on your local machine. Since some folks may not want to install preview bits of .NET on their local machine, we opted to test this within a VS Code Dev Container. There were a few obstacles we faced and this section will walk you through how we approached it.
This approach will require that you have Docker Desktop running on your local machine and the Remote - Containers VS Code extension.
It is pretty simple to get a Dev Container up and running using VS Code. If you are familiar with building Dev Containers, you can quickly skim through the content below, pick out the key steps (since we build a custom .NET 7 container), then jump to the .NET Wasm section.
In VS Code, press the F1
key on your keyboard (Ctrl+Shift+P
works too). This will open a prompt where you can type in the word devcontainer
. You should see an entry titled Remote-Containers: Add Development Container Configuration Files…. Click that to start the configuration wizard.
You will be presented with a list of predefined container configurations. We’ll use the C# (.NET) definition
If you do not see it in your list, you can search for it in the text box above the list.
In the next step of the wizard, you’ll notice the latest version of .NET that is supported is 6.0
. There is no .NET 7.0 support yet. Let’s go ahead and select the latest version and hack away at creating a new Dev Container that supports .NET 7.0.
After selecting the .NET version, you will be presented with additional Dev Container options (i.e., Node.js version and additional features to install). Click through by selecting lts/*
for NodeJS and do not select any additional features (just click the OK button without selecting any features).
This is not exactly what we were looking for, but what do we end up with is a structure on how Dev Containers are configured. We can see that all we need is a .devcontainer
directory with a devcontainer.json
file and a Dockerfile
file.
If you open the devcontainer.json
file, you’ll notice that you can pass in a VARIANT
value as a build argument. Here a tag of 6.0-bullseye
is passed in to build a .NET 6 container on Debian 11.
It would be nice if we can simply pass in something like 7.0.100-preview.6-jammy
tag to build a .NET 7 container on Ubuntu 22.04
However, this will not work since the VS Code Dev Container team is not publishing a .NET 7 image tag yet.
The list of all the published tags for the vscode/devcontainers/dotnet image can be found here
Since the VS Code team is not publishing a VS Code Dev Container that supports the .NET 7 preview tags yet, we’ll build our own.
If you head back to the devcontainer.json
file, you’ll see some information on where the container is being sourced from. Navigate to the URL to view the GitHub repo that hosts this configuration.
In the vscode-dev-containers repo, you’ll see that there too is a .devcontainer
directory. This contains instructions for building the dotnet vscode-dev-container. Click into the directory.
There’s a few files and one subdirectory in here and I’ll explain what they do.
base.Dockerfile
builds the base image forvscode/devcontainers/dotnet
containers. It uses an argument to determine which versions of .NET SDKs to build and publishlibrary-scripts
directory that contains installation scripts that thebase.Dockerfile
uses to build the base container (make a note of this - we’ll revisit it later)Dockerfile
is what VS Code actually runs to build your Dev Container based on the published “base” containerdevcontainer.json
is what VS Code uses to customize your Dev Container
Back in VS Code, open a terminal and make sure you are in your .devcontainer
directory.
We’ll need to download the base.Dockerfile
file and the installation scripts inside of the library-scripts
directory.
Run the following commands to overwrite what we already have:
mkdir library-scripts
cd library-scripts
curl -O https://raw.githubusercontent.com/microsoft/vscode-dev-containers/v0.241.1/containers/dotnet/.devcontainer/library-scripts/common-debian.sh
curl -O https://raw.githubusercontent.com/microsoft/vscode-dev-containers/v0.241.1/containers/dotnet/.devcontainer/library-scripts/node-debian.sh
curl -O https://raw.githubusercontent.com/microsoft/vscode-dev-containers/v0.241.1/containers/dotnet/.devcontainer/library-scripts/meta.env
cd -
curl -o Dockerfile https://raw.githubusercontent.com/microsoft/vscode-dev-containers/v0.241.1/containers/dotnet/.devcontainer/base.Dockerfile
We’ll keep our devcontainer.json
file and only make a few changes. Let’s update the VARIANT
value to point to the proper tag .NET 7 Preview tag (7.0-jammy
) and add some VS Code extensions.
The toolchain needed to properly build the .wasm file was built using the amd64 (x86*x64) architecture, you should use the appropriate tag. You can find the proper tag to use here.
Also, due to the reliance on amd64, your Wasm code compilation may not work properly if you are using M1 chips on newer Macs.
We can add a few VS Code extensions to the customizations.vscode.extensions
array:
ms-vsliveshare.vsliveshare
dtsvet.vscode-wasm
You can add more extensions as needed.
The file should look like this:
We now need to include steps to install Wasmtime in our Dockerfile. If you take a look at the Wasmtime homepage, you’ll find instructions on how to install it. Let’s start by creating a new script in the library-scripts
directory to perform the installation. Add the curl
command to the script.
cat << EOF > wasmtime.sh
curl https://wasmtime.dev/install.sh -sSf | bash
EOF
If you are using a Windows terminal, the
heredoc
script above will not work. In this case, simply create a new file calledwasmtime.sh
and add thecurl
command to the file.
The Wasmtime installation script relies on a package called xz-utils
to un-tar the files and we’ll need to make sure the utility is available in our container.
Head over to the common-debian.sh
file and add this to the package_list
variable (if it does not already exist). It probably makes sense to include it after the unzip
and zip
packages.
Last thing we need to do is update the Dockerfile
to run our newly created wasmtime.sh
script. Just before the line that removes the /library-scripts
directory, add a RUN
command to call our script.
# Install wasmtime
RUN su vscode -c 'bash /tmp/library-scripts/wasmtime.sh'
Since we need wasmtime when the container is running, run the code using the non-root
vscode
user.
Now we are ready to build our Dev Container and get back to building our .NET Wasm app.
.NET Wasm
Make sure all files are saved and Docker is running on your machine. Hit the F1
key and start typing open folder
. You should see an entry for Remote-Containers: Open Folder in Container…. Click that to build your dev container.
The initial container build process can take several minutes to complete.
Once the container has been built, you can open a new terminal in VS Code and run the dotnet --version
command to verify the version of dotnet that has been installed.
Progress! Let’s get back to building a .NET app using Wasi.Sdk
mkdir src
cd src
dotnet new console -o MyFirstWasiApp
cd MyFirstWasiApp
Let’s run the app to make sure it is running:
dotnet run
You should see the text Hello, World!
output to the console.
Now let’s add the Wasi.Sdk
package.
dotnet add package Wasi.Sdk --prerelease
Run the project again and you should see the text Hello, World!
again, but this time the app was run using the .wasm binary instead of the .dll binary.
Again, due to the reliance on amd64, your Wasm code compilation may not work properly if you are using M1 chips on newer Macs. Running amd64 based containers on M1 architecture uses
qemu
emulation for compatibility and it can produce odd results. You should look to run this on amd64 architecture for best results.
Not 100% convinced the dotnet run
command was using wasmtime
and the .wasm binary? Run the following command:
wasmtime bin/Debug/net7.0/MyFirstWasiApp.wasm
You should see the text Hello, World!
output to the console again.
Yo, Wasm!
I know what you are thinking… that was a lot of steps just to get a simple “Hello World” console app going. But with the Wasi-Sdk
being experimental and having a dependency on .NET 7 (preview), it’s a good path for folks that are just looking to try this out without having to install .NET preview bits on their local machine.
Once the VS Code team starts publishing a .NET 7 Dev Container, we use that container and not have to build our own.
Getting back to Wasm and the yo-wasm
repo. This repo exists to help you easily create Wasm modules which can be published to OCI registries. The yo-wasm
project currently supports publishing to either Azure Container Registry or Hippo and uses Yeoman to generate projects based on templates that are defined in this repo. There are templates for Assembly Script, C, Rust, Swift, and TinyGo. We’ve added a new template for C#, so let’s give it a try.
Head over to the yo-wasm repo, clone it to your local machine, then open the repo using VS Code.
If our pull request is still pending, you can clone and use this repo instead.
Open a terminal and follow the instructions found in the yo-wasm
README file.
npm install -g yo
npm install -g generator-wasm
Let’s also run some commands so we can run this from source.
npm install
npm run compile
npm link
Now create a new directory for your new test project.
cd
mkdir -p tmp/yo-csharp
cd tmp/yo-csharp
Create a new C# project using the yo wasm
command and follow the prompts.
yo wasm
Here is the final output. You can see all the project files are there for a basic “Hello World” console app and the .devcontainer configuration is also included in the template.
If you have .NET 7 (preview) and Wasmtime installed, feel free to run dotnet run
locally to test. If not, you can use the devcontainer to test things out 😄.
When you chose to publish your module, the template will generate a GitHub Action workflow file that uses a wasm-to-oci
executable to push your .wasm file to an OCI registry, in this case Azure Container Registry.
This does require that you the the OCI registry pre-deployed. If you are looking at publishing to Azure Container Registry, take a look at this tutorial to set one up and this guide to create a repository secret which will need to contain the value of your container registry access key.
From there, you will need to publish this newly created project to GitHub for the GitHub Actions to work.
Summary
To recap, we explored how WebAssembly is evolving to be more than something that is run on web browsers. With WASI, you can run any app that is compiled into a .wasm binary on any host. This is still an emerging area, but the concept of running Wasm on the server and having the ability to publish Wasm modules on OCI registries can open up many new opportunities for innovation. Having the ability to compile .NET apps into a single .wasm file is really exciting and I eager to see how this evolves.
The yo-wasm project and the Wasi-Sdk are great places to start digging in and there are additional resources available to learn more. I’d suggest you go check out the following:
- Keynote: WASI - A new kind of system interface & what it means for cloud native - Lin Clark, Fastly
- Wasm, WASI, Wagi: What are they? | Fermyon Technologies (@FermyonTech)
- Running .NET in WebAssembly | Fermyon Technologies (@FermyonTech)
- Future Possibilities for .NET Core and WASI (WebAssembly on the Server) | OD108
- The Future of Blazor and Web Assembly with Steve Sanderson | The Azure DevOps Podcast, ep.202
- Create WebAssembly System Interface (WASI) node pools in Azure Kubernetes Service (AKS) to run your WebAssembly (WASM) workload (preview)
Happy coding!