Exploring .NET WebAssembly with WASI and Wasmtime
Tags: webassembly, dotnet, devcontainer, tutorial
Published: 2022-08-10 05:34:16

## 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](https://twitter.com/StevenMurawski) wrote a blog series on getting started with hosting Wasm apps on an emerging PaaS platform called [Hippo](https://github.com/deislabs/hippo) which is developed by folks at [Fermyon](https://twitter.com/fermyontech). In [Part 1](https://dev.to/smurawski/getting-started-with-hippo-a-webassembly-paas-part-1-5470) of the series, he introduces topics and define some of the acronyms like "Wagi" and "WASI". He also introduced a runtime called [Wasmtime](https://wasmtime.dev/) 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](https://code.visualstudio.com/docs/remote/create-dev-container). The .NET console app produced in this article has also been contributed as a `csharp` template in the [yo-wasm](https://github.com/deislabs/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](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor) 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](https://github.com/SteveSandersonMS/dotnet-wasi-sdk) and the SDK is really simple to use. It is published as a [NuGet package](https://www.nuget.org/packages/wasi.sdk) 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](https://github.com/deislabs/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](https://code.visualstudio.com/).

```sh
mkdir MyFirstWasiApp
cd MyFirstWasiApp
code .
```

### Why Dev Container?

This experimental `Wasi.Sdk` package requires [.NET 7](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) 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](https://code.visualstudio.com/docs/remote/create-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](https://www.docker.com/products/docker-desktop/) running on your local machine and the [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.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](#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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/csg5yshbrciabsuq3o6m.png)

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

![Add C# configs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z8orz73i78mm56zlfawz.png)

> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f49witpcknyq1ba1xyih.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y1n469o2s8din9776ds6.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9yya9sjfy3rkh3evh93v.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ipmsn6dskhrzzw1aadm7.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3hu0vtf7yo8s4wz6wxx4.png)

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](https://mcr.microsoft.com/v2/vscode/devcontainers/dotnet/tags/list)

Since the VS Code team is not publishing a VS Code Dev Container that supports the [.NET 7 preview tags](https://hub.docker.com/_/microsoft-dotnet-sdk/) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7o54jpjb6c0swg0ezawj.png)

In the [vscode-dev-containers repo](https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/dotnet), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yoah69un7h30hi6ptzok.png)

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

![vscode-dev-containers devcontainer director contents](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kk6nxdip6yqcw425zc6g.png)

- `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:

```sh
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](https://hub.docker.com/_/microsoft-dotnet-sdk/).
>
> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jy5mokjxzs0f0ljqr77d.png)

We now need to include steps to install Wasmtime in our Dockerfile. If you take a look at the [Wasmtime homepage](https://wasmtime.dev/), 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.

```sh
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9oraeif2ofu33saooo9.png)

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.

```sh
# 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5bq50vxwkwbmmy0ip6ka.png)

> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6clv9csvsdrcyopgjmlf.png)

Progress! Let's get back to building a .NET app using `Wasi.Sdk`

```sh
mkdir src
cd src
dotnet new console -o MyFirstWasiApp
cd MyFirstWasiApp
```

Let's run the app to make sure it is running:

```sh
dotnet run
```

You should see the text `Hello, World!` output to the console.

Now let's add the `Wasi.Sdk` package.

```sh
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:

```sh
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](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-intro) or [Hippo](https://github.com/deislabs/hippo) and uses [Yeoman](https://yeoman.io/) 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](https://github.com/deislabs/yo-wasm), clone it to your local machine, then open the repo using VS Code.

> If our [pull request](https://github.com/deislabs/yo-wasm/pull/73) is still pending, you can clone and use this [repo](https://github.com/smurawski/yo-wasm/tree/adding_csharp) instead.

Open a terminal and follow the instructions found in the `yo-wasm` [README](https://github.com/deislabs/yo-wasm/blob/main/README.md) file.

```sh
npm install -g yo
npm install -g generator-wasm
```

Let's also run some commands so we can run this from source.

```sh
npm install
npm run compile
npm link
```

Now create a new directory for your new test project.

```sh
cd
mkdir -p tmp/yo-csharp
cd tmp/yo-csharp
```

Create a new C# project using the `yo wasm` command and follow the prompts.

```sh
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kw7cgwtu1f2re481vskg.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2t2vfigj085vzbwokc40.png)

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](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-get-started-azure-cli) to set one up and this guide to create a [repository secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository) 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](https://github.com/deislabs/yo-wasm) project and the [Wasi-Sdk](https://github.com/SteveSandersonMS/dotnet-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](https://www.youtube.com/watch?v=k3HDivg3xyc)
- [Wasm, WASI, Wagi: What are they? | Fermyon Technologies (@FermyonTech)](https://www.fermyon.com/blog/wasm-wasi-wagi)
- [Running .NET in WebAssembly | Fermyon Technologies (@FermyonTech)](https://www.fermyon.com/blog/dotnet-wasi)
- [Future Possibilities for .NET Core and WASI (WebAssembly on the Server) | OD108](https://www.youtube.com/watch?v=A0vz_BWxIMc)
- [The Future of Blazor and Web Assembly with Steve Sanderson | The Azure DevOps Podcast, ep.202](https://www.youtube.com/watch?v=QmGso7t8dgQ&t=1394s)
- [Create WebAssembly System Interface (WASI) node pools in Azure Kubernetes Service (AKS) to run your WebAssembly (WASM) workload (preview)](https://docs.microsoft.com/en-us/azure/aks/use-wasi-node-pools)

Happy coding!