FFI em Rust: como chamar código C
FFI é a sigla de Foreign Function Interface (interface de funções externas). É o jeito de o Rust conversar com código escrito em outra linguagem. Quase sempre, essa outra linguagem é o C, ou algo que segue as regras do C, porque o C é a “língua comum” que praticamente toda linguagem e todo sistema operacional entende.
Pense em duas pessoas que não têm a mesma língua materna, mas que falam um pouco de inglês. O C é essa língua em comum. Com FFI, você pode usar uma biblioteca C madura a partir do Rust, ou escrever uma biblioteca Rust rápida que programas em Python, Node.js ou C conseguem chamar. Esta página se apoia em Unsafe em Rust, então leia aquela primeiro se blocos unsafe forem novidade para você.
Chamando uma função C
Seção intitulada “Chamando uma função C”A biblioteca padrão do C sempre é ligada (linkada) aos programas Rust no Linux, no macOS e no Windows, então as funções dela são um ótimo primeiro teste. Para chamar uma, você a declara dentro de um bloco extern "C": o nome, os parâmetros e o tipo de retorno. Depois, você a chama dentro de unsafe:
use std::ffi::c_int;
unsafe extern "C" { // Esta é a função `int abs(int x);` do C, escrita em Rust. fn abs(x: c_int) -> c_int;}
fn main() { let n = unsafe { abs(-42) }; println!("abs(-42) = {n}");}Saída:
abs(-42) = 42Algumas coisas para reparar:
"C"dá nome à ABI: as regras de como os argumentos são passados e os valores são retornados. As regras do C são aquelas com que todo mundo concorda.c_inté o nome que o Rust dá aointdo C. O módulostd::ffitem tipos comoc_int,c_longec_char, para que a sua declaração bata exatamente com o C em qualquer plataforma.- Você escreve
unsafe extern "C". Na edition 2024, esseunsafeé obrigatório, porque você está prometendo que a declaração está correta. Código mais antigo (e a edition 2021) costuma escrever sóextern "C" { ... }. A versão comunsafefunciona nas duas, então esta página usa ela em todos os exemplos.
Por que toda chamada é unsafe, e como esconder isso
Seção intitulada “Por que toda chamada é unsafe, e como esconder isso”O compilador do Rust não consegue ler código C. Ele não tem como saber se abs recebe mesmo um int, se ela pode travar o programa ou se ela guarda um ponteiro que você passou. Por isso, toda chamada a uma função externa é unsafe: você assume a responsabilidade de usá-la do jeito certo.
A boa notícia: você só precisa pensar a fundo nisso uma vez. O padrão de costume é escrever um pequeno wrapper seguro (uma função “embrulho”): uma função Rust normal que confere as regras e depois faz a chamada unsafe. Todo o resto do código só chama o wrapper.
Veja um exemplo real. Em C, abs(INT_MIN) é comportamento indefinido (undefined behavior), porque a resposta é grande demais para caber em um int. O nosso wrapper verifica esse caso antes:
use std::ffi::c_int;
unsafe extern "C" { fn abs(x: c_int) -> c_int;}
// Um wrapper seguro: quem chama não precisa de `unsafe`.fn safe_abs(x: i32) -> Option<i32> { // abs(i32::MIN) não tem resposta válida em C, então recusamos. if x == i32::MIN { return None; } // SAFETY: x não é i32::MIN, então abs tem um resultado válido. Some(unsafe { abs(x) })}
fn main() { println!("{:?}", safe_abs(-42)); println!("{:?}", safe_abs(i32::MIN));}Saída:
Some(42)NoneO comentário // SAFETY: é um hábito do Rust que vale a pena copiar. Ele explica por que esse bloco unsafe específico está correto, para que a próxima pessoa que ler o código não precise adivinhar.
Se uma função C realmente não tem nenhuma regra a seguir, seja qual for a entrada, você pode marcá-la como safe fn dentro do bloco unsafe extern e chamá-la sem unsafe. abs não é uma função assim (lembre do INT_MIN), então a mantemos unsafe e verificamos a entrada nós mesmos.
Strings: CString, CStr e o byte NUL
Seção intitulada “Strings: CString, CStr e o byte NUL”As strings são onde mora a maioria dos bugs de FFI, porque o Rust e o C as guardam de jeitos diferentes:
- Uma
String/&strdo Rust sabe o próprio tamanho e pode conter qualquer byte, inclusive0. - Uma string C é só um ponteiro para bytes. A string termina no primeiro byte
0(chamado de NUL). Não existe um tamanho guardado.
Então você não pode passar um &str direto para o C. Use CString (em inglês) para criar uma cópia própria terminada em NUL, e .as_ptr() para obter o *const c_char que o C espera:
use std::ffi::{c_char, CString};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize;}
fn c_strlen(text: &str) -> Option<usize> { // CString::new coloca o NUL no final para nós. let c_text = CString::new(text).ok()?; // SAFETY: c_text é uma string válida terminada em NUL e vive até o fim desta função. Some(unsafe { strlen(c_text.as_ptr()) })}
fn main() { let hello = CString::new("hello").unwrap(); println!("{:?}", hello.as_bytes_with_nul());
println!("{:?}", c_strlen("hello")); println!("{:?}", c_strlen("héllo"));
match CString::new("hel\0lo") { Ok(_) => println!("ok"), Err(e) => println!("erro: {e}"), }}Saída:
[104, 101, 108, 108, 111, 0]Some(5)Some(6)erro: nul byte found in provided data at position: 3Os textos deste exemplo ficaram em inglês de propósito: o resultado mostra bytes e tamanhos, e letras acentuadas mudariam esses números. A mensagem depois de erro: também fica em inglês, porque é o texto que o próprio Rust gera (“foi encontrado um byte nul nos dados, na posição 3”).
Aqui há duas surpresas, e as duas são importantes:
strlen("héllo")dá 6, não 5. O C conta bytes, e oéocupa dois bytes em UTF-8.CString::newfalha se o seu texto tiver um byte0. O C acharia que a string termina ali e cortaria o"lo"sem avisar. O Rust se recusa em vez de deixar isso acontecer, e é por isso queCString::newretorna umResult.
No sentido contrário, quando o C entrega um char * para você, embrulhe o ponteiro em CStr (em inglês). CStr está para CString assim como &str está para String: é uma visão emprestada. A função strchr do C retorna um ponteiro para o primeiro caractere encontrado, o que rende uma boa demonstração:
use std::ffi::{c_char, c_int, CStr, CString};
unsafe extern "C" { fn strchr(s: *const c_char, c: c_int) -> *mut c_char;}
fn find_from(text: &CStr, ch: u8) -> Option<String> { let ptr = unsafe { strchr(text.as_ptr(), ch as c_int) }; if ptr.is_null() { return None; // o C diz "não encontrado" com um ponteiro nulo } // SAFETY: ptr aponta para dentro de `text`, que está vivo e termina em NUL. let found = unsafe { CStr::from_ptr(ptr) }; Some(found.to_string_lossy().into_owned())}
fn main() { let text = CString::new("hello, world").unwrap(); println!("{:?}", find_from(&text, b',')); println!("{:?}", find_from(&text, b'z'));
// Um literal c"..." é um &CStr com o NUL já incluído. println!("{:?}", find_from(c"rust-lang.org", b'.'));}Saída:
Some(", world")NoneSome(".org")to_string_lossy() transforma os bytes do C em texto Rust. Ele troca qualquer UTF-8 inválido por �, porque as strings C não prometem ser UTF-8. Use .to_str() no lugar se preferir receber um erro.
Compartilhando structs com #[repr(C)]
Seção intitulada “Compartilhando structs com #[repr(C)]”O Rust tem permissão para reordenar os campos de uma struct para economizar espaço. O C nunca faz isso: os campos ficam na memória na ordem em que você os escreveu, com preenchimento (padding) entre eles. Por exemplo, uma struct com os campos u8, u32, u8 ocupa hoje 8 bytes com o layout do Rust (o Rust junta os dois u8), mas 12 bytes com o layout do C. Se o C e o Rust discordarem sobre o layout, o C lê os bytes errados, e nada avisa você.
Por isso, quando uma struct cruza a fronteira, diga ao Rust para usar o layout do C com #[repr(C)]. A função div do C retorna uma struct div_t com dois campos int, quot (quociente) e rem (resto):
use std::ffi::c_int;
// Igual ao C: typedef struct { int quot; int rem; } div_t;#[repr(C)]#[derive(Debug)]struct DivT { quot: c_int, rem: c_int,}
unsafe extern "C" { fn div(numerator: c_int, denominator: c_int) -> DivT;}
fn main() { let result = unsafe { div(17, 5) }; println!("{result:?}"); println!("17 / 5 = {} com resto {}", result.quot, result.rem);}Saída:
DivT { quot: 3, rem: 2 }17 / 5 = 3 com resto 2Deixando o C chamar o Rust
Seção intitulada “Deixando o C chamar o Rust”A FFI funciona nos dois sentidos. Escreva extern "C" fn (sem bloco) e você ganha uma função Rust que segue as regras de chamada do C, para que código C possa chamá-la. A função qsort do C recebe uma função de comparação como argumento, e podemos entregar uma escrita em Rust:
use std::ffi::{c_int, c_void};
unsafe extern "C" { fn qsort( base: *mut c_void, count: usize, size: usize, compare: extern "C" fn(*const c_void, *const c_void) -> c_int, );}
// Uma função Rust que o C vai chamar.extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int { // SAFETY: o qsort só nos passa ponteiros para dentro do nosso array de i32. let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) }; a.cmp(&b) as c_int // Less = -1, Equal = 0, Greater = 1}
fn sort_with_c(numbers: &mut [i32]) { unsafe { qsort( numbers.as_mut_ptr() as *mut c_void, numbers.len(), std::mem::size_of::<i32>(), compare_i32, ); }}
fn main() { let mut numbers = [42, 7, 19, -3, 0]; sort_with_c(&mut numbers); println!("{:?}", numbers);}Saída:
[-3, 0, 7, 19, 42](Num código de verdade, você simplesmente chamaria sort(). Isto serve só para mostrar o C chamando o Rust de volta.)
Quando um programa C precisa encontrar a sua função pelo nome, adicione #[no_mangle]. Normalmente o Rust “embaralha” (mangle) os nomes em algo como _ZN4main11add_numbers17h...E, para que crates diferentes nunca entrem em conflito. #[no_mangle] mantém o nome simples add_numbers:
#[no_mangle]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}Observação sobre a edition 2024: os exemplos do Playground deste site usam a edition 2021, em que #[no_mangle] funciona normalmente. Na edition 2024 (o padrão para um projeto novo criado com cargo new), é preciso escrever #[unsafe(no_mangle)], porque duas funções com o mesmo nome sem embaralhar quebrariam o programa. A grafia nova também funciona na edition 2021.
Para gerar uma biblioteca que o C possa linkar, defina o tipo do crate como cdylib no Cargo.toml:
[lib]crate-type = ["cdylib"]// src/lib.rs (edition 2024)#[unsafe(no_mangle)]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}Para um crate chamado mathlib, cargo build --release gera target/release/libmathlib.so (.dylib no macOS, .dll no Windows). Um programa C então declara a função e é linkado com a biblioteca:
#include <stdio.h>#include <stdint.h>
int32_t add_numbers(int32_t a, int32_t b);
int main(void) { printf("2 + 3 = %d\n", add_numbers(2, 3)); return 0;}$ cc main.c -L target/release -lmathlib -o main$ LD_LIBRARY_PATH=target/release ./main2 + 3 = 5Escrever esse header C à mão cansa rápido. O cbindgen (em inglês) gera o header a partir do seu código Rust.
Compilando o seu próprio código C com build.rs e cc
Seção intitulada “Compilando o seu próprio código C com build.rs e cc”Até aqui, só usamos funções que já vêm linkadas. Para usar o seu próprio arquivo C, deixe o Cargo compilá-lo. Um arquivo chamado build.rs na raiz do projeto é um build script (script de build): o Cargo o executa antes de compilar o seu crate. O crate cc (em inglês) resolve isso em poucas linhas.
cargo new hello-ccd hello-ccargo add --build cc#include <stdio.h>
int add_ints(int a, int b) { return a + b;}
void greet(const char *name) { printf("Olá do C, %s!\n", name); fflush(stdout);}fn main() { // Compila src/greet.c como uma biblioteca estática e a linka. cc::Build::new().file("src/greet.c").compile("greet"); // Roda este script de novo só quando o arquivo C mudar. println!("cargo::rerun-if-changed=src/greet.c");}use std::ffi::{c_char, c_int, CString};
unsafe extern "C" { fn add_ints(a: c_int, b: c_int) -> c_int; fn greet(name: *const c_char);}
fn main() { let sum = unsafe { add_ints(2, 3) }; println!("2 + 3 = {sum}");
let name = CString::new("Ferris").unwrap(); unsafe { greet(name.as_ptr()) };}$ cargo run -q2 + 3 = 5Olá do C, Ferris!Você precisa de um compilador C instalado (gcc, clang ou o MSVC no Windows); o cc o encontra para você.
Para uma biblioteca C grande, com centenas de funções, escrever cada declaração extern à mão é lento e fácil de errar. O bindgen (em inglês) lê os arquivos de header do C e gera as declarações Rust para você, normalmente também a partir do build.rs. A maioria dos crates -sys do crates.io (como o openssl-sys) é feita assim.
Além do C: Python, Node.js e a web
Seção intitulada “Além do C: Python, Node.js e a web”Como o C é a língua em comum, as mesmas ideias permitem que o Rust converse com muitas outras linguagens. Nesses casos, você raramente escreve código extern "C" cru: um crate faz o trabalho unsafe e entrega uma API segura e amigável:
- Python: o PyO3 (em inglês) permite escrever módulos Python em Rust (e chamar Python a partir do Rust). A ferramenta que o acompanha, o
maturin, compila e publica esses módulos como pacotespipnormais. Bibliotecas como Polars e pydantic-core usam o PyO3. - Node.js: o napi-rs (em inglês) cria add-ons nativos do Node.js em Rust, com os tipos TypeScript gerados para você.
- Projetos C e C++:
cdylibmais o cbindgen (em inglês), como mostrado acima. - O navegador: isso não é FFI no sentido do C, mas o
wasm-bindgentem um papel parecido. Veja WebAssembly.
Erros comuns
Seção intitulada “Erros comuns”Chamar uma função externa fora de unsafe.
unsafe extern "C" { fn abs(x: i32) -> i32;}
fn main() { let n = abs(-7); println!("{n}");}error[E0133]: call to unsafe function `abs` is unsafe and requires unsafe function or block --> src/main.rs:6:13 |6 | let n = abs(-7); | ^^^^^^^ call to unsafe functionO compilador avisa que chamar a função unsafe abs exige uma função ou um bloco unsafe. Solução: coloque a chamada dentro de unsafe { ... }, ou melhor ainda, dentro de um wrapper seguro, como mostrado acima.
Um ponteiro para uma CString que já não existe. Isto compila, mas é um bug de verdade:
use std::ffi::{c_char, CString};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize;}
fn main() { let ptr = CString::new("hello").unwrap().as_ptr(); let len = unsafe { strlen(ptr) }; println!("{len}");}warning: this creates a dangling pointer because temporary `CString` is dropped at end of statement --> src/main.rs:8:46 |8 | let ptr = CString::new("hello").unwrap().as_ptr(); | ------------------------------ ^^^^^^ pointer created here | | | this `CString` is dropped at end of statement | = help: bind the `CString` to a variable such that it outlives the pointer returned by `as_ptr`O aviso diz que isso cria um ponteiro pendente (dangling pointer), porque a CString temporária é descartada no fim da instrução. Ela é liberada no ;, e ptr passa a apontar para memória já liberada. Solução: guarde a CString em uma variável própria (let s = CString::new("hello").unwrap();) e chame s.as_ptr() enquanto s estiver viva. Nunca ignore esse aviso.
extern "C" sem unsafe em um projeto da edition 2024. Código copiado de tutoriais mais antigos falha assim:
error: extern blocks must be unsafe --> src/main.rs:3:1 |3 | / extern "C" {4 | | fn add_ints(a: c_int, b: c_int) -> c_int;5 | | fn greet(name: *const c_char);6 | | } | |_^A mensagem quer dizer “blocos extern precisam ser unsafe”. Solução: escreva unsafe extern "C" { ... }.
Esquecer o #[repr(C)]. Não aparece erro nenhum: o C simplesmente lê lixo. Coloque #[repr(C)] em toda struct e todo enum que cruzar a fronteira.
- Declare funções C em um bloco
unsafe extern "C" { ... }, usando os tipos C destd::ffi, comoc_intec_char. - Toda chamada externa é
unsafe. Embrulhe-a uma única vez em uma função Rust segura que confere as regras, com um comentário// SAFETY:. - Strings C terminam em um byte NUL. Use
CStringpara mandar texto para o C eCStrpara ler texto vindo do C. Mantenha aCStringviva enquanto o C usar o ponteiro. - Use
#[repr(C)]em structs compartilhadas com o C, porque o Rust pode reordenar os campos. extern "C" fncom#[no_mangle](#[unsafe(no_mangle)]na edition 2024) permite que o C chame o Rust;cdylibgera uma biblioteca para isso.build.rs+cccompila o seu código C; obindgengera as declarações; PyO3 e napi-rs ligam o Rust ao Python e ao Node.js.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Por que toda chamada a uma função de um bloco extern "C" é unsafe?
2. O que acontece com CString::new("hel\0lo")?
3. Por que structs compartilhadas com o C precisam de #[repr(C)]?
4. Em um crate da edition 2024, como se escreve o atributo que mantém o nome simples de uma função para o C?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- FFI — The Rustonomicon (em inglês): o mergulho profundo oficial, com callbacks, linkagem, ponteiros que podem ser nulos e mais.
std::ffi— documentação da biblioteca padrão (em inglês):CString,CStr,c_int,c_chare companhia, com exemplos.- External blocks — The Rust Reference (em inglês): as regras exatas dos blocos
extern, das ABIs e dos itenssafe/unsafe. - The bindgen User Guide (em inglês): como gerar bindings Rust a partir de headers C.
- The PyO3 User Guide (em inglês): como escrever extensões Python em Rust.