PAUL'S BLOG

Learn. Build. Share. Repeat.

Exploring .NET WebAssembly with WASI

2022-08-09 12 min read Tutorial

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.

Add Development Container Configuration Files

You will be presented with a list of predefined container configurations. We’ll use the C# (.NET) definition

Add C# configs

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.

Add C# configs for bullseye

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).

Skip 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.

Dev Container configs

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.

devcontainer.json file

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

devcontainer.json file with jammy

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.

Link to vscode-dev-containers/containers/dotnet repo

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.

Link to vscode-dev-containers devcontainer directory

There’s a few files and one subdirectory in here and I’ll explain what they do.

vscode-dev-containers devcontainer director contents

  • base.Dockerfile builds the base image for vscode/devcontainers/dotnet containers. It uses an argument to determine which versions of .NET SDKs to build and publish
  • library-scripts directory that contains installation scripts that the base.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” container
  • devcontainer.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:

Dev Container extensions

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 called wasmtime.sh and add the curl 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.

Add xz-utils

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.

Re-open folder in 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.

Validate Dev Container

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.

yo-wasm output

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.

wasm to oci

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:

Happy coding!