Zig FFI: Integração Completa com C e C++

O que é FFI e Por que Zig é Excelente para Interoperabilidade?

Foreign Function Interface (FFI) é o mecanismo que permite que uma linguagem de programação chame código escrito em outra linguagem. No mundo do desenvolvimento de sistemas, onde bibliotecas legadas em C e C++ dominam, uma boa FFI é essencial.

Zig se destaca particularmente nesta área porque:

  1. Compila C/C++ nativamente: O compilador Zig pode compilar código C/C++ junto com Zig
  2. Suporte built-in a C: Zig entende diretamente headers C e pode importá-los automaticamente
  3. ABI compatível: Zig segue as mesmas convenções de chamada que C
  4. Sem runtime pesado: Diferente de outras linguagens, Zig não adiciona overhead ao interagir com C

Neste guia, você aprenderá a integrar Zig com C e C++ de forma prática, desde exemplos simples até casos reais de produção.


Conceitos Fundamentais de FFI

Antes de mergulharmos no código, é importante entender alguns conceitos:

ABI (Application Binary Interface)

ABI define como funções são chamadas em nível de máquina: onde parâmetros são passados (registradores vs pilha), como valores são retornados, e como a pilha é gerenciada.

Zig usa a ABI C por padrão ao interagir com código externo, garantindo compatibilidade total.

extern e export

  • extern: Indica que uma função ou variável vem de fora (C ou outra linguagem)
  • export: Torna uma função Zig visível para código externo

Mangling de Nomes

C++ “mangle” (embaralha) nomes de funções para suportar sobrecarga. Para interoperar com C++ de forma compatível com C, usamos extern "C".


Chamando C a partir de Zig

Vamos começar com o caso mais comum: usar uma biblioteca C existente a partir do Zig.

Exemplo 1: Usando a Biblioteca Padrão C

const std = @import("std");

// Declara funções da libc
extern "c" fn printf(format: [*c]const u8, ...) c_int;
extern "c" fn strlen(s: [*c]const u8) usize;
extern "c" fn malloc(size: usize) ?*anyopaque;
extern "c" fn free(ptr: ?*anyopaque) void;

pub fn main() void {
    // Usando printf da libc
    _ = printf("Olá de C!\n");
    
    // Usando strlen
    const msg = "Zig é incrível";
    const len = strlen(msg);
    std.debug.print("Tamanho: {d}\n", .{len});
}

Pontos importantes:

  • [*c]const u8 é o tipo de ponteiro C para string (equivalente a const char*)
  • O modificador "c" na declaração extern especifica a convenção de chamada C
  • Tipos C são convertidos para equivalentes Zig

Exemplo 2: Criando um Wrapper Idiomático

A forma direta acima funciona, mas não é idiomática. Vamos criar um wrapper mais “Zig-like”:

const std = @import("std");

// Declarações externas (low-level)
extern "c" fn fopen(filename: [*c]const u8, mode: [*c]const u8) ?*FILE;
extern "c" fn fclose(file: ?*FILE) c_int;
extern "c" fn fprintf(file: ?*FILE, format: [*c]const u8, ...) c_int;

const FILE = opaque {};

// Wrapper idiomático em Zig
pub const FileMode = enum {
    read,
    write,
    append,
};

pub const CFile = struct {
    handle: ?*FILE,
    
    pub fn open(path: []const u8, mode: FileMode) !CFile {
        const mode_str = switch (mode) {
            .read => "r",
            .write => "w",
            .append => "a",
        };
        
        // Converte []const u8 para [*c]const u8 (null-terminated)
        const c_path = try std.heap.c_allocator.dupeZ(u8, path);
        defer std.heap.c_allocator.free(c_path);
        
        const handle = fopen(c_path.ptr, mode_str);
        if (handle == null) return error.FileNotFound;
        
        return CFile{ .handle = handle };
    }
    
    pub fn close(self: *CFile) void {
        if (self.handle) |h| {
            _ = fclose(h);
            self.handle = null;
        }
    }
    
    pub fn write(self: CFile, msg: []const u8) void {
        if (self.handle) |h| {
            const c_msg = std.heap.c_allocator.dupeZ(u8, msg) catch return;
            defer std.heap.c_allocator.free(c_msg);
            _ = fprintf(h, "%s", c_msg.ptr);
        }
    }
};

pub fn main() !void {
    var file = try CFile.open("teste.txt", .write);
    defer file.close();
    
    file.write("Escrevendo via wrapper Zig!\n");
}

Vantagens desta abstração:

  • Gerenciamento de memória automático (defer)
  • Tipos seguros (FileMode enum ao invés de strings)
  • Tratamento de erros Zig (!CFile)
  • Fechamento automático com defer

Compilando C junto com Zig

Uma das características únicas do Zig é que ele inclui um compilador C/C++. Isso significa que você pode compilar código C sem precisar do GCC ou Clang instalados.

Exemplo: Biblioteca C em um Projeto Zig

Estrutura do projeto:

meu-projeto/
├── build.zig
├── src/
│   └── main.zig
└── vendor/
    └── minha-lib/
        ├── lib.c
        └── lib.h

vendor/minha-lib/lib.h:

#ifndef MINHA_LIB_H
#define MINHA_LIB_H

typedef struct {
    int x;
    int y;
} Ponto;

Ponto criar_ponto(int x, int y);
int distancia(Ponto a, Ponto b);

#endif

vendor/minha-lib/lib.c:

#include "lib.h"
#include <math.h>

Ponto criar_ponto(int x, int y) {
    Ponto p = {x, y};
    return p;
}

int distancia(Ponto a, Ponto b) {
    double dx = (double)(a.x - b.x);
    double dy = (double)(a.y - b.y);
    return (int)sqrt(dx * dx + dy * dy);
}

build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    
    // Compila a biblioteca C
    const lib = b.addStaticLibrary(.{
        .name = "minha-lib",
        .target = target,
        .optimize = optimize,
    });
    
    lib.addCSourceFiles(.{
        .files = &.{
            "vendor/minha-lib/lib.c",
        },
        .flags = &.{
            "-std=c99",
            "-Wall",
            "-Wextra",
        },
    });
    
    lib.linkLibC();
    b.installArtifact(lib);
    
    // Executável principal
    const exe = b.addExecutable(.{
        .name = "meu-app",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    
    exe.linkLibrary(lib);
    exe.linkLibC();
    
    // Adiciona header path para @cImport
    exe.addIncludePath(b.path("vendor/minha-lib"));
    
    b.installArtifact(exe);
    
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    
    const run_step = b.step("run", "Roda o app");
    run_step.dependOn(&run_cmd.step);
}

src/main.zig:

const std = @import("std");

// Importa o header C automaticamente!
const c = @cImport({
    @cInclude("lib.h");
});

pub fn main() void {
    const p1 = c.criar_ponto(0, 0);
    const p2 = c.criar_ponto(3, 4);
    
    const dist = c.distancia(p1, p2);
    
    std.debug.print("Distância entre ({d},{d}) e ({d},{d}): {d}\n", .{
        p1.x, p1.y, p2.x, p2.y, dist
    });
}

Para compilar e rodar:

zig build run

Saída:

Distância entre (0,0) e (3,4): 5

A Magia de @cImport

A função @cImport é extraordinária: ela analisa headers C em tempo de compilação e gera declarações Zig equivalentes. Isso significa:

  • ✅ Sem bindings manuais
  • ✅ Sem wrappers complexos
  • ✅ Sempre sincronizado com o header
  • ✅ Type-safe

Limitação: @cImport requer que o header C seja parseável. Headers muito complexos ou com macros complicadas podem precisar de ajustes.


Exportando Funções Zig para C

Agora vamos inverter: escrever código Zig que será chamado a partir de C.

Exemplo: Biblioteca Zig Usável por C

src/lib.zig:

const std = @import("std");

// Exporta função para C
export fn zig_soma(a: c_int, b: c_int) c_int {
    return a + b;
}

// Exporta com nome diferente
export fn zig_fatorial(n: c_int) c_int {
    if (n <= 1) return 1;
    return n * zig_fatorial(n - 1);
}

// Exporta estrutura
pub const PontoZig = extern struct {
    x: f64,
    y: f64,
};

export fn zig_distancia(a: PontoZig, b: PontoZig) f64 {
    const dx = a.x - b.x;
    const dy = a.y - b.y;
    return std.math.sqrt(dx * dx + dy * dy);
}

// Função que recebe callback C
export fn zig_processa_array(
    arr: [*]const c_int,
    len: usize,
    callback: ?*const fn (c_int) callconv(.C) void
) void {
    if (callback == null) return;
    
    var i: usize = 0;
    while (i < len) : (i += 1) {
        callback.?(arr[i]);
    }
}

build.zig (para biblioteca):

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    
    // Compila como biblioteca dinâmica
    const lib = b.addSharedLibrary(.{
        .name = "ziglib",
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
    });
    
    b.installArtifact(lib);
}

Para gerar o header C automaticamente:

Zig pode gerar um header C compatível com suas funções exportadas:

// Adicione ao build.zig:
const generate_c_header = b.addSystemCommand(&.{
    "zig", "translate-c", 
    "--target", target.zigTriple(b.allocator) catch unreachable,
    "src/lib.zig"
});

Ou você pode escrever manualmente:

ziglib.h:

#ifndef ZIGLIB_H
#define ZIGLIB_H

#ifdef __cplusplus
extern "C" {
#endif

int zig_soma(int a, int b);
int zig_fatorial(int n);

typedef struct {
    double x;
    double y;
} PontoZig;

double zig_distancia(PontoZig a, PontoZig b);

typedef void (*CallbackInt)(int);
void zig_processa_array(const int *arr, size_t len, CallbackInt callback);

#ifdef __cplusplus
}
#endif

#endif

main.c (usando a biblioteca Zig):

#include <stdio.h>
#include "ziglib.h"

void imprimir(int n) {
    printf("Valor: %d\n", n);
}

int main() {
    printf("3 + 4 = %d\n", zig_soma(3, 4));
    printf("5! = %d\n", zig_fatorial(5));
    
    PontoZig p1 = {0.0, 0.0};
    PontoZig p2 = {3.0, 4.0};
    printf("Distância: %.2f\n", zig_distancia(p1, p2));
    
    int nums[] = {10, 20, 30};
    zig_processa_array(nums, 3, imprimir);
    
    return 0;
}

Compilando:

# Compila a biblioteca Zig
zig build

# Compila o programa C que usa a biblioteca
zig cc main.c -L./zig-out/lib -lziglib -o meu_app

# Roda
./meu_app

Integração com C++

A integração com C++ é mais complexa devido ao name mangling e features como classes, templates e exceções. A estratégia recomendada é usar C como intermediário.

Padrão de Interoperabilidade C++

minha-classe.hpp (C++):

#ifndef MINHA_CLASSE_HPP
#define MINHA_CLASSE_HPP

class MinhaClasse {
public:
    MinhaClasse(int valor);
    ~MinhaClasse();
    
    int getValor() const;
    void setValor(int v);
    int dobrar() const;
    
private:
    int valor;
};

#endif

minha-classe.cpp (C++):

#include "minha-classe.hpp"

MinhaClasse::MinhaClasse(int v) : valor(v) {}
MinhaClasse::~MinhaClasse() = default;

int MinhaClasse::getValor() const { return valor; }
void MinhaClasse::setValor(int v) { valor = v; }
int MinhaClasse::dobrar() const { return valor * 2; }

wrapper-c.cpp (C wrapper):

#include "minha-classe.hpp"

extern "C" {

// Opaque pointer para esconder C++
typedef struct MinhaClasseHandle MinhaClasseHandle;

MinhaClasseHandle* minha_classe_criar(int valor) {
    return reinterpret_cast<MinhaClasseHandle*>(new MinhaClasse(valor));
}

void minha_classe_destruir(MinhaClasseHandle* handle) {
    delete reinterpret_cast<MinhaClasse*>(handle);
}

int minha_classe_get_valor(MinhaClasseHandle* handle) {
    return reinterpret_cast<MinhaClasse*>(handle)->getValor();
}

void minha_classe_set_valor(MinhaClasseHandle* handle, int valor) {
    reinterpret_cast<MinhaClasse*>(handle)->setValor(valor);
}

int minha_classe_dobrar(MinhaClasseHandle* handle) {
    return reinterpret_cast<MinhaClasse*>(handle)->dobrar();
}

} // extern "C"

Usando em Zig:

const std = @import("std");

// Declarações C das funções wrapper
extern "c" fn minha_classe_criar(valor: c_int) ?*anyopaque;
extern "c" fn minha_classe_destruir(handle: ?*anyopaque) void;
extern "c" fn minha_classe_get_valor(handle: ?*anyopaque) c_int;
extern "c" fn minha_classe_set_valor(handle: ?*anyopaque, valor: c_int) void;
extern "c" fn minha_classe_dobrar(handle: ?*anyopaque) c_int;

// Wrapper idiomático Zig
pub const MinhaClasse = struct {
    handle: ?*anyopaque,
    
    pub fn init(valor: i32) MinhaClasse {
        return .{
            .handle = minha_classe_criar(valor),
        };
    }
    
    pub fn deinit(self: *MinhaClasse) void {
        if (self.handle) |h| {
            minha_classe_destruir(h);
            self.handle = null;
        }
    }
    
    pub fn getValor(self: MinhaClasse) i32 {
        if (self.handle) |h| {
            return minha_classe_get_valor(h);
        }
        return 0;
    }
    
    pub fn setValor(self: MinhaClasse, valor: i32) void {
        if (self.handle) |h| {
            minha_classe_set_valor(h, valor);
        }
    }
    
    pub fn dobrar(self: MinhaClasse) i32 {
        if (self.handle) |h| {
            return minha_classe_dobrar(h);
        }
        return 0;
    }
};

pub fn main() void {
    var obj = MinhaClasse.init(21);
    defer obj.deinit();
    
    std.debug.print("Valor: {d}\n", .{obj.getValor()});
    std.debug.print("Dobro: {d}\n", .{obj.dobrar()});
    
    obj.setValor(50);
    std.debug.print("Novo valor: {d}\n", .{obj.getValor()});
}

build.zig para C++:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    
    // Compila o wrapper C++
    const cxx_lib = b.addStaticLibrary(.{
        .name = "cxx-wrapper",
        .target = target,
        .optimize = optimize,
    });
    
    cxx_lib.addCSourceFiles(.{
        .files = &.{
            "src/minha-classe.cpp",
            "src/wrapper-c.cpp",
        },
        .flags = &.{
            "-std=c++17",
            "-Wall",
        },
    });
    
    cxx_lib.linkLibCpp();
    b.installArtifact(cxx_lib);
    
    // Executável Zig
    const exe = b.addExecutable(.{
        .name = "app",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    
    exe.linkLibrary(cxx_lib);
    exe.linkLibCpp();
    
    b.installArtifact(exe);
}

Por que este padrão funciona:

  • Opaque pointer: Esconde detalhes de C++ do Zig
  • C wrapper: Evita name mangling e incompatibilidades ABI
  • RAII em Zig: Usamos defer para destruição automática
  • Type safety: O wrapper Zig fornece tipos seguros sobre C

Gerenciamento de Memória Através de Fronteiras

Um dos maiores desafios em FFI é o gerenciamento de memória. Regras essenciais:

Regras de Ouro

  1. Quem aloca, libera: Se C alocou memória, C deve liberar
  2. Não misture allocators: Use std.heap.c_allocator para memória que será liberada por C
  3. Cuidado com lifetimes: Ponteiros C não têm lifetime tracking

Exemplo Seguro: Passando Strings

const std = @import("std");

extern "c" fn processa_string(s: [*c]const u8) void;

// ❌ ERRADO: ponteiro pode ficar inválido
pub fn enviarMensagemErrada(msg: []const u8) void {
    processa_string(msg.ptr); // msg pode não ser null-terminated!
}

// ✅ CORRETO: aloca memória C-compatível
pub fn enviarMensagemSegura(msg: []const u8) !void {
    // dupeZ aloca e adiciona null terminator
    const c_msg = try std.heap.c_allocator.dupeZ(u8, msg);
    defer std.heap.c_allocator.free(c_msg); // Zig libera
    
    processa_string(c_msg.ptr);
}

// ✅ CORRETO: C libera memória que C alocou
extern "c" fn cria_string() ?[*c]u8;
extern "c" fn libera_string(s: ?[*c]u8) void;

pub fn usarStringC() !void {
    const c_str = cria_string() orelse return error.OutOfMemory;
    defer libera_string(c_str); // C libera
    
    // Converte para slice Zig
    const len = std.mem.len(c_str);
    const slice = c_str[0..len];
    
    std.debug.print("String de C: {s}\n", .{slice});
}

Usando std.heap.c_allocator

Este allocator usa malloc/free da libc, garantindo compatibilidade:

const std = @import("std");

extern "c" fn recebe_buffer(buf: [*c]u8, len: usize) void;

pub fn exemploAlocacao() !void {
    // Usa o allocator compatível com C
    const allocator = std.heap.c_allocator;
    
    const buffer = try allocator.alloc(u8, 1024);
    defer allocator.free(buffer);
    
    // Passa para C
    recebe_buffer(buffer.ptr, buffer.len);
    
    // C pode manter referência temporária, mas não deve guardar
    // o ponteiro após a função retornar
}

Caso Real: Integrando com SQLite

Vamos ver um exemplo prático e útil: usando SQLite a partir do Zig.

Setup do Projeto

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 = "sqlite-demo",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    
    // Link com SQLite
    exe.linkSystemLibrary("sqlite3");
    exe.linkLibC();
    
    b.installArtifact(exe);
}

Código Zig:

const std = @import("std");

// Importa header SQLite
const c = @cImport({
    @cInclude("sqlite3.h");
});

pub const SQLiteError = error{
    OpenError,
    ExecError,
    PrepareError,
    StepError,
};

pub const Database = struct {
    db: ?*c.sqlite3,
    
    pub fn open(path: []const u8) !Database {
        const c_path = try std.heap.c_allocator.dupeZ(u8, path);
        defer std.heap.c_allocator.free(c_path);
        
        var db: ?*c.sqlite3 = null;
        const rc = c.sqlite3_open(c_path.ptr, &db);
        
        if (rc != c.SQLITE_OK or db == null) {
            return error.OpenError;
        }
        
        return Database{ .db = db };
    }
    
    pub fn close(self: *Database) void {
        if (self.db) |db| {
            _ = c.sqlite3_close(db);
            self.db = null;
        }
    }
    
    pub fn execute(self: Database, sql: []const u8) !void {
        const c_sql = try std.heap.c_allocator.dupeZ(u8, sql);
        defer std.heap.c_allocator.free(c_sql);
        
        const rc = c.sqlite3_exec(self.db, c_sql.ptr, null, null, null);
        if (rc != c.SQLITE_OK) {
            return error.ExecError;
        }
    }
    
    pub fn query(self: Database, sql: []const u8) !ResultSet {
        const c_sql = try std.heap.c_allocator.dupeZ(u8, sql);
        defer std.heap.c_allocator.free(c_sql);
        
        var stmt: ?*c.sqlite3_stmt = null;
        const rc = c.sqlite3_prepare_v2(
            self.db,
            c_sql.ptr,
            -1, // lê até null terminator
            &stmt,
            null
        );
        
        if (rc != c.SQLITE_OK or stmt == null) {
            return error.PrepareError;
        }
        
        return ResultSet{ .stmt = stmt };
    }
};

pub const ResultSet = struct {
    stmt: ?*c.sqlite3_stmt,
    
    pub fn deinit(self: *ResultSet) void {
        if (self.stmt) |stmt| {
            _ = c.sqlite3_finalize(stmt);
            self.stmt = null;
        }
    }
    
    pub fn next(self: ResultSet) !bool {
        if (self.stmt) |stmt| {
            const rc = c.sqlite3_step(stmt);
            if (rc == c.SQLITE_ROW) return true;
            if (rc == c.SQLITE_DONE) return false;
            return error.StepError;
        }
        return false;
    }
    
    pub fn getInt(self: ResultSet, col: c_int) i32 {
        if (self.stmt) |stmt| {
            return c.sqlite3_column_int(stmt, col);
        }
        return 0;
    }
    
    pub fn getText(self: ResultSet, col: c_int) []const u8 {
        if (self.stmt) |stmt| {
            const ptr = c.sqlite3_column_text(stmt, col);
            const len = c.sqlite3_column_bytes(stmt, col);
            if (ptr == null or len == 0) return "";
            return ptr[0..@intCast(len)];
        }
        return "";
    }
};

pub fn main() !void {
    // Cria/abre banco
    var db = try Database.open("test.db");
    defer db.close();
    
    // Cria tabela
    try db.execute(
        \\CREATE TABLE IF NOT EXISTS users (
        \\    id INTEGER PRIMARY KEY,
        \\    name TEXT NOT NULL,
        \\    age INTEGER
        \\)
    );
    
    // Insere dados
    try db.execute(
        \\INSERT OR IGNORE INTO users (id, name, age) 
        \\VALUES (1, 'João', 30), (2, 'Maria', 25)
    );
    
    // Query
    var result = try db.query("SELECT id, name, age FROM users");
    defer result.deinit();
    
    std.debug.print("Usuários:\n");
    while (try result.next()) {
        const id = result.getInt(0);
        const name = result.getText(1);
        const age = result.getInt(2);
        
        std.debug.print("  {d}: {s} ({d} anos)\n", .{id, name, age});
    }
}

Execução:

# Precisa do SQLite instalado
# Ubuntu/Debian: sudo apt-get install libsqlite3-dev
# macOS: brew install sqlite3

zig build run

Saída:

Usuários:
  1: João (30 anos)
  2: Maria (25 anos)

Dicas de Performance

1. Minimize Chamadas de Fronteira

Cada chamada entre Zig e C tem overhead. Agrupe operações:

// ❌ Ineficiente: múltiplas chamadas
extern "c" fn adiciona_item(item: Item) void;
extern "c" fn processa_lote() void;

for (items) |item| {
    adiciona_item(item); // N chamadas
}
processa_lote();

// ✅ Eficiente: uma chamada com array
extern "c" fn processa_items(items: [*]const Item, count: usize) void;

processa_items(items.ptr, items.len); // 1 chamada

2. Use callconv(.C) Explicitamente

Para funções de callback passadas para C:

// Sempre use callconv(.C) para callbacks
const Callback = *const fn (data: *anyopaque) callconv(.C) void;

extern "c" fn registra_callback(cb: Callback) void;

3. Alinhamento e Padding

Estruturas compartilhadas devem ter alinhamento explícito:

pub const SharedStruct = extern struct {
    // Use align para garantir compatibilidade
    campo1: u32 align(4),
    campo2: u64 align(8),
    // padding automático para alinhamento C
};

4. Evite Alocações Frequentes

// ❌ Aloca em cada iteração
for (strings) |s| {
    const c_str = try std.heap.c_allocator.dupeZ(u8, s);
    defer std.heap.c_allocator.free(c_str);
    processa(c_str);
}

// ✅ Reusa buffer
var buffer: [1024]u8 = undefined;
for (strings) |s| {
    if (s.len >= buffer.len) continue;
    
    @memcpy(buffer[0..s.len], s);
    buffer[s.len] = 0; // null terminator
    
    processa(&buffer);
}

Depuração de Problemas FFI

Problemas Comuns e Soluções

ProblemaCausa ProvávelSolução
Segmentation faultPonteiro nulo ou inválidoVerifique null antes de usar; use orelse
Valores corrompidosIncompatibilidade de tiposVerifique sizes e alignments
Memory leakEsqueceu defer ou freeUse defer sempre; track allocations
Link errorsBiblioteca não linkadaAdicione exe.linkSystemLibrary("nome")
Compile errors com @cImportHeader não encontradoAdicione exe.addIncludePath(...)

Ferramentas Úteis

# Verificar símbolos em bibliotecas
nm -D libminha.so
objdump -t libminha.a

# Verificar dependências
ldd meu_executavel

# Debug com GDB
gdb ./meu_app
(gdb) break minha_funcao
(gdb) run
(gdb) bt  # backtrace

Resumo e Próximos Passos

Neste guia, você aprendeu:

  1. Fundamentos de FFI: ABI, extern, export, e convenções de chamada
  2. Chamar C de Zig: Usar @cImport, declarar funções externas, criar wrappers
  3. Exportar Zig para C: Compartilhar funções e estruturas com código C
  4. Integração C++: Padrão de wrapper C para interoperar com classes C++
  5. Gerenciamento de memória: Regras essenciais para segurança
  6. Exemplo real: Usando SQLite do Zig

Próximos Passos

Continue sua jornada Zig explorando estes tópicos relacionados:

  1. Zig Build System — Aprofunde-se em build.zig para projetos complexos
  2. Gerenciamento de Memória em Zig — Entenda allocators em profundidade
  3. Comptime em Zig — Metaprogramação poderosa
  4. Zig para Programadores C — Migração completa de C para Zig

Recursos Adicionais


Tem dúvidas sobre FFI em Zig? Entre na discussão nos comentários ou compartilhe seu projeto!

Continue aprendendo Zig

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