Build e interop com C
O sistema de build e o interop com C parecem assuntos distintos, mas os dois respondem à mesma pergunta: como um programa Zig conversa com o que não é Zig, seja o sistema operacional que vai rodá-lo, sejam as bibliotecas C que já existem. Zig trata os dois com o mesmo princípio de sempre, deixar o mecanismo à mostra, e o resultado é um dos toolchains mais agradáveis de usar em linguagens de sistema.
O build.zig é código, não configuração
Section titled “O build.zig é código, não configuração”A maioria das linguagens descreve o build num formato de dados: um Makefile, um Cargo.toml, um CMakeLists. Zig descreve o build num arquivo Zig comum, o build.zig, com uma função build que recebe um *std.Build e monta um grafo de passos. Você não configura um build, você o programa, e ganha if, for e funções para isso.
pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{ .name = "app", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }), }); b.installArtifact(exe);}As duas primeiras linhas são o que dá a compilação cruzada quase de graça. standardTargetOptions expõe uma flag -Dtarget na linha de comando, e é só isso que separa compilar para a máquina local de compilar para aarch64-linux-musl ou x86_64-windows. Zig carrega os headers e a libc de cada alvo, então zig build -Dtarget=aarch64-macos produz um binário para Mac ARM a partir de um Linux x86, sem instalar nenhum toolchain cruzado. Poucas linguagens fazem o cross-compile ser um detalhe em vez de um projeto à parte.
As dependências externas moram num manifesto separado, o build.zig.zon, e são puxadas com b.dependency("nome", .{}) de dentro do build.zig. O .zon é o formato de objeto de Zig, a mesma sintaxe de literais de struct que você usa no código, usada aqui como dados.
@cImport: o header C vira um módulo
Section titled “@cImport: o header C vira um módulo”O interop de Zig com C dispensa a escrita manual de bindings e dispensa um gerador rodado à parte. O @cImport invoca o tradutor de C embutido no compilador em tempo de compilação, lê um header, e devolve um struct Zig com tudo o que aquele header declara.
const c = @cImport({ @cInclude("stdio.h");});
pub fn main() void { _ = c.printf("de Zig, chamando C\n");}c.printf é a função C de verdade, com a assinatura traduzida para tipos Zig, chamável direto. Para funções da libc padrão basta pedir a linkagem com exe.linkLibC() no build.zig; para uma biblioteca do sistema como SQLite ou libcurl, exe.linkSystemLibrary("sqlite3") resolve o resto. Não há uma camada de FFI com marshalling: a chamada compila para a mesma instrução que um programa C faria, porque Zig e C compartilham a ABI da plataforma.
Esse mesmo tradutor está disponível como zig translate-c quando você quer gerar os bindings uma vez e versioná-los, em vez de traduzir a cada compilação. E há o caminho inverso: o zig cc é um compilador de C completo, com a compilação cruzada de Zig embutida, a ponto de muita gente adotá-lo como compilador C mesmo sem escrever Zig.
O que a linguagem não esconde por baixo
Section titled “O que a linguagem não esconde por baixo”Chamar C sem custo só é possível porque Zig respeita, à risca, as convenções que governam binários na sua plataforma, e vale nomeá-las porque elas reaparecem sempre que se desce a esse nível. A ABI é o contrato que diz como tipos são dispostos na memória e como funções recebem argumentos; a convenção de chamada é a parte da ABI que decide quais registradores carregam quais argumentos, e em Zig você a torna explícita com callconv(.C) numa função export ou extern. Quando o compilador termina, o resultado é entregue ao linker, que costura seu código com as bibliotecas em um binário no formato da plataforma, o ELF no Linux, Mach-O no macOS, PE no Windows. Zig não pede que você domine tudo isso para escrever um “olá mundo”, mas quando você linka contra uma biblioteca C, ou depura por que um símbolo não foi encontrado, esses são os termos do assunto, e a linguagem os mantém visíveis em vez de escondê-los atrás de um runtime.
Palavras-chave
Section titled “Palavras-chave”Conceitos: build como código, grafo de passos, compilação cruzada, interop sem FFI, tradução de C, ABI, convenção de chamada, linking
Ferramentas: build.zig, build.zig.zon, zig build, zig cc, zig translate-c
Linguagem: @cImport, @cInclude, linkLibC, linkSystemLibrary, export, extern, callconv(.C)
Formatos: ELF, Mach-O, PE, .zon