Введение: почему WebAssembly меняет правила игры для C# разработчиков
Долгое время веб-разработка была монополией JavaScript. Однако с появлением WebAssembly (Wasm) ситуация кардинально изменилась. Теперь разработчики на C# могут компилировать свой код в бинарный формат, который выполняется в браузере с почти нативной скоростью. Это открывает огромные возможности: от портирования тяжелых алгоритмов до создания полноценных SPA-приложений на Blazor.
В этой статье мы разберем, как работает WebAssembly в связке с C# и .NET, рассмотрим ключевые инструменты (Blazor, Wasmtime, Mono) и напишем практические примеры. К концу вы сможете самостоятельно запустить C# код в браузере или на сервере с помощью Wasm.
Основы WebAssembly и его интеграция с C#
Что такое WebAssembly простыми словами
WebAssembly — это низкоуровневый байт-код, который исполняется в виртуальной машине браузера. Он не заменяет JavaScript, а работает вместе с ним. Основные преимущества:
- Производительность: бинарный код выполняется на 30-50% быстрее JS.
- Безопасность: изолированная среда выполнения.
- Переносимость: один и тот же .wasm файл работает в Chrome, Firefox, Safari и Edge.
Для C# экосистема WebAssembly реализована через несколько проектов: Blazor WebAssembly (фреймворк для UI), Mono Wasm Runtime (исполняет IL-код) и Wasmtime (для серверных сценариев).
Как C# компилируется в WebAssembly
Процесс выглядит так:
- Вы пишете код на C#.
- Компилятор Roslyn превращает его в IL (промежуточный язык).
- Mono Runtime (или NativeAOT) транслирует IL в WebAssembly.
- Браузер загружает .wasm файл и исполняет его.
Важно: существует два подхода — интерпретация через Mono (медленнее, но проще) и AOT-компиляция (быстрее, но требует настройки).
Практика: три способа запустить C# в WebAssembly
1. Blazor WebAssembly — для веб-интерфейсов
Blazor позволяет писать клиентскую часть на C# вместо JavaScript. Весь UI-код и бизнес-логика компилируются в Wasm.
Пример простого компонента:
@page "/counter"
<h3>Счетчик: @currentCount</h3>
<button @onclick="IncrementCount">+1</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
Этот код выполняется в браузере. Blazor автоматически загружает Mono Wasm Runtime и ваш .wasm файл. Для старта создайте проект:
dotnet new blazorwasm -o MyBlazorApp
cd MyBlazorApp
dotnet run
Результат: полноценное SPA-приложение без единой строчки JavaScript.
2. Прямая компиляция в .wasm с помощью NativeAOT
Если вам нужно выполнить конкретную функцию (не UI), используйте NativeAOT — он компилирует C# напрямую в нативный WebAssembly без Mono Runtime.
Шаги:
- Установите SDK .NET 8+ и workload:
dotnet workload install wasm-tools - Создайте консольное приложение:
dotnet new console -n WasmDemo
cd WasmDemo
- Измените
.csproj, добавив поддержку Wasm:
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<UseNativeAot>true</UseNativeAot>
</PropertyGroup>
- Напишите код:
using System;
public class Program
{
public static void Main()
{
Console.WriteLine("Hello from WebAssembly!");
}
// Экспортируемая функция для вызова из JS
[System.Runtime.InteropServices.UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
public static int Add(int a, int b) => a + b;
}
- Скомпилируйте:
dotnet publish -c Release
В папке bin/Release/net8.0/browser-wasm/publish появится файл WasmDemo.wasm, который можно вызывать из JavaScript:
const wasm = await WebAssembly.instantiateStreaming(fetch('WasmDemo.wasm'));
const result = wasm.instance.exports.Add(5, 3);
console.log(result); // 8
3. Wasmtime — запуск C# WebAssembly на сервере
Wasmtime — это среда выполнения WebAssembly вне браузера. С помощью пакета Wasmtime для .NET вы можете загружать .wasm файлы в своих серверных приложениях.
Пример:
using Wasmtime;
// Загружаем скомпилированный .wasm файл
using var engine = new Engine();
using var module = Module.FromFile(engine, "calc.wasm");
using var linker = new Linker(engine);
using var store = new Store(engine);
// Создаем экземпляр модуля
var instance = linker.Instantiate(store, module);
// Получаем функцию
var add = instance.GetFunction<int, int, int>("Add");
if (add != null)
{
int result = add(store, 10, 20);
Console.WriteLine($"10 + 20 = {result}"); // Вывод: 30
}
Это полезно для песочниц кода, плагинов или изоляции сторонних скриптов.
Оптимизация и ограничения
Производительность: что нужно знать
- AOT vs Interp: NativeAOT дает прирост скорости 2-3x, но увеличивает размер .wasm файла (до 5