Jason Thorsness

github icon
linkedin icon
twitter icon
1Feb 21, 24

Better DotNet WebAssembly

Why WebAssembly?

WebAssembly provides a way to share functional examples of traditionally non-web languages to anyone with a browser. For some time, this has been awkward to do with C#. Most of the DotNet team’s focus has appeared to be on Blazor, which has a tight coupling in its tooling and documentation with UI components that just get in the way of straightforward invoke/return type examples.

Blazing a Trail

A previous incarnation of this site used Blazor despite it’s entanglement with UI features. To accomplish this, I used a Blazor project to build the WebAssembly library from C# code, then bootstrapped the library with Blazor.start and DotNet.invokeMethodAsync without using any of the Blazor UI features. In practice this approach produced very large files, especially with AOT compilation. I had to defer loading and add a loading indicator because the bootstrapping process took enough time to notice. Blazor has its place, but it was overkill for my use case.

The New Option

Luckily C# fans willing to try experimental features now have a lighter-weight option in NativeAOT-LLVM. This replaces the AOT backend used for Blazor with LLVM and produces modules that bootstrap using Emscripten. As it turned out, it worked beautifully for my use case, and the WASM + JS are less than 1.2 MiB combined and start immediately! I will repeat:

The WASM + JS are less than 1.2 MiB combined and start immediately!

The rest of this page demonstrates how I incorporated this approach into the site.

Let’s See the Results

When you drag this slider, the value is passed to the WebAssembly module, which outputs the value + 2. The C# component adds 1 and a C function called with P/Invoke adds 1 more.


Yes, this is a boring use of WebAssembly — but this can be any C# and C code!

Creating the WebAssembly module

Here’s an example project demonstrating the creation of a WebAssembly module using the NativeAOT-LLVM tool chain. The interesting bits are in the csproj: it pulls in the special compiler and includes some settings to also build and link a C file. C files can be used in this way for example to use WebAssembly’s SIMD features not yet directly supported by the C# Vector class.

  <PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM; runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="9.0.0-*" />

  <DirectPInvoke Include="lib" />
  <NativeLibrary Include="lib.o" />

<Target Name="CompileNativeLibrary" BeforeTargets="BeforeBuild">
  <Exec Command="emcc -msimd128 -c lib.c -O2 -o lib.o" />

The C# code looks like this. Note LibraryImport which uses a Source Generator to replace the older DllImport.

public partial class Example
    internal static partial int foo(int n);

    [UnmanagedCallersOnly(EntryPoint = "Answer")]
    public static unsafe int Answer(int n)
        return foo(n) + 1;

The C file is just three lines:

int foo(int input)
    return input + 1;

Building the module produces a wasm.wasm file and a wasm.js file as outputs ready to be used in a web site.

Using the WebAssembly module

The beauty compared to Blazor is how it can be incorporated with NextJS. It can be as simple as adding a <Script> tag.

<Script src="/wasm.js" />

Then to use it, just call the function on the global window object:

(self as any)?._Answer(parseInt(e.target.value));

That’s it! There’s more complexity depending on how you need to marshal data between JavaScript and the WebAssembly module, but in general Emscripten provides a lot of great helpers to make it easy.

Closing Thoughts

The Emscripten-based interface between JavaScript and WebAssembly is lower-level and more efficient than the one used by Blazor. I’m excited to convert all of the previous examples I had to this format. Going forward I hope the dotnet developers continue to invest in NativeAOT-LLVM and release it as a first-class option for .NET 9.0.