Zig e WebAssembly: Guia Prático para Compilar WASM

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?

FeatureBenefício
Cross-compilation trivialzig build -Dtarget=wasm32-freestanding
Sem runtimeBinários WASM mínimos
Interop nativa com CReutilizar bibliotecas C existentes
Controle de memóriaGerenciar a memória linear WASM
Sem dependênciasToolchain 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

  1. Crie um repositório
  2. Ative GitHub Pages nas configurações
  3. Faça push dos arquivos web

Opção 3: Netlify/Vercel

  1. Faça deploy do diretório web/
  2. 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çãoSolução
Sem acesso direto ao DOMUse JavaScript como glue
Apenas tipos numéricosSerializar dados na memória
Sem threads (ainda)Use Web Workers
Sem acesso a I/OUse APIs do browser via JS
Tamanho máximo de memóriaConfigure 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


Criou algo interessante com Zig + WebAssembly? Compartilhe com a comunidade!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.