FFI in Rust: C-Funktionen aus Rust aufrufen
FFI steht für Foreign Function Interface (Schnittstelle zu fremden Funktionen). Darüber spricht Rust mit Code, der in einer anderen Sprache geschrieben ist. Fast immer ist diese andere Sprache C – oder etwas, das sich an die Regeln von C hält. C ist nämlich die „gemeinsame Sprache“, die fast jede Programmiersprache und jedes Betriebssystem versteht.
Stell dir zwei Menschen vor, die keine gemeinsame Muttersprache haben, aber beide ein bisschen Englisch sprechen. C ist dieses Englisch. Mit FFI kannst du eine ausgereifte C-Bibliothek aus Rust heraus benutzen. Oder du schreibst eine schnelle Rust-Bibliothek, die Python, Node.js oder C-Programme aufrufen können. Diese Seite baut auf Unsafe Rust auf. Lies also zuerst dort nach, falls unsafe-Blöcke neu für dich sind.
Eine C-Funktion aufrufen
Abschnitt betitelt „Eine C-Funktion aufrufen“Die C-Standardbibliothek ist unter Linux, macOS und Windows immer in Rust-Programme eingebunden. Ihre Funktionen eignen sich deshalb perfekt für einen ersten Test. Um eine davon aufzurufen, deklarierst du sie in einem extern "C"-Block: mit Namen, Parametern und Rückgabetyp. Danach rufst du sie innerhalb von unsafe auf:
use std::ffi::c_int;
unsafe extern "C" { // Das ist Cs `int abs(int x);`, in Rust geschrieben. fn abs(x: c_int) -> c_int;}
fn main() { let n = unsafe { abs(-42) }; println!("abs(-42) = {n}");}Ausgabe:
abs(-42) = 42Ein paar Dinge fallen auf:
"C"benennt die ABI – die Regeln dafür, wie Argumente übergeben und Werte zurückgegeben werden. Auf die Regeln von C können sich alle einigen.c_intist Rusts Name für Csint. Das Modulstd::ffihat Typen wiec_int,c_longundc_char. So passt deine Deklaration auf jeder Plattform genau zu C.- Du schreibst
unsafe extern "C". In Edition 2024 ist dasunsafehier Pflicht, denn du versprichst damit, dass die Deklaration stimmt. Älterer Code (und Edition 2021) schreibt oft nurextern "C" { ... }. Die Variante mitunsafefunktioniert in beiden, deshalb nutzt diese Seite sie überall.
Warum jeder Aufruf unsafe ist – und wie du das versteckst
Abschnitt betitelt „Warum jeder Aufruf unsafe ist – und wie du das versteckst“Der Rust-Compiler kann keinen C-Code lesen. Er weiß nicht, ob abs wirklich einen int nimmt, ob die Funktion abstürzen kann oder ob sie einen Zeiger behält, den du ihr gegeben hast. Deshalb ist jeder Aufruf einer fremden Funktion unsafe: Du übernimmst die Verantwortung dafür, sie richtig zu benutzen.
Die gute Nachricht: Du musst nur einmal gründlich darüber nachdenken. Das übliche Muster ist ein kleiner sicherer Wrapper – eine normale Rust-Funktion, die die Regeln prüft und dann den unsicheren Aufruf macht. Alle anderen rufen einfach den Wrapper auf.
Hier ist ein echtes Beispiel. In C ist abs(INT_MIN) undefiniertes Verhalten (Undefined Behavior), weil das Ergebnis zu groß für einen int ist. Unser Wrapper prüft diesen Fall zuerst:
use std::ffi::c_int;
unsafe extern "C" { fn abs(x: c_int) -> c_int;}
// Ein sicherer Wrapper: Aufrufer brauchen kein `unsafe`.fn safe_abs(x: i32) -> Option<i32> { // abs(i32::MIN) hat in C kein gültiges Ergebnis, also lehnen wir es ab. if x == i32::MIN { return None; } // SAFETY: x ist nicht i32::MIN, also hat abs ein gültiges Ergebnis. Some(unsafe { abs(x) })}
fn main() { println!("{:?}", safe_abs(-42)); println!("{:?}", safe_abs(i32::MIN));}Ausgabe:
Some(42)NoneDer Kommentar // SAFETY: ist eine Rust-Gewohnheit, die du übernehmen solltest. Er erklärt, warum genau dieser unsafe-Block in Ordnung ist. So muss die nächste Person, die den Code liest, nicht raten.
Wenn eine C-Funktion wirklich für jede Eingabe sicher ist, kannst du sie im unsafe extern-Block als safe fn markieren. Dann rufst du sie ohne unsafe auf. abs ist keine solche Funktion (denk an INT_MIN), also lassen wir sie unsafe und prüfen die Eingabe selbst.
Strings: CString, CStr und das NUL-Byte
Abschnitt betitelt „Strings: CString, CStr und das NUL-Byte“Bei Strings passieren die meisten FFI-Fehler, weil Rust und C sie unterschiedlich speichern:
- Ein Rust-
String/&strkennt seine Länge und darf jedes Byte enthalten, auch0. - Ein C-String ist nur ein Zeiger auf Bytes. Der String endet beim ersten
0-Byte (genannt NUL). Eine Länge wird nicht gespeichert.
Du kannst also kein &str direkt an C übergeben. Mit CString (auf Englisch) baust du eine eigene, NUL-terminierte Kopie, und .as_ptr() liefert dir den *const c_char, den C haben will:
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 hängt das NUL für uns ans Ende. let c_text = CString::new(text).ok()?; // SAFETY: c_text ist ein gültiger NUL-terminierter String und lebt bis zum Ende dieser Funktion. 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!("Fehler: {e}"), }}Ausgabe:
[104, 101, 108, 108, 111, 0]Some(5)Some(6)Fehler: nul byte found in provided data at position: 3Die Texte in diesem Beispiel bleiben auf Englisch, weil die Ausgabe Bytes und Längen zeigt. Hier stecken zwei Überraschungen drin, und beide sind wichtig:
strlen("héllo")ist 6, nicht 5. C zählt Bytes, undébraucht in UTF-8 zwei Bytes.CString::newschlägt fehl, wenn dein Text ein0-Byte enthält. C würde denken, dass der String dort endet, und"lo"stillschweigend abschneiden. Rust lässt das nicht zu und meldet lieber einen Fehler – deshalb gibtCString::neweinResultzurück. Die Fehlermeldung selbst kommt aus der Standardbibliothek und ist deshalb auf Englisch: Sie sagt, dass an Position 3 ein NUL-Byte gefunden wurde.
Andersherum: Wenn C dir ein char * gibt, verpackst du es in CStr (auf Englisch). CStr verhält sich zu CString wie &str zu String: eine geliehene Ansicht. Cs strchr gibt einen Zeiger auf das erste passende Zeichen zurück. Das ergibt eine schöne Demo:
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; // C sagt „nicht gefunden“ mit einem Null-Zeiger } // SAFETY: ptr zeigt in `text` hinein, das noch lebt und NUL-terminiert ist. 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'));
// Ein c"..."-Literal ist ein &CStr, an das das NUL schon angehängt ist. println!("{:?}", find_from(c"rust-lang.org", b'.'));}Ausgabe:
Some(", world")NoneSome(".org")to_string_lossy() macht aus den C-Bytes Rust-Text. Ungültiges UTF-8 wird dabei durch � ersetzt, denn C-Strings versprechen nicht, UTF-8 zu sein. Nimm stattdessen .to_str(), wenn du lieber einen Fehler bekommen willst.
Structs teilen mit #[repr(C)]
Abschnitt betitelt „Structs teilen mit #[repr(C)]“Rust darf die Felder eines Structs umsortieren, um Platz zu sparen. C macht das nie – die Felder liegen in genau der Reihenfolge im Speicher, in der du sie geschrieben hast, mit Füllbytes (Padding) dazwischen. Ein Struct mit den Feldern u8, u32, u8 braucht zum Beispiel mit Rusts heutigem Layout 8 Bytes (Rust schiebt die beiden u8 zusammen), mit dem Layout von C aber 12 Bytes. Wenn C und Rust sich beim Layout nicht einig sind, liest C die falschen Bytes – und nichts warnt dich.
Wenn ein Struct also die Grenze überquert, sag Rust mit #[repr(C)], dass es das Layout von C verwenden soll. Cs Funktion div gibt ein Struct div_t mit zwei int-Feldern zurück, quot und rem:
use std::ffi::c_int;
// Passt zu 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 = {} Rest {}", result.quot, result.rem);}Ausgabe:
DivT { quot: 3, rem: 2 }17 / 5 = 3 Rest 2C ruft Rust auf
Abschnitt betitelt „C ruft Rust auf“FFI funktioniert in beide Richtungen. Schreib extern "C" fn (ohne Block), und du bekommst eine Rust-Funktion, die sich an die Aufrufregeln von C hält. C-Code kann sie also aufrufen. Cs qsort nimmt eine Vergleichsfunktion als Argument – wir können ihm eine aus Rust geben:
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, );}
// Eine Rust-Funktion, die C aufrufen wird.extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int { // SAFETY: qsort gibt uns nur Zeiger in unser i32-Array. 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);}Ausgabe:
[-3, 0, 7, 19, 42](In echtem Code würdest du einfach sort() aufrufen. Das Beispiel zeigt nur, wie C zurück in Rust ruft.)
Wenn ein C-Programm deine Funktion über ihren Namen finden muss, füge #[no_mangle] hinzu. Normalerweise „verstümmelt“ Rust Namen (Name Mangling) zu etwas wie _ZN4main11add_numbers17h...E, damit sich verschiedene Crates nie in die Quere kommen. #[no_mangle] behält den einfachen Namen add_numbers:
#[no_mangle]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}Hinweis zu Edition 2024: Die Playground-Beispiele dieser Seite nutzen Edition 2021, in der #[no_mangle] in Ordnung ist. In Edition 2024 (Standard für ein neues Projekt mit cargo new) musst du #[unsafe(no_mangle)] schreiben, denn zwei Funktionen mit demselben unverstümmelten Namen würden das Programm kaputt machen. Die neue Schreibweise funktioniert auch in Edition 2021.
Um eine Bibliothek zu bauen, gegen die C linken kann, setzt du den Crate-Typ in der Cargo.toml auf cdylib:
[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}cargo build --release erzeugt für einen Crate namens mathlib die Datei target/release/libmathlib.so (.dylib unter macOS, .dll unter Windows). Ein C-Programm deklariert dann die Funktion und linkt gegen die Bibliothek:
#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 = 5Diesen C-Header von Hand zu schreiben, wird schnell lästig. cbindgen (auf Englisch) erzeugt ihn aus deinem Rust-Code.
Eigenen C-Code mit build.rs und cc kompilieren
Abschnitt betitelt „Eigenen C-Code mit build.rs und cc kompilieren“Bisher haben wir nur Funktionen benutzt, die schon eingebunden sind. Für deine eigene C-Datei lässt du Cargo die Arbeit machen. Eine Datei namens build.rs im Projektordner ist ein Build-Skript: Cargo führt es aus, bevor es deinen Crate kompiliert. Mit dem Crate cc (auf Englisch) sind das nur ein paar Zeilen.
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("Hallo aus C, %s!\n", name); fflush(stdout);}fn main() { // src/greet.c zu einer statischen Bibliothek kompilieren und einbinden. cc::Build::new().file("src/greet.c").compile("greet"); // Dieses Skript nur neu ausführen, wenn sich die C-Datei ändert. 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 = 5Hallo aus C, Ferris!Du brauchst einen installierten C-Compiler (gcc, clang oder MSVC unter Windows); cc findet ihn für dich.
Bei einer großen C-Bibliothek mit Hunderten von Funktionen ist es langsam und fehleranfällig, jede extern-Deklaration von Hand zu schreiben. bindgen (auf Englisch) liest die C-Header-Dateien und erzeugt die Rust-Deklarationen für dich, meist ebenfalls aus build.rs heraus. Die meisten -sys-Crates auf crates.io (etwa openssl-sys) entstehen so.
Über C hinaus: Python, Node.js und das Web
Abschnitt betitelt „Über C hinaus: Python, Node.js und das Web“Weil C die gemeinsame Sprache ist, kann Rust mit denselben Ideen mit vielen anderen Sprachen reden. Dafür schreibst du selten rohen extern "C"-Code – ein Crate erledigt die unsicheren Teile und gibt dir eine sichere, freundliche API:
- Python: Mit PyO3 (auf Englisch) schreibst du Python-Module in Rust (und rufst Python aus Rust auf). Das Begleitwerkzeug
maturinbaut sie und veröffentlicht sie als ganz normalepip-Pakete. Bibliotheken wie Polars und pydantic-core nutzen das. - Node.js: napi-rs (auf Englisch) baut native Node.js-Add-ons in Rust und erzeugt die TypeScript-Typen gleich mit.
- C- und C++-Projekte:
cdylibplus cbindgen (auf Englisch), wie oben gezeigt. - Der Browser: Das ist kein FFI im Sinne von C, aber
wasm-bindgenspielt eine ähnliche Rolle. Siehe WebAssembly.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“Eine fremde Funktion außerhalb von unsafe aufrufen.
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 functionDer Compiler sagt: Der Aufruf der unsicheren Funktion abs braucht eine unsafe-Funktion oder einen unsafe-Block. Lösung: Pack den Aufruf in unsafe { ... } – oder besser in eine sichere Wrapper-Funktion, wie oben gezeigt.
Ein Zeiger auf einen CString, den es schon nicht mehr gibt. Das kompiliert, ist aber ein echter 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`Die Warnung sagt: Hier entsteht ein hängender Zeiger (Dangling Pointer), weil der temporäre CString am Ende der Anweisung freigegeben wird. Der CString ist nur temporär, also wird er beim ; freigegeben. ptr zeigt danach auf freigegebenen Speicher. Lösung: Leg den CString in einer eigenen Variable ab (let s = CString::new("hello").unwrap();) und ruf s.as_ptr() auf, solange s lebt. Ignorier diese Warnung nie.
Einfaches extern "C" in einem Projekt mit Edition 2024. Code aus älteren Tutorials scheitert so:
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 | | } | |_^Die Meldung heißt: extern-Blöcke müssen unsafe sein. Lösung: Schreib unsafe extern "C" { ... }.
#[repr(C)] vergessen. Hier gibt es überhaupt keinen Fehler – C liest einfach Datenmüll. Setz #[repr(C)] auf jedes Struct und jedes Enum, das die Grenze überquert.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Deklariere C-Funktionen in einem
unsafe extern "C" { ... }-Block und verwende die C-Typen ausstd::ffiwiec_intundc_char. - Jeder fremde Aufruf ist
unsafe. Verpack ihn einmal in eine sichere Rust-Funktion, die die Regeln prüft, mit einem// SAFETY:-Kommentar. - C-Strings enden bei einem NUL-Byte. Mit
CStringschickst du Text an C, mitCStrliest du Text von C. Halte denCStringam Leben, solange C den Zeiger benutzt. - Verwende
#[repr(C)]für Structs, die du mit C teilst, weil Rust Felder umsortieren darf. extern "C" fnplus#[no_mangle](#[unsafe(no_mangle)]in Edition 2024) lässt C Rust aufrufen;cdylibbaut die passende Bibliothek.build.rs+cckompiliert deinen C-Code;bindgenerzeugt Deklarationen; PyO3 und napi-rs verbinden Rust mit Python und Node.js.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Warum ist jeder Aufruf einer Funktion aus einem extern "C"-Block unsafe?
2. Was passiert bei CString::new("hel\0lo")?
3. Warum brauchen Structs, die du mit C teilst, #[repr(C)]?
4. Wie schreibst du in einem Crate mit Edition 2024 das Attribut, das den einfachen Namen einer Funktion für C behält?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- FFI – The Rustonomicon (auf Englisch) – die offizielle Vertiefung: Callbacks, Linken, Null-Zeiger und mehr.
std::ffi– Standardbibliothek (auf Englisch) –CString,CStr,c_int,c_charund Co., mit Beispielen.- External blocks – The Rust Reference (auf Englisch) – die genauen Regeln für
extern-Blöcke, ABIs undsafe-/unsafe-Elemente. - The bindgen User Guide (auf Englisch) – Rust-Bindings aus C-Headern erzeugen.
- The PyO3 User Guide (auf Englisch) – Python-Erweiterungen in Rust schreiben.