Zum Inhalt springen

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.

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) = 42
▶ Selbst ausprobieren

Ein 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_int ist Rusts Name für Cs int. Das Modul std::ffi hat Typen wie c_int, c_long und c_char. So passt deine Deklaration auf jeder Plattform genau zu C.
  • Du schreibst unsafe extern "C". In Edition 2024 ist das unsafe hier Pflicht, denn du versprichst damit, dass die Deklaration stimmt. Älterer Code (und Edition 2021) schreibt oft nur extern "C" { ... }. Die Variante mit unsafe funktioniert 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)
None
▶ Selbst ausprobieren

Der 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.

Bei Strings passieren die meisten FFI-Fehler, weil Rust und C sie unterschiedlich speichern:

  • Ein Rust-String / &str kennt seine Länge und darf jedes Byte enthalten, auch 0.
  • 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: 3
▶ Selbst ausprobieren

Die Texte in diesem Beispiel bleiben auf Englisch, weil die Ausgabe Bytes und Längen zeigt. Hier stecken zwei Überraschungen drin, und beide sind wichtig:

  1. strlen("héllo") ist 6, nicht 5. C zählt Bytes, und é braucht in UTF-8 zwei Bytes.
  2. CString::new schlägt fehl, wenn dein Text ein 0-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 gibt CString::new ein Result zurü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")
None
Some(".org")
▶ Selbst ausprobieren

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.

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 2
▶ Selbst ausprobieren

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]
▶ Selbst ausprobieren

(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;
}
Terminal window
$ cc main.c -L target/release -lmathlib -o main
$ LD_LIBRARY_PATH=target/release ./main
2 + 3 = 5

Diesen C-Header von Hand zu schreiben, wird schnell lästig. cbindgen (auf Englisch) erzeugt ihn aus deinem Rust-Code.

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.

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("Hallo aus C, %s!\n", name);
fflush(stdout);
}
build.rs
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");
}
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
Hallo 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.

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 maturin baut sie und veröffentlicht sie als ganz normale pip-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: cdylib plus cbindgen (auf Englisch), wie oben gezeigt.
  • Der Browser: Das ist kein FFI im Sinne von C, aber wasm-bindgen spielt eine ähnliche Rolle. Siehe WebAssembly.

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 function

Der 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.

  • Deklariere C-Funktionen in einem unsafe extern "C" { ... }-Block und verwende die C-Typen aus std::ffi wie c_int und c_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 CString schickst du Text an C, mit CStr liest du Text von C. Halte den CString am Leben, solange C den Zeiger benutzt.
  • Verwende #[repr(C)] für Structs, die du mit C teilst, weil Rust Felder umsortieren darf.
  • extern "C" fn plus #[no_mangle] (#[unsafe(no_mangle)] in Edition 2024) lässt C Rust aufrufen; cdylib baut die passende Bibliothek.
  • build.rs + cc kompiliert deinen C-Code; bindgen erzeugt Deklarationen; PyO3 und napi-rs verbinden Rust mit Python und Node.js.

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