Home

Awesome

WACS (C# WebAssembly Interpreter)

wasm wast spec Platform License NuGet Downloads

Overview

WACS is a pure C# WebAssembly Interpreter for running WASM modules in .NET environments, including AOT environments like Unity's IL2CPP.

WACS supports the latest standardized webassembly feature extensions including Garbage Collection and JSPI-like async execution.

Wasm in Unity

Table of Contents

Features

WACS is for mobile games.

Because WebAssembly is memory-safe and can be ahead-of-time validated, WACS makes it possible to build safe, verifiable UGC, DLC, or plugin systems that include executable logic.

WebAssembly Feature Extensions

WACS is based on the WebAssembly Core 2 + gc spec and passes the associated test suite.

Support for all standardized extensions is listed below.

Harnessed results from wasm-feature-detect as compares to other runtimes:

ProposalFeatures
Phase 5
JavaScript BigInt to WebAssembly i64 integration<span title="Browser idiom, but conceptually supported">✳️</span>
Bulk memory operations
Extended Constant Expressionsextended_const
Garbage collectiongc
Multiple memoriesmulti-memory
Multi-valuemulti_value
Import/Export of Mutable Globals
Reference Types
Relaxed SIMDrelaxed_simd
Non-trapping float-to-int conversions
Sign-extension operators
Fixed-width SIMD
Tail calltail_call
Typed Function Referencesfunction-references
Phase 4
Exception handlingexceptions
JS String Builtins
Memory64memory64
Threadsthreads
Phase 3
JS Promise Integrationjspi<span title="Browser idiom, but conceptually supported">✳️</span>
Type Reflection for WebAssembly JavaScript APItype-reflection<span title="Browser idioms, not directly supported">🌐</span>
Legacy Exception Handlingexceptions
Streaming Compilationstreaming_compilation<span title="Browser idioms, not directly supported">🌐</span>
This table was generated with the Feature.Detect test harness.

Getting Started

Installation

The easiest way to use WACS is to add the package from NuGet

dotnet add package WACS
dotnet add package WACS.WASIp1

If you prefer to build WACS from source, you can clone the repo and build it with the .NET SDK:

git clone https://github.com/kelnishi/WACS.git
cd WACS
dotnet build

Usage

Basic usage example, how to load and run a WebAssembly module:

using System;
using System.IO;
using Wacs.Core;
using Wacs.Core.Runtime;

//Create a runtime
var runtime = new WasmRuntime();

//Bind a host function
//  This can be any regular C# delegate.
//  The type here will be validated against module imports.
runtime.BindHostFunction<Action<char>>(("env", "sayc"), c =>
{
    System.Console.Write(c);
});

//Load a module from a binary file
using var fileStream = new FileStream("HelloWorld.wasm", FileMode.Open);
var module = BinaryModuleParser.ParseWasm(fileStream);

//Instantiate the module
var modInst = runtime.InstantiateModule(module);

//Register the module to add its exported functions to the export table
runtime.RegisterModule("hello", modInst);

//Get the module's exported function
if (runtime.TryGetExportedFunction(("hello", "main"), out var mainAddr))
{
    //For wasm functions you can expect return types as Wacs.Core.Runtime.Value
    //  Value has implicit conversion to many useful primitive types
    var mainInvoker = runtime.CreateInvoker<Func<Value>>(mainAddr);
    
    //Call the wasm function and get the result
    //  Implicit conversion from Value to int
    int result = mainInvoker();
    
    System.Console.Error.WriteLine($"hello.main() => {result}");
}

Integration with Unity

With Unity Package Manager

  1. Window>Package Manager
  2. Click + Add package from git URL...
  3. Enter the package repo URL:
git@github.com:kelnishi/WACS-Unity.git
  1. Click Add

This will put the DLLs into your project. Import the WasmRunner sample to get started.

Manually

To manually add WACS to a Unity project, you'll need to add the following DLLs to your Assets directory:

Set Player Settings>Other Settings>Api Compatibility Level to .NET Standard 2.1.

Interop Bindings

WACS simplifies host function bindings, allowing you to easily call .NET functions from WebAssembly modules. This allows seamless communication between your host environment and WebAssembly without boilerplate code. Similarly, calling into wasm code is done by generating a typed delegate.

Example from WASIp1:

//Alias your types for readability
using ptr = System.Int32;

//WACS can bit-convert types like Enums and explicit layout structs
[WasmType(nameof(ValType.I32))]
public enum ErrNo : ushort
{
    Success = 0,
    ...
}

//Supply the delegate definition when binding
//  ExecContext is an optional first parameter for Memory and Stack manipulation
runtime.BindHostFunction<Func<ExecContext,ptr,ptr,ErrNo>>(
   (module, "args_get"), ArgsGet);

// WASIp1's args_get
public ErrNo ArgsGet(ExecContext ctx, ptr argvPtr, ptr argvBufPtr)
{
    var mem = ctx.DefaultMemory;
            
    foreach (string arg in _config.Arguments)
    {
        // Copy argument string to argvBufPtr.
        int strLen = mem.WriteUtf8String((uint)argvBufPtr, arg, true);
                
        // Write pointer to argument in argvPtr.
        mem.WriteInt32(argvPtr, argvBufPtr);

        // Update offsets.
        argvBufPtr += strLen;
        argvPtr += sizeof(ptr);
    }

    return ErrNo.Success;
}

Customization

If you'd like to customize the wasm runtime environment, I recommend downloading the full source for examples.

The Wacs.WASIp1 implementation is a good starting point for how to set up your own library of bindings. It also contains examples of more advanced usage like binding multiple return values and full operand stack access.

The Spec.Test project runs the wasm spec test suite. This also contains examples for binding other runtime environment objects like Tables, Memories, and Variables.

Custom Instruction implementations can be patched in by replacing or inheriting from SpecFactory.

Performance

WACS is a bytecode (wasm) interpreter running on a bytecode interpreted (or JIT'd) language (CIL/CLR). This is, as you can imagine, not a recipe for raw performance. However, recognizing this dynamic allows us to make certain optimizations to achieve performance closer to other languages in other VMs.

The Spec-Defined Implementation

The Wasm Virtual Machine is a stack machine. This means that instructions produce operands, place them on the stack, and then other instructions consume them by popping them from the stack. WACS uses a pre-allocated linear stack for more register-like performance. However, even a virtualized stack is costly to manage as the CLR will still need to manage memory and objects at its boundaries. To optimize further, we'll need to opportunistically use register-machine semantics by swapping out equivalent operations.

In-Memory Transpiling

Here's where we break WASM semantics and go off-road to claw back some performance. A linear list of WASM instructions can be inverted into an expression tree. The WAT text format supports both the linear and the tree structure; they are conceptually equivalent. We'll use this similarity by applying the transform to the binary AST. Take for example, this sequence:

i32.const 5 <- Pushes 5 onto the stack

i32.const 7 <- Pushes 7 onto the stack

i32.add <- Pops 7, Pops 5, Pushes 12 onto the stack

For a sequence representing 5+7, this is performing potentially 8+ function calls, multiple Value.ctors, memory bounds checks, etc. All this, not even including the actual computation (+). Knowing this, we have an alternative.

Expression Tree Compilation

       i32.add
     /         \
i32.const 5  i32.const 7

If enabled, WACS will do a linear pass through the instruction sequences and roll up interdependent instructions into directed acyclic graphs. Instructions are replaced with functionally equivalent expression trees InstAggregate. The new aggregate instructions are in-memory and are implemented with pre-built relational functions. Ultimately, these instructions are compiled by the dotnet build process into bytecode to be run by the runtime. Thus, at runtime aggregate wasm instruction sequences map to pre-compiled implementations (semi-transpiled).

How does this differ from executing the wasm instructions linearly with the WACS VM?

In my testing, this leads to roughly 60% higher instruction processing throughput (128Mips -> 210Mips). These gains are situational however. Linking of the instructions into a tree cannot 100% be determined across block boundaries. So in these cases, the transpiler just passes the sequence through unaltered. So WASM code with lots of function calls or branches will see less benefit.

Prebaked Block Labels

The design of the WASM VM includes block labelling for branch instructions and a heterogeneous operand/control stack. WACS uses a split stack that separates operands and control. This enables us to make some key optimizations:

Optimization is an ongoing process and I have a few other strategies yet to implement.

My plan for 1.0 includes:

Expected Runtime Performance

When built in AOT or Release mode, my benchmarks show WACS runs between 2~10% native throughput for benchmark programs like coremark. This is roughly on par with interpreted-only Python or about ~25% of an equivalent program written in C# on dotnet.

Roadmap

The current TODO list includes:

Sponsorship & Collaboration

I built and maintain WACS as a solo developer.

If you find it useful, please consider supporting me through sponsorship or work opportunities. Your support can help me continue improving WACS to make WebAssembly accessible for everyone.

Sponsor me or connect with me on LinkedIn if you're interested in collaborating!

License

WACS is distributed under the Apache 2.0 License. This permissive license allows you to use WACS freely in both open and closed source projects.


I would love for you to get involved and contribute to WACS! Whether it's bug fixes, new features, or improvements to documentation, your help can make WACS better for everyone.

Star this project on GitHub if you find WACS helpful!