WebAssembly (WASM) é um formato binário que permite executar código de alto desempenho em navegadores web. Com Zig, você pode compilar código para WebAssembly de forma trivial — sem toolchains complexos ou configurações extensas.
Neste tutorial, vamos criar um módulo WebAssembly em Zig, integrá-lo com JavaScript, e fazer deploy de uma aplicação funcional no browser.
O que você vai aprender:
- Configurar Zig para compilar para WebAssembly
- Criar funções exportadas para JavaScript
- Alocar e gerenciar memória compartilhada
- Trabalhar com strings entre Zig e JavaScript
- Criar uma aplicação web completa com Zig + WASM
Pré-requisitos
- Zig instalado
- Conhecimento básico de HTML e JavaScript
- Um navegador moderno (Chrome, Firefox, Edge, Safari)
O que é WebAssembly?
WebAssembly é um formato binário de instruções de máquina projetado para ser executado em navegadores web. Ele oferece:
- Performance próxima de nativa — executa em velocidade comparável a C/C++
- Segurança — sandboxed, sem acesso direto ao sistema
- Portabilidade — roda em qualquer navegador moderno
- Interoperabilidade — trabalha junto com JavaScript
Por que usar Zig para WASM?
| Feature | Benefício |
|---|---|
| Cross-compilation trivial | zig build -Dtarget=wasm32-freestanding |
| Sem runtime | Binários WASM mínimos |
| Interop nativa com C | Reutilizar bibliotecas C existentes |
| Controle de memória | Gerenciar a memória linear WASM |
| Sem dependências | Toolchain completo em um único binário |
Configuração do Projeto
Crie a estrutura do projeto:
mkdir zig-wasm-demo
cd zig-wasm-demo
zig init
Estrutura:
zig-wasm-demo/
├── build.zig
├── build.zig.zon
├── src/
│ └── main.zig
└── web/
├── index.html
└── app.js
Passo 1: Configurar o Build para WASM
Modifique o build.zig para adicionar o target WASM:
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
// Target: WebAssembly freestanding (sem sistema operacional)
const target = b.resolveTargetQuery(.{
.cpu_arch = .wasm32,
.os_tag = .freestanding,
});
// Otimização para release pequena
const optimize = .ReleaseSmall;
const wasm = b.addExecutable(.{
.name = "zig-wasm",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Exportar símbolos para JavaScript
wasm.entry = .disabled;
wasm.rdynamic = true;
b.installArtifact(wasm);
}
Passo 2: Criar Funções Exportadas
Crie o código Zig que será chamado do JavaScript:
// src/main.zig
// Exportar funções para JavaScript
export fn add(a: i32, b: i32) i32 {
return a + b;
}
export fn multiply(a: i32, b: i32) i32 {
return a * b;
}
export fn factorial(n: u32) u64 {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
Compile:
zig build
O arquivo gerado estará em zig-out/bin/zig-wasm.wasm.
Passo 3: Integrar com JavaScript
Crie a interface HTML e JavaScript:
<!-- web/index.html -->
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zig + WebAssembly Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #1a1a2e;
color: #eee;
}
h1 { color: #ffd700; }
.demo-box {
background: #16213e;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
input {
padding: 8px 12px;
margin: 5px;
border: 1px solid #0f3460;
border-radius: 4px;
background: #1a1a2e;
color: #eee;
}
button {
padding: 10px 20px;
margin: 5px;
background: #e94560;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover { background: #ff6b6b; }
.result {
margin-top: 10px;
padding: 10px;
background: #0f3460;
border-radius: 4px;
font-family: monospace;
}
</style>
</head>
<body>
<h1>⚡ Zig + WebAssembly</h1>
<div class="demo-box">
<h2>Operações Matemáticas</h2>
<div>
<input type="number" id="a" placeholder="A" value="5">
<input type="number" id="b" placeholder="B" value="3">
<button onclick="calcAdd()">Add (Zig)</button>
<button onclick="calcMultiply()">Multiply (Zig)</button>
<div class="result" id="math-result">Resultado aparecerá aqui</div>
</div>
</div>
<div class="demo-box">
<h2>Fatorial</h2>
<div>
<input type="number" id="n" placeholder="N" value="10" min="0" max="20">
<button onclick="calcFactorial()">Calcular Fatorial (Zig)</button>
<div class="result" id="factorial-result">Resultado aparecerá aqui</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
// web/app.js
let wasmModule = null;
// Carregar o módulo WASM
async function loadWasm() {
try {
const response = await fetch('../zig-out/bin/zig-wasm.wasm');
const bytes = await response.arrayBuffer();
const wasm = await WebAssembly.instantiate(bytes, {
env: {
// Funções importadas do JavaScript (se necessário)
memory: new WebAssembly.Memory({ initial: 256, maximum: 256 })
}
});
wasmModule = wasm.instance.exports;
console.log('WASM carregado com sucesso!');
console.log('Funções exportadas:', Object.keys(wasmModule));
} catch (error) {
console.error('Erro ao carregar WASM:', error);
}
}
// Funções de cálculo
function calcAdd() {
if (!wasmModule) return;
const a = parseInt(document.getElementById('a').value) || 0;
const b = parseInt(document.getElementById('b').value) || 0;
const result = wasmModule.add(a, b);
document.getElementById('math-result').textContent = `${a} + ${b} = ${result}`;
}
function calcMultiply() {
if (!wasmModule) return;
const a = parseInt(document.getElementById('a').value) || 0;
const b = parseInt(document.getElementById('b').value) || 0;
const result = wasmModule.multiply(a, b);
document.getElementById('math-result').textContent = `${a} × ${b} = ${result}`;
}
function calcFactorial() {
if (!wasmModule) return;
const n = parseInt(document.getElementById('n').value) || 0;
const start = performance.now();
const result = wasmModule.factorial(n);
const end = performance.now();
document.getElementById('factorial-result').textContent =
`${n}! = ${result} (calculado em ${(end - start).toFixed(3)}ms)`;
}
// Carregar WASM quando a página iniciar
loadWasm();
Passo 4: Trabalhar com Memória Compartilhada
Para dados mais complexos (strings, arrays), precisamos gerenciar a memória linear do WASM:
// src/main.zig
// Alocador simples para WASM
var wasm_allocator_state = std.heap.WasmPageAllocator.init;
const wasm_allocator = wasm_allocator_state.allocator();
// Buffer para comunicação
var buffer: [1024]u8 = undefined;
var buffer_len: usize = 0;
// Exportar ponteiro para o buffer
export fn getBufferPtr() [*]u8 {
return &buffer;
}
export fn getBufferLen() usize {
return buffer_len;
}
// Função que processa string
export fn reverseString(ptr: [*]u8, len: usize) void {
buffer_len = len;
// Copiar para buffer
@memcpy(buffer[0..len], ptr[0..len]);
// Reverter in-place
var i: usize = 0;
while (i < len / 2) : (i += 1) {
const temp = buffer[i];
buffer[i] = buffer[len - 1 - i];
buffer[len - 1 - i] = temp;
}
}
// Calcular Fibonacci
export fn fibonacci(n: u32) u64 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// Soma de array
export fn sumArray(ptr: [*]const i32, len: usize) i64 {
var sum: i64 = 0;
for (0..len) |i| {
sum += ptr[i];
}
return sum;
}
Atualize o JavaScript para trabalhar com memória:
// web/app.js (atualizado)
let wasmMemory = null;
async function loadWasm() {
try {
wasmMemory = new WebAssembly.Memory({ initial: 256, maximum: 512 });
const response = await fetch('../zig-out/bin/zig-wasm.wasm');
const bytes = await response.arrayBuffer();
const wasm = await WebAssembly.instantiate(bytes, {
env: {
memory: wasmMemory
}
});
wasmModule = wasm.instance.exports;
console.log('WASM carregado!');
} catch (error) {
console.error('Erro:', error);
}
}
// Converter string para WASM
function stringToWasm(str) {
const encoder = new TextEncoder();
const bytes = encoder.encode(str);
// Alocar na memória WASM (simplificado)
const ptr = 1024; // Offset na memória
const mem = new Uint8Array(wasmMemory.buffer);
mem.set(bytes, ptr);
return { ptr, len: bytes.length };
}
// Ler string do WASM
function wasmToString(ptr, len) {
const mem = new Uint8Array(wasmMemory.buffer, ptr, len);
const decoder = new TextDecoder();
return decoder.decode(mem);
}
// Reverter string usando WASM
function reverseString() {
const input = document.getElementById('string-input').value;
const { ptr, len } = stringToWasm(input);
wasmModule.reverseString(ptr, len);
const resultPtr = wasmModule.getBufferPtr();
const resultLen = wasmModule.getBufferLen();
const result = wasmToString(resultPtr, resultLen);
document.getElementById('string-result').textContent = result;
}
// Calcular soma de array
function sumArray() {
const input = document.getElementById('array-input').value;
const numbers = input.split(',').map(n => parseInt(n.trim())).filter(n => !isNaN(n));
// Copiar array para memória WASM
const ptr = 2048;
const mem = new Int32Array(wasmMemory.buffer);
for (let i = 0; i < numbers.length; i++) {
mem[ptr / 4 + i] = numbers[i];
}
const result = wasmModule.sumArray(ptr, numbers.length);
document.getElementById('array-result').textContent =
`[${numbers.join(', ')}] → Soma = ${result}`;
}
Passo 5: Exemplo Completo — Processamento de Imagem
Vamos criar um exemplo mais avançado — converter imagem para escala de cinza:
// src/main.zig
// Processar pixels (RGBA)
export fn grayscale(input: [*]const u8, output: [*]u8, len: usize) void {
var i: usize = 0;
while (i < len) : (i += 4) {
const r = @as(u16, input[i]);
const g = @as(u16, input[i + 1]);
const b = @as(u16, input[i + 2]);
// Fórmula de luminância
const gray = @as(u8, @intCast((r * 299 + g * 587 + b * 114) / 1000));
output[i] = gray;
output[i + 1] = gray;
output[i + 2] = gray;
output[i + 3] = input[i + 3]; // Alpha inalterado
}
}
// Aplicar brilho
export fn brightness(input: [*]const u8, output: [*]u8, len: usize, factor: i16) void {
var i: usize = 0;
while (i < len) : (i += 4) {
inline for (0..3) |c| {
const val = @as(i16, @intCast(input[i + c])) + factor;
output[i + c] = @intCast(std.math.clamp(val, 0, 255));
}
output[i + 3] = input[i + 3]; // Alpha
}
}
<!-- Adicionar à página HTML -->
<div class="demo-box">
<h2>Processamento de Imagem</h2>
<input type="file" id="image-input" accept="image/*">
<button onclick="processImage()">Converter para Escala de Cinza</button>
<canvas id="canvas"></canvas>
</div>
async function processImage() {
const fileInput = document.getElementById('image-input');
const file = fileInput.files[0];
if (!file) return;
const img = new Image();
img.onload = () => {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
// Obter dados da imagem
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
// Processar com WASM
const inputPtr = 4096;
const outputPtr = 4096 + pixels.length;
const mem = new Uint8Array(wasmMemory.buffer);
mem.set(pixels, inputPtr);
const start = performance.now();
wasmModule.grayscale(inputPtr, outputPtr, pixels.length);
const end = performance.now();
// Ler resultado
const result = new Uint8Array(wasmMemory.buffer, outputPtr, pixels.length);
imageData.data.set(result);
ctx.putImageData(imageData, 0, 0);
console.log(`Processado em ${(end - start).toFixed(2)}ms`);
};
img.src = URL.createObjectURL(file);
}
Deploy da Aplicação
Opção 1: Servidor Local
# Python 3
python -m http.server 8080
# Node.js
npx serve .
# PHP
php -S localhost:8080
Acesse http://localhost:8080/web/
Opção 2: GitHub Pages
- Crie um repositório
- Ative GitHub Pages nas configurações
- Faça push dos arquivos web
Opção 3: Netlify/Vercel
- Faça deploy do diretório
web/ - Configure o build para copiar o
.wasm
Dicas de Performance
1. Compile com otimizações adequadas
# Tamanho mínimo (recomendado para web)
zig build -Doptimize=ReleaseSmall
# Performance máxima
zig build -Doptimize=ReleaseFast
# Com safety checks (para debug)
zig build -Doptimize=ReleaseSafe
2. Minimize o tamanho do WASM
// build.zig - adicione:
wasm.strip = true; // Remove símbolos de debug
3. Use SharedArrayBuffer para dados grandes
// Para processamento de grandes volumes de dados
const sharedMemory = new WebAssembly.Memory({
initial: 256,
maximum: 512,
shared: true
});
Exercícios Práticos
Exercício 1: Criptografia Simples
Implemente cifra de César em Zig e exporte para WASM:
export fn caesarCipher(input: [*]u8, len: usize, shift: i8) void {
// Implementar cifra de César in-place
}
Ver solução
export fn caesarCipher(input: [*]u8, len: usize, shift: i8) void {
for (0..len) |i| {
const c = input[i];
if (c >= 'a' and c <= 'z') {
input[i] = @intCast(((c - 'a' + @as(u8, @intCast(shift))) % 26) + 'a');
} else if (c >= 'A' and c <= 'Z') {
input[i] = @intCast(((c - 'A' + @as(u8, @intCast(shift))) % 26) + 'A');
}
}
}
Exercício 2: Sorting
Implemente quicksort em Zig e compare performance com JavaScript nativo.
Exercício 3: Simulação Física
Crie uma simulação simples de partículas em Zig e renderize com Canvas.
Limitações do WASM
| Limitação | Solução |
|---|---|
| Sem acesso direto ao DOM | Use JavaScript como glue |
| Apenas tipos numéricos | Serializar dados na memória |
| Sem threads (ainda) | Use Web Workers |
| Sem acesso a I/O | Use APIs do browser via JS |
| Tamanho máximo de memória | Configure no Memory object |
Conclusão
Neste tutorial, você aprendeu a:
✅ Compilar Zig para WebAssembly
✅ Exportar funções para JavaScript
✅ Gerenciar memória compartilhada
✅ Processar strings e arrays
✅ Criar aplicações web com Zig + WASM
Próximos Passos
- 📦 Como Instalar o Zig — revise o básico
- ⚡ Comptime em Zig — otimize código em tempo de compilação
- 🎮 Criando Jogos com Zig — use WASM para jogos no browser
Criou algo interessante com Zig + WebAssembly? Compartilhe com a comunidade!