Wasmtime, the WebAssembly runtime from theBytecode Alliance, recently added an early preview of an API for.NET Core, Microsoft’s free, open-source, and cross-platform application runtime. This API enables developers to programmatically load and execute WebAssembly code directly from their .NET programs.
. NET Core is already a cross-platform runtime, so why should .NET developers pay any attention to WebAssembly?
There are several reasons to be excited about WebAssembly if you’re a .NET developer, such as sharing the same executable code across platforms, being able to securely isolate untrusted code, and having a seamless interop experience with the upcoming WebAssembly interface types proposal.
Share more code across platforms
. NET assemblies can already be built for cross-platform use, but using anative library(for example, a library written in C or Rust) can be difficult because it requires native interop and distributing a platform-specific build of the library for each supported platform.
However, if the native library were compiled to WebAssembly, the same WebAssembly module could be used across many different platforms and programming environments, including .NET; this would simplify the distribution of the library and the applications that depend on it.
Securely isolate untrusted code
The .NET Framework attempted to sandbox untrusted code with technologies such asCode Access SecurityandApplication Domains, but ultimately these failed to properly isolate untrusted code. As a result, Microsoft deprecated their use for sandboxing and ultimately removed them from .NET Core.
Have you ever wanted to load untrusted plugins in your application but couldn’t figure out a way to prevent the plugin from invoking arbitrary system calls or from directly reading your process’ memory? You can do this with WebAssembly because it was designed for the web, an environment where untrusted code executes every time you visit a website.
A WebAssembly module can only call the external functions it explicitly imports from a host environment, and may only access a region of memory given to it by the host. We can leverage this design to sandbox code in a .NET program too!
Improved interoperability with interface types
TheWebAssembly interface types proposalintroduces a way for WebAssembly to better integrate with programming languages by reducing the amount of glue code that is necessary to pass more complex types back and forth between the hosting application and a WebAssembly module.
When support for interface types is eventually implemented by the Wasmtime for .NET API, it will enable a seamless experience for exchanging complex types between WebAssembly and .NET.
Diving into using WebAssembly from .NET
In this article we’ll dive into using a Rust library compiled to WebAssembly from .NET with the Wasmtime for .NET API, so it will help to be a little familiar with the C # programming language to follow along.
The API described here is fairly low-level. That means that there is quite a bit of glue code required for conceptually simple operations, such as passing or receiving a string value.
In the future we’ll also provide a higher-level API based onWebAssembly interface typeswhich will significantly reduce the code required for the same operations. Using that API will enable interacting with a WebAssembly module from .NET as easily as you would a .NET assembly.
Note also that the API is still under active development and will change in backwards-incompatible ways. We’re aiming to stabilize it as we stabilize Wasmtime itself.
If you’re reading this and you aren’t a .NET developer, that’s okay! Check out theWasmtime Demosrepository for corresponding implementations for Python, Node .js, and Rust too!
Creating the WebAssembly module
We’ll start by building a Rust library that can be used to renderMarkdownto HTML. However, instead of compiling the Rust library for your processor architecture, we’ll be compiling it to WebAssembly so we can use it from .NET.
You don’t need to be familiar with theRust programming languageto follow along, but it will help to have a Rust toolchain installed if you want to build the WebAssembly module. See the homepage forRustupfor an easy way to install a Rust toolchain.
Additionally, we’re going to usecargo-wasi, a command that bootstraps everything we need for Rust to target WebAssembly:
cargo install cargo-wasi
Next, clone the Wasmtime Demos repository:
git clone https://github.com/bytecodealliance/wasmtime-demos.git cd wasmtime-demos
This repository includes themarkdown
directory that contains a Rust library. The library wraps a well-known Rust crate that can render Markdown as HTML. (Note for .NET developers: a crate is like a NuGet package, in a way).
Let’s build themarkdown
WebAssembly module usingcargo-wasi
:
cd markdown cargo wasi build --release
There should now be amarkdown.wasm
file in the (target / wasm) – wasi / releasedirectory.
If you’re curious about the Rust implementation, opensrc / lib.rs
; it contains the following:
use pulldown_cmark :: {html, Parser}; use wasm_bindgen :: prelude :: *; # [wasm_bindgen] pub fn render (input: & str) ->String { let parser=Parser :: new (input); let mut html_output=String :: new (); html :: push_html (& mut html_output, parser); return html_output; }
The Rust library is exporting only a single function,render
, that takes a string (the Markdown) as input and returns a string (the rendered HTML). All of the code required to parse Markdown and translate it to HTML is provided by thepulldown-cmarkcrate.
Let’s step back and simply appreciate what is about to happen here. We’re taking a popular Rust crate, wrapping it with a few lines of code that exposes the functionality as a WebAssembly function, and then compiling it to a WebAssembly module that we can load from .NET regardless of the platform we’re running on . How cool is that!?
Peeking under the hood of a WebAssembly module
Now that we have the WebAssembly module we’re going to use, what does it need from a host to function and what functionality does it offer the host?
To figure that out, let’s disassemble the module to a textual representation using thewasm2wat
tool from theWebAssembly Binary Toolkitto a file calledmarkdown.wat
:
wasm2wat markdown.wasm --enable-multi-value>markdown.wat
Note: the- enable-multi-value
option enables support for functions that return multiple values and is required to disassemble themarkdown
Module.
What the module needs from a host
The module’s imports define what the host should provide for the module to work.
Here are the imports for themarkdown
module:
(import "wasi_unstable" "fd_write" (func $ fd_write (param i (i) **************************************************************************** (i) i 32) (result i 32))) (import "wasi_unstable" "random_get" (func $ random_get (param i (i) ) (result i 32)))
This tells us that the module will need two functions from the host:fd_write
andrandom_get
. These are actuallyWebAssembly System Interface(WASI) functions that have well-defined behavior:fd_write
is used to write data to a file descriptor andrandom_get
will fill a buffer with random data.
Shortly we’ll implement these functions for a .NET host, but it is important to understand thatthis module canonlycall these functions from the host; the host gets to decide how, and even if, the functions are implemented.
What the module offers a host
The module’s exports define what functionality it offers the host.
Here are the exports for themarkdown
module:
(export "memory" (memory 0)) (export "render" (func $ render_multivalue_shim)) (export "__wbindgen_malloc" (func $ __ wbindgen_malloc)) (export "__wbindgen_realloc" (func $ __ wbindgen_realloc)) (export "__wbindgen_free" (func $ __ wbindgen_free)) ... (func $ render_multivalue_shim (param i (i) ) (result i (i) ))) (func $ __ wbindgen_malloc (param i 32) (result i 32) ...) (func $ __ wbindgen_realloc (param i (i) i) ) (result i 32))) (func $ __ wbindgen_free (param i (i) ) ...)
First, the module is exporting amemory. A WebAssembly memory is the linear address space accessible to the module;it will be the only region of memory the module can read from or write to. As the module cannot access any other region of the host’s address space directly, the exported memory is where the host will exchange data with the WebAssembly module.
Second, the module exports therender
function we implemented in Rust. But wait a second, why does it have two parameters and return two values when the Rust implementation only has one parameter and one return value?
In Rust, both a string slice (& str
) and an owned string (String
) are represented as an address and length (in bytes) pair when compiled to WebAssembly. Thus, the WebAssembly version of the Rust function takes an address-length pair for the markdown input string and returns an address-length pair for the rendered HTML string. Here, addresses are represented as integer offsets into the exported memory.
Note that since the Rust code returns aString
, which is anownedtype, the caller ofrender
will be responsible for freeing the returned memory containing the rendered string.
During the implementation of the .NET host we’ll discuss the rest of the exports.
Creating the .NET project
We will need a. NET Core SDKto create a .NET Core project, so make sure you have a3.0 or laterSDK installed.
Start by creating a directory for the project:
mkdir WasmtimeDemo cd WasmtimeDemo
Next, create a new .NET Core console project:
dotnet new console
Finally, add a reference to theWasmtime NuGet package:
dotnet add package wasmtime --version 0.8.0-preview1
That’s it! Now we’re ready to use the Wasmtime for .NET API to load and execute themarkdown
WebAssembly module.
Importing .NET code from WebAssembly
Importing .NET functions from WebAssembly is as simple as implementing theIHost
interface in .NET. This only requires a publicInstance
property that will represent the WebAssembly module instance the host is bound to.
TheImport
attribute is then used to mark functions and fields as imports to a WebAssembly module.
As we discussed earlier, the module requires two imports from the host:fd_write
andrandom_get
, so let’s create implementations for those functions.
Create a file namedHost.cs
in the project directory and add the following content:
using System.Security.Cryptography; using Wasmtime; namespace WasmtimeDemo { class Host: IHost { // These are from the current WASI proposal. const int WASI_ERRNO_NOTSUP=58; const int WASI_ERRNO_SUCCESS=0; public Instance Instance {get; set; } [Import("fd_write", Module="wasi_unstable")] public int WriteFile (int fd, int iovs, int iovs_len, int nwritten) { return WASI_ERRNO_NOTSUP; } [Import("random_get", Module="wasi_unstable")] public int GetRandomBytes (int buf, int buf_len) { _random.GetBytes (Instance.Externs.Memories [0]. Span.Slice (buf, buf_len)); return WASI_ERRNO_SUCCESS; } private RNGCryptoServiceProvider _random=new RNGCryptoServiceProvider (); } }
Thefd_write
implementation simply returns an error indicating the operation is not supported. It is used by the module for writing errors tostderr
, which will not happen for this example.
Therandom_get
implementation fills the requested buffer with random bytes. It slices the(Span)representing the entire exported memory of the module so that the .NET implementation can writedirectlyto the requested buffer without having to perform any intermediate copies. Therandom_get
function is being called by the implementation of (HashMap) from Rust’s standard library.
That’s all it takes to expose .NET functions to the WebAssembly module with the Wasmtime for .NET API.
However, before we can load the WebAssembly module and use it from .NET, we need to discuss how a string gets passed from the .NET host as a parameter to therender
function.
Being a good host
Based on the exports of the module, we know it exports amemory. From the host’s perspective, think of a WebAssembly module’s exported memory as being granted access to the address space of a foreign process, even though the modulesharesthe same process of the host itself.
If you randomly write data to a foreign address space, Bad Things Happen ™ because it’s quite easy to corrupt the state of the other program and cause undefined behavior, such as a crash or the total protonic reversal of the universe. So how can a host pass data to the WebAssembly module in a safe manner?
Internally the Rust program uses amemory allocatorto manage its memory. So, for .NET to be a good host to the WebAssembly module, it must also use thesamememory allocator when allocating and freeing memory accessible to the WebAssembly module.
Thankfully,wasm-bindgen, used by the Rust program to export itself as WebAssembly, also exported two functions for that purpose:__ wbindgen_malloc
and__ wbindgen_free
. These two functions are essentiallymalloc
and (free) from C, except
__ wbindgen_freeneeds the size of the previous allocation in addition to the memory address.
With this in mind, let us write a simple wrapper for these exported functions in C # so we can easily allocate and free memory accessible to the WebAssembly module.
Create a file namedAllocator.cs
in the project directory and add the following content:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Wasmtime.Externs; namespace WasmtimeDemo { class Allocator { public Allocator (ExternMemory memory, IReadOnlyListfunctions) { _memory=memory ?? throw new ArgumentNullException (nameof (memory)); _malloc=functions .Where (f=>f.Name=="__wbindgen_malloc") .SingleOrDefault () ?? throw new ArgumentException ("Unable to resolve malloc function."); _free=functions .Where (f=>f.Name=="__wbindgen_free") .SingleOrDefault () ?? throw new ArgumentException ("Unable to resolve free function."); } public int Allocate (int length) { return (int) _malloc.Invoke (length); } public (int Address, int Length) AllocateString (string str) { var length=Encoding.UTF8.GetByteCount (str); int addr=Allocate (length); _memory.WriteString (addr, str); return (addr, length); } public void Free (int address, int length) { _free.Invoke (address, length); } private ExternMemory _memory; private ExternFunction _malloc; private ExternFunction _free; } }
This code looks complicated, but all it is doing is finding the needed exported functions by name from the module and wrapping them with an easier to use interface.
We’ll use this helperAllocator
class to allocate the input string to the exportedrender
function.
Now we’re ready to render some Markdown.
Rendering the Markdown
OpenProgram.cs
in the project directory and replace it with the following content:
using System; using System.Linq; using Wasmtime; namespace WasmtimeDemo { class Program { const string MarkdownSource= "# Hello,` .NET`! Welcome to ** WebAssembly ** with [Wasmtime] (https://wasmtime.dev)! "; static void Main () { using var engine=new Engine (); using var store=engine.CreateStore (); using var module=store.CreateModule ("markdown.wasm"); using var instance=module.Instantiate (new Host ()); var memory=instance.Externs.Memories.SingleOrDefault () ?? throw new InvalidOperationException ("Module must export a memory."); var allocator=new Allocator (memory, instance.Externs.Functions); (var inputAddress, var inputLength)=allocator.AllocateString (MarkdownSource); try { object [] results=(instance as dynamic) .render (inputAddress, inputLength); var outputAddress=(int) results [0]; var outputLength=(int) results [1]; try { Console.WriteLine (memory.ReadString (outputAddress, outputLength)); } finally { allocator.Free (outputAddress, outputLength); } } finally { allocator.Free (inputAddress, inputLength); } } } }
Let’s walk through what the code doing. Step-by-step, it:
- ********************************************** (Creates an) (Engine) . The engine represents the Wasmtime runtime itself. The runtime is what enables loading and executing WebAssembly modules from .NET.
- Then it creates a
Store
. A store is where all WebAssembly objects, such as modules and their instantiations, are kept. There can be multiple stores in an engine, but their associated objects cannot interact with one another. - Next it creates a
Module
from themarkdown.wasm
file on disk . AModule
represents the data of the WebAssembly module itself, such as what it imports and exports. A module can have one or moreinstantiations. An instantiation is theruntimerepresentation of a WebAssembly module. It compiles the module’sWebAssemblyinstructions to instructions of thecurrent CPU architecture, allocates the memory accessible to the module, and binds imports from the host. - It instantiates the module using an instance of the
Host
class we implemented earlier, binding the .NET functions as imports. - Finds the memory exported by the module.
- Creates an allocator and then allocates a string for the Markdown source we want to render.
- Invokes the
render
function with the input string by casting the instance todynamic
. This is a C # feature that enables dynamic binding of functions at runtime; think of it simply as a shortcut to searching for the exportedrender
function and invoking it. - Outputs the rendered HTML by reading the returned string from the exported memory of the WebAssembly module.
- Finally , it frees both the input st ring it allocated and the returned string that the Rust program gave us to own.
That’s it for the implementation; onwards to actually running the code!
Running the .NET program
Before we can run the program, we need to copymarkdown.wasm
to the project directory, as this is where we’ll run the program from. You can find themarkdown.wasm
file in the (target / wasm) – wasi / releasedirectory from where you built it.
From theProgram.cs
source above, we see that the program hard-coded some Markdown to render:
# Hello, `.NET`! Welcome to ** WebAssembly ** with [Wasmtime] (https://wasmtime.dev)!
Run the program to render it as HTML:
dotnet run
If everything went according to plan, this should be the result:
Hello,.NET
! Welcome toWebAssemblywithWasmtime!
What’s next for Wasmtime for .NET?
That was a surprisingly large amount of C # code that was necessary to implement this demo, wasn’t it?
There are two major features we have planned that will help simplify this:
- Exposing Wasmtime’s WASI implementation to .NET (and other languages)
In our implementation of
Host
above, we had to manually implementfd_write
andrandom_get
, which are WASI functions.Wasmtime itself has a WASI implementation, but currently it isn’t accessible to the .NET API.
Once the .NET API can access and configure the WASI implementation of Wasmtime, there will no longer be a need for .NET hosts to provide their own implementation of WASI functions.
-
Implementing interface types for .NET
As discussed earlier, WebAssembly interface types enable a more idiomatic integration of WebAssembly with a hosting programming language.
Once the .NET API implements the interface types proposal, there shouldn’t be a need to create an
Allocator
class like the one we implemented .Instead, functions that use types like
string
should simply work without having to write any glue code in .NET.
The hope, then, is that this is what it might look like in the future to implement this demo from .NET:
using System; using Wasmtime; namespace WasmtimeDemo { interface Markdown { string Render (string input); } class Program { const string MarkdownSource= "# Hello,` .NET`! Welcome to ** WebAssembly ** with [Wasmtime] (https://wasmtime.dev)! "; static void Main () { using var markdown=Module.Load("markdown.wasm"); Console.WriteLine (markdown.Render (MarkdownSource)); } } }
I think we can all agree that looks so much better!
That’s a wrap!
This is the exciting beginning of using WebAssembly outside of the web browser from many different programming environments, including Microsoft’s .NET platform.
If you’re a .NET developer, we hope you’ll join us on this journey!
The .NET demo code from this article can be found in theWasmtime Demos repository.
Peter is a software developer working on Wasmtime, Cranelift, and WASI at Mozilla.
GIPHY App Key not set. Please check settings