WebAssembly for .NET: build, run and convert WASM without an interpreter
Create, read, modify, write and execute WebAssembly (WASM) files from .NET-based applications.
At a glance
- What is it?
- The WebAssembly NuGet package gives .NET code an object model for reading, editing, writing and executing WASM binaries, and maps WASM instructions onto CIL so the .NET JIT compiles them. It covers ratified WebAssembly 3.0 and stops there.
- Who is it for?
- Teams that already run .NET and want to load, edit or run WebAssembly inside their own process should adopt this package, because it removes the external runtime and keeps the interop in-process. It is the wrong choice for anything built on post-3.0 WebAssembly proposals, for code that expects an interpreter with its own sandbox, or for anything but .NET 9 and later when the goal is a saved DLL.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 101 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A WASM library for .NET hosts that stays in the same process
Most WebAssembly tooling either targets the browser or ships a standalone runtime you call out to. The WebAssembly package takes a different route. It is a library that lets a .NET application create, read, modify, write, execute and DLL-convert WebAssembly binary files, and it executes the code inside the host process rather than in a separate engine.
The audience is .NET developers who need to treat WASM as a data format and as a runtime target at the same time. A server that accepts uploaded WASM modules and has to validate, patch or run them is the clearest case. A tool that inspects WASM binaries is another, since the library exposes the module structure as an object graph you can change and write back out.
Three properties define the design, and all three come from the README. It has zero dependencies, so nothing third-party is pulled into your application graph. Execution does not use an interpreter. And WASM instructions are mapped to their .NET CIL equivalents and then converted to native machine language by the .NET JIT compiler, so the module runs on the same runtime and with the same performance characteristics as the rest of your application.
The library is published on NuGet at https://www.nuget.org/packages/WebAssembly, and the source lives in the RyanLamansky/dotnet-webassembly repository with a WebAssembly.sln at its root, a WebAssembly project directory, a Tests directory, an Examples directory and a docs directory.
The module is an editable object graph, not a byte array
The API is built around a WebAssembly.Module class that holds the pieces of a WASM binary as collections. You create one empty and populate it, or you read an existing file into one and inspect or change what is there.
The collections are the five parts of the binary format that a host usually cares about. Types are function signatures, the list of parameters and returns. Functions associate a function index to a type index. Codes hold the instruction sequence for each function, and they must be passed in the exact same order as the functions they belong to. Exports give external code a name to call, with a kind and an index that points at the function being exported. Imports complete the picture, though the sample in the README passes an empty import dictionary because that example imports nothing.
Two details in the sample are worth copying into your own code rather than rediscovering. The first added type gets index 0, and the first added function gets index 0, so ordering in the collections determines the numbering that the rest of the module refers to. The second is that types can be re-used for multiple functions to reduce WASM size.
Instructions are modelled as classes rather than raw bytes, so a local read becomes an object you can place in a code list and a return becomes an End instruction, and every function has to end with one.
Execution goes through the JIT, not through an interpreter
The execution path is the part that distinguishes this library from the usual WebAssembly runtimes. A WASM instruction has a .NET CIL equivalent, and the CIL is handed to the .NET JIT compiler, which converts it to native machine language for the host platform. The consequence stated in the README is fast interop between the host application and the loaded WASM, because there is no boundary to cross and no serialisation of calls.
The entry point is the WebAssembly.Runtime.Compile class. It compiles a WASM binary for execution, and it can also convert that binary into a .NET DLL. A module can be compiled directly from an in-memory object graph, or from a stream, and the README notes that for stream-based compilation WebAssembly.Compile should be used.
Instances are created from a creator delegate and are disposable, so the sample wraps them in a using block for automatic release. The same creator can be called more than once to produce independent instances, which is what you want when the module holds state and you need isolation between calls.
The compilation surface is also where the host contract becomes visible. The README states that most WASM files have many imports and exports and that you need to cover these yourself. In other words, the library gives you the calling convention and the runtime, but the set of functions your module expects from the outside world is your problem to implement.
Creating and executing a module in memory, step by step
There is no install script to show here. The package is distributed through NuGet, and the README points at the package page directly. Add the WebAssembly package to your project and reference its namespaces, then build a module by hand.
Start with an empty module, add the type that describes your function signature, and then add the function that uses it:
var module = new Module(); // In this case, we're creating a new one.
module.Functions.Add(new Function // The first added function gets index 0.
{
Type = 0, // The index for the "type" value added above.
});The code section carries the instructions, and the export section publishes the entry point under a name:
module.Exports.Add(new Export
{
Kind = ExternalKind.Function,
Index = 0, // This should match the function index from above.
Name = "Demo", // Anything legal in Unicode is legal in an export name.
});Compilation produces a creator, and the creator produces instances. The sample declares an abstract class so the generated instance exposes a typed method:
using (var instance = instanceCreator(new ImportDictionary()))
{
Console.WriteLine(instance.Exports.Demo(0)); // Binary 0, result 0
Console.WriteLine(instance.Exports.Demo(1)); // Binary 1, result 1,
Console.WriteLine(instance.Exports.Demo(42)); // Binary 101010, result 3
}Reading an existing binary follows the same object model. Module.ReadFromBinary reads a stream into an instance you can inspect and modify through its properties, and WriteToBinary writes the module back out to a stream, which is what makes this library an editor as much as a loader.
Converting a WASM file into a .NET DLL, and the .NET 9 requirement
The second execution path produces a managed assembly instead of an in-process instance, and it is the more demanding one. The README states that saving to a DLL requires .NET 9 or higher and has several additional steps.
The mechanism is the PersistedAssemblyBuilder feature introduced in .NET 9, which lets the library emit a real assembly you can reference from another project. Aided by MetadataLoadContext, the example in the README produces a DLL for .NET 8. The resolution step is the fiddly part: you have to point the resolver at a core reference assembly that contains System.String and other basic features, and at the matching WebAssembly.dll built against that core DLL, then hand the resolved references to the persisted assembly builder.
The resulting file carries a type name that includes the namespace. In the README sample the default would be WebAssembly.CompiledFromWasm, and the example overrides it to Converted.HelloWorld:
var assembly = Compile.CreatePersistedAssembly(
File.OpenRead("HelloWorld.wasm"), // This is part of the "RunExisting" sample.
new(
context.CoreAssembly,
resolver.Resolve(context, new("WebAssembly")),
new(name),
$"{name}.dll"
)
{
// The type name includes the namespace.
// If not set, defaults to WebAssembly.CompiledFromWasm.
TypeName = "Converted.HelloWorld"
}
);
assembly.Save($"{name}.dll");Consuming it is ordinary .NET work. You reference the DLL in your project file and use the type as you would any other library type:
<ItemGroup>
<Reference Include="HelloWorld">
<HintPath>bin\HelloWorld.dll</HintPath> <!-- Relative path to the URL. -->
</Reference>
</ItemGroup>The path in that snippet is Windows-style and specific to the sample, so adjust it to wherever the DLL actually lands in your output.
Ratified 3.0 is the ceiling, and imports are yours to supply
Two limits define where this library stops working, and the README is explicit about both.
The first is the standard. WebAssembly 3.0 is supported, and the note at the top of the README warns that files using features newer than the ratified 3.0 standard may encounter errors. The Getting Started section repeats it: files using ratified 3.0 features load fine, while those using newer proposals may experience errors, and proposals newer than the ratified 3.0 standard are not supported yet. The compile path has the same boundary.
Within 3.0 the coverage is good. The README states that every ratified-3.0 spec-suite category passes, with only minor known deviations in NaN bit-pattern edge cases. Execution follows the deterministic profile, so results are reproducible across hosts, which matters if you intend to run the same module on several machines.
The second limit is the host contract. Most real WASM files import and export a large set of functions, and the library does not supply implementations for them. You provide an import dictionary when you instantiate, and the exported surface is whatever you choose to expose. This is the part that decides whether the library fits your use case. A self-contained module with a couple of exported functions is straightforward. A module built for the browser, expecting an entire host environment, is a substantial amount of work, and the library will not hide that.
There is also no mention of memory limits, sandboxing or resource control in the documentation, so do not assume a loaded module is confined.
Version 2.1.0, the Apache-2.0 licence, and how the project is moving
The package is on version 2.1.0, released on 2026-07-14, and the repository is not archived. The last push was on 2026-07-14 as well, so the release and the source are aligned.
Recent history is quick rather than gradual. Version 2.0.0 shipped on 2026-06-21 and was followed by 2.0.1 on 2026-06-27, then 2.1.0 on 2026-07-14. A 2.0 to 2.1 move inside three weeks suggests active work on the project, but it also means the API you read about may not match the version you pin. Read the release notes for the exact version before you lock it into a dependency file.
The licence is Apache-2.0, which is permissive and includes an explicit patent grant, so embedding the package in a commercial .NET product is a normal use rather than a special case. The patent grant and the notice requirements still apply, and the licence text governs rather than any summary.
The repository layout gives you the entry points worth knowing. The solution file is WebAssembly.sln, the library itself is in the WebAssembly directory, the test project is WebAssembly.Tests, and the Examples directory holds GenerateClassFromWasm, ReadMeSample and RunExisting, with a README of its own. The docs directory and the CLAUDE.md file at the root are also there for anyone reading the source. Note that the DLL conversion path depends on .NET 9 APIs, while the library targets .NET Standard, so a consumer on an older runtime can still load and run modules even though it cannot save them as assemblies.
Editorial conclusion
Teams that already run .NET and want to load, edit or run WebAssembly inside their own process should adopt this package, because it removes the external runtime and keeps the interop in-process. It is the wrong choice for anything built on post-3.0 WebAssembly proposals, for code that expects an interpreter with its own sandbox, or for anything but .NET 9 and later when the goal is a saved DLL. Before adopting, check that the features your binaries use are in ratified 3.0, budget for supplying your own imports and exports, and read the NuGet page for the version you plan to pin.
Frequently asked questions
Which WebAssembly standard does the WebAssembly package for .NET support?
WebAssembly 3.0, the ratified standard. The README notes that every ratified-3.0 spec-suite category passes, with only minor known deviations in NaN bit-pattern edge cases, and that proposals newer than 3.0 are not supported yet.
Does the WebAssembly library for .NET need a third-party interpreter?
No. It has zero dependencies, and execution does not use an interpreter. WASM instructions are mapped to their .NET CIL equivalents and converted to native machine language by the .NET JIT compiler, so the module runs in-process.
How do I convert a WASM file into a .NET DLL?
The README uses PersistedAssemblyBuilder, introduced in .NET 9, together with MetadataLoadContext, and states that saving to a DLL requires .NET 9 or higher and several additional steps. The resulting DLL is then referenced from your project file with a Reference item and a HintPath.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ryanlamansky-dotnet-webassembly)Was this useful?