Rust und C verbinden: C-Header und Bindings mit Cbindgen und Cargo-c generieren
Min-jun Kim
Dev Intern · Leapcell

Einleitung
Rust hat sich mit seinem Fokus auf Leistung, Speichersicherheit und Nebenläufigkeit in verschiedenen Bereichen schnell durchgesetzt, von der Systemprogrammierung bis zum WebAssembly. Die Welt läuft jedoch immer noch auf C, und die Interoperabilität mit bestehenden C-Codebasen ist oft eine kritische Anforderung für die Integration von Rust-Bibliotheken in größere Projekte. Der direkte Konsum der Application Binary Interface (ABI) von Rust ist aufgrund seiner ständigen Weiterentwicklung instabil und komplex. Hier entsteht die Notwendigkeit stabiler C Application Programming Interfaces (APIs). Durch die Bereitstellung von Rust-Funktionalität über C-kompatible Bindings können wir die Stärken von Rust nutzen und gleichzeitig die Kompatibilität mit einem riesigen Ökosystem von C- und C-kompatiblen Sprachen aufrechterhalten. Dieser Artikel führt Sie durch den Prozess der Generierung von C-Header-Dateien und Bindings für Ihre Rust-Bibliotheken unter Verwendung von zwei leistungsstarken Werkzeugen: cbindgen
und cargo-c
, die eine nahtlose Integration ermöglichen und neue Möglichkeiten für Ihre Rust-Kreationen erschließen.
Kernkonzepte für sprachübergreifende Interoperabilität
Bevor wir uns mit den praktischen Aspekten befassen, wollen wir ein grundlegendes Verständnis der Schlüsselkonzepte entwickeln, die bei der Verbindung von Rust und C auftreten.
- Foreign Function Interface (FFI): FFI ist ein Mechanismus, der es einem in einer Programmiersprache geschriebenen Programm ermöglicht, Funktionen aufzurufen oder Dienste zu nutzen, die in einer anderen Programmiersprache geschrieben wurden. In unserem Kontext ermöglicht Rusts FFI die Interaktion mit C-Code und umgekehrt.
- C ABI (Application Binary Interface): Die C ABI definiert, wie Funktionen aufgerufen werden, wie Daten im Speicher angeordnet sind und wie Parameter und Rückgabewerte zwischen Funktionen übergeben werden, die in C geschrieben sind. Wenn Rust-Funktionen für C bereitgestellt werden, müssen wir sicherstellen, dass sie den C-ABI-Konventionen entsprechen, um undefiniertes Verhalten und Abstürze zu vermeiden.
no_mangle
Attribut: In Rust werden Funktionsnamen vom Compiler "gemangelt" (mangled), um Funktionen wie Überladung und eindeutige Symbolidentifikation zu unterstützen. Das Attribut#[no_mangle]
, das vor einerpub extern "C"
-Funktion platziert wird, verhindert dieses Namens-Mangling und stellt sicher, dass der Name der Funktion in der kompilierten Ausgabe mit ihrem Quellcode-Namen übereinstimmt, wodurch sie für C-Compiler auffindbar wird.extern "C"
Aufrufkonvention: Dies gibt an, dass eine Funktion die C-Aufrufkonvention verwenden soll, die festlegt, wie Argumente auf den Stapel gelegt werden, wie der Rückgabewert behandelt wird und wie der Stapel nach dem Aufruf bereinigt wird. Die Einhaltung dieser Konvention ist entscheidend für korrekte FFI-Interaktionen.cbindgen
: Ein Werkzeug, das automatisch C/C++-Header-Dateien aus Rust-Code generiert, der mitpub extern "C"
-Funktionen und C-kompatiblen Datenstrukturen annotiert ist. Es vereinfacht den manuellen Prozess des Schreibens von Header-Dateien, der fehleranfällig und mühsam sein kann.cargo-c
: Ein Cargo-Unterbefehl, der bei der Erstellung C-kompatibler Bibliotheken aus Rust-Projekten hilft. Er automatisiert den Build-Prozess, generiert die erforderlichen C-Header, statischen und dynamischen Bibliotheken und kann sogarpkg-config
-Dateien für eine einfachere Integration in C-Build-Systeme erstellen.
Generierung von C-Headern und Bindings
Lassen Sie uns ein Beispiel durchgehen, um zu veranschaulichen, wie cbindgen
und cargo-c
in der Praxis funktionieren. Wir erstellen eine einfache Rust-Bibliothek, die die Fibonacci-Folge berechnet und sie für C bereitstellt.
Schritt 1: Initialisierung einer Rust-Bibliothek
Erstellen Sie zunächst ein neues Rust-Bibliotheksprojekt:
car go new --lib fib_lib cd fib_lib
Schritt 2: Implementierung der Rust-Funktionalität
Bearbeiten Sie src/lib.rs
, um unsere Fibonacci-Funktion einzufügen und sie mit extern "C"
und no_mangle
bereitzustellen. Wir definieren auch eine einfache Struktur.
// src/lib.rs #[derive(Debug)] #[repr(C)] // Stellt sicher, dass die Speicherlayout C-kompatibel ist pub struct MyData { pub value: i32, pub name: *const std::os::raw::c_char, // C-kompatibler String-Zeiger } /// Berechnet die n-te Fibonacci-Zahl. /// # Safety /// Diese Funktion kann sicher für nicht-negative n aufgerufen werden. #[no_mangle] pub extern "C" fn fibonacci(n: i32) -> i32 { if n <= 1 { n } else { fibonacci(n - 1) + fibonacci(n - 2) } } /// Druckt eine Nachricht unter Verwendung der bereitgestellten Daten. /// # Safety /// Der `data`-Zeiger muss gültig sein und auf eine `MyData`-Struktur zeigen. /// Das `name`-Feld innerhalb von `MyData` muss ein gültiger C-Style-String-Zeiger oder null sein. #[no_mangle] pub extern "C" fn greet_with_data(data: *const MyData) { if data.is_null() { println!("Erhielt null Daten."); return; } unsafe { let actual_data = *data; let c_str = std::ffi::CStr::from_ptr(actual_data.name); let name_str = c_str.to_string_lossy(); println!("Hallo, {}! Ihr Wert ist {}", name_str, actual_data.value); } }
Wichtige Überlegungen für FFI:
#[repr(C)]
: Dieses Attribut ist für Strukturen entscheidend. Es weist den Rust-Compiler an, die Felder der Struktur im Speicher genau so anzuordnen, wie es ein C-Compiler tun würde, und verhindert so unerwartete Auffüllungen oder Neuordnungen, die zu ABI-Inkompatibilitäten führen könnten.*const std::os::raw::c_char
: RustsString
- undstr
-Typen sind nicht C-kompatibel. Für die Übergabe von Strings über die FFI-Grenze verwenden wir C-Style-Null-terminierte Strings, die durch*const std::os::raw::c_char
(für unveränderliche Strings) oder*mut std::os::raw::c_char
(für veränderliche Strings) dargestellt werden. Denken Sie daran, dass Rusts Eigentumsregeln nicht für Rohzeiger gelten, daher ist sorgfältiges Speichermanagement auf beiden Seiten erforderlich. In diesem Beispiel gehen wir davon aus, dass die C-Seite den Speicher für das Feldname
verwaltet und besitzt.unsafe
Blöcke: Wenn Sie mit Rohzeigern und C-FFI arbeiten, werden Sie häufig aufunsafe
-Blöcke stoßen. Diese Blöcke zeigen an, dass der Code innerhalb von Operationen ausführt, die der Rust-Compiler nicht garantieren kann, dass sie speichersicher sind, wie z. B. das Dereferenzieren von Rohzeigern oder das Aufrufen von C-Funktionen. Es liegt in der Verantwortung des Entwicklers, die Sicherheit dieser Operationen zu gewährleisten.
Schritt 3: Integration von cbindgen
Fügen Sie cbindgen
als Build-Abhängigkeit in Cargo.toml
hinzu:
# Cargo.toml [package] name = "fib_lib" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib", "staticlib"] # Generiert dynamische und statische C-Bibliotheken [build-dependencies] cbindgen = "0.24" # Verwenden Sie die neueste stabile Version
Erstellen Sie ein Build-Skript build.rs
, um cbindgen
auszuführen:
// build.rs extern crate cbindgen; use std::env; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); cbindgen::Builder::new() .with_crate(crate_dir) .with_language(cbindgen::Language::C) .generate() .expect("Unable to generate bindings") .write_to_file("target/fib_lib.h"); // Ausgabe ins Zielverzeichnis }
Wenn Sie nun cargo build
ausführen, generiert cbindgen
automatisch target/fib_lib.h
.
Schritt 4: Integration von cargo-c
cargo-c
vereinfacht das Packaging Ihrer Rust-Bibliothek als C-kompatible Bibliothek. Installieren Sie es zuerst:
car go install cargo-c
Führen Sie nun einfach cargo cbuild
aus, um Ihre Bibliothek für C zu erstellen. Dieser Befehl wird Folgendes tun:
- Kompiliert Ihren Rust-Code zu einer statischen Bibliothek (
.a
) und einer dynamischen Bibliothek (.so
unter Linux,.dylib
unter macOS,.dll
unter Windows). - Führt Ihr
build.rs
-Skript aus, dasfib_lib.h
generiert. - Platziert diese Ausgabeartefakte in einem strukturierten Verzeichnis (z. B.
target/release/capi/
).
Lassen Sie uns build.rs
geringfügig ändern, um den Header während eines normalen Builds direkt in das target
-Verzeichnis auszugeben, und cargo-c
wird ihn dann in seine standardmäßige C-kompatible Ausgabe-Struktur verschieben.
Nachdem Sie cargo cbuild --release
ausgeführt haben, finden Sie die generierten Dateien in target/release/capi
:
target/release/capi/
├── include/
│ └── fib_lib.h
├── lib/
│ ├── libfib_lib.a
│ └── libfib_lib.so (oder .dylib/.dll)
└── pkgconfig/
└── fib_lib.pc
Die Datei fib_lib.h
wird ungefähr so aussehen (vereinfacht):
// fib_lib.h (generiert von cbindgen) #include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdlib.h> typedef struct MyData { int32_t value; const char *name; } MyData; int32_t fibonacci(int32_t n); void greet_with_data(const struct MyData *data);
Schritt 5: Konsumieren der Bibliothek in C
Erstellen Sie eine C-Datei (z. B. main.c
), um unsere Rust-Bibliothek zu verwenden:
// main.c #include <stdio.h> #include <stdlib.h> // Für malloc, free #include <string.h> // Für strdup #include "fib_lib.h" // Fügen Sie den generierten Header ein int main() { // Testen der fibonacci-Funktion int n = 10; int result = fibonacci(n); printf("Fibonacci(%d) = %d\n", n, result); // Testen der greet_with_data-Funktion MyData my_c_data; my_c_data.value = 42; my_c_data.name = strdup("World from C"); // C-Style-String zuweisen greet_with_data(&my_c_data); free((void*)my_c_data.name); // Den zugewiesenen C-Style-String freigeben return 0; }
Kompilieren und linken Sie das C-Programm mit der generierten Rust-Bibliothek:
# Angenommen, Sie befinden sich im Verzeichnis, das main.c und die fib_lib.h, .a/.so-Dateien enthält # (passen Sie die Pfade bei Bedarf an, um auf target/release/capi zu verweisen) # Für dynamisches Linking (Linux/macOS) gcc main.c -o my_c_app -Itarget/release/capi/include -Ltarget/release/capi/lib -lfib_lib # Für statisches Linking (Linux/macOS) # gcc main.c -o my_c_app -Itarget/release/capi/include target/release/capi/lib/libfib_lib.a # Führen Sie die C-Anwendung aus ./my_c_app
Sie sollten eine Ausgabe ähnlich der folgenden sehen:
Fibonacci(10) = 55
Hallo, World from C! Ihr Wert ist 42
Dies demonstriert eine erfolgreiche Interoperabilität. Das C-Programm ruft Rust-Funktionen auf und verwendet Rust-definierte Datenstrukturen nahtlos.
Anwendungsfälle
- Integration von Rust in bestehende C/C++-Codebasen: Nutzen Sie die Speichersicherheit und Leistung von Rust für kritische Komponenten, ohne die gesamte Anwendung neu schreiben zu müssen.
- Erstellung von einbettbaren Modulen: Erstellen Sie Rust-gestützte Plugins oder Erweiterungen, die von Anwendungen geladen und verwendet werden können, die in C oder anderen Sprachen mit C-FFI-Unterstützung geschrieben wurden.
- Entwicklung von Low-Level-OS-Komponenten: Verwenden Sie Rust für Treiber, Kernel-Module oder Bootloader, die mit C-Schnittstellen interagieren müssen.
- Sprachenübergreifende Entwicklung: Erleichtern Sie die Zusammenarbeit zwischen Teams, die in Rust und C arbeiten, ohne dass diese beide Sprachen gründlich lernen müssen.
Fazit
Durch die Beherrschung von cbindgen
und cargo-c
können Sie mühelos C-kompatible Header und Bibliotheken aus Ihren Rust-Projekten generieren. Diese leistungsstarke Kombination eröffnet eine Welt voller Möglichkeiten und ermöglicht die nahtlose Integration Ihres robusten und sicheren Rust-Codes in das riesige C-Ökosystem. Diese Brückenfähigkeit bietet einen praktischen und effizienten Weg für Rust, seine Reichweite und Wirkung über verschiedene Softwarelandschaften hinweg zu erweitern.