Introdução
Migrar de Rust para Zig pode parecer contra-intuitivo — por que trocar as garantias de segurança do borrow checker? A resposta geralmente envolve simplicidade: Zig resolve os mesmos problemas com menos complexidade, sem lifetimes, sem traits, sem borrow checker. A segurança vem de verificações em runtime (em debug) e de boas práticas com allocators.
Para comparação detalhada, veja Zig vs Rust e Zig para Desenvolvedores Rust.
Pré-requisitos
- Zig instalado (versão 0.13+). Veja Como Instalar Zig
- Conhecimento de Rust
- Familiaridade básica com Zig. Consulte Introdução ao Zig
Ownership e Borrowing
Rust
fn processar(dados: Vec<u8>) -> Vec<u8> {
// dados é movido para esta função
dados.into_iter().map(|b| b + 1).collect()
}
fn analisar(dados: &[u8]) -> usize {
// dados é emprestado (borrowed)
dados.iter().filter(|&&b| b > 0).count()
}
Zig
fn processar(allocator: std.mem.Allocator, dados: []const u8) ![]u8 {
const resultado = try allocator.alloc(u8, dados.len);
for (dados, 0..) |b, i| {
resultado[i] = b + 1;
}
return resultado;
}
fn analisar(dados: []const u8) usize {
var count: usize = 0;
for (dados) |b| {
if (b > 0) count += 1;
}
return count;
}
Em Zig, não há ownership tracking automático. A responsabilidade é do programador:
- Quem aloca, documenta quem libera
- Use
defereerrdeferpara garantir limpeza - Allocators tornam alocações visíveis
Veja Padrões Errdefer e ArenaAllocator.
Result e Option
Rust
fn dividir(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Divisão por zero".to_string())
} else {
Ok(a / b)
}
}
fn buscar(id: u32) -> Option<Usuario> {
if id == 0 { None } else { Some(Usuario { id }) }
}
Zig
fn dividir(a: f64, b: f64) !f64 {
if (b == 0) return error.DivisaoPorZero;
return a / b;
}
fn buscar(id: u32) ?Usuario {
if (id == 0) return null;
return Usuario{ .id = id };
}
Result<T,E> em Rust mapeia para !T (error union) em Zig. Option<T> mapeia para ?T (optional). Veja Error Sets Customizados.
Encadeamento
// Rust
let resultado = valor
.ok_or("não encontrado")?
.processar()?
.finalizar();
// Zig
const intermediario = valor orelse return error.NaoEncontrado;
const processado = try intermediario.processar();
const resultado = processado.finalizar();
Traits para Comptime
Rust
trait Serializar {
fn serializar(&self) -> Vec<u8>;
fn tamanho(&self) -> usize;
}
fn salvar<T: Serializar>(item: &T) -> Result<(), std::io::Error> {
let dados = item.serializar();
std::fs::write("saida.bin", &dados)
}
Zig
fn salvar(item: anytype) !void {
const dados = item.serializar();
defer item.allocator.free(dados); // se aplicável
try std.fs.cwd().writeFile("saida.bin", dados);
}
// Com verificação explícita em compilação
fn salvarVerificado(comptime T: type, item: T) !void {
comptime {
if (!@hasDecl(T, "serializar")) {
@compileError("Tipo deve implementar serializar()");
}
}
const dados = item.serializar();
try std.fs.cwd().writeFile("saida.bin", dados);
}
Pattern Matching
Rust
match resultado {
Ok(valor) => println!("Sucesso: {}", valor),
Err(e) => eprintln!("Erro: {}", e),
}
match forma {
Forma::Circulo { raio } => PI * raio * raio,
Forma::Retangulo { largura, altura } => largura * altura,
Forma::Triangulo { base, altura } => base * altura / 2.0,
}
Zig
if (resultado) |valor| {
std.debug.print("Sucesso: {}\n", .{valor});
} else |err| {
std.debug.print("Erro: {}\n", .{err});
}
switch (forma) {
.circulo => |c| std.math.pi * c.raio * c.raio,
.retangulo => |r| r.largura * r.altura,
.triangulo => |t| t.base * t.altura / 2.0,
}
Enums com Dados
Rust
enum Mensagem {
Texto(String),
Imagem { url: String, largura: u32, altura: u32 },
Ping,
}
Zig
const Mensagem = union(enum) {
texto: []const u8,
imagem: struct {
url: []const u8,
largura: u32,
altura: u32,
},
ping: void,
};
Iteradores
Rust
let soma: i32 = numeros
.iter()
.filter(|&&n| n > 0)
.map(|&n| n * 2)
.sum();
Zig
var soma: i32 = 0;
for (numeros) |n| {
if (n > 0) {
soma += n * 2;
}
}
Zig não tem iteradores lazy como Rust. Use loops for explícitos — mais verboso, mas mais claro sobre o que acontece.
Lifetimes
Zig não tem lifetimes. A gestão de tempo de vida é responsabilidade do programador:
Rust
struct Parser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, pos: 0 }
}
}
Zig
const Parser = struct {
input: []const u8,
pos: usize,
pub fn init(input: []const u8) Parser {
return .{ .input = input, .pos = 0 };
}
};
// O programador garante que input vive mais que Parser
Vec, HashMap, String
| Rust | Zig |
|---|---|
Vec<T> | std.ArrayList(T) |
HashMap<K,V> | std.HashMap(K,V,...) |
String | std.ArrayList(u8) |
&str | []const u8 |
Box<T> | allocator.create(T) |
Arc<T> | Manual (sem equivalente direto) |
Closures
Rust
let dobrar = |x: i32| x * 2;
let resultado: Vec<i32> = numeros.iter().map(dobrar).collect();
Zig
// Zig não tem closures com captura
// Usar funções nomeadas ou structs
fn dobrar(x: i32) i32 {
return x * 2;
}
// Ou usar ponteiros de função
const operacao: *const fn (i32) i32 = dobrar;
Testes
Rust
#[test]
fn test_soma() {
assert_eq!(soma(2, 3), 5);
}
Zig
test "soma" {
try std.testing.expectEqual(@as(i32, 5), soma(2, 3));
}
Veja Testes Unitários e Testes com Allocator.
Cargo para build.zig
Veja Migrar de Makefile para build.zig para conceitos similares. O build.zig substitui tanto Cargo.toml quanto build.rs.
Conclusão
A migração de Rust para Zig troca verificações de compilação (borrow checker) por simplicidade e controle explícito. O código resultante é mais simples de ler e entender, mas exige mais disciplina do programador para garantir segurança de memória.
Se a complexidade do borrow checker está diminuindo a produtividade da sua equipe e o projeto não exige garantias formais de segurança de memória, Zig pode ser uma alternativa pragmática. Consulte Quando Usar Zig para avaliar se é a escolha certa.