Pular para o conteúdo

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ê.

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) = 42
▶ Experimente você mesmo

Algumas 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á ao int do C. O módulo std::ffi tem tipos como c_int, c_long e c_char, para que a sua declaração bata exatamente com o C em qualquer plataforma.
  • Você escreve unsafe extern "C". Na edition 2024, esse unsafe é 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 com unsafe funciona 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)
None
▶ Experimente você mesmo

O 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.

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 / &str do Rust sabe o próprio tamanho e pode conter qualquer byte, inclusive 0.
  • 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: 3
▶ Experimente você mesmo

Os 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:

  1. strlen("héllo") dá 6, não 5. O C conta bytes, e o é ocupa dois bytes em UTF-8.
  2. CString::new falha se o seu texto tiver um byte 0. 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 que CString::new retorna um Result.

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")
None
Some(".org")
▶ Experimente você mesmo

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.

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 2
▶ Experimente você mesmo

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]
▶ Experimente você mesmo

(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;
}
Terminal window
$ cc main.c -L target/release -lmathlib -o main
$ LD_LIBRARY_PATH=target/release ./main
2 + 3 = 5

Escrever 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.

Terminal window
cargo new hello-c
cd hello-c
cargo add --build cc
src/greet.c
#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);
}
build.rs
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");
}
src/main.rs
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()) };
}
Terminal window
$ cargo run -q
2 + 3 = 5
Olá 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.

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 pacotes pip normais. 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++: cdylib mais o cbindgen (em inglês), como mostrado acima.
  • O navegador: isso não é FFI no sentido do C, mas o wasm-bindgen tem um papel parecido. Veja WebAssembly.

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 function

O 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 de std::ffi, como c_int e c_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 CString para mandar texto para o C e CStr para ler texto vindo do C. Mantenha a CString viva enquanto o C usar o ponteiro.
  • Use #[repr(C)] em structs compartilhadas com o C, porque o Rust pode reordenar os campos.
  • extern "C" fn com #[no_mangle] (#[unsafe(no_mangle)] na edition 2024) permite que o C chame o Rust; cdylib gera uma biblioteca para isso.
  • build.rs + cc compila o seu código C; o bindgen gera as declarações; PyO3 e napi-rs ligam o Rust ao Python e ao Node.js.

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