Create and publish a module
Install the templates, create a page module, and publish it with its working sample page:
$ dotnet new install NetWasm.Templates
$ dotnet new netwasm-app -n Greeter --host page
$ cd Greeter
$ dotnet publish -c Release
A JsModule project has exports instead of Main:
using System.Runtime.InteropServices.JavaScript;
public static partial class PageExports
{
[JSImport("show", "app.host")]
private static extern void Show(string message);
[JSExport("greet")]
public static string Greet(string name)
{
var message = $"Hello, {name}, from C#.";
Show(message);
return message;
}
}
The project file selects the output type and the host:
<PropertyGroup>
<OutputType>JsModule</OutputType>
<NetWasmJavaScriptHosts>page</NetWasmJavaScriptHosts>
<TargetFramework>netwasm0.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
Publishing writes a static site to bin/Release/netwasm0.1/publish. It contains the sample index.html and app.mjs, the module browser/Greeter.mjs, and its TypeScript declarations in browser/Greeter.d.mts.
Call C# from the page
Import open from the generated module. Each call to open() starts an independent instance and resolves to an object with one method per export.
import { open } from "./browser/Greeter.mjs";
const app = await open({ stdout: line => console.log(line) });
app.failure.then(error => console.error(error));
const message = app.greet("browser"); // returns synchronously
console.log(message);
await app.close();
A synchronous export runs on the page's thread and returns its value directly. An export that returns Task or ValueTask returns a Promise. The declarations name each parameter after its C# parameter and follow C# nullability, so the editor shows greet(name: string): string.
close()releases everything the instance holds.failuresettles when the instance traps or fails, or afterclose(). Later calls throwNetWasmHostError.open()options supply guest output, environment variables, import modules at runtime, and a notification handler. Every option is optional.
Call JavaScript from C#
A [JSImport] names the JavaScript module that implements it. On the page host, import modules run in the page, so they can use the DOM directly:
export function show(message) {
document.querySelector("#result").textContent = message;
}
The project bundles it with a NetWasmJavaScriptModule item, or a page supplies it at runtime through the modules option of open():
<ItemGroup>
<NetWasmJavaScriptModule Include="app.host.mjs" Module="app.host" />
</ItemGroup>
JavaScript that an import runs can call an export again before the import returns, nested up to 32 levels.
Run on the page or in a worker
NetWasmJavaScriptHosts chooses where the same module runs:
page(default): synchronous exports return values, and import modules can use the DOM. Use it when C# drives page UI or calls are short.worker: the module runs in a Web Worker behind a Promise-based client, so long work does not block the page. See Run C# in a Web Worker.page;worker: publishes both, for a page that needs some calls of each kind.
What crosses the boundary
Booleans, numbers, strings, and number arrays cross directly. 64-bit integers arrive as bigint, and a number array arrives as the typed array with the same element type, in both directions. Strings and arrays are copied. Page exports can also take and return JSObject, a handle to a JavaScript object that stays in JavaScript.
A synchronous import can also take a Span<T> or ReadOnlySpan<T> of numbers. JavaScript receives the same typed array as for an array, as a view of the span's memory, so nothing is copied. The view is valid only until the import returns, and JavaScript must not write to the view of a ReadOnlySpan<T>. A synchronous import whose parameters and result are all primitive scalars, or void, compiles to a direct WebAssembly call into the JavaScript function, with nothing copied through memory; use such imports on a hot path.
Async exports accept strings, number arrays, and JSObject, and async imports can settle with them. The compiler generates the adapters for exactly the exports and imports your program declares; nothing is discovered by reflection at runtime.
Serve the published files
The publish directory is a static site. Copy it to any static host. It needs no server code and no Cross-Origin-Opener-Policy or Cross-Origin-Embedder-Policy headers.
- Serve it over HTTPS or from
localhost. Startup verifies files withcrypto.subtle, which browsers provide only in secure contexts. - Open the page through HTTP. A
file://URL does not work, because browsers block module scripts andfetchthere. - Serve
.mjsfiles astext/javascript. Browsers refuse a module script with another MIME type. Python'shttp.serveron Windows uses the type the Windows registry gives.mjs, which can betext/plain; the SDK guide shows how to start it with the right type. - Under a strict Content Security Policy, allow
script-src 'self' blob: 'wasm-unsafe-eval'.
Current limits
A synchronous export blocks the page until it returns. Return a Task for long work, or host the module in a worker. A JSObject stays in the realm that created it, so a worker-hosted export cannot take or return one.
The boundary carries the values above, not arbitrary object graphs: there is no automatic POCO serialization, dynamic overload resolution, or sharing of managed objects between realms. Use generated JSON contracts or WIT when the boundary is better described as data. The JavaScript interop reference documents every option and the exact boundary.
Ready to build?