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.
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.
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.
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!
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.
<ItemGroup>
<PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM;
runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="9.0.0-*" />
</ItemGroup>
<ItemGroup>
<DirectPInvoke Include="lib" />
<NativeLibrary Include="lib.o" />
</ItemGroup>
<Target Name="CompileNativeLibrary" BeforeTargets="BeforeBuild">
<Exec Command="emcc -msimd128 -c lib.c -O2 -o lib.o" />
</Target>
The C# code looks like this. Note LibraryImport which uses a Source Generator to replace the older DllImport.
public partial class Example
{
[LibraryImport("lib")]
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.
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.
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.