Se você é um programador Rust experiente e está curioso sobre Zig, este guia é para você. Vamos explorar, lado a lado, como os conceitos que você já domina em Rust se traduzem para Zig — e entender por que tantos desenvolvedores de sistemas estão adotando essa linguagem como complemento ou alternativa ao Rust.
Introdução: Por que Programadores Rust se Interessam por Zig?
Rust revolucionou a programação de sistemas com seu borrow checker e garantias de segurança em tempo de compilação. Então por que um programador Rust deveria conhecer Zig?
A resposta está na filosofia de simplicidade. Zig resolve muitos dos mesmos problemas que Rust, mas com uma abordagem radicalmente diferente:
- Sem borrow checker, sem lifetimes: Zig confia no programador e fornece ferramentas de detecção em runtime (em modo debug) em vez de um sistema de tipos complexo.
- Sem macros procedurais:
comptimesubstitui todo o sistema de macros de Rust com código Zig real, depurável. - Sem traits nem generics tradicionais: Polimorfismo é feito via
comptimee interfaces baseadas em convenção. - Interop com C nativa: Importe headers C diretamente, sem
bindgenouunsafeblocks. - Build system integrado: Sem Cargo.toml, sem TOML, sem dependências externas. O
build.zigé escrito na própria linguagem. - Binários pequenos e previsíveis: Sem runtime, sem unwinding de exceções, controle total sobre alocações.
Aqui está uma visão geral das diferenças fundamentais:
| Característica | Rust | Zig |
|---|---|---|
| Segurança de memória | Borrow checker (compilação) | Allocators explícitos + detecção em debug |
| Tratamento de erros | Result<T, E> e ? | Error unions (!T) e try/catch |
| Generics | Traits + type parameters | comptime + duck typing |
| Macros | macro_rules! e proc macros | comptime (código real) |
| Build system | Cargo | build.zig integrado |
| Async | async/.await + runtime (Tokio) | io_uring nativo (sem runtime) |
| Strings | String / &str | []const u8 (slices) |
| Null safety | Option<T> | Tipos opcionais (?T) |
| FFI com C | extern "C" + unsafe | @cImport nativo, sem unsafe |
| Gerenciamento de pacotes | crates.io | Zig Package Manager (build.zig.zon) |
Modelo de Memória: Ownership/Borrow Checker vs Allocators Explícitos
Esta talvez seja a maior diferença conceitual entre as duas linguagens. Em Rust, o compilador rastreia a propriedade (ownership) de cada valor e garante que referências sejam sempre válidas. Em Zig, o programador gerencia a memória explicitamente usando allocators.
Ownership e Move Semantics
Rust — O valor é movido e a variável original se torna inválida:
fn main() {
let nome = String::from("Zig Brasil");
let outro = nome; // move ocorre aqui
// println!("{}", nome); // ERRO: nome foi movido
println!("{}", outro);
}
Zig — Não existe conceito de “move”. O programador controla a alocação:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const nome = try std.fmt.allocPrint(allocator, "Zig Brasil", .{});
defer allocator.free(nome);
// Copiar o slice é apenas copiar o ponteiro e o tamanho (não há "move")
const outro = nome;
std.debug.print("{s}\n", .{outro});
std.debug.print("{s}\n", .{nome}); // Perfeitamente válido
}
Borrowing vs Slices
Rust — O borrow checker garante que referências mutáveis sejam exclusivas:
fn processar(dados: &mut Vec<i32>) {
dados.push(42);
}
fn visualizar(dados: &[i32]) {
for item in dados {
println!("{}", item);
}
}
fn main() {
let mut lista = vec![1, 2, 3];
processar(&mut lista);
visualizar(&lista);
}
Zig — Slices são passados explicitamente, sem borrow checker:
const std = @import("std");
fn processar(lista: *std.ArrayList(i32)) !void {
try lista.append(42);
}
fn visualizar(dados: []const i32) void {
for (dados) |item| {
std.debug.print("{d}\n", .{item});
}
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var lista = std.ArrayList(i32).init(allocator);
defer lista.deinit();
try lista.appendSlice(&.{ 1, 2, 3 });
try processar(&lista);
visualizar(lista.items);
}
Lifetimes vs defer/errdefer
Rust — Lifetimes anotam quanto tempo uma referência vive:
struct Parser<'a> {
input: &'a str,
posicao: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, posicao: 0 }
}
fn proximo_char(&mut self) -> Option<char> {
let ch = self.input[self.posicao..].chars().next()?;
self.posicao += ch.len_utf8();
Some(ch)
}
}
Zig — defer e errdefer garantem limpeza determinística:
const std = @import("std");
const Parser = struct {
input: []const u8,
posicao: usize,
pub fn init(input: []const u8) Parser {
return .{
.input = input,
.posicao = 0,
};
}
pub fn proximoChar(self: *Parser) ?u8 {
if (self.posicao >= self.input.len) return null;
const ch = self.input[self.posicao];
self.posicao += 1;
return ch;
}
};
pub fn main() void {
var parser = Parser.init("Olá Zig");
while (parser.proximoChar()) |ch| {
std.debug.print("{c}", .{ch});
}
std.debug.print("\n", .{});
}
Arena Allocator: Padrão Comum em Zig
Em Rust, o borrow checker lida com a maioria dos casos de vida útil de memória. Em Zig, o padrão ArenaAllocator é extremamente comum para agrupar alocações e liberá-las de uma só vez:
const std = @import("std");
fn processarDocumento(allocator: std.mem.Allocator, conteudo: []const u8) ![]const u8 {
// Todas as alocações temporárias usam o arena
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit(); // Libera TUDO de uma vez
const arena_alloc = arena.allocator();
// Alocações temporárias — não precisam de free individual
const linhas = try splitLinhas(arena_alloc, conteudo);
const processadas = try processarLinhas(arena_alloc, linhas);
// Apenas o resultado final é alocado no allocator externo
return try std.fmt.allocPrint(allocator, "{s}", .{processadas});
}
fn splitLinhas(allocator: std.mem.Allocator, texto: []const u8) ![][]const u8 {
var lista = std.ArrayList([]const u8).init(allocator);
var iter = std.mem.splitScalar(u8, texto, '\n');
while (iter.next()) |linha| {
try lista.append(linha);
}
return lista.toOwnedSlice();
}
fn processarLinhas(allocator: std.mem.Allocator, linhas: [][]const u8) ![]const u8 {
var resultado = std.ArrayList(u8).init(allocator);
for (linhas) |linha| {
try resultado.appendSlice(linha);
try resultado.append('\n');
}
return resultado.toOwnedSlice();
}
Tratamento de Erros: Result<T,E> vs Error Unions
Ambas as linguagens tratam erros como valores (sem exceções), mas a sintaxe e os mecanismos são distintos.
Padrão Básico
Rust:
use std::fs;
use std::io;
fn ler_config(caminho: &str) -> Result<String, io::Error> {
let conteudo = fs::read_to_string(caminho)?;
Ok(conteudo.trim().to_string())
}
fn main() {
match ler_config("config.toml") {
Ok(config) => println!("Config: {}", config),
Err(e) => eprintln!("Erro: {}", e),
}
}
Zig:
const std = @import("std");
fn lerConfig(allocator: std.mem.Allocator, caminho: []const u8) ![]const u8 {
const arquivo = std.fs.cwd().openFile(caminho, .{}) catch |err| {
std.log.err("Não foi possível abrir {s}: {}", .{ caminho, err });
return err;
};
defer arquivo.close();
return try arquivo.readToEndAlloc(allocator, 1024 * 1024);
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const config = lerConfig(allocator, "config.toml") catch |err| {
std.debug.print("Erro: {}\n", .{err});
return;
};
defer allocator.free(config);
std.debug.print("Config: {s}\n", .{config});
}
Definindo Erros Personalizados
Rust:
#[derive(Debug)]
enum MeuErro {
ArquivoNaoEncontrado,
FormatoInvalido(String),
SemPermissao,
}
impl std::fmt::Display for MeuErro {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
MeuErro::ArquivoNaoEncontrado => write!(f, "Arquivo não encontrado"),
MeuErro::FormatoInvalido(msg) => write!(f, "Formato inválido: {}", msg),
MeuErro::SemPermissao => write!(f, "Sem permissão"),
}
}
}
impl std::error::Error for MeuErro {}
Zig:
const MeuErro = error{
ArquivoNaoEncontrado,
FormatoInvalido,
SemPermissao,
};
fn operacao() MeuErro!u32 {
return MeuErro.FormatoInvalido;
}
fn operacaoComDetalhes() MeuErro!u32 {
// Em Zig, erros são valores simples. Para detalhes extras,
// use logging ou retorne informação adicional via parâmetro.
std.log.err("Formato inválido: campo 'nome' ausente", .{});
return MeuErro.FormatoInvalido;
}
errdefer: O Padrão que Rust Não Tem
Um dos recursos mais elegantes de Zig é errdefer, que executa código de limpeza apenas quando a função retorna um erro:
const std = @import("std");
const Conexao = struct {
socket: std.posix.socket_t,
buffer: []u8,
pub fn init(allocator: std.mem.Allocator, endereco: []const u8) !Conexao {
const socket = try std.posix.socket(
std.posix.AF.INET,
std.posix.SOCK.STREAM,
0,
);
errdefer std.posix.close(socket); // Fecha socket se algo falhar abaixo
const buffer = try allocator.alloc(u8, 4096);
errdefer allocator.free(buffer); // Libera buffer se algo falhar abaixo
// Se a conexão falhar, socket e buffer são liberados automaticamente
try conectar(socket, endereco);
return .{
.socket = socket,
.buffer = buffer,
};
}
pub fn deinit(self: *Conexao, allocator: std.mem.Allocator) void {
std.posix.close(self.socket);
allocator.free(self.buffer);
}
};
fn conectar(socket: std.posix.socket_t, endereco: []const u8) !void {
_ = socket;
_ = endereco;
// Implementação da conexão...
}
Em Rust, esse padrão requer Drop traits ou uso manual de guard patterns. O errdefer de Zig é mais explícito e direto.
Generics: Traits vs Comptime
Esta é uma diferença filosófica profunda. Rust usa traits para polimorfismo parametrizado com verificação em tempo de compilação. Zig usa comptime com duck typing em tempo de compilação.
Função Genérica
Rust:
use std::ops::Add;
fn somar<T: Add<Output = T>>(a: T, b: T) -> T {
a + b
}
fn main() {
println!("{}", somar(10i32, 20i32));
println!("{}", somar(1.5f64, 2.5f64));
}
Zig:
const std = @import("std");
fn somar(comptime T: type, a: T, b: T) T {
return a + b;
}
pub fn main() void {
std.debug.print("{d}\n", .{somar(i32, 10, 20)});
std.debug.print("{d}\n", .{somar(f64, 1.5, 2.5)});
}
Traits vs Interfaces por Convenção
Rust — Traits definem interfaces formais:
trait Serializavel {
fn serializar(&self) -> String;
fn tamanho(&self) -> usize;
}
struct Usuario {
nome: String,
idade: u32,
}
impl Serializavel for Usuario {
fn serializar(&self) -> String {
format!("{}:{}", self.nome, self.idade)
}
fn tamanho(&self) -> usize {
self.nome.len() + 4 // 4 bytes para idade
}
}
fn processar<T: Serializavel>(item: &T) {
println!("Dados: {}", item.serializar());
println!("Tamanho: {} bytes", item.tamanho());
}
Zig — Duck typing em comptime (se o tipo tem os métodos necessários, funciona):
const std = @import("std");
const Usuario = struct {
nome: []const u8,
idade: u32,
pub fn serializar(self: Usuario, buffer: []u8) []u8 {
return std.fmt.bufPrint(buffer, "{s}:{d}", .{ self.nome, self.idade }) catch buffer[0..0];
}
pub fn tamanho(self: Usuario) usize {
return self.nome.len + 4;
}
};
const Produto = struct {
titulo: []const u8,
preco: f64,
pub fn serializar(self: Produto, buffer: []u8) []u8 {
return std.fmt.bufPrint(buffer, "{s}:{d:.2}", .{ self.titulo, self.preco }) catch buffer[0..0];
}
pub fn tamanho(self: Produto) usize {
return self.titulo.len + 8;
}
};
fn processar(comptime T: type, item: T) void {
var buffer: [256]u8 = undefined;
const dados = item.serializar(&buffer);
std.debug.print("Dados: {s}\n", .{dados});
std.debug.print("Tamanho: {d} bytes\n", .{item.tamanho()});
}
pub fn main() void {
const usuario = Usuario{ .nome = "Ana", .idade = 30 };
const produto = Produto{ .titulo = "Teclado", .preco = 199.90 };
processar(Usuario, usuario);
processar(Produto, produto);
}
Trait Objects vs Interfaces Dinâmicas (vtable)
Rust — dyn Trait para dispatch dinâmico:
trait Animal {
fn som(&self) -> &str;
fn nome(&self) -> &str;
}
struct Cachorro { nome: String }
struct Gato { nome: String }
impl Animal for Cachorro {
fn som(&self) -> &str { "Au au!" }
fn nome(&self) -> &str { &self.nome }
}
impl Animal for Gato {
fn som(&self) -> &str { "Miau!" }
fn nome(&self) -> &str { &self.nome }
}
fn apresentar(animal: &dyn Animal) {
println!("{}: {}", animal.nome(), animal.som());
}
Zig — Usa ponteiros opacos e vtables manuais ou std.mem.Allocator-style interfaces:
const std = @import("std");
const Animal = struct {
ptr: *anyopaque,
somFn: *const fn (*anyopaque) []const u8,
nomeFn: *const fn (*anyopaque) []const u8,
pub fn som(self: Animal) []const u8 {
return self.somFn(self.ptr);
}
pub fn nome(self: Animal) []const u8 {
return self.nomeFn(self.ptr);
}
pub fn init(comptime T: type, ptr: *T) Animal {
return .{
.ptr = @ptrCast(ptr),
.somFn = @ptrCast(&T.som),
.nomeFn = @ptrCast(&T.nome),
};
}
};
const Cachorro = struct {
nome_animal: []const u8,
pub fn som(_: *Cachorro) []const u8 {
return "Au au!";
}
pub fn nome(self: *Cachorro) []const u8 {
return self.nome_animal;
}
};
const Gato = struct {
nome_animal: []const u8,
pub fn som(_: *Gato) []const u8 {
return "Miau!";
}
pub fn nome(self: *Gato) []const u8 {
return self.nome_animal;
}
};
fn apresentar(animal: Animal) void {
std.debug.print("{s}: {s}\n", .{ animal.nome(), animal.som() });
}
pub fn main() void {
var cachorro = Cachorro{ .nome_animal = "Rex" };
var gato = Gato{ .nome_animal = "Mimi" };
apresentar(Animal.init(Cachorro, &cachorro));
apresentar(Animal.init(Gato, &gato));
}
Build System: Cargo vs Zig Build
Programadores Rust estão acostumados com o excelente Cargo. O sistema de build do Zig é diferente, mas igualmente poderoso.
Estrutura de Projeto
Rust (Cargo):
meu-projeto/
├── Cargo.toml
├── Cargo.lock
├── src/
│ ├── main.rs
│ ├── lib.rs
│ └── utils/
│ └── mod.rs
└── tests/
└── integration_test.rs
Zig:
meu-projeto/
├── build.zig
├── build.zig.zon
├── src/
│ ├── main.zig
│ └── utils.zig
└── test/
└── integration_test.zig
Arquivo de Configuração
Rust — Cargo.toml:
[package]
name = "meu-projeto"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
criterion = "0.5"
Zig — build.zig:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "meu-projeto",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
// Testes unitários
const testes = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const rodar_testes = b.addRunArtifact(testes);
const passo_teste = b.step("test", "Executar testes unitários");
passo_teste.dependOn(&rodar_testes.step);
// Passo para rodar o executável
const rodar = b.addRunArtifact(exe);
rodar.step.dependOn(b.getInstallStep());
const passo_rodar = b.step("run", "Executar o programa");
passo_rodar.dependOn(&rodar.step);
}
Dependências Externas
Zig — build.zig.zon:
.{
.name = "meu-projeto",
.version = "0.1.0",
.dependencies = .{
.zap = .{
.url = "https://github.com/zigzap/zap/archive/v0.1.0.tar.gz",
.hash = "122013fd26b5cf2608da3905cf04e2b8eff9e74310ff8cfb15e41c1e83847e01e5",
},
},
.paths = .{"."},
}
Comandos Comparados
| Ação | Rust (Cargo) | Zig |
|---|---|---|
| Compilar | cargo build | zig build |
| Executar | cargo run | zig build run |
| Testar | cargo test | zig build test |
| Release | cargo build --release | zig build -Doptimize=ReleaseFast |
| Cross-compile | Precisa de toolchain | zig build -Dtarget=aarch64-linux |
| Formatar | cargo fmt | zig fmt src/ |
Async: Tokio vs io_uring
Esta é uma das maiores diferenças entre as duas linguagens. Rust depende de runtimes de terceiros (como Tokio) para async. Zig integra suporte nativo a io_uring diretamente.
Servidor HTTP Básico
Rust com Tokio:
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Servidor rodando em :8080");
loop {
let (mut socket, addr) = listener.accept().await?;
println!("Conexão de: {}", addr);
tokio::spawn(async move {
let mut buf = [0u8; 1024];
match socket.read(&mut buf).await {
Ok(n) => {
let resposta = "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\nOlá";
let _ = socket.write_all(resposta.as_bytes()).await;
let _ = socket.shutdown().await;
}
Err(e) => eprintln!("Erro: {}", e),
}
});
}
}
Zig com std.http (síncrono, mas leve):
const std = @import("std");
const net = std.net;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var server = try std.http.Server.init(allocator, .{});
defer server.deinit();
const endereco = try net.Address.parseIp("127.0.0.1", 8080);
try server.listen(endereco);
std.debug.print("Servidor rodando em :8080\n", .{});
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("Olá");
try response.finish();
}
}
Async sem Runtime
A diferença fundamental entre Rust e Zig em relação a async:
| Aspecto | Rust | Zig |
|---|---|---|
| Runtime | Necessário (Tokio, async-std) | Nenhum (usa io_uring ou epoll) |
| Modelo | Futures + executor | Event loop no kernel |
| Overhead | Alocações para tasks | Zero-copy, sem alocações extras |
| Ecossistema | Divisão sync/async | Uma única API para tudo |
| Coloração de funções | Sim (async fn vs fn) | Não existe diferenciação |
Em Zig, operações de I/O podem ser feitas de forma eficiente usando threads do sistema operacional ou integrações diretas com io_uring no Linux, sem necessidade de um runtime separado como Tokio.
Macros vs Comptime
Se você já escreveu uma proc macro em Rust, sabe a dor: crate separado, syn, quote, TokenStream, compilação lenta. Em Zig, comptime faz tudo isso com código Zig normal.
Derive Macro vs Comptime Reflection
Rust — Derive macro para serialização (simplificado):
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Config {
host: String,
porta: u16,
debug: bool,
}
fn main() {
let config = Config {
host: "localhost".to_string(),
porta: 8080,
debug: true,
};
let json = serde_json::to_string_pretty(&config).unwrap();
println!("{}", json);
}
Zig — Reflexão com comptime para gerar JSON:
const std = @import("std");
const Config = struct {
host: []const u8,
porta: u16,
debug: bool,
};
fn paraJson(comptime T: type, valor: T, writer: anytype) !void {
const info = @typeInfo(T);
switch (info) {
.@"struct" => |s| {
try writer.writeAll("{");
inline for (s.fields, 0..) |field, i| {
if (i > 0) try writer.writeAll(",");
try writer.print("\"{s}\":", .{field.name});
try paraJson(field.type, @field(valor, field.name), writer);
}
try writer.writeAll("}");
},
.pointer => |p| {
if (p.size == .Slice and p.child == u8) {
try writer.print("\"{s}\"", .{valor});
}
},
.int => try writer.print("{d}", .{valor}),
.bool => try writer.writeAll(if (valor) "true" else "false"),
else => try writer.writeAll("null"),
}
}
pub fn main() !void {
const config = Config{
.host = "localhost",
.porta = 8080,
.debug = true,
};
const stdout = std.io.getStdOut().writer();
try paraJson(Config, config, stdout);
try stdout.writeAll("\n");
}
O comptime de Zig itera sobre os campos da struct em tempo de compilação, gerando código especializado para cada tipo. Sem macros, sem crate separado, sem TokenStream.
Macro para Logging vs Comptime
Rust:
macro_rules! log_debug {
($($arg:tt)*) => {
if cfg!(debug_assertions) {
eprintln!("[DEBUG {}:{}] {}", file!(), line!(), format!($($arg)*));
}
};
}
fn main() {
log_debug!("Valor: {}", 42);
}
Zig:
const std = @import("std");
const builtin = @import("builtin");
fn logDebug(comptime fmt: []const u8, args: anytype) void {
if (builtin.mode == .Debug) {
std.debug.print("[DEBUG {s}:{d}] " ++ fmt ++ "\n", .{@src().file, @src().line} ++ args);
}
}
pub fn main() void {
logDebug("Valor: {d}", .{42});
}
Geração de Código em Comptime
Um dos poderes mais impressionantes do comptime é gerar tipos e funções inteiramente em tempo de compilação:
const std = @import("std");
fn CriarVetor(comptime T: type, comptime N: usize) type {
return struct {
dados: [N]T,
const Self = @This();
pub fn init(valor_padrao: T) Self {
return .{ .dados = [_]T{valor_padrao} ** N };
}
pub fn get(self: Self, indice: usize) ?T {
if (indice >= N) return null;
return self.dados[indice];
}
pub fn set(self: *Self, indice: usize, valor: T) void {
if (indice < N) {
self.dados[indice] = valor;
}
}
pub fn soma(self: Self) T {
var total: T = 0;
for (self.dados) |v| {
total += v;
}
return total;
}
pub fn tamanho() usize {
return N;
}
};
}
pub fn main() void {
const Vec3f = CriarVetor(f32, 3);
var v = Vec3f.init(0.0);
v.set(0, 1.0);
v.set(1, 2.0);
v.set(2, 3.0);
std.debug.print("Soma: {d}\n", .{v.soma()});
std.debug.print("Tamanho: {d}\n", .{Vec3f.tamanho()});
}
Em Rust, esse tipo de geração de código exigiria macros procedurais complexas ou genéricos com bounds extensos.
FFI e Interoperabilidade com C
Tanto Rust quanto Zig são excelentes para FFI com C, mas a abordagem é drasticamente diferente.
Importando Funções C
Rust:
// Precisa de bindings manuais ou bindgen
extern "C" {
fn strlen(s: *const std::os::raw::c_char) -> usize;
fn printf(format: *const std::os::raw::c_char, ...) -> i32;
}
fn main() {
let msg = std::ffi::CString::new("Olá do Rust!\n").unwrap();
unsafe {
printf(msg.as_ptr());
let tamanho = strlen(msg.as_ptr());
println!("Tamanho: {}", tamanho);
}
}
Zig:
const std = @import("std");
const c = @cImport({
@cInclude("string.h");
@cInclude("stdio.h");
});
pub fn main() void {
const msg = "Olá do Zig!\n";
_ = c.printf(msg.ptr);
const tamanho = c.strlen(msg.ptr);
std.debug.print("Tamanho: {d}\n", .{tamanho});
}
Em Zig, @cImport lê headers C diretamente e gera bindings automaticamente. Não há unsafe, não há CString, não há bindgen.
Usando uma Biblioteca C Completa
Rust — Usando SQLite via rusqlite (crate com bindings):
use rusqlite::{Connection, Result};
fn main() -> Result<()> {
let conn = Connection::open_in_memory()?;
conn.execute(
"CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nome TEXT NOT NULL)",
[],
)?;
conn.execute("INSERT INTO usuarios (nome) VALUES (?1)", ["Ana"])?;
let mut stmt = conn.prepare("SELECT id, nome FROM usuarios")?;
let rows = stmt.query_map([], |row| {
Ok((row.get::<_, i32>(0)?, row.get::<_, String>(1)?))
})?;
for row in rows {
let (id, nome) = row?;
println!("{}: {}", id, nome);
}
Ok(())
}
Zig — Usando SQLite diretamente via @cImport:
const std = @import("std");
const c = @cImport({
@cInclude("sqlite3.h");
});
pub fn main() !void {
var db: ?*c.sqlite3 = null;
if (c.sqlite3_open(":memory:", &db) != c.SQLITE_OK) {
std.debug.print("Erro ao abrir banco\n", .{});
return error.DatabaseError;
}
defer _ = c.sqlite3_close(db);
// Criar tabela
var err_msg: [*c]u8 = null;
_ = c.sqlite3_exec(
db,
"CREATE TABLE usuarios (id INTEGER PRIMARY KEY, nome TEXT NOT NULL)",
null,
null,
&err_msg,
);
// Inserir dados
_ = c.sqlite3_exec(
db,
"INSERT INTO usuarios (nome) VALUES ('Ana')",
null,
null,
&err_msg,
);
// Consultar
var stmt: ?*c.sqlite3_stmt = null;
_ = c.sqlite3_prepare_v2(db, "SELECT id, nome FROM usuarios", -1, &stmt, null);
while (c.sqlite3_step(stmt) == c.SQLITE_ROW) {
const id = c.sqlite3_column_int(stmt, 0);
const nome_ptr = c.sqlite3_column_text(stmt, 1);
std.debug.print("{d}: {s}\n", .{ id, nome_ptr });
}
_ = c.sqlite3_finalize(stmt);
}
A vantagem de Zig aqui: você usa a API C diretamente, sem camada de abstração intermediária. Para o build, basta adicionar ao build.zig:
exe.linkSystemLibrary("sqlite3");
exe.linkLibC();
Comparação de Padrões Comuns
Iteradores
Rust:
fn main() {
let numeros = vec![1, 2, 3, 4, 5];
let resultado: Vec<i32> = numeros
.iter()
.filter(|&&x| x > 2)
.map(|&x| x * x)
.collect();
println!("{:?}", resultado); // [9, 16, 25]
}
Zig:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const numeros = [_]i32{ 1, 2, 3, 4, 5 };
var resultado = std.ArrayList(i32).init(allocator);
defer resultado.deinit();
for (numeros) |x| {
if (x > 2) {
try resultado.append(x * x);
}
}
for (resultado.items) |v| {
std.debug.print("{d} ", .{v});
}
std.debug.print("\n", .{});
}
Zig não tem iteradores encadeáveis estilo funcional. Os loops for são explícitos e diretos.
Enums com Dados
Rust:
enum Mensagem {
Texto(String),
Imagem { url: String, largura: u32, altura: u32 },
Ping,
}
fn processar(msg: &Mensagem) {
match msg {
Mensagem::Texto(t) => println!("Texto: {}", t),
Mensagem::Imagem { url, largura, altura } => {
println!("Imagem: {} ({}x{})", url, largura, altura)
}
Mensagem::Ping => println!("Ping!"),
}
}
Zig — Tagged unions:
const std = @import("std");
const Mensagem = union(enum) {
texto: []const u8,
imagem: struct {
url: []const u8,
largura: u32,
altura: u32,
},
ping: void,
};
fn processar(msg: Mensagem) void {
switch (msg) {
.texto => |t| std.debug.print("Texto: {s}\n", .{t}),
.imagem => |img| std.debug.print(
"Imagem: {s} ({d}x{d})\n",
.{ img.url, img.largura, img.altura },
),
.ping => std.debug.print("Ping!\n", .{}),
}
}
pub fn main() void {
const msg1 = Mensagem{ .texto = "Olá!" };
const msg2 = Mensagem{ .imagem = .{
.url = "foto.png",
.largura = 800,
.altura = 600,
} };
const msg3 = Mensagem{ .ping = {} };
processar(msg1);
processar(msg2);
processar(msg3);
}
Testes
Rust:
fn soma(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_soma() {
assert_eq!(soma(2, 3), 5);
}
#[test]
#[should_panic]
fn test_overflow() {
let _ = soma(i32::MAX, 1);
}
}
Zig:
const std = @import("std");
const expect = std.testing.expect;
fn soma(a: i32, b: i32) i32 {
return a + b;
}
test "soma basica" {
try expect(soma(2, 3) == 5);
}
test "soma com valores negativos" {
try expect(soma(-1, 1) == 0);
try expect(soma(-5, -3) == -8);
}
test "soma com zero" {
try expect(soma(0, 42) == 42);
}
Em Zig, blocos test ficam no mesmo arquivo que o código, sem necessidade de módulos de teste separados. Execute com zig build test ou zig test src/arquivo.zig.
Quando Escolher Zig vs Rust
Nem toda ferramenta é ideal para todo trabalho. Aqui está uma comparação honesta de quando cada linguagem brilha:
Escolha Rust Quando:
- Segurança de memória em tempo de compilação é crítica: Sistemas financeiros, infraestrutura de segurança.
- O ecossistema crates.io importa: Rust tem um ecossistema massivo de bibliotecas maduras.
- Async complexo é necessário: Tokio é extremamente maduro para servidores de alta concorrência.
- A equipe valoriza garantias do compilador: O borrow checker previne classes inteiras de bugs antes do deploy.
- WebAssembly no frontend: Rust tem o melhor suporte WASM com wasm-bindgen e Yew.
Escolha Zig Quando:
- Interop com C é prioridade: Zig importa headers C nativamente, perfeito para usar bibliotecas C existentes.
- Binários mínimos são necessários: Sem runtime, Zig gera executáveis extremamente pequenos. Ideal para sistemas embarcados.
- Cross-compilation é frequente: Zig compila para qualquer plataforma com um único comando, sem configurar toolchains extras.
- Simplicidade importa mais que garantias: Para equipes que preferem explicitação a abstrações complexas.
- Substituir C em projetos legados: Zig pode ser adotado incrementalmente em projetos C existentes.
- Kernels e drivers: Zig não tem runtime, não tem exceções, não tem hidden control flow.
Tabela de Decisão por Domínio
| Domínio | Rust | Zig | Notas |
|---|---|---|---|
| Web backend | Excelente | Bom | Rust tem Actix/Axum maduros |
| Sistemas embarcados | Bom | Excelente | Zig tem binários menores |
| Game engines | Bom | Excelente | Zig tem controle total, sem GC |
| CLI tools | Excelente | Bom | Rust tem clap, serde, ecossistema |
| Kernels/drivers | Bom | Excelente | Linux adota Zig oficialmente |
| Networking | Excelente | Bom | Tokio é muito maduro |
| Interop com C | Bom | Excelente | Zig importa headers direto |
| WASM | Excelente | Bom | Rust tem melhor tooling WASM |
| DevOps/Infra | Excelente | Bom | Rust tem ecossistema maior |
Podem Coexistir?
Sim. Zig e Rust podem ser usados juntos no mesmo projeto. Ambos geram código compatível com a ABI C, então é possível:
- Escrever uma biblioteca em Zig e usá-la em Rust via FFI.
- Usar o compilador C do Zig (
zig cc) para compilar dependências C de projetos Rust. - Substituir gradualmente componentes C em um projeto misto Rust/C usando Zig.
// lib_zig.zig — exporta função com ABI C
export fn calcular_hash(dados: [*]const u8, tamanho: usize) u64 {
var hash: u64 = 5381;
for (dados[0..tamanho]) |byte| {
hash = ((hash << 5) +% hash) +% byte;
}
return hash;
}
// main.rs — importa a função Zig
extern "C" {
fn calcular_hash(dados: *const u8, tamanho: usize) -> u64;
}
fn main() {
let dados = b"Zig + Rust";
let hash = unsafe { calcular_hash(dados.as_ptr(), dados.len()) };
println!("Hash: {}", hash);
}
Próximos Passos
Agora que você entende como os conceitos de Rust se traduzem para Zig, aqui estão os próximos recursos recomendados:
- Como Instalar Zig — Configure seu ambiente de desenvolvimento Zig.
- Zig Build System — Domine o
build.zige substitua o Cargo na sua rotina. - Zig para Desenvolvedores — Guia geral com exemplos práticos para programadores experientes.
- Zig Async e io_uring — Aprofunde-se no modelo async de Zig sem runtime.
- FFI e Interoperabilidade com C — Como integrar bibliotecas C sem bindings manuais.
- Comptime e Reflexão — Domine o
comptimecomo substituto de macros. - Tratamento de Erros em Zig — Guia completo sobre error unions e padrões de erro.
- Testes em Zig — Como testar código Zig de forma eficiente.
Recursos Externos
- Documentação oficial do Zig — Referência completa da linguagem.
- Zig Learn — Tutorial interativo em inglês.
- Zig no GitHub — Código-fonte e issues.
Se você vem de Rust, vai encontrar em Zig uma linguagem que respeita sua experiência com sistemas, mas oferece um caminho diferente: menos abstração, mais controle explícito, e uma simplicidade que pode ser surpreendentemente produtiva.