FFI: chamando código C a partir do Rust
1. Introdução ao FFI e extern "C"
FFI (Foreign Function Interface) é o mecanismo que permite ao Rust interagir com código escrito em outras linguagens, principalmente C. Isso é essencial para reutilizar bibliotecas legadas, acessar APIs do sistema operacional ou otimizar partes críticas com código C já testado.
O bloco extern "C" declara funções e variáveis externas seguindo a convenção de chamada C. Os tipos primitivos têm correspondência direta: i32 ↔ int, f64 ↔ double, *const c_void ↔ void*. Ponteiros brutos (*const T, *mut T) representam ponteiros C sem garantias de segurança.
extern "C" {
fn printf(format: *const u8, ...) -> i32;
static errno: i32;
}
2. Configuração do projeto e linking com bibliotecas C
Para compilar código C junto com Rust, usamos o crate cc no build.rs. Isso gera uma biblioteca estática que o Rust pode linkar.
// build.rs
fn main() {
cc::Build::new()
.file("src/math_ops.c")
.compile("math_ops");
}
Para bibliotecas do sistema, usamos #[link(name = "...")]:
#[link(name = "m")] // libm (math library)
extern "C" {
fn sqrt(x: f64) -> f64;
}
Bibliotecas dinâmicas (.so/.dylib) podem ser carregadas em tempo de execução com libloading ou linkadas estaticamente com #[link(name = "...", kind = "static")].
3. Tipos de dados complexos: structs e enums
Structs C exigem #[repr(C)] para garantir o mesmo layout de memória. O alinhamento e padding devem ser idênticos.
#[repr(C)]
struct Point {
x: f64,
y: f64,
}
extern "C" {
fn distance(p1: *const Point, p2: *const Point) -> f64;
}
Enums C sem tag (apenas inteiros) mapeiam para enums Rust com #[repr(C)]:
#[repr(C)]
enum Status {
Ok = 0,
Error = -1,
Timeout = 1,
}
Unions C são representadas por union em Rust, mas o acesso é inseguro:
#[repr(C)]
union Data {
int_val: i32,
float_val: f32,
}
4. Ponteiros, strings e arrays
Ponteiros brutos são convertidos com as cast. Strings C usam CStr (leitura) e CString (criação).
use std::ffi::{CStr, CString};
extern "C" {
fn strlen(s: *const u8) -> usize;
fn greet(name: *const u8) -> *mut u8;
}
fn safe_strlen(s: &str) -> usize {
let c_str = CString::new(s).expect("CString::new failed");
unsafe { strlen(c_str.as_ptr()) }
}
fn safe_greet(name: &str) -> String {
let c_name = CString::new(name).unwrap();
let result_ptr = unsafe { greet(c_name.as_ptr()) };
let result = unsafe { CStr::from_ptr(result_ptr) };
result.to_str().unwrap().to_owned()
}
Arrays C são passados como ponteiros + tamanho explícito. Convertemos para slices Rust com std::slice::from_raw_parts:
extern "C" {
fn process_buffer(buf: *const i32, len: usize);
}
fn safe_process(slice: &[i32]) {
unsafe {
process_buffer(slice.as_ptr(), slice.len());
}
}
5. Callbacks: passando funções Rust para C
Callbacks são declaradas como extern "C" fn com tipos de função. Para passar contexto, usamos *mut c_void apontando para um Box.
use std::os::raw::c_void;
type Callback = extern "C" fn(data: *mut c_void, value: i32);
extern "C" {
fn register_callback(cb: Option<Callback>, user_data: *mut c_void);
fn trigger_callbacks();
}
extern "C" fn my_callback(data: *mut c_void, value: i32) {
let state = unsafe { &mut *(data as *mut i32) };
*state += value;
}
fn main() {
let mut counter = 42i32;
let counter_ptr = &mut counter as *mut i32 as *mut c_void;
unsafe {
register_callback(Some(my_callback), counter_ptr);
trigger_callbacks();
}
println!("Counter: {}", counter); // 42 + valor do callback
}
Evite closures que capturam variáveis não estáticas, pois isso exigiria closures FnOnce e gerenciamento complexo de lifetime.
6. Tratamento de erros e valores de retorno
C frequentemente retorna int indicando sucesso (0) ou erro (-1). Convertemos para Result<T, E>.
use std::io::{Error, ErrorKind};
extern "C" {
fn open_file(path: *const u8) -> i32;
}
fn safe_open(path: &str) -> Result<i32, Error> {
let c_path = CString::new(path).map_err(|_| Error::new(ErrorKind::InvalidInput, "nul byte in path"))?;
let fd = unsafe { open_file(c_path.as_ptr()) };
if fd < 0 {
Err(Error::last_os_error()) // captura errno
} else {
Ok(fd)
}
}
Sempre valide ponteiros nulos antes de dereferenciar:
fn safe_process_ptr(ptr: *const i32) -> Option<i32> {
if ptr.is_null() {
return None;
}
Some(unsafe { *ptr })
}
7. Segurança e boas práticas no FFI
O unsafe é necessário para chamar funções externas, mas deve ser isolado em wrappers seguros. Nunca exponha unsafe na API pública.
// Wrapper seguro
pub struct MathLib;
impl MathLib {
pub fn sqrt_safe(x: f64) -> f64 {
unsafe { sqrt(x) }
}
}
Regras de gerenciamento de memória:
- Se C aloca com malloc, C deve liberar com free
- Se Rust aloca, Rust deve liberar (ou passar ownership para C via Box::into_raw)
- Use extern "C" fn com Box para callbacks com lifetime controlado
Ferramentas de verificação:
- cargo test com unsafe blocks testados
- Valgrind para detectar vazamentos de memória
- Sanitizers (-Z sanitizer=address) em builds de debug
// Exemplo completo: biblioteca C simples
// math_ops.c:
// double multiply(double a, double b) { return a * b; }
// Rust wrapper
mod ffi {
extern "C" {
pub fn multiply(a: f64, b: f64) -> f64;
}
}
pub fn multiply_safe(a: f64, b: f64) -> f64 {
unsafe { ffi::multiply(a, b) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiply() {
assert_eq!(multiply_safe(2.0, 3.0), 6.0);
}
}
Referências
- The Rustonomicon: Foreign Function Interface — Guia oficial sobre FFI no Rust, cobrindo tipos, convenções e safety.
- Rust FFI Omnibus — Coleção de exemplos práticos de FFI entre Rust e C.
- cc crate documentation — Documentação do crate
ccpara compilar código C em build scripts. - std::ffi module — Documentação oficial dos tipos
CStr,CStringeOsString. - FFI with Rust and C: A Practical Guide — Tutorial prático com exemplos de structs, callbacks e tratamento de erros.
- Using Unsafe for FFI in Rust — Artigo sobre boas práticas de segurança ao usar
unsafeem FFI. - Rust and C: A Love Story — Introdução detalhada com exemplos de linking e tipos complexos.