Salta ai contenuti

FFI in Rust: chiamare il C da Rust

FFI sta per Foreign Function Interface (interfaccia per funzioni esterne). È il modo in cui Rust parla con codice scritto in un altro linguaggio. Quasi sempre quell’altro linguaggio è il C, o qualcosa che segue le regole del C, perché il C è la «lingua comune» che capiscono quasi tutti i linguaggi e i sistemi operativi.

Pensalo come due persone che non hanno la stessa lingua madre, ma parlano entrambe un po’ di inglese. Il C è quella lingua condivisa. Con l’FFI puoi usare da Rust una libreria C matura, oppure scrivere una libreria Rust veloce che programmi Python, Node.js o C possono chiamare. Questa pagina si basa su Unsafe in Rust: se i blocchi unsafe sono una novità per te, leggi prima quella.

La libreria standard del C è sempre collegata (linkata) ai programmi Rust su Linux, macOS e Windows, quindi le sue funzioni sono perfette per un primo test. Per chiamarne una, la dichiari dentro un blocco extern "C": il nome, i parametri e il tipo di ritorno. Poi la chiami dentro unsafe:

use std::ffi::c_int;
unsafe extern "C" {
// Questo è `int abs(int x);` del C, scritto in Rust.
fn abs(x: c_int) -> c_int;
}
fn main() {
let n = unsafe { abs(-42) };
println!("abs(-42) = {n}");
}

Output:

abs(-42) = 42
▶ Provalo tu

Alcune cose da notare:

  • "C" indica l’ABI, cioè le regole su come si passano gli argomenti e si restituiscono i valori. Quelle del C sono le regole su cui sono tutti d’accordo.
  • c_int è il nome che Rust dà all’int del C. Il modulo std::ffi ha tipi come c_int, c_long e c_char, così la tua dichiarazione corrisponde esattamente al C su ogni piattaforma.
  • Scrivi unsafe extern "C". Nell’edizione 2024 questo unsafe è obbligatorio, perché stai promettendo che la dichiarazione è corretta. Il codice più vecchio (e l’edizione 2021) spesso scrive semplicemente extern "C" { ... }. La versione con unsafe funziona in entrambe, quindi in questa pagina la usiamo ovunque.

Perché ogni chiamata è unsafe, e come nasconderlo

Sezione intitolata “Perché ogni chiamata è unsafe, e come nasconderlo”

Il compilatore Rust non sa leggere il codice C. Non ha idea se abs prenda davvero un int, se possa andare in crash o se conservi un puntatore che le hai passato. Per questo ogni chiamata a una funzione esterna è unsafe: sei tu a prenderti la responsabilità di usarla correttamente.

La buona notizia: ci devi pensare seriamente una volta sola. Lo schema tipico è scrivere un piccolo wrapper sicuro, cioè una normale funzione Rust che controlla le regole e poi fa la chiamata unsafe. Tutti gli altri chiamano soltanto il wrapper.

Ecco un esempio reale. In C, abs(INT_MIN) è comportamento indefinito, perché il risultato è troppo grande per stare in un int. Il nostro wrapper controlla prima quel caso:

use std::ffi::c_int;
unsafe extern "C" {
fn abs(x: c_int) -> c_int;
}
// Un wrapper sicuro: chi lo chiama non ha bisogno di `unsafe`.
fn safe_abs(x: i32) -> Option<i32> {
// abs(i32::MIN) non ha un risultato valido in C, quindi lo rifiutiamo.
if x == i32::MIN {
return None;
}
// SAFETY: x non è i32::MIN, quindi abs ha un risultato valido.
Some(unsafe { abs(x) })
}
fn main() {
println!("{:?}", safe_abs(-42));
println!("{:?}", safe_abs(i32::MIN));
}

Output:

Some(42)
None
▶ Provalo tu

Il commento // SAFETY: è un’abitudine di Rust che vale la pena copiare. Spiega perché questo particolare blocco unsafe va bene, così chi legge dopo di te non deve tirare a indovinare.

Se una funzione C non ha davvero nessuna regola da rispettare per qualunque input, puoi marcarla safe fn dentro il blocco unsafe extern e poi chiamarla senza unsafe. abs non è una funzione del genere (ricorda INT_MIN), quindi la lasciamo unsafe e controlliamo noi l’input.

È nelle stringhe che si nasconde la maggior parte dei bug di FFI, perché Rust e C le memorizzano in modo diverso:

  • Una String / &str di Rust conosce la propria lunghezza e può contenere qualsiasi byte, anche 0.
  • Una stringa C è solo un puntatore a dei byte. La stringa finisce al primo byte 0 (chiamato NUL). Non c’è nessuna lunghezza memorizzata.

Quindi non puoi passare una &str direttamente al C. Usa CString (in inglese) per costruire una copia posseduta e terminata da NUL, e .as_ptr() per ottenere il *const c_char che il C si aspetta:

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 aggiunge per noi il NUL alla fine.
let c_text = CString::new(text).ok()?;
// SAFETY: c_text è una stringa valida terminata da NUL e vive fino alla fine di questa funzione.
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!("errore: {e}"),
}
}

Output:

[104, 101, 108, 108, 111, 0]
Some(5)
Some(6)
errore: nul byte found in provided data at position: 3
▶ Provalo tu

Qui ci sono due sorprese, ed entrambe sono importanti (le parole di prova restano in inglese perché qui contano proprio i byte):

  1. strlen("héllo") vale 6, non 5. Il C conta i byte, ed é occupa due byte in UTF-8.
  2. CString::new fallisce se il testo contiene un byte 0. Il C penserebbe che la stringa finisca lì e taglierebbe via "lo" senza dire niente. Rust invece si rifiuta, ed è per questo che CString::new restituisce un Result. Il messaggio dell’errore («nul byte found in provided data at position: 3») viene dalla libreria standard, quindi resta in inglese: dice che è stato trovato un byte NUL in posizione 3.

Nella direzione opposta, quando è il C a darti un char *, avvolgilo in un CStr (in inglese). CStr sta a CString come &str sta a String: una vista presa in prestito. La funzione strchr del C restituisce un puntatore al primo carattere che corrisponde, ed è perfetta per una dimostrazione:

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; // il C dice «non trovato» con un puntatore nullo
}
// SAFETY: ptr punta dentro `text`, che è vivo e terminato da 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'));
// Un letterale c"..." è un &CStr con il NUL già aggiunto.
println!("{:?}", find_from(c"rust-lang.org", b'.'));
}

Output:

Some(", world")
None
Some(".org")
▶ Provalo tu

to_string_lossy() trasforma i byte del C in testo Rust. Sostituisce l’eventuale UTF-8 non valido con �, perché le stringhe C non promettono di essere in UTF-8. Se preferisci ricevere un errore, usa invece .to_str().

Rust ha il permesso di riordinare i campi di una struct per risparmiare spazio. Il C non lo fa mai: i campi stanno in memoria nell’ordine in cui li hai scritti, con del padding in mezzo. Per esempio, una struct con i campi u8, u32, u8 oggi occupa 8 byte con il layout di Rust (Rust mette vicini i due u8), ma 12 byte con il layout del C. Se C e Rust non sono d’accordo sul layout, il C legge i byte sbagliati, e niente ti avverte.

Quindi, quando una struct attraversa il confine, di’ a Rust di usare il layout del C con #[repr(C)]. La funzione div del C restituisce una struct div_t con due campi int, quot e rem:

use std::ffi::c_int;
// Corrisponde a quella del 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 = {} con resto {}", result.quot, result.rem);
}

Output:

DivT { quot: 3, rem: 2 }
17 / 5 = 3 con resto 2
▶ Provalo tu

L’FFI funziona in entrambe le direzioni. Scrivi extern "C" fn (senza blocco) e ottieni una funzione Rust che segue le regole di chiamata del C, così il codice C può chiamarla. La funzione qsort del C accetta come argomento una funzione di confronto: possiamo passargliene una scritta in 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,
);
}
// Una funzione Rust che verrà chiamata dal C.
extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int {
// SAFETY: qsort ci passa solo puntatori dentro il nostro array di 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);
}

Output:

[-3, 0, 7, 19, 42]
▶ Provalo tu

(Nel codice vero chiameresti semplicemente sort(). Qui serve solo a mostrare il C che richiama Rust.)

Quando un programma C deve trovare la tua funzione per nome, aggiungi #[no_mangle]. Di solito Rust «storpia» (mangling) i nomi in qualcosa come _ZN4main11add_numbers17h...E, così crate diversi non entrano mai in conflitto. #[no_mangle] mantiene il nome semplice add_numbers:

#[no_mangle]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}

Nota sull’edizione 2024: gli esempi Playground di questo sito usano l’edizione 2021, dove #[no_mangle] va bene. Nell’edizione 2024 (quella predefinita per un nuovo progetto cargo new) va scritto #[unsafe(no_mangle)], perché due funzioni con lo stesso nome non storpiato romperebbero il programma. La nuova forma funziona anche nell’edizione 2021.

Per costruire una libreria a cui il C possa collegarsi, imposta il tipo di crate a cdylib in Cargo.toml:

[lib]
crate-type = ["cdylib"]
// src/lib.rs (edizione 2024)
#[unsafe(no_mangle)]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}

cargo build --release produce target/release/libmathlib.so (.dylib su macOS, .dll su Windows) per un crate chiamato mathlib. Un programma C poi dichiara la funzione e si collega alla libreria:

#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

Scrivere quell’header C a mano stanca presto. cbindgen (in inglese) lo genera a partire dal tuo codice Rust.

Finora abbiamo usato solo funzioni già collegate. Per usare un tuo file C, lascia che sia Cargo a compilarlo. Un file chiamato build.rs nella radice del progetto è uno script di build: Cargo lo esegue prima di compilare il tuo crate. Il crate cc (in inglese) riduce il tutto a poche righe.

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("Hello from C, %s!\n", name);
fflush(stdout);
}
build.rs
fn main() {
// Compila src/greet.c in una libreria statica e la collega.
cc::Build::new().file("src/greet.c").compile("greet");
// Riesegui questo script solo quando cambia il file C.
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
Hello from C, Ferris!

Ti serve un compilatore C installato (gcc, clang o MSVC su Windows); cc lo trova da solo.

Per una grossa libreria C con centinaia di funzioni, scrivere a mano ogni dichiarazione extern è lento e facile da sbagliare. bindgen (in inglese) legge gli header C e genera per te le dichiarazioni Rust, di solito anche questo da build.rs. La maggior parte dei crate -sys su crates.io (come openssl-sys) è costruita così.

Visto che il C è la lingua condivisa, le stesse idee permettono a Rust di parlare con molti altri linguaggi. In questi casi raramente scrivi codice extern "C" grezzo: c’è un crate che fa il lavoro unsafe e ti offre un’API sicura e comoda:

  • Python: PyO3 (in inglese) ti permette di scrivere moduli Python in Rust (e di chiamare Python da Rust). Il suo strumento compagno maturin li compila e li pubblica come normali pacchetti pip. Lo usano librerie come Polars e pydantic-core.
  • Node.js: napi-rs (in inglese) costruisce add-on nativi per Node.js in Rust, generando per te anche i tipi TypeScript.
  • Progetti C e C++: cdylib più cbindgen (in inglese), come visto sopra.
  • Il browser: non è FFI nel senso del C, ma wasm-bindgen ha un ruolo simile. Vedi WebAssembly.

Chiamare una funzione esterna fuori da 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

Il compilatore dice che la chiamata alla funzione unsafe abs richiede una funzione o un blocco unsafe. Soluzione: avvolgi la chiamata in unsafe { ... }, o meglio ancora in una funzione wrapper sicura, come visto sopra.

Un puntatore a una CString che non esiste più. Questo compila, ma è un bug vero:

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`

Il warning avverte che si crea un puntatore pendente, perché la CString temporanea viene distrutta alla fine dell’istruzione. La CString è un valore temporaneo, quindi viene liberata al ;, e ptr ora punta a memoria già liberata. Soluzione: tieni la CString in una variabile tutta sua (let s = CString::new("hello").unwrap();) e chiama s.as_ptr() finché s è viva. Non ignorare mai questo warning.

extern "C" semplice in un progetto con edizione 2024. Il codice copiato da tutorial più vecchi fallisce così:

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 | | }
| |_^

Il messaggio dice che i blocchi extern devono essere unsafe. Soluzione: scrivi unsafe extern "C" { ... }.

Dimenticare #[repr(C)]. Non c’è nessun errore: il C legge semplicemente spazzatura. Metti #[repr(C)] su ogni struct ed enum che attraversa il confine.

  • Dichiara le funzioni C in un blocco unsafe extern "C" { ... }, usando i tipi C di std::ffi come c_int e c_char.
  • Ogni chiamata esterna è unsafe. Avvolgila una volta sola in una funzione Rust sicura che controlla le regole, con un commento // SAFETY:.
  • Le stringhe C finiscono con un byte NUL. Usa CString per mandare testo al C e CStr per leggere testo dal C. Tieni viva la CString finché il C usa il puntatore.
  • Usa #[repr(C)] per le struct condivise con il C, perché Rust può riordinare i campi.
  • extern "C" fn più #[no_mangle] (#[unsafe(no_mangle)] nell’edizione 2024) permette al C di chiamare Rust; cdylib costruisce una libreria apposta.
  • build.rs + cc compila il tuo codice C; bindgen genera le dichiarazioni; PyO3 e napi-rs collegano Rust a Python e Node.js.

Verifica veloce

1. Perché ogni chiamata a una funzione di un blocco extern "C" è unsafe?

2. Cosa succede con CString::new("hel\0lo")?

3. Perché le struct condivise con il C hanno bisogno di #[repr(C)]?

4. In un crate con edizione 2024, come scrivi l'attributo che mantiene il nome semplice di una funzione per il C?

Punteggio: 0 / 4