O mundo real raramente é monolítico. Sistemas legados em C, bibliotecas de alto desempenho em C++, scripts Python, aplicações Java — na prática, software de produção é uma colagem de linguagens e tecnologias. A FFI (Foreign Function Interface) é o mecanismo que permite que Rust converse com esse ecossistema.
Neste artigo cobrimos os dois sentidos da integração: chamar código C a partir de Rust, e expor Rust para ser chamado por C, Python, Java e outras linguagens. É território que exige atenção — a fronteira entre linguagens é onde as garantias do compilador terminam e o unsafe começa.
Chamando C a partir de Rust
O básico: extern "C"
// Declara funções C que queremos chamar
extern "C" {
// Da libc padrão
fn strlen(s: *const libc::c_char) -> libc::size_t;
fn malloc(size: libc::size_t) -> *mut libc::c_void;
fn free(ptr: *mut libc::c_void);
fn memcpy(
dest: *mut libc::c_void,
src: *const libc::c_void,
n: libc::size_t,
) -> *mut libc::c_void;
fn printf(format: *const libc::c_char, ...) -> libc::c_int;
fn abs(x: libc::c_int) -> libc::c_int;
}
use std::ffi::CString;
fn demonstrar_ffi_basico() {
// strlen
let s = CString::new("Olá, mundo!").unwrap();
let comprimento = unsafe { strlen(s.as_ptr()) };
println!("strlen: {comprimento}");
// abs
let n: libc::c_int = -42;
let resultado = unsafe { abs(n) };
println!("abs(-42): {resultado}");
}
fn main() {
demonstrar_ffi_basico();
}
Adicione ao Cargo.toml:
[dependencies]
libc = "0.2"
Strings entre Rust e C
A fronteira de strings é a fonte mais comum de bugs em FFI:
use std::ffi::{CStr, CString, NulError};
use std::os::raw::c_char;
// Rust → C: CString garante terminação nula
fn rust_para_c(s: &str) -> Result<CString, NulError> {
// Erro se a string contém bytes nulos internos
CString::new(s)
}
// C → Rust: CStr para leitura sem cópia
unsafe fn c_para_rust_borrow(ptr: *const c_char) -> &'static str {
// SAFETY: ptr deve ser não-nulo e válido por toda a vida útil
CStr::from_ptr(ptr)
.to_str()
.expect("String C não é UTF-8 válida")
}
// C → Rust: String alocada (mais seguro para ownership)
unsafe fn c_para_rust_owned(ptr: *const c_char) -> String {
CStr::from_ptr(ptr)
.to_string_lossy()
.into_owned()
}
// Exemplo prático: envolver função C que retorna string
extern "C" {
fn strerror(errnum: libc::c_int) -> *const c_char;
}
fn mensagem_erro_sistema(codigo: i32) -> String {
unsafe {
let ptr = strerror(codigo as libc::c_int);
if ptr.is_null() {
return format!("Erro desconhecido: {codigo}");
}
CStr::from_ptr(ptr).to_string_lossy().into_owned()
}
}
fn main() {
// Erros POSIX comuns
for codigo in [0, 1, 2, 13, 22] {
println!("errno {codigo:2}: {}", mensagem_erro_sistema(codigo));
}
// Convertendo strings
let s_rust = "texto em Rust";
let s_c = CString::new(s_rust).unwrap();
println!("CString: {:?}", s_c);
println!("Bytes incluindo nul: {}", s_c.as_bytes_with_nul().len());
}
Vinculando a uma biblioteca C
Criando e vinculando a uma lib C customizada
src/matematica.c:
#include <math.h>
#include <stdlib.h>
// Função simples
double raiz_quadrada_segura(double x) {
if (x < 0.0) return -1.0;
return sqrt(x);
}
// Struct que será usada em Rust
typedef struct {
double x;
double y;
double z;
} Vetor3D;
Vetor3D vetor_normalizar(Vetor3D v) {
double mag = sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
if (mag == 0.0) return v;
Vetor3D resultado = {v.x/mag, v.y/mag, v.z/mag};
return resultado;
}
double vetor_magnitude(Vetor3D v) {
return sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
}
// Callback — Rust vai passar função para C chamar
typedef int (*FuncaoFiltro)(int valor);
int filtrar_array(
const int* entrada,
int* saida,
int tamanho,
FuncaoFiltro filtro
) {
int count = 0;
for (int i = 0; i < tamanho; i++) {
if (filtro(entrada[i])) {
saida[count++] = entrada[i];
}
}
return count;
}
build.rs — script de build para compilar e vincular:
fn main() {
// Compila a biblioteca C
cc::Build::new()
.file("src/matematica.c")
.compile("matematica");
// Vincula com libm (para sqrt)
println!("cargo:rustc-link-lib=m");
// Recompila se o arquivo C mudar
println!("cargo:rerun-if-changed=src/matematica.c");
}
Adicione ao Cargo.toml:
[build-dependencies]
cc = "1"
src/main.rs:
use std::os::raw::c_int;
// Struct correspondente ao Vetor3D em C
// repr(C) garante o mesmo layout de memória
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct Vetor3D {
x: f64,
y: f64,
z: f64,
}
impl Vetor3D {
fn novo(x: f64, y: f64, z: f64) -> Self {
Vetor3D { x, y, z }
}
}
// Tipo para callback C
type FuncaoFiltro = extern "C" fn(c_int) -> c_int;
extern "C" {
fn raiz_quadrada_segura(x: f64) -> f64;
fn vetor_normalizar(v: Vetor3D) -> Vetor3D;
fn vetor_magnitude(v: Vetor3D) -> f64;
fn filtrar_array(
entrada: *const c_int,
saida: *mut c_int,
tamanho: c_int,
filtro: FuncaoFiltro,
) -> c_int;
}
// Callback em Rust para passar ao C
// extern "C" garante a ABI correta
extern "C" fn e_par(valor: c_int) -> c_int {
(valor % 2 == 0) as c_int
}
extern "C" fn e_positivo(valor: c_int) -> c_int {
(valor > 0) as c_int
}
fn main() {
// Funções matemáticas
for x in [-1.0, 0.0, 2.0, 9.0, 16.0] {
let raiz = unsafe { raiz_quadrada_segura(x) };
println!("sqrt({x:5.1}) = {raiz:.4}");
}
println!();
// Operações com vetores
let v = Vetor3D::novo(3.0, 4.0, 0.0);
let mag = unsafe { vetor_magnitude(v) };
let norm = unsafe { vetor_normalizar(v) };
println!("Vetor : {:?}", v);
println!("Magnitude: {mag:.4}");
println!("Normaliz.: {:?}", norm);
println!("Mag norm : {:.4}", unsafe { vetor_magnitude(norm) });
println!();
// Callbacks: passando função Rust para C
let dados: Vec<c_int> = (-5..=10).collect();
let mut saida = vec![0c_int; dados.len()];
let n_pares = unsafe {
filtrar_array(
dados.as_ptr(),
saida.as_mut_ptr(),
dados.len() as c_int,
e_par,
)
};
let pares = &saida[..n_pares as usize];
println!("Números pares em {:?}:", dados);
println!(" {:?}", pares);
let n_positivos = unsafe {
filtrar_array(
dados.as_ptr(),
saida.as_mut_ptr(),
dados.len() as c_int,
e_positivo,
)
};
println!("Positivos: {:?}", &saida[..n_positivos as usize]);
}
bindgen — gerando bindings automaticamente
Para bibliotecas C grandes, escrever bindings manualmente é impraticável. bindgen lê headers C e gera o código Rust automaticamente:
[build-dependencies]
bindgen = "0.69"
cc = "1"
build.rs:
fn main() {
// Compila a biblioteca
cc::Build::new()
.file("src/matematica.c")
.compile("matematica");
println!("cargo:rustc-link-lib=m");
println!("cargo:rerun-if-changed=src/matematica.h");
println!("cargo:rerun-if-changed=build.rs");
// Gera bindings automaticamente do header C
let bindings = bindgen::Builder::default()
.header("src/matematica.h")
// Tipos que queremos incluir
.allowlist_function("raiz_quadrada_segura")
.allowlist_function("vetor_.*")
.allowlist_function("filtrar_array")
.allowlist_type("Vetor3D")
// Deriva traits úteis
.derive_debug(true)
.derive_copy(true)
.derive_clone(true)
.generate()
.expect("Falha ao gerar bindings");
let out_dir = std::path::PathBuf::from(
std::env::var("OUT_DIR").unwrap()
);
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("Falha ao escrever bindings");
}
src/main.rs:
// Inclui os bindings gerados automaticamente
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
fn main() {
let v = Vetor3D { x: 1.0, y: 2.0, z: 3.0 };
let mag = unsafe { vetor_magnitude(v) };
println!("Magnitude: {mag:.4}");
}
Expondo Rust para C
Para que outros programas chamem código Rust, você precisa criar uma biblioteca com ABI C:
Cargo.toml:
[lib]
name = "minha_lib"
crate-type = ["cdylib", "staticlib"]
# cdylib: .so/.dll para vinculação dinâmica
# staticlib: .a/.lib para vinculação estática
src/lib.rs:
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_double};
// Struct exportada para C
#[repr(C)]
pub struct Estatisticas {
pub media: c_double,
pub desvio_padrao: c_double,
pub minimo: c_double,
pub maximo: c_double,
pub contagem: c_int,
}
// Funções exportadas com ABI C
// #[no_mangle]: não ofusca o nome da função
// pub extern "C": exporta com ABI C
#[no_mangle]
pub extern "C" fn calcular_estatisticas(
dados: *const c_double,
tamanho: c_int,
) -> Estatisticas {
// Validação de ponteiro nulo
if dados.is_null() || tamanho <= 0 {
return Estatisticas {
media: 0.0,
desvio_padrao: 0.0,
minimo: 0.0,
maximo: 0.0,
contagem: 0,
};
}
// SAFETY: caller garante que dados aponta para
// tamanho elementos válidos de f64
let slice = unsafe {
std::slice::from_raw_parts(dados, tamanho as usize)
};
let n = slice.len() as f64;
let soma: f64 = slice.iter().sum();
let media = soma / n;
let variancia = slice.iter()
.map(|&x| (x - media).powi(2))
.sum::<f64>() / n;
let minimo = slice.iter().cloned().fold(f64::INFINITY, f64::min);
let maximo = slice.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
Estatisticas {
media,
desvio_padrao: variancia.sqrt(),
minimo,
maximo,
contagem: tamanho,
}
}
#[no_mangle]
pub extern "C" fn processar_texto(
entrada: *const c_char,
) -> *mut c_char {
if entrada.is_null() {
return std::ptr::null_mut();
}
let texto = unsafe {
match CStr::from_ptr(entrada).to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
}
};
// Processa: reverte palavras e coloca em maiúsculas
let resultado: String = texto
.split_whitespace()
.rev()
.map(str::to_uppercase)
.collect::<Vec<_>>()
.join(" ");
// IMPORTANTE: usa CString::into_raw() para transferir ownership
// O caller é responsável por chamar liberar_string() depois
match CString::new(resultado) {
Ok(s) => s.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
// Função para liberar strings alocadas por Rust
// CRÍTICO: o caller C DEVE chamar esta função
#[no_mangle]
pub extern "C" fn liberar_string(ptr: *mut c_char) {
if !ptr.is_null() {
// SAFETY: ptr foi criado por CString::into_raw()
unsafe { drop(CString::from_raw(ptr)) };
}
}
// Versão opaca de uma struct — C não precisa conhecer o layout
pub struct ProcessadorTexto {
separador: String,
maiusculas: bool,
contagem: usize,
}
#[no_mangle]
pub extern "C" fn processador_novo(
separador: *const c_char,
maiusculas: c_int,
) -> *mut ProcessadorTexto {
let sep = if separador.is_null() {
" ".to_string()
} else {
unsafe { CStr::from_ptr(separador) }
.to_string_lossy()
.into_owned()
};
Box::into_raw(Box::new(ProcessadorTexto {
separador: sep,
maiusculas: maiusculas != 0,
contagem: 0,
}))
}
#[no_mangle]
pub extern "C" fn processador_processar(
proc: *mut ProcessadorTexto,
texto: *const c_char,
) -> *mut c_char {
if proc.is_null() || texto.is_null() {
return std::ptr::null_mut();
}
let processador = unsafe { &mut *proc };
let texto_str = unsafe {
match CStr::from_ptr(texto).to_str() {
Ok(s) => s,
Err(_) => return std::ptr::null_mut(),
}
};
processador.contagem += 1;
let partes: Vec<&str> = texto_str.split_whitespace().collect();
let resultado = if processador.maiusculas {
partes.iter()
.map(|s| s.to_uppercase())
.collect::<Vec<_>>()
.join(&processador.separador)
} else {
partes.join(&processador.separador)
};
match CString::new(resultado) {
Ok(s) => s.into_raw(),
Err(_) => std::ptr::null_mut(),
}
}
#[no_mangle]
pub extern "C" fn processador_contagem(
proc: *const ProcessadorTexto,
) -> c_int {
if proc.is_null() {
return -1;
}
unsafe { (*proc).contagem as c_int }
}
#[no_mangle]
pub extern "C" fn processador_liberar(proc: *mut ProcessadorTexto) {
if !proc.is_null() {
unsafe { drop(Box::from_raw(proc)) };
}
}
Header C gerado (minha_lib.h):
#ifndef MINHA_LIB_H
#define MINHA_LIB_H
#include <stdint.h>
typedef struct {
double media;
double desvio_padrao;
double minimo;
double maximo;
int contagem;
} Estatisticas;
Estatisticas calcular_estatisticas(const double* dados, int tamanho);
char* processar_texto(const char* entrada);
void liberar_string(char* ptr);
typedef struct ProcessadorTexto ProcessadorTexto;
ProcessadorTexto* processador_novo(const char* separador, int maiusculas);
char* processador_processar(ProcessadorTexto* proc, const char* texto);
int processador_contagem(const ProcessadorTexto* proc);
void processador_liberar(ProcessadorTexto* proc);
#endif
Usando a biblioteca em C:
#include <stdio.h>
#include <stdlib.h>
#include "minha_lib.h"
int main() {
// Estatísticas
double dados[] = {1.0, 2.5, 3.7, 4.1, 5.9, 2.3, 8.1};
int n = sizeof(dados) / sizeof(dados[0]);
Estatisticas stats = calcular_estatisticas(dados, n);
printf("Média: %.4f
", stats.media);
printf("Desvio: %.4f
", stats.desvio_padrao);
printf("Min/Max: %.2f / %.2f
", stats.minimo, stats.maximo);
// Processamento de texto
char* resultado = processar_texto("hello world from rust");
if (resultado) {
printf("Processado: %s
", resultado);
liberar_string(resultado); // OBRIGATÓRIO
}
// Objeto opaco
ProcessadorTexto* proc = processador_novo("|", 1);
char* s1 = processador_processar(proc, "foo bar baz");
printf("Resultado: %s
", s1);
liberar_string(s1);
printf("Chamadas: %d
", processador_contagem(proc));
processador_liberar(proc); // OBRIGATÓRIO
return 0;
}
Integrando com Python via PyO3
PyO3 é a forma idiomática de criar extensões Python em Rust:
[lib]
name = "modulo_rust"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.21", features = ["extension-module"] }
src/lib.rs:
use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;
// Função exportada para Python
#[pyfunction]
fn fibonacci(n: u64) -> PyResult<u64> {
if n > 93 {
return Err(PyValueError::new_err(
"n > 93 causaria overflow em u64"
));
}
if n <= 1 {
return Ok(n);
}
let (mut a, mut b) = (0u64, 1u64);
for _ in 2..=n {
(a, b) = (b, a + b);
}
Ok(b)
}
#[pyfunction]
fn ordenar_strings(mut lista: Vec<String>) -> Vec<String> {
lista.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
lista
}
// Classe exportada para Python
#[pyclass]
struct Contador {
valor: i64,
passo: i64,
}
#[pymethods]
impl Contador {
#[new]
fn novo(passo: Option<i64>) -> Self {
Contador {
valor: 0,
passo: passo.unwrap_or(1),
}
}
fn incrementar(&mut self) {
self.valor += self.passo;
}
fn decrementar(&mut self) {
self.valor -= self.passo;
}
fn resetar(&mut self) {
self.valor = 0;
}
#[getter]
fn valor(&self) -> i64 {
self.valor
}
fn __repr__(&self) -> String {
format!("Contador(valor={}, passo={})", self.valor, self.passo)
}
fn __add__(&self, outro: &Contador) -> Contador {
Contador {
valor: self.valor + outro.valor,
passo: self.passo,
}
}
}
// Processamento numérico de alta performance
#[pyfunction]
fn calcular_estatisticas(dados: Vec<f64>) -> PyResult<(f64, f64, f64, f64)> {
if dados.is_empty() {
return Err(PyValueError::new_err("Lista vazia"));
}
let n = dados.len() as f64;
let soma: f64 = dados.iter().sum();
let media = soma / n;
let variancia = dados.iter()
.map(|&x| (x - media).powi(2))
.sum::<f64>() / n;
let min = dados.iter().cloned().fold(f64::INFINITY, f64::min);
let max = dados.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
Ok((media, variancia.sqrt(), min, max))
}
// Registra o módulo Python
#[pymodule]
fn modulo_rust(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(fibonacci, m)?)?;
m.add_function(wrap_pyfunction!(ordenar_strings, m)?)?;
m.add_function(wrap_pyfunction!(calcular_estatisticas, m)?)?;
m.add_class::<Contador>()?;
Ok(())
}
Compile e instale:
pip install maturin
maturin develop # desenvolvimento
maturin build --release # produção
# Usa em Python:
python3 -c "
import modulo_rust
print(modulo_rust.fibonacci(10)) # 55
print(modulo_rust.ordenar_strings(['Banana', 'apple', 'Cherry']))
c = modulo_rust.Contador(passo=5)
c.incrementar()
c.incrementar()
print(c.valor) # 10
print(c) # Contador(valor=10, passo=5)
media, dp, mn, mx = modulo_rust.calcular_estatisticas([1.0, 2.0, 3.0, 4.0, 5.0])
print(f'média={media}, dp={dp:.4f}')
"
Integrando com Java via JNI
[dependencies]
jni = "0.21"
src/lib.rs:
use jni::JNIEnv;
use jni::objects::{JClass, JString, JObject};
use jni::sys::{jint, jlong, jstring, jdouble, jdoubleArray};
// Convenção de nomenclatura JNI:
// Java_<pacote_com_underscores>_<Classe>_<metodo>
#[no_mangle]
pub extern "system" fn Java_com_exemplo_RustLib_fibonacci(
_env: JNIEnv,
_class: JClass,
n: jlong,
) -> jlong {
if n <= 1 {
return n;
}
let (mut a, mut b) = (0i64, 1i64);
for _ in 2..=n {
let c = a.saturating_add(b);
a = b;
b = c;
}
b
}
#[no_mangle]
pub extern "system" fn Java_com_exemplo_RustLib_processarTexto(
env: JNIEnv,
_class: JClass,
texto: JString,
) -> jstring {
let texto_rust: String = env
.get_string(&texto)
.expect("String Java inválida")
.into();
let resultado: String = texto_rust
.split_whitespace()
.map(|w| {
let mut chars = w.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ");
env.new_string(resultado)
.expect("Falha ao criar string Java")
.into_raw()
}
#[no_mangle]
pub extern "system" fn Java_com_exemplo_RustLib_calcularMedia(
env: JNIEnv,
_class: JClass,
array: jdoubleArray,
) -> jdouble {
let dados = unsafe {
env.get_array_elements(&env.get_double_array_elements(array, jni::objects::ReleaseMode::NoCopyBack).unwrap())
};
// Versão simplificada
let elementos: Vec<f64> = env
.get_double_array_elements(array, jni::objects::ReleaseMode::NoCopyBack)
.unwrap()
.iter()
.cloned()
.collect();
if elementos.is_empty() {
return 0.0;
}
elementos.iter().sum::<f64>() / elementos.len() as f64
}
Uso em Java (RustLib.java):
package com.exemplo;
public class RustLib {
static {
System.loadLibrary("minha_lib");
}
public static native long fibonacci(long n);
public static native String processarTexto(String texto);
public static native double calcularMedia(double[] dados);
public static void main(String[] args) {
System.out.println(fibonacci(10)); // 55
System.out.println(processarTexto("hello world")); // Hello World
System.out.println(calcularMedia(new double[]{1.0, 2.0, 3.0})); // 2.0
}
}
cbindgen — gerando headers C automaticamente
Assim como bindgen lê headers C e gera Rust, cbindgen faz o inverso:
[build-dependencies]
cbindgen = "0.26"
build.rs:
fn main() {
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
cbindgen::Builder::new()
.with_crate(&crate_dir)
.with_language(cbindgen::Language::C)
.with_include_guard("MINHA_LIB_H")
.with_documentation(true)
.generate()
.expect("Falha ao gerar header C")
.write_to_file("include/minha_lib.h");
println!("cargo:rerun-if-changed=src/lib.rs");
}
Boas práticas de FFI
Nunca retorne referências para dados locais através da fronteira FFI. Os dados precisam sobreviver à chamada — use Box::into_raw para heap-allocated, ou garanta que o lifetime é suficientemente longo.
Toda função que aloca deve ter um par que libera. O padrão _criar/_liberar é a forma idiomática. Documente claramente quem é responsável por liberar cada ponteiro.
Valide todos os ponteiros recebidos de C. C não tem tipos opcionais — null é válido em C e pode chegar para Rust sem aviso. Sempre verifique is_null() antes de desreferenciar.
Use #[repr(C)] em todas as structs que cruzam a fronteira. O layout de memória padrão de Rust não é estável e pode mudar entre versões do compilador.
Documente o contrato de segurança de cada função unsafe. O comentário // SAFETY: deve explicar por que o código é correto — não apenas que é unsafe.
Prefira envelopes seguros a unsafe exposto. A função pública deve ser segura; mova o unsafe para a implementação interna com documentação clara.
Fontes e leituras recomendadas
- "The Rustonomicon" — FFI chapter — guia oficial sobre FFI unsafe — https://doc.rust-lang.org/nomicon/ffi.html
bindgendocumentation — geração automática de bindings — https://rust-lang.github.io/rust-bindgen/cbindgendocumentation — geração de headers C — https://github.com/mozilla/cbindgenPyO3User Guide — integração Rust-Python — https://pyo3.rsjnicrate documentation — integração com Java — https://docs.rs/jnilibccrate — tipos e funções da libc — https://docs.rs/libc- "Mixing Rust and C" — Embedded Rust Book — FFI para sistemas embarcados — https://docs.rust-embedded.org/book/interoperability/