Cross-Compilation com Zig: Compile para Qualquer Plataforma

Uma das capacidades mais impressionantes da zig lang é a cross-compilation embutida. Enquanto outras linguagens exigem toolchains complexas para compilar para plataformas diferentes, a linguagem Zig permite compilar para mais de 30 alvos diferentes com um único comando, sem instalar nada extra. Neste tutorial, vamos explorar como usar esse recurso poderoso na prática.

O Que é Cross-Compilation

Cross-compilation (compilação cruzada) é o processo de compilar código em uma plataforma para ser executado em outra. Por exemplo, você está no macOS e quer gerar um executável para Linux, ou está no Linux e precisa de um binário para Windows.

Cenários comuns onde cross-compilation é essencial:

  • Distribuição de software: Gerar binários para todas as plataformas dos seus usuários a partir de uma única máquina de desenvolvimento.
  • Sistemas embarcados: Compilar para ARM, RISC-V ou outros processadores a partir do seu desktop x86.
  • CI/CD: Construir releases multiplataforma em um pipeline de integração contínua.
  • Servidores: Compilar no seu Mac para deploy em servidores Linux.

A Vantagem Única do Zig

Em C e C++, cross-compilation é notoriamente difícil. Você precisa instalar toolchains específicas, configurar sysroots, encontrar headers e bibliotecas para a plataforma alvo, e lidar com incompatibilidades. Com Zig, tudo isso é desnecessário.

Zig inclui no próprio compilador:

  • Linker embutido: Não depende de linkers externos.
  • Headers C da libc: Para todas as plataformas suportadas.
  • Backend LLVM: Gera código nativo otimizado para qualquer alvo.
  • Sem dependências externas: Um único binário do Zig é tudo que você precisa.

Listando Alvos Disponíveis

Para ver todos os alvos que Zig suporta, use o comando:

zig targets

Esse comando retorna um JSON extenso com todas as combinações de CPU e sistema operacional. Os alvos mais comuns incluem:

AlvoDescrição
x86_64-linuxLinux 64-bit (servidores, desktops)
x86_64-windowsWindows 64-bit
x86_64-macosmacOS Intel
aarch64-linuxLinux ARM 64-bit (Raspberry Pi 4, servidores ARM)
aarch64-macosmacOS Apple Silicon (M1/M2/M3)
wasm32-freestandingWebAssembly
arm-linux-gnueabihfLinux ARM 32-bit (Raspberry Pi antigo)
riscv64-linuxLinux RISC-V 64-bit

Para filtrar rapidamente os alvos disponíveis:

zig targets | python3 -c "import sys,json; t=json.load(sys.stdin); print('\n'.join(t['arch']))"

Compilação Cruzada na Prática

Vamos criar um programa simples e compilá-lo para diferentes plataformas.

// main.zig
const std = @import("std");
const builtin = @import("builtin");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();

    try stdout.print("Olá do Zig!\n", .{});
    try stdout.print("Arquitetura: {s}\n", .{@tagName(builtin.cpu.arch)});
    try stdout.print("Sistema: {s}\n", .{@tagName(builtin.os.tag)});
    try stdout.print("Tamanho do ponteiro: {} bytes\n", .{@sizeOf(usize)});
}

Agora, compile para diferentes alvos a partir de qualquer plataforma:

Compilar para Linux x86_64

zig build-exe main.zig -target x86_64-linux

Compilar para Windows x86_64

zig build-exe main.zig -target x86_64-windows

Isso gera um arquivo .exe que pode ser executado diretamente no Windows, sem precisar de nenhuma ferramenta do Windows instalada.

Compilar para macOS ARM (Apple Silicon)

zig build-exe main.zig -target aarch64-macos

Compilar para Raspberry Pi (ARM Linux)

zig build-exe main.zig -target aarch64-linux

Para Raspberry Pi mais antigos (32-bit):

zig build-exe main.zig -target arm-linux-gnueabihf

Compilar com Otimização

Para builds de release, adicione a flag de otimização:

zig build-exe main.zig -target x86_64-linux -O ReleaseSmall
zig build-exe main.zig -target x86_64-linux -O ReleaseFast
zig build-exe main.zig -target x86_64-linux -O ReleaseSafe
ModoDescrição
DebugPadrão. Informações de debug, sem otimização
ReleaseSafeOtimizado, mantém verificações de segurança
ReleaseFastMáxima performance, remove verificações
ReleaseSmallMenor binário possível

Cross-Compilation com o Build System

Para projetos maiores, use o build.zig para configurar cross-compilation de forma programática. Se você ainda não conhece o build system, veja o tutorial Zig Build System: Guia Completo.

// build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    // Permite que o usuário escolha o alvo via linha de comando
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

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

    b.installArtifact(exe);
}

Agora você pode compilar para qualquer alvo:

zig build -Dtarget=x86_64-linux -Doptimize=ReleaseFast
zig build -Dtarget=x86_64-windows -Doptimize=ReleaseSmall
zig build -Dtarget=aarch64-macos -Doptimize=ReleaseFast

Build para Múltiplos Alvos

Você pode criar um step personalizado que compila para todos os alvos de uma vez:

// build.zig
const std = @import("std");

const alvos = [_]std.Target.Query{
    .{ .cpu_arch = .x86_64, .os_tag = .linux },
    .{ .cpu_arch = .x86_64, .os_tag = .windows },
    .{ .cpu_arch = .aarch64, .os_tag = .linux },
    .{ .cpu_arch = .aarch64, .os_tag = .macos },
};

pub fn build(b: *std.Build) void {
    const optimize = b.standardOptimizeOption(.{});

    for (alvos) |t| {
        const nome = std.fmt.allocPrint(b.allocator, "meu-app-{s}-{s}", .{
            @tagName(t.cpu_arch.?),
            @tagName(t.os_tag.?),
        }) catch "meu-app";

        const exe = b.addExecutable(.{
            .name = nome,
            .root_source_file = b.path("src/main.zig"),
            .target = b.resolveTargetQuery(t),
            .optimize = optimize,
        });

        b.installArtifact(exe);
    }
}

Execute com:

zig build -Doptimize=ReleaseFast

Isso gera quatro binários no diretório zig-out/bin/, um para cada plataforma.

Zig como Cross-Compiler C: zig cc

Uma das funcionalidades mais revolucionárias do Zig é poder ser usado como um compilador C cross-platform, substituindo gcc ou clang.

Compilando C com zig cc

// hello.c
#include <stdio.h>

int main() {
    printf("Hello from C, compiled with Zig!\n");
    return 0;
}
# Compilar para a plataforma local
zig cc hello.c -o hello

# Cross-compilar para Linux
zig cc hello.c -o hello-linux -target x86_64-linux-gnu

# Cross-compilar para Windows
zig cc hello.c -o hello.exe -target x86_64-windows-gnu

# Cross-compilar para ARM
zig cc hello.c -o hello-arm -target aarch64-linux-gnu

Usando zig cc como Drop-in Replacement

Muitos projetos C usam Makefiles ou CMake. Você pode substituir o compilador por zig cc sem alterar nada no projeto:

# Com Make
CC="zig cc" make

# Com CMake
cmake -DCMAKE_C_COMPILER="zig cc" -DCMAKE_CXX_COMPILER="zig c++" ..

# Com autotools/configure
CC="zig cc" CXX="zig c++" ./configure --host=aarch64-linux-gnu

Isso é especialmente útil para compilar bibliotecas C/C++ que seu projeto Zig vai consumir via @cImport. Para saber mais sobre como integrar código C, consulte o tutorial de interoperabilidade C em Zig.

Exemplo Prático: Compilar SQLite para ARM

# Baixar o código-fonte do SQLite
curl -O https://www.sqlite.org/2024/sqlite-amalgamation-3450000.zip
unzip sqlite-amalgamation-3450000.zip
cd sqlite-amalgamation-3450000

# Compilar para ARM Linux
zig cc -O2 shell.c sqlite3.c -o sqlite3-arm \
    -target aarch64-linux-gnu \
    -lpthread -ldl -lm

Isso gera um binário SQLite funcional para ARM Linux, compilado no seu desktop x86.

CI/CD: GitHub Actions Multiplataforma

Uma aplicação prática muito comum de cross-compilation é gerar releases para múltiplas plataformas no CI/CD.

Workflow do GitHub Actions

# .github/workflows/release.yml
name: Release Multiplataforma

on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-linux
          - x86_64-windows
          - aarch64-linux
          - aarch64-macos
          - x86_64-macos

    steps:
      - uses: actions/checkout@v4

      - name: Instalar Zig
        uses: goto-bus-stop/setup-zig@v2
        with:
          version: 0.13.0

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

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

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4

      - name: Criar Release
        uses: softprops/action-gh-release@v1
        with:
          files: build-*/meu-app*

Com esse workflow, cada tag v* gera automaticamente binários para cinco plataformas, tudo em runners Linux. Não é necessário usar macOS runners para compilar para macOS ou Windows runners para compilar para Windows.

Script de Release Local

Para criar releases localmente sem CI/CD:

#!/bin/bash
# release.sh
VERSION=$1
TARGETS=(
    "x86_64-linux"
    "x86_64-windows"
    "aarch64-linux"
    "aarch64-macos"
    "x86_64-macos"
)

mkdir -p releases/$VERSION

for TARGET in "${TARGETS[@]}"; do
    echo "Compilando para $TARGET..."
    zig build -Dtarget=$TARGET -Doptimize=ReleaseFast

    # Copiar binário com nome descritivo
    cp zig-out/bin/meu-app* "releases/$VERSION/meu-app-$TARGET"
done

echo "Binários gerados em releases/$VERSION/"
ls -la releases/$VERSION/

Linkagem Estática vs Dinâmica

Por padrão, Zig produz binários com linkagem estática da musl libc no Linux, resultando em executáveis portáteis que rodam em qualquer distribuição.

# Linkagem estática (padrão no Linux com musl)
zig build-exe main.zig -target x86_64-linux-musl

# Linkagem dinâmica (usa glibc do sistema)
zig build-exe main.zig -target x86_64-linux-gnu

A vantagem da linkagem estática com musl é que o binário resultante não tem nenhuma dependência externa. Você pode copiar o executável para qualquer máquina Linux e ele simplesmente funciona, sem se preocupar com versões de glibc.

# Verificar dependências do binário
file meu-app
# meu-app: ELF 64-bit LSB executable, x86-64, statically linked

ldd meu-app
# not a dynamic executable (é estático!)

Cross-Compilation de Projetos Mistos (Zig + C)

Zig se integra perfeitamente com código C, e a cross-compilation funciona igualmente bem para projetos mistos.

// build.zig para projeto Zig + C
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

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

    // Adicionar código-fonte C
    exe.addCSourceFiles(.{
        .files = &.{
            "vendor/sqlite3.c",
            "vendor/miniz.c",
        },
        .flags = &.{
            "-DSQLITE_OMIT_LOAD_EXTENSION",
            "-O2",
        },
    });

    // Adicionar diretório de includes
    exe.addIncludePath(b.path("vendor/"));

    // Linkar biblioteca do sistema
    exe.linkLibC();

    b.installArtifact(exe);
}

Agora, zig build -Dtarget=aarch64-linux compila tanto o código Zig quanto o código C para ARM, tudo de uma vez.

Verificando o Binário Gerado

Após a cross-compilation, é útil verificar se o binário foi gerado corretamente:

# Verificar o tipo do arquivo
file meu-app
# meu-app: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV)

# Verificar o tamanho
ls -lh meu-app

# Para Windows, verificar com file também
file meu-app.exe
# meu-app.exe: PE32+ executable (console) x86-64, for MS Windows

Dicas e Boas Práticas

  1. Teste em emuladores: Use QEMU para testar binários ARM no seu desktop x86: qemu-aarch64 ./meu-app-arm.
  2. Use Docker para validação: Teste binários Linux em containers Docker para garantir compatibilidade.
  3. Cuidado com syscalls específicas: Algumas chamadas de sistema são específicas de uma plataforma. Use builtin.os.tag para condicionais de plataforma.
  4. Prefira musl para distribuição: Binários estáticos com musl são mais portáteis que glibc dinâmico.
  5. Automatize no CI: Configure cross-compilation no GitHub Actions para gerar releases automaticamente.
// Código condicional por plataforma
const std = @import("std");
const builtin = @import("builtin");

pub fn obterDiretorioConfig() []const u8 {
    return switch (builtin.os.tag) {
        .windows => "C:\\ProgramData\\MeuApp",
        .macos => "/Library/Application Support/MeuApp",
        .linux => "/etc/meuapp",
        else => "/tmp/meuapp",
    };
}

Conclusão

A cross-compilation embutida é um dos maiores trunfos do Zig. Em vez de configurar toolchains complexas ou depender de containers Docker para compilar para outras plataformas, Zig resolve tudo com um único binário e uma flag -target. Seja para distribuir software multiplataforma, compilar para sistemas embarcados ou otimizar pipelines de CI/CD, a capacidade de cross-compilation do Zig transforma tarefas que costumavam ser dolorosas em operações triviais.

Leia Também

Continue aprendendo Zig

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