Zig para Programadores JavaScript: Do V8 ao Metal

Se você trabalha com JavaScript e já ouviu falar do Bun, provavelmente se perguntou: o que é essa linguagem usada para construí-lo? A resposta é zig lang, uma linguagem de programação de sistemas que está ganhando tração no ecossistema JavaScript. A linguagem Zig combina a performance do C com uma experiência de desenvolvimento moderna, e neste guia vamos explorar como seus conhecimentos de JavaScript se traduzem para Zig.

Por Que Desenvolvedores JS Deveriam Conhecer Zig

O ecossistema JavaScript está cada vez mais conectado ao Zig. Veja por quê:

  • Bun é escrito em Zig: O runtime JavaScript mais rápido do mercado foi construído com Zig. Entender Zig é entender o motor por trás do Bun.
  • Performance nativa: Quando seu servidor Node.js não aguenta a carga, reescrever partes críticas em Zig pode resolver o gargalo.
  • WebAssembly: Zig compila diretamente para WASM, permitindo executar código de alta performance no navegador.
  • Node.js addons: Crie extensões nativas para Node.js com Zig, substituindo C++ como linguagem padrão para addons.
  • Entendimento profundo: Compreender como o V8 e o event loop funcionam por baixo fica muito mais fácil quando você conhece programação de sistemas.

Diferenças Fundamentais: JavaScript vs Zig

AspectoJavaScriptZig
ExecuçãoV8/SpiderMonkey (JIT)Compilado (LLVM)
TipagemDinâmica, fracaEstática, forte
MemóriaGarbage CollectorGerenciamento manual
ConcorrênciaEvent loop (single thread)Threads reais
Pacotesnpm/yarn/pnpmzig build + packages
Nullnull e undefinednull (optional explícito)
Errosthrow/try/catchError unions
OOPPrototypes/ClassesStructs

Variáveis: let/const vs var/const

JavaScript:

let nome = "Maria";
const idade = 28;
let contador = 0;
contador += 1;

// Tipo é inferido e pode mudar
let valor = 42;
valor = "quarenta e dois"; // OK em JS

Zig:

var nome: []const u8 = "Maria";
const idade: u32 = 28;
var contador: u32 = 0;
contador += 1;

// Tipo é fixo após declaração
var valor: i32 = 42;
// valor = "quarenta e dois"; // ERRO de compilação!

Em Zig, const é verdadeiramente imutável (diferente do const de JS que permite mutação de objetos). A variável var permite reatribuição, mas o tipo nunca muda. Essa distinção é especialmente importante quando se trabalha com gerenciamento de memória em Zig, onde controlar mutabilidade é essencial.

Zig também tem inferência de tipos quando o valor inicial é claro:

const nome = "Maria";       // tipo inferido: *const [5:0]u8
const idade = @as(u32, 28); // tipo explicitado via @as

Funções

JavaScript:

function somar(a, b) {
    return a + b;
}

const multiplicar = (a, b) => a * b;

// Função com valor padrão
function cumprimentar(nome = "mundo") {
    return `Olá, ${nome}!`;
}

Zig:

const std = @import("std");

fn somar(a: i32, b: i32) i32 {
    return a + b;
}

// Não existe arrow function, mas funções podem ser constantes
const multiplicar = struct {
    fn call(a: i32, b: i32) i32 {
        return a * b;
    }
}.call;

pub fn main() void {
    const resultado = somar(3, 5);
    std.debug.print("Soma: {}\n", .{resultado});

    const produto = multiplicar(4, 6);
    std.debug.print("Produto: {}\n", .{produto});
}

Diferenças importantes:

  • Parâmetros e retorno sempre têm tipos explícitos.
  • Não existem arrow functions, mas Zig tem formas de passar funções como valores.
  • Não existem valores padrão para parâmetros (mas há técnicas com structs de opções).

Closures e Callbacks

JavaScript depende muito de closures e callbacks. Zig tem uma abordagem diferente.

JavaScript:

function criarContador() {
    let count = 0;
    return {
        incrementar: () => ++count,
        valor: () => count
    };
}

const contador = criarContador();
contador.incrementar();
console.log(contador.valor()); // 1

// Callback
[1, 2, 3].map(x => x * 2); // [2, 4, 6]

Zig:

const std = @import("std");

const Contador = struct {
    count: i32 = 0,

    pub fn incrementar(self: *Contador) void {
        self.count += 1;
    }

    pub fn valor(self: Contador) i32 {
        return self.count;
    }
};

pub fn main() void {
    var contador = Contador{};
    contador.incrementar();
    std.debug.print("Valor: {}\n", .{contador.valor()});
}

Zig não tem closures como JavaScript. Em vez disso, usamos structs com estado. Para operações como map, Zig usa loops explícitos:

const std = @import("std");

pub fn main() void {
    const entrada = [_]i32{ 1, 2, 3 };
    var saida: [3]i32 = undefined;

    for (entrada, 0..) |valor, i| {
        saida[i] = valor * 2;
    }

    // saida agora é { 2, 4, 6 }
    for (saida) |v| {
        std.debug.print("{} ", .{v});
    }
}

Async/Await: Event Loop vs Threads

Uma das maiores diferenças conceituais está na concorrência.

JavaScript:

async function buscarDados(url) {
    try {
        const response = await fetch(url);
        const dados = await response.json();
        return dados;
    } catch (erro) {
        console.error("Falha:", erro);
    }
}

// Promise.all para paralelismo
const [usuarios, posts] = await Promise.all([
    buscarDados("/api/usuarios"),
    buscarDados("/api/posts")
]);

Em Zig, a concorrência é baseada em threads reais do sistema operacional:

const std = @import("std");

fn tarefaPesada(id: u32) void {
    std.debug.print("Thread {} iniciada\n", .{id});
    // Simular trabalho pesado
    std.time.sleep(1_000_000_000); // 1 segundo
    std.debug.print("Thread {} finalizada\n", .{id});
}

pub fn main() !void {
    var threads: [4]std.Thread = undefined;

    // Criar 4 threads
    for (0..4) |i| {
        threads[i] = try std.Thread.spawn(.{}, tarefaPesada, .{@as(u32, @intCast(i))});
    }

    // Aguardar todas finalizarem (equivalente conceitual ao Promise.all)
    for (&threads) |*t| {
        t.join();
    }

    std.debug.print("Todas as threads finalizaram\n", .{});
}

Em JavaScript, o event loop gerencia I/O assíncrono em uma única thread. Em Zig, você tem controle direto sobre threads do sistema operacional, podendo executar código verdadeiramente paralelo em múltiplos núcleos.

Tratamento de Erros

JavaScript:

function dividir(a, b) {
    if (b === 0) throw new Error("Divisão por zero");
    return a / b;
}

try {
    const resultado = dividir(10, 0);
    console.log(resultado);
} catch (e) {
    console.error(e.message);
}

Zig:

const std = @import("std");

const MathError = error{
    DivisaoPorZero,
};

fn dividir(a: f64, b: f64) MathError!f64 {
    if (b == 0.0) return MathError.DivisaoPorZero;
    return a / b;
}

pub fn main() void {
    const resultado = dividir(10.0, 0.0) catch |err| {
        std.debug.print("Erro: {}\n", .{err});
        return;
    };
    std.debug.print("Resultado: {d}\n", .{resultado});
}

A diferença fundamental: em JavaScript, qualquer função pode lançar uma exceção a qualquer momento, e não há como saber pelo tipo. Em Zig, se uma função pode falhar, o tipo de retorno deixa isso explícito (MathError!f64). O compilador garante que todo erro seja tratado. Para se aprofundar nesse tema, veja o guia completo sobre tratamento de erros em Zig.

O operador try em Zig propaga erros automaticamente, similar a não usar try/catch em JavaScript e deixar a exceção subir:

fn processarDados() !void {
    const valor = try dividir(10.0, 2.0); // Se falhar, propaga o erro
    std.debug.print("Valor: {d}\n", .{valor});
}

Objetos e JSON: Prototypes vs Structs

JavaScript:

const usuario = {
    nome: "Carlos",
    idade: 30,
    ativo: true
};

const json = JSON.stringify(usuario);
const parsed = JSON.parse(json);

Zig:

const std = @import("std");

const Usuario = struct {
    nome: []const u8,
    idade: u32,
    ativo: bool,
};

pub fn main() !void {
    const usuario = Usuario{
        .nome = "Carlos",
        .idade = 30,
        .ativo = true,
    };

    // Serializar para JSON
    var buf: [256]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buf);
    try std.json.stringify(usuario, .{}, stream.writer());

    const json_str = stream.getWritten();
    std.debug.print("JSON: {s}\n", .{json_str});
}

Em JavaScript, objetos são dinâmicos e podem ter qualquer propriedade adicionada em tempo de execução. Em Zig, structs têm campos fixos definidos em tempo de compilação.

Como o Bun Usa Zig

O Bun, criado por Jarred Sumner, é um runtime JavaScript que compete com Node.js e Deno. Ele escolheu Zig em vez de C++ ou Rust pelos seguintes motivos:

  • Interoperabilidade com C: Zig pode chamar código C diretamente, sem FFI overhead. Isso foi essencial para integrar com o JavaScriptCore (motor JS do Safari/WebKit).
  • Performance previsível: Sem garbage collector, o Bun pode garantir latências baixas e consistentes.
  • Alocadores customizados: Zig permite usar diferentes estratégias de alocação de memória, o que o Bun explora para otimizar diferentes tipos de operação.
  • Compilação rápida: O tempo de compilação de Zig é significativamente menor que C++, acelerando o desenvolvimento.

Exemplo conceitual de como o Bun usa Zig para processar requisições HTTP:

const std = @import("std");
const net = std.net;

pub fn main() !void {
    // Servidor TCP básico (conceito simplificado do que o Bun faz)
    var server = try net.StreamServer.init(.{});
    defer server.deinit();

    try server.listen(net.Address.parseIp("0.0.0.0", 3000) catch unreachable);
    std.debug.print("Servidor rodando na porta 3000\n", .{});

    while (true) {
        if (server.accept()) |conn| {
            defer conn.stream.close();
            // Processar requisição...
            _ = try conn.stream.write("HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK");
        } else |_| {
            continue;
        }
    }
}

WebAssembly: Usando Zig no Navegador

Uma das formas mais naturais de usar Zig como desenvolvedor JavaScript é compilando para WebAssembly.

// math.zig - será compilado para WASM
export fn fibonacci(n: u32) u32 {
    if (n <= 1) return n;
    var a: u32 = 0;
    var b: u32 = 1;
    var i: u32 = 2;
    while (i <= n) : (i += 1) {
        const temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

export fn fatorial(n: u32) u64 {
    var resultado: u64 = 1;
    var i: u32 = 2;
    while (i <= n) : (i += 1) {
        resultado *= @as(u64, i);
    }
    return resultado;
}

Compile para WASM:

zig build-lib math.zig -target wasm32-freestanding -dynamic -O ReleaseSmall

Use no JavaScript:

async function carregarWasm() {
    const response = await fetch('math.wasm');
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes);

    console.log(instance.exports.fibonacci(40));  // Resultado instantâneo
    console.log(instance.exports.fatorial(20));
}

carregarWasm();

Construindo Addons Nativos para Node.js

Tradicionalmente, addons nativos para Node.js são escritos em C++. Com Zig, o processo fica mais simples.

// addon.zig
const c = @cImport({
    @cInclude("node_api.h");
});

export fn napi_register_module_v1(env: c.napi_env, exports: c.napi_value) c.napi_value {
    // Registrar funções nativas aqui
    var fn_value: c.napi_value = undefined;
    _ = c.napi_create_function(env, "hello", 5, &helloNative, null, &fn_value);
    _ = c.napi_set_named_property(env, exports, "hello", fn_value);
    return exports;
}

fn helloNative(env: c.napi_env, info: c.napi_callback_info) c.napi_value {
    _ = info;
    var result: c.napi_value = undefined;
    _ = c.napi_create_string_utf8(env, "Olá do Zig!", 13, &result);
    return result;
}

No lado JavaScript:

const addon = require('./zig-addon.node');
console.log(addon.hello()); // "Olá do Zig!"

npm vs Zig Build System

npm/package.jsonZig Build System
npm initzig init
package.jsonbuild.zig
npm install pacoteDependências via build.zig.zon
npm run buildzig build
npm testzig build test
node_modules/Cache global de pacotes

Um build.zig.zon típico (equivalente ao package.json):

.{
    .name = "meu-projeto",
    .version = "0.1.0",
    .dependencies = .{
        .zap = .{
            .url = "https://github.com/zigzap/zap/archive/v0.1.0.tar.gz",
            .hash = "...",
        },
    },
}

Tabela de Referência Rápida

JavaScriptZigNotas
let x = 5var x: i32 = 5Zig exige tipo ou inferência
const x = 5const x: i32 = 5const é realmente imutável
console.log()std.debug.print()Sintaxe de formatação diferente
null / undefinednullTipo optional ?T
Array[N]T ou std.ArrayListArrays fixos ou dinâmicos
ObjectstructCampos definidos em compilação
Promisestd.ThreadParalelismo real
JSON.parsestd.json.parseFromSliceParsing tipado
require/import@importSistema de módulos
=====Zig não tem coerção de tipos

Conclusão

Aprender Zig como desenvolvedor JavaScript abre uma dimensão completamente nova da programação. Você vai entender o que acontece por baixo do V8, como o Bun consegue ser tão rápido e como escrever código que roda sem runtime. A curva de aprendizado é real, mas cada conceito novo em Zig vai tornar você um desenvolvedor JavaScript mais consciente e eficiente.

Leia Também

Continue aprendendo Zig

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