Zig para FreeBSD e NetBSD: Cross-Compilation para Sistemas BSD

Um dos maiores diferenciais da linguagem Zig é sua capacidade de cross-compilation integrada ao compilador. Diferente de outras linguagens onde configurar uma toolchain para compilar para outro sistema operacional pode levar horas, Zig torna o processo trivial — e em 2026, o suporte a FreeBSD e NetBSD atingiu maturidade de produção.

Neste artigo, vamos explorar como compilar projetos Zig para sistemas BSD, usar zig cc como cross-compiler C para BSD, e configurar pipelines de CI/CD que geram binários para múltiplos targets BSD.

Por Que BSD Importa para Desenvolvedores Zig

FreeBSD e NetBSD são sistemas operacionais amplamente usados em infraestrutura de produção. Netflix, WhatsApp e Sony (PlayStation) usam FreeBSD em seus servidores e dispositivos. NetBSD é conhecido por rodar em praticamente qualquer hardware existente.

Para desenvolvedores de sistemas e ferramentas de baixo nível, poder compilar para BSD a partir de qualquer máquina de desenvolvimento — seja Linux, macOS ou Windows — é essencial. Zig elimina a necessidade de ter uma máquina BSD disponível para compilar.

O Que Mudou em 2026

O suporte a BSD no Zig evoluiu significativamente:

  • aarch64-freebsd e x86_64-freebsd são agora targets testados nativamente no CI do Zig
  • aarch64-netbsd e x86_64-netbsd também ganharam testes nativos no CI
  • Zig agora inclui headers de sistema e libc para FreeBSD 14.0+ e NetBSD 10.1+
  • O formato abilists compacto permite que o compilador conheça todos os símbolos públicos da libc de cada target

Compilando Zig para FreeBSD

Exemplo Básico

Compilar um programa Zig para FreeBSD é tão simples quanto adicionar o flag -target:

// src/main.zig — servidor HTTP simples
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var server = std.http.Server.init(allocator, .{
        .reuse_address = true,
    });
    defer server.deinit();

    const address = std.net.Address.parseIp("0.0.0.0", 8080) catch unreachable;
    try server.listen(address);

    std.log.info("Servidor rodando na porta 8080", .{});

    while (true) {
        var response = try server.accept(.{
            .allocator = allocator,
        });
        defer response.deinit();

        try response.headers.append("Content-Type", "text/plain");
        try response.do();
        try response.writeAll("Zig rodando em FreeBSD!");
        try response.finish();
    }
}

Para compilar esse servidor para FreeBSD x86_64:

# Compilar para FreeBSD x86_64
zig build-exe src/main.zig -target x86_64-freebsd -O ReleaseSafe

# Compilar para FreeBSD ARM64 (ex: servidor ARM)
zig build-exe src/main.zig -target aarch64-freebsd -O ReleaseSafe

# Verificar o binário gerado
file main
# main: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD)

Usando o Build System

Para projetos maiores que usam o build system do Zig, a configuração de targets BSD é feita no build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    // Target padrão (máquina local)
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "meu-servidor",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    b.installArtifact(exe);

    // Steps dedicados para cada target BSD
    const freebsd_x86 = b.resolveTargetQuery(.{
        .cpu_arch = .x86_64,
        .os_tag = .freebsd,
    });

    const freebsd_arm = b.resolveTargetQuery(.{
        .cpu_arch = .aarch64,
        .os_tag = .freebsd,
    });

    const netbsd_x86 = b.resolveTargetQuery(.{
        .cpu_arch = .x86_64,
        .os_tag = .netbsd,
    });

    // Executáveis para cada plataforma
    const exe_freebsd_x86 = b.addExecutable(.{
        .name = "meu-servidor-freebsd-x86",
        .root_source_file = b.path("src/main.zig"),
        .target = freebsd_x86,
        .optimize = .ReleaseSafe,
    });

    const exe_freebsd_arm = b.addExecutable(.{
        .name = "meu-servidor-freebsd-arm",
        .root_source_file = b.path("src/main.zig"),
        .target = freebsd_arm,
        .optimize = .ReleaseSafe,
    });

    const exe_netbsd = b.addExecutable(.{
        .name = "meu-servidor-netbsd-x86",
        .root_source_file = b.path("src/main.zig"),
        .target = netbsd_x86,
        .optimize = .ReleaseSafe,
    });

    // Step "bsd" compila todos os targets BSD
    const bsd_step = b.step("bsd", "Compilar para todos os targets BSD");
    bsd_step.dependOn(&b.addInstallArtifact(exe_freebsd_x86, .{}).step);
    bsd_step.dependOn(&b.addInstallArtifact(exe_freebsd_arm, .{}).step);
    bsd_step.dependOn(&b.addInstallArtifact(exe_netbsd, .{}).step);
}

Agora, basta rodar:

# Compilar para todos os targets BSD
zig build bsd

# Ou especificar target via linha de comando
zig build -Dtarget=x86_64-freebsd

Compilando Zig para NetBSD

O processo para NetBSD é idêntico, mudando apenas o target:

# NetBSD x86_64
zig build-exe src/main.zig -target x86_64-netbsd -O ReleaseSafe

# NetBSD ARM64
zig build-exe src/main.zig -target aarch64-netbsd -O ReleaseSafe

Diferenças entre FreeBSD e NetBSD como Targets

Embora a experiência do desenvolvedor seja a mesma, há diferenças nas syscalls e APIs disponíveis:

const std = @import("std");
const builtin = @import("builtin");

pub fn getPlatformInfo() []const u8 {
    return switch (builtin.os.tag) {
        .freebsd => "FreeBSD — kqueue, jails, ZFS nativo",
        .netbsd => "NetBSD — portabilidade extrema, rump kernels",
        .linux => "Linux — epoll, cgroups, namespaces",
        else => "Plataforma desconhecida",
    };
}

// Adaptar comportamento por SO em comptime
pub fn createEventLoop() !void {
    if (comptime builtin.os.tag == .freebsd or builtin.os.tag == .netbsd) {
        // BSD usa kqueue para I/O assíncrono
        std.log.info("Usando kqueue para event loop", .{});
    } else if (comptime builtin.os.tag == .linux) {
        // Linux usa epoll (ou io_uring)
        std.log.info("Usando epoll/io_uring para event loop", .{});
    }
}

Para entender mais sobre I/O assíncrono em Zig, veja o artigo sobre io_uring e async e os padrões de concorrência.

Usando zig cc como Cross-Compiler C para BSD

Um caso de uso extremamente poderoso é usar zig cc para compilar projetos C existentes para BSD. O Zig embarca sua própria libc e headers de sistema para cada target suportado, eliminando a necessidade de instalar toolchains separadas.

Compilando um Projeto C para FreeBSD

# Compilar um arquivo C para FreeBSD
zig cc -target x86_64-freebsd -O2 -o servidor servidor.c

# Compilar com linkagem a bibliotecas do sistema
zig cc -target x86_64-freebsd -O2 -lm -lpthread -o app main.c utils.c

# Compilar código C++ para FreeBSD
zig c++ -target x86_64-freebsd -O2 -std=c++17 -o app main.cpp

Integração com Projetos C Existentes via Makefile

# Makefile para cross-compilation BSD com zig cc
CC = zig cc
CXX = zig c++

FREEBSD_TARGET = x86_64-freebsd
NETBSD_TARGET = x86_64-netbsd

CFLAGS = -O2 -Wall -Wextra
SRC = src/main.c src/utils.c src/network.c

.PHONY: freebsd netbsd all

all: freebsd netbsd

freebsd:
	$(CC) -target $(FREEBSD_TARGET) $(CFLAGS) $(SRC) -o build/app-freebsd

netbsd:
	$(CC) -target $(NETBSD_TARGET) $(CFLAGS) $(SRC) -o build/app-netbsd

Isso é particularmente útil para quem está migrando projetos de C para Zig gradualmente — você pode compilar a parte C do projeto com zig cc e a parte nova com Zig nativo, tudo no mesmo pipeline de build.

Integração com a Interoperabilidade C de Zig

Projetos que usam a interoperabilidade C do Zig para chamar bibliotecas C nativas funcionam perfeitamente com targets BSD:

const c = @cImport({
    @cInclude("sys/sysctl.h");
    @cInclude("sys/types.h");
});

pub fn getSystemInfo(allocator: std.mem.Allocator) ![]const u8 {
    var size: usize = 0;
    // sysctl está disponível em FreeBSD e NetBSD
    const name = [_]c_int{ c.CTL_KERN, c.KERN_OSRELEASE };
    _ = c.sysctl(&name, name.len, null, &size, null, 0);

    const buf = try allocator.alloc(u8, size);
    _ = c.sysctl(&name, name.len, buf.ptr, &size, null, 0);

    return buf[0..size];
}

Pipeline de CI/CD Multi-Target com BSD

Para projetos que precisam gerar releases para múltiplos sistemas operacionais, um pipeline automatizado é essencial. Aqui está um exemplo usando o Docker com Zig:

# .gitea/workflows/release.yml
name: Release Multi-Plataforma
on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-linux
          - aarch64-linux
          - x86_64-freebsd
          - aarch64-freebsd
          - x86_64-netbsd
    steps:
      - uses: actions/checkout@v4

      - name: Instalar Zig
        run: |
          wget -q https://ziglang.org/download/0.16.0/zig-linux-x86_64-0.16.0.tar.xz
          tar xf zig-linux-x86_64-0.16.0.tar.xz
          echo "$PWD/zig-linux-x86_64-0.16.0" >> $GITHUB_PATH

      - name: Compilar para ${{ matrix.target }}
        run: zig build -Dtarget=${{ matrix.target }} -Doptimize=ReleaseSafe

      - name: Upload artefato
        uses: actions/upload-artifact@v4
        with:
          name: build-${{ matrix.target }}
          path: zig-out/bin/*

Este pipeline compila seu projeto para Linux, FreeBSD e NetBSD automaticamente a cada nova tag. Veja o guia sobre cross-compilation para configurações mais avançadas.

Considerações sobre Alocação de Memória em BSD

Os allocators do Zig funcionam de forma consistente entre Linux e BSD, mas há nuances a considerar:

const std = @import("std");
const builtin = @import("builtin");

pub fn createOptimalAllocator() std.mem.Allocator {
    // page_allocator usa mmap em todos os Unix-like (Linux, FreeBSD, NetBSD)
    // O comportamento é consistente entre plataformas
    return std.heap.page_allocator;
}

// Para aplicações de alta performance em servidores BSD
pub fn serverAllocator() std.heap.GeneralPurposeAllocator(.{
    .thread_safe = true,
    // Habilitar safety em produção para detectar bugs
    .safety = builtin.mode == .Debug or builtin.mode == .ReleaseSafe,
}) {
    return .{};
}

O page_allocator do Zig usa mmap tanto em Linux quanto em BSD, garantindo comportamento consistente. Para cenários de alta performance, como processamento de dados ou networking, os mesmos padrões de alocação se aplicam.

Comparação: Cross-Compilation para BSD em Diferentes Linguagens

AspectoZigGoRustC/C++
Setup necessárioNenhum (embutido)GOOS=freebsdInstalar target + linkerToolchain completa
Suporte FreeBSDNativo, CI testadoNativoVia rustup targetManual
Suporte NetBSDNativo, CI testadoParcialTier 3 (sem CI)Manual
Libc incluídaSim (headers + abilists)Sim (Go runtime)Depende do targetNão
Cross-compile CSim (zig cc)Via CGO (complexo)Via cc-rs (complexo)Toolchain externa
Binário estáticoPadrãoPadrãoCom muslCom musl

Para uma comparação mais detalhada entre Zig e outras linguagens, veja os artigos Zig vs Rust, Zig vs Go e o comparativo completo.

Desenvolvedores que usam Rust rustlang.com.br notarão que o suporte BSD no Zig é significativamente mais simples, sem necessidade de instalar targets adicionais via rustup. Já quem vem de Go golang.com.br encontrará uma experiência similar, mas com o bônus de poder cross-compilar código C junto.

Deploy em Servidores FreeBSD

Após compilar, enviar o binário para um servidor FreeBSD é direto:

# Compilar localmente (de qualquer SO)
zig build -Dtarget=x86_64-freebsd -Doptimize=ReleaseSafe

# Enviar para o servidor
scp zig-out/bin/meu-servidor usuario@servidor-freebsd:/usr/local/bin/

# No servidor FreeBSD — criar service rc.d
ssh usuario@servidor-freebsd << 'EOF'
cat > /usr/local/etc/rc.d/meu-servidor << 'RC'
#!/bin/sh
# PROVIDE: meu_servidor
# REQUIRE: NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="meu_servidor"
rcvar="meu_servidor_enable"
command="/usr/local/bin/meu-servidor"
pidfile="/var/run/${name}.pid"

load_rc_config $name
run_rc_command "$1"
RC
chmod +x /usr/local/etc/rc.d/meu-servidor
echo 'meu_servidor_enable="YES"' >> /etc/rc.conf
service meu_servidor start
EOF

Perguntas Frequentes

Preciso de uma máquina FreeBSD para compilar para FreeBSD?

Não. Esse é o grande diferencial do Zig: o compilador inclui todos os headers e informações de libc necessários para gerar binários FreeBSD e NetBSD a partir de qualquer sistema operacional. Basta usar -target x86_64-freebsd ou -target aarch64-freebsd.

O zig cc funciona como substituto do clang no FreeBSD?

Sim, para a maioria dos casos. zig cc é um wrapper sobre o backend LLVM que inclui libc e headers de sistema. Projetos C que compilam com clang ou gcc normalmente compilam com zig cc sem alterações — inclusive para targets BSD.

Quais versões de FreeBSD e NetBSD são suportadas?

O Zig 0.16 suporta oficialmente FreeBSD 14.0+ e NetBSD 10.1+. O compilador inclui headers e abilists destas versões. Versões mais antigas podem funcionar, mas não são testadas no CI oficial.

Posso usar bibliotecas nativas do FreeBSD como jails ou ZFS?

Sim, via @cImport do Zig. Você pode importar headers do FreeBSD e chamar APIs nativas como jail_attach(), funções de libzfs, ou qualquer API disponível nos headers incluídos. A interoperabilidade C do Zig facilita esse processo.

Continue aprendendo Zig

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