# While Microsoft Is Busy Learning Rust, I'm Rebuilding .NET for WASI

> Source: <https://dev.to/zionsati/while-microsoft-is-busy-learning-rust-im-rebuilding-net-for-wasi-52fk>
> Published: 2026-09-23 12:36:12+00:00

Apparently [Stephen Toub](https://x.com/stephentoub) is busy with Rust now, so I've decided to cover his .NET shift.

Okay, that's a tongue in cheek obviously. Toub recently wrote about leading [the migration of GitHub Copilot's agent runtime from TypeScript to more than 800,000 lines of production Rust](https://github.blog/ai-and-ml/generative-ai/migrating-the-github-copilot-runtime-to-rust-using-copilot/), with AI agents writing most of the code. But the timing made me laugh. One of the engineers most closely associated with modern .NET is moving a major Microsoft-owned runtime to Rust. Meanwhile, I'm over here building an independent .NET compiler, CoreLib, and runtime for WASI.

C# syntax has always felt like driving a luxury car to me. I love writing in it. But for years I also wanted something I could get from C++: a native binary without having to ship the conventional .NET runtime alongside it.

In 2008, fourteen years before Native AOT shipped with .NET 7, I made my first attempt at this idea and wrote my own generational concurrent garbage collector and coupled it with Borland's C++ compiler. In a later project, I built a managed programming model on top of C++ using template metaprogramming, ran Boehm GC in precise mode, and used GCC plugins to generate the GC maps. I was already chasing the same combination: the productivity and memory model I liked from C#, with native output and control over the runtime.

The term **AOT** is overloaded here. In 2008, Microsoft's .NET Framework couldn't publish this kind of standalone native executable. NGen existed, but it installed precompiled machine code into the computer's native image cache for the CLR to load. Mono had an AOT mode too, but its output still ran with the Mono runtime. Neither gave me the deployment model I wanted: one native executable containing the runtime support it needed, with no CLR or virtual machine to install on the target system. That is what .NET developers now call Native AOT. I didn't have that name for it at the time. I just wanted the binary.

Today my answer is [NetWasm](https://www.netwasm.com) ([GitHub Repo](https://github.com/zion-sati/NetWasm)): an independent CIL-to-WebAssembly compiler, a new CoreLib, and a runtime designed around what the program actually uses. It doesn't compile C# into C++; it takes a different route to the same ambition. The output includes a runtime and a garbage collector, built for the target and linked according to what the program needs.

Here's the deliberately boring result that made me think the idea was worth pursuing:

```
Console.WriteLine(42);
```

A clean Release build produces an **88,419-byte, uncompressed WASI Preview 2 component**. That file includes the program, the pieces of CoreLib and runtime it needs, and a precise Boehm garbage collector. It doesn't include the Wasm host. It's one small-program measurement, not a claim that every C# application will be 88 KB or that a collection cycle happened while printing `42`. The [exact setup and reproduction notes are here](https://github.com/zion-sati/NetWasm/blob/main/docs/size-and-methodology.md).

The number is interesting. The choices that make it possible are the actual project.

While working on [EffinDOM](https://github.com/zion-sati/EffinDOM) and [Fui](https://github.com/zion-sati/fui-rs), Kotlin/Wasm brought that old question back into focus. It has a path to WebAssembly that doesn't have to take the JVM along for the ride. I wanted to see what a similarly deliberate path for C# could look like.

Microsoft has done real work on AOT and trimming. But when the foundations of a runtime were designed for different environments, making the output smaller can only take you so far. I wanted to change the foundations: start with a small runtime, define the supported surface, and make features pay their way into the final binary.

That meant I couldn't simply reuse CoreLib and swap out the last compilation step. I wrote a separate one, and built the compiler and runtime together.

Roslyn compiles C# to CIL, the .NET intermediate language. NetWasm takes that CIL, follows the code the application can reach, and compiles it to WebAssembly. It ships its own CoreLib and runtime with the output. The resulting application doesn't need CoreCLR, Mono, `dotnet.js`, or a pile of managed application DLLs alongside it.

Today's supported language is **C#**, including C# 15 when you select the .NET 11 SDK. “It reads CIL” doesn't mean every F# or VB.NET application, or every NuGet package, works today.

The main target is a **WASI Preview 2 component**. Think of WASI as the host interface: the component gets capabilities from the environment that runs it. It can run standalone with a suitable Wasm host. The [playground](https://playground.netwasm.com/) is another way to try it: the compiler runs as Wasm *in your browser*, so the code you enter is compiled locally rather than sent to my compilation server.

NetWasm has no runtime reflection and **no runtime type-name catalog**. There's no switch that puts full names and namespaces back into the output. `dynamic` and reflection are intentional design boundaries, and I don't plan to support them in the future. Keeping that machinery would change the cost model for every small app, including apps that never ask for it.

I strongly believe the future of .NET won't need them. Source generation, static analysis, and closed-world compilation can move work to build time and generate exactly what an application needs. NetWasm is designed for that future rather than carrying a dynamic runtime model forward indefinitely.

The programming model is closer to *C++ with a garbage collector* than to shrinking a conventional desktop .NET application. Given my earlier C++ and Boehm GC work, that's deliberate. You still write C#. You can still use things like LINQ. But a feature doesn't get a free ride just because it's familiar from desktop .NET.

Time zone data, for example, is opt in. Serialization is a good example of the tradeoff too: source-generated JSON support is there when you need typed serialization, and you pay for it when you use it. If all you need is a lightweight parsed dictionary, you can choose that instead.

This isn't limited to toy programs. There's a [separate libraries repository](https://github.com/zion-sati/NetWasm.Libraries) with ports covering areas such as LINQ, HTTP JSON, configuration, options, dependency injection, and caching. There's also TUnit support through `dotnet test`. I'm building toward a useful ecosystem without making a clean `Console.WriteLine(42)` carry the whole ecosystem around.

With the .NET SDK installed:

```
dotnet new install NetWasm.Templates
dotnet new netwasm-app -n HelloNetWasm
cd HelloNetWasm
dotnet restore
dotnet run
dotnet publish -c Release -o publish/local
```

If you have Wasmtime installed, you can run the published component directly:

```
wasmtime run publish/local/HelloNetWasm.wasm
```

That prints `42`. You can also [open the browser playground](https://playground.netwasm.com/) without installing anything.

NetWasm is early (I already have a known list of bugs I need to fix). Expression trees, managed threading, and the remaining WASI surface—including general managed `File`/` FileStream`/` Directory` APIs and raw sockets—aren't supported yet. They are on the roadmap. `dynamic` and runtime reflection are different: they are intentional non-goals. The [support inventory](https://github.com/zion-sati/NetWasm/blob/main/docs/support-status.md) is the place to check before trying a package. The compiler and tooling are source available under the [NetWasm Community License](https://github.com/zion-sati/NetWasm/blob/main/docs/licensing.md); the CoreLib, runtime, templates, and ported libraries are MIT licensed.

I'd also like to explore WasmGC as a future collector backend. For now, the precise collector is part of the result you can build and run today.

What excites me about Mono's history is that it showed another implementation could change what .NET was *for*. I wanted that room to experiment again: a .NET subset built for small, portable WebAssembly components, with a different answer to what should be included by default.

If you have a small C# workload you'd like to run as a WASI component, [try NetWasm on GitHub](https://github.com/zion-sati/NetWasm) or [paste it into the playground](https://playground.netwasm.com/). I'm especially interested in what breaks and which missing capability matters to you.
