Interoperabilidade com C: cbindgen e extern blocks
1. Fundamentos da Interoperabilidade com C em Rust
A interoperabilidade entre Rust e C é possível graças à compatibilidade na ABI (Application Binary Interface). Rust pode se comunicar diretamente com código C utilizando o bloco extern "C", que define a convenção de chamada e a representação binária esperada.
O módulo std::os::raw fornece tipos primitivos compatíveis:
use std::os::raw::{c_int, c_char, c_void, c_double};
// Equivalências comuns:
// c_char -> i8 (ou u8 dependendo da plataforma)
// c_int -> i32
// c_void -> () em Rust, mas representado como *mut c_void
// c_double -> f64
O bloco extern "C" permite declarar funções e variáveis definidas em código C, tornando-as acessíveis no contexto Rust.
2. Declarando e Chamando Funções C a partir de Rust
Para chamar funções de bibliotecas C, usamos extern blocks dentro de blocos unsafe, pois o compilador não pode garantir a segurança das chamadas FFI (Foreign Function Interface).
use std::ffi::CString;
use std::os::raw::c_char;
extern "C" {
fn printf(format: *const c_char, ...) -> i32;
fn malloc(size: usize) -> *mut c_void;
fn free(ptr: *mut c_void);
}
fn main() {
unsafe {
let msg = CString::new("Hello from Rust via C printf!\n").unwrap();
printf(msg.as_ptr());
let ptr = malloc(64);
if !ptr.is_null() {
// Utiliza a memória alocada...
free(ptr);
}
}
}
Note que toda chamada a funções declaradas em extern "C" deve estar em contexto unsafe. O programador é responsável por garantir que os argumentos estejam corretos e que a memória seja gerenciada adequadamente.
3. Exportando Funções Rust para C
Para expor funções Rust para código C, utilizamos #[no_mangle] (desabilita o name mangling) e extern "C":
// lib.rs - Biblioteca Rust que será consumida por C
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_greet(name: *const c_char) -> *mut c_char {
let c_str = unsafe { CStr::from_ptr(name) };
let rust_str = c_str.to_str().unwrap_or("unknown");
let greeting = format!("Hello, {}!", rust_str);
CString::new(greeting).unwrap().into_raw()
}
#[no_mangle]
pub extern "C" fn rust_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe { let _ = CString::from_raw(ptr); }
}
}
Para compilar como biblioteca dinâmica:
cargo build --release
# Gera: target/release/libminha_biblioteca.so (Linux) ou .dll (Windows) ou .dylib (macOS)
O código C correspondente:
// main.c
#include <stdio.h>
#include <stdlib.h>
extern int rust_add(int a, int b);
extern char* rust_greet(const char* name);
extern void rust_free_string(char* ptr);
int main() {
printf("Soma: %d\n", rust_add(10, 20));
char* msg = rust_greet("Mundo");
printf("%s\n", msg);
rust_free_string(msg);
return 0;
}
4. Gerenciamento de Memória e Ponteiros
O gerenciamento de memória no FFI requer cuidados especiais. Rust usa Box::into_raw e Box::from_raw para transferir ownership através da fronteira C:
#[repr(C)]
pub struct Point {
x: f64,
y: f64,
}
#[no_mangle]
pub extern "C" fn point_new(x: f64, y: f64) -> *mut Point {
Box::into_raw(Box::new(Point { x, y }))
}
#[no_mangle]
pub extern "C" fn point_get_x(p: *const Point) -> f64 {
unsafe { (*p).x }
}
#[no_mangle]
pub extern "C" fn point_free(p: *mut Point) {
if !p.is_null() {
unsafe { let _ = Box::from_raw(p); }
}
}
A regra fundamental é: quem aloca, libera. Se Rust aloca com Box::into_raw, Rust deve liberar com Box::from_raw. Se C aloca com malloc, C deve liberar com free.
5. cbindgen: Gerando Headers C Automaticamente
cbindgen é uma ferramenta que gera arquivos .h automaticamente a partir de código Rust anotado. Instalação:
cargo install cbindgen
Arquivo cbindgen.toml:
language = "C"
include_guard = "MINHA_BIB_H"
autogen_warning = "/* Gerado automaticamente por cbindgen */"
Exemplo de código Rust com anotações:
/// Soma dois números inteiros.
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Estrutura representando um ponto 2D.
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
Geração do header:
cbindgen --config cbindgen.toml --crate minha_biblioteca --output include/minha_biblioteca.h
Resultado (include/minha_biblioteca.h):
/* Gerado automaticamente por cbindgen */
#include <stdint.h>
#include <stdlib.h>
typedef struct Point {
double x;
double y;
} Point;
int32_t add(int32_t a, int32_t b);
6. Trabalhando com Structs, Enums e Callbacks
Structs Rust precisam de #[repr(C)] para garantir layout compatível com C:
#[repr(C)]
pub struct Config {
pub buffer_size: usize,
pub timeout_ms: u32,
pub flags: u8,
}
Enums com representação C:
#[repr(C)]
pub enum Color {
Red = 0,
Green = 1,
Blue = 2,
}
#[repr(u8)]
pub enum Status {
Ok = 0,
Error = 1,
Timeout = 2,
}
Callbacks — passando funções Rust como ponteiros de função C:
type Callback = extern "C" fn(i32, *mut c_void);
#[no_mangle]
pub extern "C" fn register_callback(cb: Option<Callback>, user_data: *mut c_void) {
if let Some(callback) = cb {
callback(42, user_data);
}
}
// Uso no lado C:
// void my_handler(int value, void* data) { ... }
// register_callback(my_handler, NULL);
7. Segurança e Boas Práticas em FFI
Sempre valide ponteiros recebidos do lado C:
#[no_mangle]
pub extern "C" fn process_data(data: *const u8, len: usize) -> i32 {
if data.is_null() {
return -1; // Código de erro
}
let slice = unsafe { std::slice::from_raw_parts(data, len) };
// Processa os dados...
0 // Sucesso
}
Padrão de design recomendado: separe a lógica interna segura da API C exposta:
// Interno (seguro)
mod internal {
pub struct Engine {
data: Vec<u8>,
}
impl Engine {
pub fn new() -> Self {
Engine { data: Vec::new() }
}
pub fn process(&mut self, input: &[u8]) -> Vec<u8> {
// Lógica segura
input.to_vec()
}
}
}
// API C (exposta, com validações)
#[no_mangle]
pub extern "C" fn engine_new() -> *mut c_void {
Box::into_raw(Box::new(internal::Engine::new())) as *mut c_void
}
#[no_mangle]
pub extern "C" fn engine_process(engine: *mut c_void, input: *const u8, len: usize) -> *mut u8 {
if engine.is_null() || input.is_null() {
return std::ptr::null_mut();
}
let eng = unsafe { &mut *(engine as *mut internal::Engine) };
let input_slice = unsafe { std::slice::from_raw_parts(input, len) };
let result = eng.process(input_slice);
// Retorna o resultado como buffer alocado por Rust
let mut boxed = result.into_boxed_slice();
let ptr = boxed.as_mut_ptr();
std::mem::forget(boxed); // Evita desalocação prematura
ptr
}
Testes de integração com código C real:
// tests/integration_test.rs
use std::process::Command;
#[test]
fn test_c_integration() {
// Compila o código C de teste
let output = Command::new("gcc")
.args(&[
"-o", "test_c_binding",
"tests/test_main.c",
"-L.", "-lminha_biblioteca",
])
.output()
.expect("Falha ao compilar código C");
assert!(output.status.success());
// Executa o binário gerado
let run = Command::new("./test_c_binding")
.output()
.expect("Falha ao executar teste C");
assert!(run.status.success());
}
Referências
- The Rust FFI Omnibus — Coleção de exemplos práticos de FFI entre Rust e C, cobrindo desde tipos básicos até callbacks e structs complexas.
- cbindgen Documentation — Repositório oficial da ferramenta cbindgen, com guia de instalação, configuração e exemplos de uso.
- Rust nomicon: Foreign Function Interface — Capítulo detalhado do Rustonomicon sobre FFI, incluindo safety, representação de tipos e padrões avançados.
- The Rust Reference: External blocks — Documentação oficial sobre a sintaxe e semântica de
externblocks em Rust. - Rust and C Interoperability Guide — Guia da equipe Rust sobre práticas seguras para código inseguro, incluindo seções específicas sobre interoperabilidade com C.
- Using Rust to make a C library — Tutorial prático passo a passo sobre como criar uma biblioteca Rust que pode ser consumida por código C.
- Building FFI-safe APIs in Rust — Artigo técnico sobre como projetar APIs FFI seguras, abordando layout de memória, padding e compatibilidade binária.