Originally published on dev.to
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.
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.
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 .
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.
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
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.Dockerfilebuilds the base image for
vscode/devcontainers/dotnetcontainers. It uses an argument to determine which versions of .NET SDKs to build and publish
library-scriptsdirectory that contains installation scripts that the
base.Dockerfileuses to build the base container (make a note of this - we'll revisit it later)
Dockerfileis what VS Code actually runs to build your Dev Container based on the published "base" container
devcontainer.jsonis what VS Code uses to customize your Dev Container
Back in VS Code, open a terminal and make sure you are in your
We'll need to download the
base.Dockerfile file and the installation scripts inside of the
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
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
heredocscript above will not work. In this case, simply create a new file called
wasmtime.shand add the
curlcommand 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
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
Now we are ready to build our Dev Container and get back to building our .NET Wasm app.
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
mkdir src cd src dotnet new console -o MyFirstWasiApp cd MyFirstWasiApp
Let's run the app to make sure it is running:
You should see the text
Hello, World! output to the console.
Now let's add the
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
qemuemulation 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:
You should see the text
Hello, World! output to the console again.
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.
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.
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.
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.
- 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)