A WebAssembly runtime implemented in C# for .NET
English | 日本語
Caution
DotWasm is currently in alpha. It is not suitable for production use and may undergo breaking changes without notice.
DotWasm is an experimental WebAssembly runtime written in C#. It is implemented in 100% pure C# and supports nearly all WebAssembly 3.0 proposals, including Wasm GC.
- Implemented entirely in pure C#
- Implements WebAssembly 3.0–equivalent proposals (excluding Threads)
- Nearly 100% passing the official test suite
- No dynamic code generation; compatible with NativeAOT
DotWasm requires .NET 10.0 or above.
dotnet add DotWasm;; example.wat
(module
(func $add (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add
)
(export "add" (func $add))
)using DotWasm.Encoding;
using DotWasm.Runtime;
var bytes = File.ReadAllBytes("example.wasm");
var module = WasmEncoding.Decode(bytes);
var store = new WasmStore();
var linker = new WasmLinker(store);
var instance = linker.Instantiate(module);
Span<WasmValue> results = new WasmValue[1];
instance.Invoke("add", [1, 2], results);
int result = results[0].I32;
Console.WriteLine(result); // 3You can obtain exported globals, memories, etc. from the Wasm side using the instance.TryGetExported**() family of methods.
;; example.wat
(module
(global $g (export "my_global") (mut i32) (i32.const 42))
)var instance = linker.Instantiate(module);
if (instance.TryGetExportedGlobal("my_global", out var global))
{
Console.WriteLine(global.Value.I32);
global.Value = 100;
}Through the WasmLinker API you can provide host functions, memories, and other imports to the Wasm side.
var addFunc = new HostFunction
{
Type = new FuncType
{
Parameters = [WasmTypes.I32, WasmTypes.I32],
Results = [WasmTypes.I32],
},
Delegate = static (ReadOnlySpan<WasmValue> args, Span<WasmValue> results) =>
{
var a = args[0].I32;
var b = args[1].I32;
results[0] = WasmValue.FromI32(a + b);
},
};
linker.RegisterFunction("env", "hostAdd", addFunc);
var memory = new MemoryInstance(1);
source.CopyTo(memory.Data);
linker.RegisterMemory("env", "hostMemory", memory);
var global = new GlobalInstance
{
Mutable = true,
ValueType = WasmTypes.I32,
Value = 43,
};
linker.RegisterMemory("env", "hostGlobal", global);You can pass opaque host references to the Wasm side as ExternalReference.
var gameObject = new GameObject("obj");
var global = new GlobalInstance
{
Mutable = true,
ValueType = WasmTypes.ExternRef(isNullable: false),
Value = ExternalReference.Create(gameObject),
};
linker.RegisterMemory("env", "object", global);DotWasm currently supports the following proposals:
| Proposal | Status | Note |
|---|---|---|
| WebAssembly 1.0 Core Spec | ✅ | |
| Mutable Globals | ✅ | |
| Sign-extension operators | ✅ | |
| Non-trapping Float-to-int Conversions | ✅ | |
| Multi-value | ✅ | |
| Bulk Memory Operations | ✅ | |
| Reference Types | ✅ | |
| SIMD | ✅ | |
| Component Model | ❌ | |
| Relaxed SIMD | ✅ | |
| Multi Memory | ✅ | |
| Tail Call | ✅ | |
| Extended Constant Expressions | ✅ | |
| Memory64 | ✅ | |
| Exception Handling | ✅ | |
| Typed Function References | ✅ | |
| GC | ✅ | |
| Threads | ❌ |
DotWasm is designed to minimize dynamic GC allocations, but it is not currently heavily optimized for execution speed. Because DotWasm avoids dynamic code generation to support AOT, its performance lags significantly behind JIT-based runtimes like Wasmtime.
Below are benchmark results for converting a 128x128 image to grayscale.
| Method | Mean | Error | StdDev |
|---|---|---|---|
| Wasmtime | 20.79 us | 0.520 us | 1.493 us |
| WaaS | 4,713.03 us | 94.249 us | 141.067 us |
| DotWasm | 11,470.14 us | 217.310 us | 213.428 us |
| WACS | 14,586.17 us | 285.006 us | 279.914 us |
The comparison used the following libraries:
As shown above, DotWasm is considerably slower. This is a known issue and is planned to be improved before a stable release.
