FFI en Rust : appeler du C depuis Rust
FFI signifie Foreign Function Interface (interface de fonctions étrangères). C’est la façon dont Rust dialogue avec du code écrit dans un autre langage. Presque toujours, cet autre langage est le C, ou quelque chose qui suit les règles du C, car le C est la « langue commune » que comprennent presque tous les langages et systèmes d’exploitation.
Imaginez deux personnes qui n’ont pas la même langue maternelle, mais qui parlent toutes les deux un peu anglais. Le C joue le rôle de cette langue partagée. Avec la FFI, vous pouvez utiliser depuis Rust une bibliothèque C éprouvée, ou écrire une bibliothèque Rust rapide que des programmes Python, Node.js ou C pourront appeler. Cette page s’appuie sur Unsafe en Rust : lisez-la d’abord si les blocs unsafe sont nouveaux pour vous.
Appeler une fonction C
Section intitulée « Appeler une fonction C »La bibliothèque standard du C est toujours liée aux programmes Rust sous Linux, macOS et Windows : ses fonctions sont donc parfaites pour un premier essai. Pour en appeler une, vous la déclarez dans un bloc extern "C" : son nom, ses paramètres et son type de retour. Ensuite, vous l’appelez dans un bloc unsafe :
use std::ffi::c_int;
unsafe extern "C" { // C'est le `int abs(int x);` du C, écrit en Rust. fn abs(x: c_int) -> c_int;}
fn main() { let n = unsafe { abs(-42) }; println!("abs(-42) = {n}");}Sortie :
abs(-42) = 42Quelques points à remarquer :
"C"désigne l’ABI (Application Binary Interface) : les règles qui fixent comment les arguments sont passés et les valeurs renvoyées. Les règles du C sont celles sur lesquelles tout le monde s’accorde.c_intest le nom Rust du typeintdu C. Le modulestd::ffipropose des types commec_int,c_longetc_char, pour que votre déclaration corresponde exactement au C sur toutes les plateformes.- On écrit
unsafe extern "C". Dans l’édition 2024, ceunsafeest obligatoire, car vous promettez que la déclaration est correcte. Le code plus ancien (et l’édition 2021) écrit souvent un simpleextern "C" { ... }. La version avecunsafefonctionne dans les deux cas, donc cette page l’utilise partout.
Pourquoi chaque appel est unsafe, et comment le cacher
Section intitulée « Pourquoi chaque appel est unsafe, et comment le cacher »Le compilateur Rust ne sait pas lire le code C. Il ignore si abs prend vraiment un int, si elle risque de planter, ou si elle conserve un pointeur que vous lui avez donné. Chaque appel à une fonction étrangère est donc unsafe : c’est vous qui prenez la responsabilité de l’utiliser correctement.
La bonne nouvelle : vous n’avez à y réfléchir sérieusement qu’une seule fois. La méthode habituelle consiste à écrire une petite enveloppe sûre (safe wrapper) : une fonction Rust normale qui vérifie les règles, puis fait l’appel unsafe. Tout le reste du code appelle simplement l’enveloppe.
Voici un vrai exemple. En C, abs(INT_MIN) est un comportement indéfini, car le résultat est trop grand pour tenir dans un int. Notre enveloppe vérifie d’abord ce cas :
use std::ffi::c_int;
unsafe extern "C" { fn abs(x: c_int) -> c_int;}
// Une enveloppe sûre : les appelants n'ont pas besoin de `unsafe`.fn safe_abs(x: i32) -> Option<i32> { // abs(i32::MIN) n'a pas de résultat valide en C, donc on le refuse. if x == i32::MIN { return None; } // SAFETY: x n'est pas i32::MIN, donc abs a un résultat valide. Some(unsafe { abs(x) })}
fn main() { println!("{:?}", safe_abs(-42)); println!("{:?}", safe_abs(i32::MIN));}Sortie :
Some(42)NoneLe commentaire // SAFETY: est une habitude Rust qui mérite d’être copiée. Il explique pourquoi ce bloc unsafe précis est correct, pour que la personne qui lira le code ensuite n’ait pas à deviner.
Si une fonction C n’a vraiment aucune règle à respecter, quelle que soit l’entrée, vous pouvez la marquer safe fn dans le bloc unsafe extern, puis l’appeler sans unsafe. abs n’est pas une telle fonction (souvenez-vous d’INT_MIN) : on la garde donc unsafe et on vérifie l’entrée nous-mêmes.
Les chaînes : CString, CStr et l’octet NUL
Section intitulée « Les chaînes : CString, CStr et l’octet NUL »C’est dans les chaînes de caractères que se cachent la plupart des bugs de FFI, car Rust et le C ne les stockent pas de la même façon :
- Une
String/&strRust connaît sa longueur et peut contenir n’importe quel octet, y compris0. - Une chaîne C n’est qu’un pointeur vers des octets. La chaîne se termine au premier octet
0(appelé NUL). Aucune longueur n’est stockée.
Vous ne pouvez donc pas passer directement un &str au C. Utilisez CString (en anglais) pour construire une copie possédée, terminée par NUL, et .as_ptr() pour obtenir le *const c_char qu’attend le C :
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 ajoute le NUL à la fin pour nous. let c_text = CString::new(text).ok()?; // SAFETY: c_text est une chaîne valide terminée par NUL, qui vit jusqu'à la fin de cette fonction. 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!("erreur : {e}"), }}Sortie :
[104, 101, 108, 108, 111, 0]Some(5)Some(6)erreur : nul byte found in provided data at position: 3Ici, les mots "hello" et "héllo" restent tels quels, car tout l’exemple porte justement sur le nombre d’octets. Le message d’erreur après « erreur : » vient de Rust lui-même et reste donc en anglais : il signale qu’un octet nul a été trouvé à la position 3.
Deux surprises, toutes deux importantes :
strlen("héllo")vaut 6, pas 5. Le C compte des octets, etéoccupe deux octets en UTF-8.CString::newéchoue si votre texte contient un octet0. Le C croirait que la chaîne s’arrête là et couperait"lo"sans rien dire. Rust refuse plutôt que de laisser faire : c’est pour cela queCString::newrenvoie unResult.
Dans l’autre sens, quand le C vous donne un char *, enveloppez-le dans un CStr (en anglais). CStr est à CString ce que &str est à String : une vue empruntée. La fonction C strchr renvoie un pointeur vers le premier caractère correspondant, ce qui fait une jolie démonstration :
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; // le C dit « pas trouvé » avec un pointeur nul } // SAFETY: ptr pointe à l'intérieur de `text`, qui est vivant et terminé par 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 littéral c"..." est un &CStr dont le NUL est déjà ajouté. println!("{:?}", find_from(c"rust-lang.org", b'.'));}Sortie :
Some(", world")NoneSome(".org")to_string_lossy() transforme les octets C en texte Rust. Il remplace tout UTF-8 invalide par �, car les chaînes C ne promettent pas d’être en UTF-8. Utilisez plutôt .to_str() si vous préférez obtenir une erreur.
Partager des structs avec #[repr(C)]
Section intitulée « Partager des structs avec #[repr(C)] »Rust a le droit de réordonner les champs d’une struct pour gagner de la place. Le C ne le fait jamais : les champs sont rangés en mémoire dans l’ordre où vous les avez écrits, avec du remplissage (padding) entre eux. Par exemple, une struct avec les champs u8, u32, u8 occupe aujourd’hui 8 octets avec la disposition de Rust (Rust regroupe les deux u8), mais 12 octets avec celle du C. Si le C et Rust ne sont pas d’accord sur la disposition, le C lit les mauvais octets, et rien ne vous prévient.
Quand une struct traverse la frontière, demandez donc à Rust d’utiliser la disposition du C avec #[repr(C)]. La fonction C div renvoie une struct div_t avec deux champs int, quot (quotient) et rem (reste) :
use std::ffi::c_int;
// Correspond au 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 = {} reste {}", result.quot, result.rem);}Sortie :
DivT { quot: 3, rem: 2 }17 / 5 = 3 reste 2Laisser le C appeler Rust
Section intitulée « Laisser le C appeler Rust »La FFI fonctionne dans les deux sens. Écrivez extern "C" fn (sans bloc) et vous obtenez une fonction Rust qui suit les conventions d’appel du C : du code C peut donc l’appeler. La fonction C qsort prend une fonction de comparaison en argument, et nous pouvons lui en donner une écrite en 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, );}
// Une fonction Rust que le C va appeler.extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int { // SAFETY: qsort ne nous donne que des pointeurs vers notre tableau 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);}Sortie :
[-3, 0, 7, 19, 42](Dans du vrai code, vous appelleriez simplement sort(). Ceci sert uniquement à montrer le C qui rappelle du code Rust.)
Quand un programme C doit trouver votre fonction par son nom, ajoutez #[no_mangle]. Normalement, Rust « décore » (mangles) les noms en quelque chose comme _ZN4main11add_numbers17h...E, pour que des crates différentes n’entrent jamais en conflit. #[no_mangle] conserve le nom simple add_numbers :
#[no_mangle]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}À propos de l’édition 2024 : les exemples Playground de ce site utilisent l’édition 2021, où #[no_mangle] convient. Dans l’édition 2024 (celle par défaut d’un nouveau projet cargo new), il faut écrire #[unsafe(no_mangle)], car deux fonctions ayant le même nom non décoré casseraient le programme. Cette nouvelle écriture fonctionne aussi dans l’édition 2021.
Pour construire une bibliothèque que le C peut lier, réglez le type de crate sur cdylib dans Cargo.toml :
[lib]crate-type = ["cdylib"]// src/lib.rs (édition 2024)#[unsafe(no_mangle)]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}cargo build --release produit target/release/libmathlib.so (.dylib sous macOS, .dll sous Windows) pour une crate nommée mathlib. Un programme C déclare alors la fonction et se lie à la bibliothèque :
#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 = 5Écrire cet en-tête C à la main devient vite pénible. cbindgen (en anglais) le génère à partir de votre code Rust.
Compiler votre propre code C avec build.rs et cc
Section intitulée « Compiler votre propre code C avec build.rs et cc »Jusqu’ici, nous n’avons utilisé que des fonctions déjà liées. Pour utiliser votre propre fichier C, laissez Cargo le compiler. Un fichier nommé build.rs à la racine du projet est un script de build : Cargo l’exécute avant de compiler votre crate. La crate cc (en anglais) réduit cela à quelques lignes.
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("Bonjour depuis le C, %s !\n", name); fflush(stdout);}fn main() { // Compiler src/greet.c en bibliothèque statique et la lier au programme. cc::Build::new().file("src/greet.c").compile("greet"); // Ne relancer ce script que si le fichier C change. 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 = 5Bonjour depuis le C, Ferris !Il vous faut un compilateur C installé (gcc, clang, ou MSVC sous Windows) ; cc le trouve pour vous.
Pour une grosse bibliothèque C avec des centaines de fonctions, écrire chaque déclaration extern à la main est lent et source d’erreurs. bindgen (en anglais) lit les fichiers d’en-tête C et génère les déclarations Rust pour vous, généralement depuis build.rs lui aussi. La plupart des crates -sys de crates.io (comme openssl-sys) sont construites de cette façon.
Au-delà du C : Python, Node.js et le web
Section intitulée « Au-delà du C : Python, Node.js et le web »Comme le C est la langue commune, les mêmes idées permettent à Rust de parler à beaucoup d’autres langages. Dans ces cas, vous écrivez rarement du code extern "C" brut : une crate fait le travail unsafe et vous offre une API sûre et agréable :
- Python : PyO3 (en anglais) permet d’écrire des modules Python en Rust (et d’appeler Python depuis Rust). Son outil compagnon
maturinles construit et les publie comme des paquetspipnormaux. Des bibliothèques comme Polars et pydantic-core l’utilisent. - Node.js : napi-rs (en anglais) construit en Rust des modules natifs pour Node.js, et génère les types TypeScript pour vous.
- Projets C et C++ :
cdylibplus cbindgen, comme montré plus haut. - Le navigateur : ce n’est pas de la FFI au sens du C, mais
wasm-bindgenjoue un rôle similaire. Voir Rust et WebAssembly.
Erreurs courantes
Section intitulée « Erreurs courantes »Appeler une fonction étrangère en dehors d’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 functionLe compilateur indique (en anglais) que l’appel à une fonction unsafe exige un bloc ou une fonction unsafe. La solution : envelopper l’appel dans unsafe { ... }, ou mieux, dans une fonction d’enveloppe sûre, comme plus haut.
Un pointeur vers une CString qui n’existe plus. Ce code compile, mais c’est un vrai bug :
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`L’avertissement dit que ce code crée un pointeur pendant (dangling pointer). La CString est temporaire : elle est donc libérée au ;. ptr pointe alors vers de la mémoire libérée. La solution : gardez la CString dans sa propre variable (let s = CString::new("hello").unwrap();) et appelez s.as_ptr() tant que s est vivante. N’ignorez jamais cet avertissement.
Un simple extern "C" dans un projet en édition 2024. Du code copié depuis d’anciens tutoriels échoue ainsi :
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 | | } | |_^La solution : écrire unsafe extern "C" { ... }.
Oublier #[repr(C)]. Il n’y a aucune erreur : le C lit simplement n’importe quoi. Mettez #[repr(C)] sur chaque struct et chaque enum qui traverse la frontière.
Récapitulatif
Section intitulée « Récapitulatif »- Déclarez les fonctions C dans un bloc
unsafe extern "C" { ... }, avec les types C destd::fficommec_intetc_char. - Chaque appel étranger est
unsafe. Enveloppez-le une fois pour toutes dans une fonction Rust sûre qui vérifie les règles, avec un commentaire// SAFETY:. - Les chaînes C se terminent par un octet NUL. Utilisez
CStringpour envoyer du texte au C etCStrpour lire du texte venant du C. Gardez laCStringvivante tant que le C utilise le pointeur. - Utilisez
#[repr(C)]pour les structs partagées avec le C, car Rust peut réordonner les champs. extern "C" fnplus#[no_mangle](#[unsafe(no_mangle)]en édition 2024) permettent au C d’appeler Rust ;cdylibconstruit une bibliothèque pour cela.build.rs+cccompilent votre code C ;bindgengénère les déclarations ; PyO3 et napi-rs relient Rust à Python et à Node.js.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Pourquoi chaque appel à une fonction d'un bloc extern "C" est-il unsafe ?
2. Que se passe-t-il avec CString::new("hel\0lo") ?
3. Pourquoi les structs partagées avec le C ont-elles besoin de #[repr(C)] ?
4. Dans une crate en édition 2024, comment écrit-on l'attribut qui conserve le nom simple d'une fonction pour le C ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- FFI — The Rustonomicon (en anglais) : l’exploration officielle en profondeur : callbacks, édition de liens, pointeurs nullables et plus encore.
std::ffi— documentation de la bibliothèque standard (en anglais) :CString,CStr,c_int,c_charet compagnie, avec des exemples.- External blocks — The Rust Reference (en anglais) : les règles exactes des blocs
extern, des ABI et des élémentssafe/unsafe. - The bindgen User Guide (en anglais) : générer des bindings Rust à partir d’en-têtes C.
- The PyO3 User Guide (en anglais) : écrire des extensions Python en Rust.