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.
Chiamare una funzione C
Sezione intitolata “Chiamare una funzione C”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) = 42Alcune 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’intdel C. Il modulostd::ffiha tipi comec_int,c_longec_char, così la tua dichiarazione corrisponde esattamente al C su ogni piattaforma.- Scrivi
unsafe extern "C". Nell’edizione 2024 questounsafeè obbligatorio, perché stai promettendo che la dichiarazione è corretta. Il codice più vecchio (e l’edizione 2021) spesso scrive semplicementeextern "C" { ... }. La versione conunsafefunziona 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)NoneIl 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.
Stringhe: CString, CStr e il byte NUL
Sezione intitolata “Stringhe: CString, CStr e il byte NUL”È nelle stringhe che si nasconde la maggior parte dei bug di FFI, perché Rust e C le memorizzano in modo diverso:
- Una
String/&strdi Rust conosce la propria lunghezza e può contenere qualsiasi byte, anche0. - 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: 3Qui ci sono due sorprese, ed entrambe sono importanti (le parole di prova restano in inglese perché qui contano proprio i byte):
strlen("héllo")vale 6, non 5. Il C conta i byte, edéoccupa due byte in UTF-8.CString::newfallisce se il testo contiene un byte0. Il C penserebbe che la stringa finisca lì e taglierebbe via"lo"senza dire niente. Rust invece si rifiuta, ed è per questo cheCString::newrestituisce unResult. 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")NoneSome(".org")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().
Condividere struct con #[repr(C)]
Sezione intitolata “Condividere struct con #[repr(C)]”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 2Far chiamare Rust dal C
Sezione intitolata “Far chiamare Rust dal C”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](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;}$ cc main.c -L target/release -lmathlib -o main$ LD_LIBRARY_PATH=target/release ./main2 + 3 = 5Scrivere quell’header C a mano stanca presto. cbindgen (in inglese) lo genera a partire dal tuo codice Rust.
Compilare il tuo codice C con build.rs e cc
Sezione intitolata “Compilare il tuo codice C con build.rs e cc”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.
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("Hello from C, %s!\n", name); fflush(stdout);}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");}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 = 5Hello 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ì.
Oltre il C: Python, Node.js e il web
Sezione intitolata “Oltre il C: Python, Node.js e il web”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
maturinli compila e li pubblica come normali pacchettipip. 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++:
cdylibpiù cbindgen (in inglese), come visto sopra. - Il browser: non è FFI nel senso del C, ma
wasm-bindgenha un ruolo simile. Vedi WebAssembly.
Errori comuni
Sezione intitolata “Errori comuni”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 functionIl 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.
Riepilogo
Sezione intitolata “Riepilogo”- Dichiara le funzioni C in un blocco
unsafe extern "C" { ... }, usando i tipi C distd::fficomec_intec_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
CStringper mandare testo al C eCStrper leggere testo dal C. Tieni viva laCStringfinché il C usa il puntatore. - Usa
#[repr(C)]per le struct condivise con il C, perché Rust può riordinare i campi. extern "C" fnpiù#[no_mangle](#[unsafe(no_mangle)]nell’edizione 2024) permette al C di chiamare Rust;cdylibcostruisce una libreria apposta.build.rs+cccompila il tuo codice C;bindgengenera le dichiarazioni; PyO3 e napi-rs collegano Rust a Python e Node.js.
Verifica veloce
Sezione intitolata “Verifica veloce”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
Per approfondire
Sezione intitolata “Per approfondire”- FFI — The Rustonomicon (in inglese): l’approfondimento ufficiale su callback, linking, puntatori nullabili e altro.
std::ffi— documentazione della libreria standard (in inglese):CString,CStr,c_int,c_chare compagnia, con esempi.- External blocks — The Rust Reference (in inglese): le regole esatte per i blocchi
extern, le ABI e gli elementisafe/unsafe. - The bindgen User Guide (in inglese): come generare binding Rust dagli header C.
- The PyO3 User Guide (in inglese): scrivere estensioni Python in Rust.