WACS is a pure C# WebAssembly Interpreter for running WASM modules in .NET environments, including Godot and AOT environments like Unity's IL2CPP.
WACS supports the latest standardized webassembly feature extensions including Garbage Collection and JSPI-like async execution.
- Packages
- Features
- WebAssembly Feature Extensions
- Component Model & WASI Preview 2
- Repository Layout
- Getting Started
- Installation
- Usage
- Integration with Unity
- Integration with Godot
- Interop Bindings
- Customization
- Performance
- WebAssembly Text Format (WAT / WAST)
- License
Versions auto-update from NuGet. See the CHANGELOG for release notes.
Renaming notice (0.11.0): the
WACS.WASIp1package has been renamed toWACS.WASI.Preview1to make room forWACS.WASI.Preview2/.Preview3under one prefix. The old id still restores (it's now a metapackage) but emits a build-time warning. See docs/MIGRATION_WASIp1_to_WASI.md for the one-shot sed.
CLI: install the unified
wacsglobal tool withdotnet tool install -g WACS.Cli. The legacyWACS.Transpiler(wasm-transpile) is deprecated and superseded — seeWacs.Console/Wacs.Console/README.mdfor the verb-based subcommand reference.
| Package | Version | Notes |
|---|---|---|
WACS |
Core interpreter runtime | |
WACS.Cli |
Unified wacs global CLI (run / build / aot / harness / bindgen / inspect / wast2json) |
|
WACS.Transpiler.Lib |
AOT transpiler: WASM → .NET IL, JIT or NativeAOT | |
WACS.ComponentModel |
Component runtime, canonical-ABI lift/lower, ComponentBridge |
|
WACS.ComponentModel.Parser |
AOT-safe component-binary parser split out of WACS.ComponentModel — what harness consumers reference under NativeAOT / IL2CPP |
|
WACS.ComponentModel.Bindgen.Lib |
Programmatic forward / reverse bindgen (used by wacs bindgen) |
|
WACS.ComponentModel.Harness.Lib |
IL emitter producing typed {World}Harness + I{World} assemblies from a WIT directory (the wacs harness verb's emit core) |
|
WACS.ComponentModel.Harness.Runtime |
Runtime primitives the emitted harness IL calls: MemoryHelpers, StringCoding, HarnessLoader.Load, Borrowed<T> host-handle table |
|
WACS.ComponentModel.Async.SourceGen |
Roslyn IIncrementalGenerator emitting [AsyncComponentHarness] / [AsyncExport] / [SyncExport] adapters at consumer build time |
|
WACS.HostBindings.Abstractions |
[WacsImport] attribute surface for typed host bindings |
|
WACS.HostBindings.SourceGen |
Source generator emitting IImports adapters from [WacsImport] |
|
WACS.WASI.Preview1 |
wasi_snapshot_preview1 host implementation |
|
WACS.WASI.Preview2 |
Typed host impls for every WASI 0.2.3 subsystem + IBindable composite |
|
WACS.WASI.Preview2.DependencyInjection |
One-call DI registration of the WASI Preview 2 bundle | |
WACS.WASI.Preview3 |
WASI Preview 3 host impls — canon-async dispatcher, stream / future / error-context handle tables. Pinned to wasi-0.3.0-rc-2026-03-15 |
|
WACS.WASI.Preview3.DependencyInjection |
DI registration of the Preview 3 bundle + per-runtime canon-async binder | |
WACS.WASI.Threads |
wasi:thread-spawn host adapter |
|
WACS.WASI.NN |
wasi-nn host bindings (WIT + WITX), backend-agnostic core | |
WACS.WASI.NN.DependencyInjection |
DI scope + WasiNNBundle for the transpiler's direct-link emit |
|
WACS.WASI.NN.OnnxRuntime |
wasi-nn ONNX backend (Microsoft.ML.OnnxRuntime, EP-selectable) | |
WACS.WASI.NN.OnnxRuntimeGenAI |
wasi-nn generative-LLM backend (Gemma / Llama / Qwen / Phi) | |
WACS.WASI.NN.LlamaSharp |
wasi-nn GGUF / llama.cpp backend (Metal on Apple Silicon) | |
WACS.WASI.NN.MLNet |
wasi-nn ML.NET-flavored ONNX backend | |
WACS.WASI.NN.TorchSharp |
wasi-nn TorchScript backend (PyTorch ecosystem) | |
WACS.WASI.NN.OpenVino |
wasi-nn OpenVINO backend (IR loader; macOS arm64 runtime bundled) | |
WACS.WASI.GFX |
wasi-gfx host bindings — graphics-context / surface / frame-buffer / webgpu (proposal at WASI Phase 2 — packages ship -preview) |
|
WACS.WASI.GFX.Webgpu |
wasi:webgpu@0.0.1 contract — typed host interfaces + canonical-ABI dispatcher |
|
WACS.WASI.GFX.DependencyInjection |
One-call DI registration + WasiPreview2GfxBundle composite for transpiler direct-link |
|
WACS.WASI.GFX.Silk |
Silk.NET/SDL + wgpu-native backend — drives all four wasi-gfx WIT packages against one SDL window |
- Unity Compatibility: Compatible with Unity 2021.3+ including AOT/IL2CPP modes for iOS.
- Godot Compatibility: Compatible with Godot Engine - .NET.
- Pure C# Implementation: Written in C# 9.0/.NET Standard 2.1. (No
unsafekeyword blocks, no raw pointer arithmetic — see notes onSystem.Runtime.CompilerServices.Unsafein the switch dispatcher.) - No Complex Dependencies: Uses FluentValidation and Microsoft.Extensions.ObjectPool as its only dependencies.
- WebAssembly 3.0 Spec Compliance: Passes the WebAssembly 3.0 spec test suite.
- First-class WAT / WAST: Pure-C# reader and writer for the WebAssembly text format. The
wacsCLI takes.watdirectly; the spec.wastsuite parses natively with no externalwast2json/ wabt dependency. - Magical Interop: Host bindings are validated with reflection, no boilerplate code required.
- Async Tasks: JSPI-like non-blocking calls for async functions.
- WASI: WACS.WASI.Preview1 provides a wasi_snapshot_preview1 implementation.
- Component Model & WASI Preview 2: Full canonical-ABI lift/lower with WIT ↔ C# bindgen (forward and reverse);
Wacs.WASI.Preview2ships default host implementations for every WASI 0.2.3 subsystem (cli/clocks/filesystem/http/io/random/sockets). - WIT-shaped harness:
wacs harness <wit-dir> -o <out.dll>emits a typed C# façade against a.witcontract;--harness/--wit-dirflags onwacs run/build/aotvalidate components against the harness's embedded contract before run / IL emit. Interpreter ({World}Harness.LoadFrom) and transpiler ({World}HarnessImpl : I{World}) implement the same interface — engine choice is a deployment detail, not an API divergence. Seedocs/WIT_HARNESS_APPROACH.md. - wasi-nn (machine learning): seven backend packages — ONNX Runtime, OnnxRuntime-GenAI (LLMs: Gemma / Llama / Qwen / Phi), LlamaSharp (llama.cpp / GGUF, Metal on Apple Silicon), ML.NET, TorchSharp, OpenVINO — chosen at runtime via
--bind. Seedocs/WASI_NN_USAGE.md. - wasi-gfx (graphics / GPU):
graphics-context/surface/frame-buffer/webgpuproposals against one SDL window via Silk.NET + wgpu-native.wacs run --wasi-gfx --windowed --call start <component>boots the bundled CPU + GPU hosts together. Packages ship-previewwhile the upstream proposal stabilizes. Seedocs/WASI_GFX_USAGE.md. - Stack Switching:
cont.new/resume/suspendsemantics implemented as the substrate the canon-async dispatcher builds on. Seedocs/stack-switching-architecture.md.
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.
WACS is based on the WebAssembly Core 3 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:
| Proposal | Features | |
|---|---|---|
| Phase 5 – Standardized | ||
| Branch Hinting | ✅ | |
| Bulk memory operations | ✅ | |
| Custom Annotation Syntax in the Text Format | ✅ | |
| Exception handling | exceptions | ✅ |
| Extended Constant Expressions | extended_const | ✅ |
| Fixed-width SIMD | ✅ | |
| Garbage collection | gc | ✅ |
| Import/Export of Mutable Globals | ✅ | |
| JavaScript BigInt to WebAssembly i64 integration | ✳️ | |
| JS String Builtins | ✳️ | |
| Memory64 | memory64 | ✅ |
| Multiple memories | multi-memory | ✅ |
| Multi-value | multi_value | ✅ |
| Non-trapping float-to-int conversions | ✅ | |
| Reference Types | ✅ | |
| Relaxed SIMD | relaxed_simd | ✅ |
| Sign-extension operators | ✅ | |
| Tail call | tail_call | ✅ |
| Typed Function References | function-references | ✅ |
| Phase 4 – Standardize | ||
| JS Promise Integration | jspi | ✳️ |
| Threads | threads | ✅ |
| Web Content Security Policy | 🌐 | |
| Phase 3 – Implementation | ||
| Stack Switching | ✅ |
Legend: ✅ supported · ❌ not yet · ✳️ conceptually supported (browser idiom) · 🌐 browser-only / N/A for non-web hosts
Grouping follows webassembly.org/features: Phase 5 is the combined standardized set (including finished proposals merged to the core spec) and Phase 4 is the active standardize queue. Phase assignments cross-checked against WebAssembly/proposals@1584fdf (2026-03-24) and WebAssembly/proposals/finished-proposals.md. Browser-idiom ✳️/🌐 results harnessed from wasm-feature-detect via the Feature.Detect test harness.
WACS implements the WebAssembly Component Model on the same parse / validate / link pipeline used for core modules. The WACS.ComponentModel package handles canonical-ABI lift/lower, WIT type emission, and cross-engine composition; WACS.WASI.Preview2 ships default host implementations for every WASI 0.2.3 subsystem.
- Component runtime — instantiation, resource handles (
own/borrow), variants, options, results, lists, records / tuples / flags. Both interpreted and AOT-transpiled execution. - Canonical ABI — full lift/lower including aggregate returns through retArea, UTF-8 / UTF-16 / Latin-1 string encodings, recursive variant arms, and instance-method resource calls.
- WIT ↔ C# bindgen — forward (
.wit→ typed C# interfaces tagged with[WitSource]) and reverse (transpiled.dll→ regenerated WIT + bindings) directions, available aswacs bindgenor programmatically viaWACS.ComponentModel.Bindgen.Lib. - Direct-linked imports — under
--engine transpiler, Preview 2 calls bypass the delegate dispatch table and inline straight into the generated CLR methods. - Cross-engine composition —
ComponentBridge.AsTypedInterface<T>/AsHostBundleadapters let interpreted and transpiled components compose against the same typed contract. - Contract validation —
Linker.Validate(WitContract.FromAssembly(...))cross-checks bound host implementations against the WIT contract embedded in the bindings assembly, catching drift at link time.
# Run a component with WASI Preview 2 + wasi-nn (ONNX) wired
wacs run my.component.wasm --wasip2 --wasi-nn -d ./models::/models
# Run a component that opens a window through wasi-gfx + wasi:webgpu
wacs run my.component.wasm --wasip2 --wasi-gfx --windowed --call start
# Generate C# bindings from a WIT package
wacs bindgen wit/cli/world.wit -o ./gen
# Reverse: regenerate bindings from a transpiled .dll
wacs bindgen app.dll -o ./regenThe CLI shorthands (--wasip2, --wasi-nn, --wasi-threads,
--wasi-gfx) load the matching host packages AND their
DependencyInjection siblings — both are needed because typed
[WitSource] interfaces and the SourceGen-shape impl classes the
transpiler instantiates live in sibling assemblies.
| Capability | Packages on load path |
|---|---|
--wasip2 |
WACS.WASI.Preview2 + WACS.WASI.Preview2.DependencyInjection |
--wasi-nn |
adds WACS.WASI.NN + WACS.WASI.NN.DependencyInjection + WACS.WASI.NN.OnnxRuntime (default bundled backend). Non-ONNX backends — MLNet, LlamaSharp, TorchSharp, OpenVino, OnnxRuntimeGenAI — ride on --bind <backend.dll>; the auto-pull resolver wires the typed-surface + DI siblings when the bound assembly's identity starts with Wacs.WASI.NN. |
--wasi-gfx |
adds WACS.WASI.GFX + WACS.WASI.GFX.DependencyInjection + WACS.WASI.GFX.Webgpu + WACS.WASI.GFX.Silk. One Silk.NET/SDL + wgpu-native backend drives all four wasi-gfx WIT packages (graphics-context / surface / frame-buffer / webgpu). Pair with --windowed when the guest opens surfaces — macOS swap-chain verified; headless wgpu works on Windows/Linux. The packages currently ship as NuGet -preview (wasi-gfx proposal is at WASI Phase 2) |
--wasi-threads |
adds WACS.WASI.Threads |
| Custom imports | --bind <Asm> (any IBindable) |
When a component imports multiple WASI packages, HostPackageResolver
auto-discovers the composite bundle (WasiPreview2NNBundle) that
forwards every [WitSource] interface through one CLR object, plus
the single WasiPreview2Resources registry so resource handles
share one namespace across all subsystems.
See docs/COMPONENT_CHAINING.md for the
full chaining model, the AppDomain-fallback contract for
dynamically loaded DI siblings, and how to add a new WASI subsystem
to the composite. Subsystem-specific deep-dives: docs/WASI_NN_USAGE.md
(every backend's --bind recipe + the GGUF / TorchScript / OpenVINO IR conventions);
docs/WASI_GFX_USAGE.md (threading model, swap-chain bridge, parity fixtures).
For a programmatic embedder doing the same as wacs run,
WasiPreview2RuntimeScope wraps the DI graph + composite-bundle
discovery + Linker prebind into one disposable:
using Wacs.Core.Runtime;
using Wacs.WASI.Preview2.DependencyInjection;
var runtime = new WasmRuntime();
using var wasi = new WasiPreview2RuntimeScope(
runtime,
preopens: new[] { ("./models", "/models") });
// wasi.Bundle → composite hostBundle (Preview2 + WASI.NN
// when WASI.NN.DependencyInjection is loadable)
// wasi.Resources → single resource registry shared across subsystems
// wasi.Runtime → runtime, with every wasi:* binding wired
// Now instantiate the component — see docs/COMPONENT_CHAINING.md
// for the full lift example.For per-subsystem IServiceCollection wiring (selective overrides,
ASP.NET-scoped execution, custom impls), see
Wacs.WASI/Wacs.WASI.Preview2/Wacs.WASI.Preview2/README.md.
For the interpreter-only embedding (one-line runtime extensions without DI):
using Wacs.Core.Runtime;
using Wacs.WASI.Preview2;
using Wacs.WASI.NN;
using Wacs.WASI.NN.OnnxRuntime;
using Wacs.WASI.NN.Types;
var runtime = new WasmRuntime();
runtime.UseWasiPreview2(b => b.EnableSockets());
runtime.UseWasiNN(b => b.AddBackend(GraphEncoding.ONNX, new OnnxBackend()));
// Or auto-discover any [WasiHostPackage]-tagged assembly already loaded:
runtime.AutoDiscoverHostPackages();| Package | Purpose |
|---|---|
WACS.ComponentModel |
Component runtime, canonical-ABI lift/lower, ComponentBridge cross-engine adapter |
WACS.ComponentModel.Bindgen.Lib |
Programmatic forward / reverse bindgen (used by wacs bindgen) |
WACS.WASI.Preview2 |
Typed host impls + WasiPreview2Host : IBindable composite + runtime.UseWasiPreview2(...) |
WACS.WASI.Preview2.DependencyInjection |
One-call DI registration of the bundle |
WACS.WASI.NN |
wasi-nn host bindings (WIT + WITX) + WasiNNHost : IBindable + source-gen [WitSource] interfaces |
WACS.WASI.NN.DependencyInjection |
WasiNNBundle, WasiPreview2NNBundle composite, concrete resource impls (Tensor, Graph, GraphExecutionContext, Error) |
WACS.WASI.NN.{OnnxRuntime,OnnxRuntimeGenAI,MLNet,LlamaSharp,TorchSharp,OpenVino} |
Backend implementations + parameterless IBindable adapters (WasiNNOnnxBindable / WasiNNOnnxGenAIBindable / WasiNNMLNetBindable / WasiNNLlamaSharpBindable / WasiNNTorchSharpBindable / WasiNNOpenVinoBindable) for --bind |
WACS.WASI.GFX |
wasi-gfx host bindings — graphics-context / surface / frame-buffer + WasiGfxHost : IBindable + source-gen [WitSource] interfaces (-preview — Phase 2 proposal) |
WACS.WASI.GFX.Webgpu |
wasi:webgpu@0.0.1 contract — host interfaces + canonical-ABI dispatcher (-preview) |
WACS.WASI.GFX.DependencyInjection |
WasiGfxBundle + WasiPreview2GfxBundle composite for the transpiler's direct-link path (-preview) |
WACS.WASI.GFX.Silk |
Silk.NET/SDL + wgpu-native backend — one backend drives all four wasi-gfx WIT packages against a single SDL window (-preview) |
WACS.WASI.Threads |
wasi:thread-spawn host adapter + runtime.UseWasiThreads() |
WACS.WASI.Preview1 implements all 47 wasi_snapshot_preview1 host
functions — args / environ, two-clock time, the full file-descriptor
and path surface (read / write / pread / pwrite / readdir / seek /
sync / allocate / fdstat / filestat / advise / renumber / link /
symlink / unlink / rename / create_directory / remove_directory /
readlink), poll_oneoff, proc_exit / proc_raise / sched_yield,
random_get, and the full sock_* surface (accept / recv / send /
shutdown). Network sockets are gated behind a default-off
WasiConfiguration.AllowNetworkSockets flag plus the requirement that
the embedder hand WACS pre-bound, pre-listening sockets via
PreopenedSockets — two layers of explicit consent before any guest
can do network IO.
Conformance is verified continuously against the official
WebAssembly/wasi-testsuite
fixtures (Rust + C + AssemblyScript). The runner lives at
Wacs.WASI/Wacs.WASI.Preview1/Wacs.WASI.Preview1.Test/ and runs as part of dotnet test in CI;
the test project's skip.json documents which conformance fixtures
are deliberately not yet asserting (each entry carries a reason).
The package was renamed from
WACS.WASIp1in 0.11.0. The old id still restores via a metapackage shim (with a build-time warning). See docs/MIGRATION_WASIp1_to_WASI.md.
Source projects are grouped into family folders by functional pillar. Each folder has its own README explaining the projects underneath.
| Family | Pillar |
|---|---|
| Wacs.Core/ | The interpreter — WasmRuntime, parsers, polymorphic + switch runtimes, full op set. Source generator for the switch dispatcher lives alongside. |
| Wacs.Transpiler/ | Ahead-of-time wasm → .NET IL transpiler. WACS.Transpiler.Lib is the programmatic API; the deprecated wasm-transpile CLI sits here too. |
| Wacs.Console/ | The unified wacs CLI (NuGet WACS.Cli) — wacs run / build / aot / harness / bindgen / inspect / wast2json, with --wasi / --wasip2 / --wasi-gfx / --wasi-nn / --wasi-threads / --harness / --wit-dir baking in host packages or contract validation. |
| Wacs.ComponentModel/ | Component-model runtime + WIT parser + canonical-ABI engine + wit-bindgen-wacs (forward & reverse C# bindgen) + the typed WIT-harness emitter (Harness.Lib IL emit + Harness.Runtime consumer surface) + the [AsyncComponentHarness] source generator. |
| Wacs.HostBindings/ | Attribute contract ([WacsImport], WacsHostMemory) + Roslyn source generator that emits dispatch glue for the transpiler's NativeAOT path. |
| Wacs.WASI/ | All WASI host implementations, organized by sub-family — Wacs.WASI.Preview1/, Wacs.WASI.Preview2/ (WASI 0.2.3), Wacs.WASI.Preview3/ (canon-async + stream/future bridges, pinned to wasi-0.3.0-rc-2026-03-15), Wacs.WASI.NN/ (wasi-nn + 6 backends: ONNX Runtime, OnnxRuntime-GenAI, ML.NET, LlamaSharp, TorchSharp, OpenVINO), Wacs.WASI.GFX/ (wasi-gfx: graphics-context / surface / frame-buffer / webgpu against one SDL window via Silk.NET + wgpu-native), Wacs.WASI.Threads/. |
| Wacs.Bench/ | Developer-only perf harnesses — bench, AOT bench, opcode profiler. Not packaged. |
Top-level non-Wacs.* projects:
| Folder | Role |
|---|---|
| Spec.Test/ | The wasm spec wast test runner + fixtures (Spec.Test/spec/ git submodule, Spec.Test/wasi/ for the wasi-testsuite submodule). The Spec.Test/Data/ subfolder holds the harness library types other test projects reference. |
| Feature.Detect/ | Generates the wasm-feature-detect probe table that the WebAssembly Feature Extensions section above is sourced from. |
The easiest way to use WACS is to add the package from NuGet
dotnet add package WACS
dotnet add package WACS.WASI.Preview1WACS.Cli is the unified WebAssembly toolchain — wacs runs,
compiles, and inspects WASM modules and components, backed by the
WACS interpreter and the AOT transpiler library. Installs as a
dotnet global tool:
dotnet tool install -g WACS.Cli
# Run (interpreter, default)
wacs run module.wasm
# Build to a .NET assembly
wacs build module.wasm -o module.dll
# Build directly to a self-contained NativeAOT native binary
wacs aot module.wasm -o module
# (transpile + scaffold + dotnet publish -p:PublishAot=true,
# all in one shot. Output is a real native exe — no .NET runtime
# required to run.)For WASI preview1 modules (CoreMark, anything built against wasi-libc):
wacs run coremark.wasm --wasi --engine transpiler
# AOT-transpile in-process, then run through CLR-native dispatch
# with WASI imports proxied through the interpreter (mixed-mode)
# Or precompile then run through `dotnet`:
wacs build coremark.wasm --wasi --emit-main -o coremark.dll
# Or go all the way to a NativeAOT binary:
wacs aot coremark.wasm --wasi -o coremark--wasi binds WACS.WASI.Preview1, forwards all wasi_snapshot_preview1
imports, shares memory with the runtime, and invokes the entry-point
export. For component-mode WASI Preview 2 (direct-linked, no
delegate hop), use --wasip2:
wacs run app.component.wasm --wasip2
wacs aot app.component.wasm --wasip2 -o app # all the way to NativeAOTCommand components don't need --call — wacs run --wasip2
auto-dispatches the canonical wasi:cli/run@<version>#run export,
matching wasmtime / jco / wasmer. Reactor modules
(_initialize without _start) get _initialize invoked before
any explicit --call. wasi:cli/exit.exit(N) propagates to the
host process exit code.
For wasi-nn (ONNX inference) and wasi-threads, the matching shorthands wire the bundled host packages:
wacs run my.component.wasm --wasip2 --wasi-nn -d ./models
wacs run my.threaded.wasm --wasi-threadsFor custom host imports (env.sayc, game bindings, etc.), use
--bind <asm> to load any IBindable host library. --bind
accepts file paths (Assembly.LoadFrom) or assembly names
(Assembly.Load); honored on every dispatch path including the
component paths. For programmatic embedding, reference
WACS.Transpiler.Lib
and use BindHostFunction + the built-in TranspiledModuleLoader
or a custom ImportDispatcher proxy.
Want to know which host packages a component needs? wacs inspect <comp.wasm> --imports enumerates the component-level imports
(instance + name + version) so you can predict what to wire before
running.
See Wacs.Console/Wacs.Console/README.md for the full
verb reference (run / build / aot / harness / bindgen /
inspect / wast2json), the direct-run shortcut, the
engine-choice trade-off, and concrete migrations from the
deprecated wasm-transpile.
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 buildBasic 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.CreateInvokerFunc<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}");
}- Window>Package Manager
- Click + Add package from git URL...
- Enter the package repo URL:
git@github.com:kelnishi/WACS-Unity.git
- Click Add
This will put the DLLs into your project. Import the WasmRunner sample to get started.
To manually add WACS to a Unity project, you'll need to add the following DLLs to your Assets directory:
- Wacs.Core.dll
- FluentValidation.dll
- Microsoft.Extensions.ObjectPool.dll
Set Player Settings>Other Settings>Api Compatibility Level to .NET Standard 2.1.
WACS is compatible with Godot Engine -.NET in C# projects.
- Add WACS via NuGet with the commandline or your IDE's NuGet tool.
- See sample/GodotSample.cs for loading wasm files.
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 WASI Preview 1:
//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);
// WASI Preview 1'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;
}If you'd like to customize the wasm runtime environment, I recommend downloading the full source for examples.
The Wacs.WASI.Preview1 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.
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 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.
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:
- Non-flushing branch jumps. We can leave operands on the stack if intermediate states don't interfere.
- Precomputed block labels. We can ditch the control frame's label stack entirely!
- Modern C# ObjectPools and ArrayPools minimize unavoidable allocation
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 Rewriting
i32.add
/ \
i32.const 5 i32.const 7
When enabled (runtime.SuperInstruction = true), WACS does a linear pass through the instruction sequences and rolls 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. The rewriter lives in Wacs.Core.Runtime.SuperInstruction (SuperInstructionRewriter.Rewrite) with
the synthetic instructions in Wacs.Core.Instructions.SuperInstruction.
How does this differ from executing the wasm instructions linearly with the WACS VM?
- No OpStack manipulation
- Values are passed directly without casting or boxing
- The CLR's implementation can use hardware more effectively (use registers instead of heap memory)
- Avoids instruction fetching and dispatch
Throughput win is roughly 20–25% on compute-bound microbenchmarks (see the
Running wacs section below for CoreMark numbers).
Gains are situational: linking instructions into a tree can't always be
determined across block boundaries, so WASM code heavy on function calls
or branches sees less benefit — the rewriter passes those sequences
through unaltered.
An alternative interpreter backed by a source-generated monolithic switch
over an annotated bytecode stream. Immediates are pre-decoded at instantiation
(no LEB128 at runtime), branch targets are resolved to absolute stream offsets,
and every reachable function is compiled eagerly when UseSwitchRuntime is
set before InstantiateModule. AOT-safe — no Reflection.Emit, no
DynamicMethod, build-time source generation only.
Opt-in at the API level:
var runtime = new WasmRuntime();
runtime.UseSwitchRuntime = true; // route wasm→wasm dispatch through the switch runtime
runtime.ExecContext.Attributes.UseSwitchSuperInstructions = true; // (optional) run the bytecode-stream super-instruction fuser
var modInst = runtime.InstantiateModule(module); // must happen AFTER UseSwitchRuntime is setSee the Running wacs section below for all CLI
combinations across the polymorphic, super-instruction, switch, and
AOT paths, plus benchmark numbers.
Architectural details live in
Wacs.Core/Wacs.Core/Compilation/SWITCH_RUNTIME.md.
The polymorphic runtime remains the canonical path; the switch runtime is
a parallel back-end on top of the same parse/validate/link pipeline.
Super-instruction threading squeezes more out of the interpreter, but each WASM instruction still goes through a dispatch
layer. The WACS.Transpiler package takes the next step: it compiles the module into a real .NET assembly at
ahead-of-time, producing native CLR methods the JIT can optimize like any other managed code.
Architecture
- IL emission. For every local WASM function, the transpiler walks the parsed instruction stream and emits CIL
directly into a
TypeBuilder, so the JIT sees ordinary static methods — no interpreter, no OpStack, no value boxing. - Typed CLR shapes. Module exports/imports surface as generated C# interfaces with WASM-qualified names
(
long FacSsa(long)), andref/GC types are emitted as native CLR classes with typed fields rather than boxedValue[]wrappers. - Dual SIMD paths.
v128ops have two implementations — a spec-compliant scalar reference path (--simd scalar, the default) and aSystem.Runtime.Intrinsics-backed path (--simd intrinsics). A third mode (--simd interpreter) falls back to the scalar SIMD inWacs.Core. - Transpile-time validation. A
CilValidatorverifies stack balance, typing, and branch targets as IL is emitted, so any invalid module trips at transpile time rather than as a runtimeInvalidProgramException. - Mixed-mode execution. Transpilation is opportunistic: any function the transpiler declines (e.g. very large bodies
under
--max-fn-size) falls back to the Wacs.Core interpreter for that function only, so the module still runs. - CLI + library. The
WACS.CliNuGet package installs thewacsdotnet global tool —wacs buildfor one-shot.wasm → .dllcompilation,wacs run --engine transpilerfor in-process AOT execution. The programmatic surface ships separately asWACS.Transpiler.Lib— reference it to drive transpilation and loading from inside a host. The library also includesTranspiledModuleLoaderfor seamless dynamic loading of saved.dlls withoutReflection.Emit. SeeWacs.Console/Wacs.Console/README.mdfor the full CLI verb reference. The legacyWACS.Transpiler/wasm-transpilepackage is deprecated in favor ofWACS.Clibut remains installable for migration purposes.
The transpiler is spec-equivalent to the interpreter on the WebAssembly 3.0 test suite (473/473), verified on macOS ARM64 and Linux x64.
Measured CoreMark throughput on a MacBook Pro M3 Max, .NET 8. The
native baseline is the EEMBC CoreMark C source built with
clang -O3 and run directly on the CPU (single-core, 600 000
iterations, three runs within 0.2% of each other). WACS numbers come
from running Wacs.Console/Wacs.Console/Data/coremark.wasm — the same C source,
compiled to WASM with clang -O3 via emscripten — through each mode.
| Mode | CoreMark iter/s | % of native |
|---|---|---|
wacs run (polymorphic interpreter, default) |
274 | 0.79% |
wacs run --super |
337 | 0.98% |
wacs run --switch |
358 | 1.04% |
wacs run --switch --super |
385 | 1.12% |
wacs run --engine transpiler (Reflection.Emit AOT) |
17 552 | 50.9% |
wacs build pre-compiled .dll loaded via TranspiledModuleLoader |
17 552 | 50.9% |
Native clang -O3 (same C source, no wasm) |
34 488 | 100% |
Ballpark comparison to other WASM runtimes on the same workload (not measured on this machine — pulled from published numbers; treat as positioning, not apples-to-apples):
- WACS AOT (
--engine transpiler) ≈ WAMR "fast JIT" / Wasmer. AOT-class wasm runtimes typically land at 50–70% of native C on CoreMark; WACS's 51% is in that band. - WACS interpreter modes (274–385 iter/s) are slower than Wasm3 (typically 4–6 k iter/s on M-series). Wasm3 is heavily tuned for CoreMark-like tight loops; WACS's interpreter prioritises Unity AOT compatibility and WASM 3.0 spec completeness over interpreter micro-benchmarks. Think "Python-for-WASM" speed rather than "Wasm3-class interpreter."
- All five modes are C#-on-CLR, so speedups over interpreted Python or IronPython for equivalent logic look larger than the CoreMark-vs-C ratios above would suggest.
Choosing a mode:
- Can you JIT? (Desktop, server,
dotnet run, Godot Mono) — usewacs run --engine transpiler. It's ~60× the interpreter and within ~2× of native C speed. - AOT-only target? (Unity IL2CPP,
PublishAot, iOS, full-AOT Mono) — pre-compile the.wasm → .dllon a JIT host withwacs build, ship the.dll, load it at runtime withWACS.Transpiler.Lib'sTranspiledModuleLoader. Same AOT speed, noReflection.Emitdependency at runtime. - Locked-down / policy-reviewed target, can't ship a pre-compiled
.dll? — usewacs run --switch --super. Build-time source-genSystem.Runtime.CompilerServices.Unsafeintrinsics only, no runtime codegen, nounsafekeyword blocks. ~1.4× over the polymorphic baseline.
- Maximum conservatism? — default polymorphic, or
wacs run --superfor a small safe boost. Pure managed, no source-gen, noUnsafeusage. Fine for cold-path scripting, UGC validation, or logic that doesn't dominate a frame budget — but don't put it in a hot inner loop.
The runtime numbers above are steady-state. The first invocation
also pays the .NET tier-1 JIT cost, and a transpiler embedding pays
its Reflection.Emit warm-up — together that's ~30–55 ms of cold
start out of the box on a tiny module. Two build-time switches knock
that down dramatically; pick the row that matches what your embedding
ships:
| tier | build flag | cold to first call (fib(100), 242-byte module) | trade-off |
|---|---|---|---|
| default | plain dotnet publish -c Release |
35–55 ms | Smallest deploy artifact, every runtime works. JIT warmup dominates. |
| better | -p:PublishReadyToRun=true --self-contained |
16–30 ms | Larger publish (~70 MB self-contained). All four runtimes still work. ~1.5–2.8× faster cold; the --switch interpreter sees the biggest win (45 → 16 ms) because R2R skips the giant prefix-split-switch tier-1 JIT. |
| best (interpreter-only) | <PublishAot>true</PublishAot> in csproj |
0.3–1.0 ms | Single ~10 MB native binary, no JIT in the process. Excludes transpiler and Wacs.ComponentModel (both need runtime IL emission). --switch interpreter hits 319 µs cold. |
| best (transpiler ok) | wacs build then TranspiledModuleLoader.Load under R2R |
~1 ms | Run the transpile once at build time and ship the .dll. Same R2R caveats, plus a one-time ~100 ms transpile cost off the runtime path. Steady-state matches the in-process transpiler. |
For the absolute cold-start floor — both startup and steady-state on
a single machine — there's a fifth path worth knowing about: link the
transpiled .dll statically into a NativeAOT-published consumer (501 µs
cold and ~100 ns per fib(100) call, in a 4.3 MB native binary). The
wacs aot verb automates the whole pipeline end-to-end today
(wacs aot module.wasm -o module → transpile → scaffold consumer
csproj → dotnet publish -p:PublishAot=true → copy native binary
out). Full breakdown, methodology, and the four-runtime ×
three-build-flag matrix in docs/COLDSTART.md.
wacs is the reference CLI — verb-based subcommand layout (run
/ build / aot / harness / bindgen / inspect /
wast2json) with a direct-run shortcut so a bare
wacs file.wasm defaults to run. All examples assume the
WACS.Cli global tool is installed (dotnet tool install -g WACS.Cli); the dotnet run --project Wacs.Console -c Release --
form works identically when running from a checkout.
# Default (polymorphic interpreter)
wacs run Wacs.Console/Wacs.Console/Data/coremark.wasm
# Polymorphic + super-instruction rewriter
wacs run --super Wacs.Console/Wacs.Console/Data/coremark.wasm
# Source-generated switch runtime
wacs run --switch Wacs.Console/Wacs.Console/Data/coremark.wasm
# Switch runtime + bytecode-stream super-instruction fuser
wacs run --switch --super Wacs.Console/Wacs.Console/Data/coremark.wasm
# AOT transpile in-process and run through the JITted code
wacs run --engine transpiler Wacs.Console/Wacs.Console/Data/coremark.wasm
# AOT with hardware SIMD intrinsics + persist the .dll for re-loading
wacs build --simd intrinsics -o out.dll Wacs.Console/Wacs.Console/Data/coremark.wasm
wacs run --engine transpiler --simd intrinsics Wacs.Console/Wacs.Console/Data/coremark.wasmInvoking a specific export with arguments (applies across every mode):
wacs run --call fib Wacs.Bench/Wacs.Bench/fib.wasm -- 10
# Args after `--` are parsed per the function's wasm signature.Engine + interpreter cheatsheet:
| Flag | Effect |
|---|---|
| (default) | Polymorphic virtual-dispatch interpreter. Baseline, canonical. |
--super |
Enable super-instruction fusion on whichever runtime ends up executing — the polymorphic block-level rewriter, and (when paired with --switch) the switch runtime's bytecode-stream fuser. |
--switch |
Source-generated monolithic-switch interpreter. Build-time code generation (Roslyn) + System.Runtime.CompilerServices.Unsafe intrinsics — see note below. |
--engine transpiler |
AOT transpile the WASM module to .NET IL and run through the generated code. Requires Reflection.Emit at runtime — see note below. |
--simd {scalar|intrinsics|interpreter} |
SIMD strategy for the transpiler engine. |
--no-tail-calls |
Drop the CIL tail. prefix on the transpiler engine (debugging only). |
--max-fn-size N |
Skip functions larger than N instructions in the transpile pass. |
--data-storage {compressed|raw|static} |
How data segments are embedded in the saved assembly (build verb). |
Build verb (for shipping a pre-compiled .dll):
# Single-file → .dll
wacs build app.wasm -o app.dll
# Multi-input ModuleLinker composition (siblings land at <basename>.dll)
wacs build a.wasm b.wasm -o b.dll
# Component with WASI Preview 2 + bake a runnable Main entry-point
wacs build app.component.wasm --wasip2 --emit-main \
--entry-point greet -o app.dllInspect verb (diagnostics + format conversion):
wacs inspect module.wasm # stats summary (default)
wacs inspect module.wasm --exports
wacs inspect module.wasm --imports
wacs inspect app.component.wasm # component metadata
# WAT ↔ wasm round-trip:
wacs inspect module.wasm --dump-wat # binary → WAT (stdout)
wacs inspect module.wat --dump-wasm # WAT → binary (stdout pipe)
wacs inspect module.wat --dump-wasm --output-dir out/ # write out/module.wasmRound-trips preserve function $names via the standard name
custom section and preserve WAT comments / (@…) annotations via
a wacs.trivia custom section. The wacs.trivia section is
WACS-specific (no spec) and ignored by other engines — strippable
when shipping a release binary.
Spec-test bundle (wast2json verb):
wacs wast2json forward.wast -o out/
# wrote out/forward.json (5 commands, 1 side-car modules)
wacs wast2json i32.wast -o out/
# wrote out/i32.json (460 commands, 86 side-car modules)Mirrors wabt's wast2json output shape — one .json file
listing commands plus one side-car .wasm per referenced module.
Consumable by any spec-test runner that reads the format,
including WACS's own Spec.Test (previously required wast2json
from the upstream wabt toolchain).
Sampled CoreMark performance (M3 Max, .NET 8, Wacs.Console/Wacs.Console/Data/coremark.wasm, default 6000 iterations; single run each):
| Mode | CoreMark (iter/s) | Relative |
|---|---|---|
wacs run (polymorphic) |
274 | 1.00× |
wacs run --super |
337 | 1.23× |
wacs run --switch |
358 | 1.31× |
wacs run --switch --super |
385 | 1.40× |
wacs run --engine transpiler |
17 552 | 64× |
The AOT path emits ordinary .NET methods that the CLR JIT optimizes as native code, so the ~64× jump over the fastest interpreter mode is the gap between "dispatch + pop/push per op" and "inline register arithmetic." Expect similar ratios on any compute-bound workload; IO-bound / WASI-heavy workloads see a smaller lift because WASI calls still bridge back to the interpreter's host-function machinery.
"AOT" here means ahead-of-time compilation of the WASM module, not that the WACS runtime itself is AOT-safe.
wacs run --engine transpilerusesSystem.Reflection.Emitat runtime to synthesize a dynamic .NET assembly containing the transpiled module. That's incompatible with environments that disable dynamic code: Unity IL2CPP, .NET Native AOT (PublishAot=true), iOS, Mono AOT-only builds, etc. On those platforms use one of the three interpreter modes instead — all of them (including the source-generated--switchruntime) are fully AOT-compatible. If you need native-class speed in an IL2CPP target, pre-compile the.wasm → .dllon a JIT-capable host withwacs buildand ship the resulting assembly — the saved.dllruns withoutReflection.EmitviaWACS.Transpiler.Lib'sTranspiledModuleLoader.
--switchnotes for restrictive targets. The switch runtime is AOT-compatible — it uses no runtime codegen — but two implementation choices are worth flagging in case your environment forbids them:
- Build-time source generation (Roslyn). The dispatcher method is emitted by a Roslyn incremental source generator (
Wacs.Compilation) during the build ofWacs.Core. The shipped.dllcontains ordinary IL — there's no runtime Roslyn, no dynamic code. If your build system runs Roslyn-based compilation (the standard SDK path does), you're fine. If you consume WACS as a pre-built NuGet package, the generation already happened upstream and the binary is self-contained.System.Runtime.CompilerServices.Unsafeintrinsics. The hot loop usesUnsafe.Add/Unsafe.ReadUnalignedfor inline array indexing and LEB-free immediate reads — notunsafepointer blocks, notfixedstatements.System.Runtime.CompilerServices. Unsafeis part of the BCL on every .NET 8+ target (including IL2CPP, PublishAot, iOS, trimmed assemblies). If your policy reviews flag the keyword name, note that these are JIT/AOT intrinsics, same family asMemoryMarshal— no raw pointer math, no unverifiable IL.Both the polymorphic (default) and the polymorphic-super (
--super) modes are pure managed code with no source-gen and no Unsafe usage — pick those if you need the most conservative surface. Expect ~25–40% lower throughput vs--switch --superon compute-bound workloads.
WACS ships a pure-C# reader and writer for the WebAssembly text format.
No wabt / wast2json toolchain is required — .wat modules and
.wast spec scripts feed directly into the same Module and runtime
pipeline the binary parser uses.
.watmodules. The full text grammar of WebAssembly 3.0, including every enabled proposal (GC structs / arrays / sub / rec groups, typed function references, tail-call, exception handling, SIMD, relaxed SIMD, multi-memory, memory64, threads/atomics, annotations). Abbreviations (inline imports/exports, implicit typeuse, folded instructions, anonymous blocks) are desugared into the sameModuleshape the binary parser produces..wastscripts.module,register,invoke,get, and everyassert_*form — including(module binary …)and(module quote …)— producing aScriptCommand[]aligned with the existing spec-runner command shape.- Writer.
TextModuleWriter.Write(module)emits canonical, parser-friendly WAT that round-trips back through the text parser and produces a structurally equivalentModule. - AOT-safe. No runtime
Reflection.Emit. Reflection over[OpCode("mnemonic")]attributes happens once at static-ctor time to build the mnemonic→ByteCode lookup; everything else is plain managed code.PublishAot=truebuilds continue to pass.
The text pipeline lives under Wacs.Core.Text:
Lexer/Token/SExpr/SExprParser— a WAT-specific tokenizer (line / block comments, string escapes, annotations, quoted identifiers with full\XX/\u{…}UTF-8 decoding) feeding a lightweight s-expression tree. No WASM semantics at this layer.Mnemonics— aFrozenDictionary<string, ByteCode>built once by reflecting over the[OpCode(...)]attributes already present on every real opcode enum field (OpCode,GcCode,ExtCode,SimdCode,AtomCode). Parse and render share the same source of truth, so a mnemonic added in one direction is automatically visible in the other.TextModuleParser— two-pass section driver. Pass 1 pre-declares names and pre-populates the type section (including rec-group flattening for GC); pass 2 resolves$namereferences, synthesizes inline typeuses with rec-isolated dedup, and produces the sameModuleobject the binary parser produces. Each instruction's text-specific immediate decoding lives in a per-classParseTexthook co-located with the binaryParseoverride — no parallel hierarchy of text decoders.TextScriptParser—.wast→ScriptCommand[]. Nested(module …)forms inside assertions are re-parsed through the same module parser;(module binary …)dispatches to the binary parser on an in-memory stream.TextModuleWriter— canonical round-trip emitter. Distinct from the existingModuleRenderer.RenderWatToStream(debug/display variant with stack annotations and(;id;)comments), which is kept for inspection use.
CLI (wacs). Pass a .wat path exactly like a .wasm path
— wacs auto-detects via the file extension:
# Run a text-format module through the polymorphic interpreter
wacs run path/to/module.wat
# Round-trip a binary module out as parser-friendly WAT
# (writes module.wat to the chosen --output-dir via TextModuleWriter)
wacs inspect path/to/module.wasm --dump-wat --output-dir .
# Re-run the emitted .wat through the interpreter to confirm round-trip
wacs run path/to/module.watEvery back-end (--super, --switch, --engine transpiler, …)
works identically on .wat input since the parser produces the
same Module object the binary path does.
Library. One entry point, drops into any existing WACS flow:
using Wacs.Core;
using Wacs.Core.Text;
using var fs = new FileStream("module.wat", FileMode.Open);
Module module = TextModuleParser.ParseWat(fs);
// Or from a string:
Module m2 = TextModuleParser.ParseWat(File.ReadAllText("module.wat"));
// Emit canonical, round-trip WAT:
string wat = TextModuleWriter.Write(module);The Wacs.Core.Test xUnit project runs two gates across the full
WebAssembly 3.0 spec suite (Spec.Test/spec/test/core/*.wast):
SpecWastSmokeTests— every.wastfile in the spec suite parses without error. 120 / 120 files. No skipped files. TheSkipListis empty.SpecWastEquivalenceTests— every module embedded in a spec script parses through the text parser and through the binary parser (via(module binary …)orwast2json-produced.wasmsidecars), and the twoModuleobjects are compared for structural equivalence (types, imports, functions, tables, memories, globals, exports, element segments, data segments, custom sections, plus instruction streams including preservedtry_tableshapes and rec-group layouts). 3457 / 3457 modules match.
The text parser is held to the same wasm-3.0 bar as the runtime: GC,
typed function references, exception handling, tail-call, SIMD,
relaxed SIMD, multi-memory, memory64, threads/atomics, and
annotations all parse to structurally identical Module objects
regardless of which parser built them.
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!
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! ⭐