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:
| Alvo | Descrição |
|---|---|
x86_64-linux | Linux 64-bit (servidores, desktops) |
x86_64-windows | Windows 64-bit |
x86_64-macos | macOS Intel |
aarch64-linux | Linux ARM 64-bit (Raspberry Pi 4, servidores ARM) |
aarch64-macos | macOS Apple Silicon (M1/M2/M3) |
wasm32-freestanding | WebAssembly |
arm-linux-gnueabihf | Linux ARM 32-bit (Raspberry Pi antigo) |
riscv64-linux | Linux 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
| Modo | Descrição |
|---|---|
Debug | Padrão. Informações de debug, sem otimização |
ReleaseSafe | Otimizado, mantém verificações de segurança |
ReleaseFast | Máxima performance, remove verificações |
ReleaseSmall | Menor 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
- Teste em emuladores: Use QEMU para testar binários ARM no seu desktop x86:
qemu-aarch64 ./meu-app-arm. - Use Docker para validação: Teste binários Linux em containers Docker para garantir compatibilidade.
- Cuidado com syscalls específicas: Algumas chamadas de sistema são específicas de uma plataforma. Use
builtin.os.tagpara condicionais de plataforma. - Prefira musl para distribuição: Binários estáticos com musl são mais portáteis que glibc dinâmico.
- 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.