Interoperabilität von Rust und C++ für sicherere Anwendungen
Emily Parker
Product Engineer · Leapcell

Einleitung
In der sich rasant entwickelnden Landschaft der Softwareentwicklung ist die Fähigkeit, die Stärken verschiedener Programmiersprachen zu kombinieren, oft entscheidend für den Aufbau robuster und effizienter Anwendungen. Rust, gefeiert für seine unübertroffene Speichersicherheit und Leistung, wird zunehmend für kritische Systeme übernommen. Ein erheblicher Teil der hochoptimierten und zeitbewährten Softwareinfrastruktur der Welt ist jedoch in C oder C++ geschrieben. Dies umfasst alles von Hochleistungs-Numerikbibliotheken und Betriebssystem-APIs bis hin zu Firmware für eingebettete Systeme.
Die Herausforderung besteht dann darin: Wie können Rust-Anwendungen diese bestehenden, leistungsstarken C/C++-Bibliotheken nutzen, ohne die Kern Garantien von Rust für Speichersicherheit und Datenintegrität zu opfern? Hier kommt die Foreign Function Interface (FFI) ins Spiel. FFI ermöglicht es Rust-Programmen, in anderen Sprachen geschriebene Funktionen aufzurufen und umgekehrt. Insbesondere für C/C++ ermöglichen die FFI-Funktionen von Rust eine Brücke, die es uns ermöglicht, auf jahrzehntealte etablierte C/C++-Codebasen zuzugreifen, leistungsoptimierte Abschnitte zu verbessern oder mit systemnahen Funktionen zu interagieren, die überwiegend über C-APIs exponiert werden. Dieser Artikel befasst sich mit den praktischen Aspekten der Verwendung von Rust FFI, um C/C++-Code sicher aus Ihren Rust-Anwendungen aufzurufen, und demonstriert, wie dies unter Beibehaltung der Sicherheitsprinzipien von Rust erreicht werden kann.
Vorhandene C/C++-Bibliotheken in Rust nutzen
Um die Lücke zwischen Rust und C/C++ mithilfe von FFI effektiv zu schließen, ist es wichtig, einige Kernkonzepte und Terminologien zu verstehen.
Kernkonzepte
- Foreign Function Interface (FFI): Ein Mechanismus, der es einem Programm, das in einer Programmiersprache geschrieben wurde, ermöglicht, auf Routinen oder Funktionen zuzugreifen, die in einer anderen Sprache geschrieben wurden. In Rust wird
ffi
hauptsächlich zur Interaktion mit C Application Binary Interfaces (ABIs) verwendet. - ABI (Application Binary Interface): Definiert, wie Funktionen auf niedriger Ebene aufgerufen werden, einschließlich der Art und Weise, wie Parameter übergeben, Rückgabewerte empfangen und Stack Frames verwaltet werden. Die C-ABI ist ein De-facto-Standard, mit dem viele Sprachen, einschließlich Rust, interoperieren können.
unsafe
Schlüsselwort: Rusts Methode zur Kennzeichnung von Codeblöcken, die möglicherweise die Garantien der Speichersicherheit verletzen, wenn sie nicht sorgfältig verwendet werden. FFI-Operationen erfordern oftunsafe
-Blöcke, da der Rust-Compiler die Sicherheit von in anderen Sprachen geschriebenem Code nicht garantieren kann.extern
-Blöcke: Werden in Rust verwendet, um Funktionen zu deklarieren, die in externen Bibliotheken (z. B. C/C++-Bibliotheken) definiert sind. Diese Blöcke geben die Funktionssignatur und optional die zu verwendende ABI an.no_mangle
Attribut: Ein Rust-Attribut, das verwendet wird, um zu verhindern, dass der Rust-Compiler den Namen einer Funktion „verunstaltet“ (ändert), um sicherzustellen, dass sie einen C-kompatiblen Namen in der kompilierten Binärdatei hat. Dies ist entscheidend, wenn Rust-Funktionen von C/C++ aufgerufen werden.libc
Crate: Ein Rust-Crate, das rohe FFI-Bindings für gängige C-Bibliotheksfunktionen, Datentypen und Konstanten bereitstellt. Obwohl dies nützlich ist, sind für benutzerdefinierten C/C++-Code direkteextern
-Blöcke üblicher.
Das Prinzip der Integration
Das grundlegende Prinzip dreht sich darum, eine C-kompatible Schnittstelle für Ihren C/C++-Code zu erstellen. Dies bedeutet typischerweise:
- Freigabe C-kompatibler Funktionen: C++-Funktionen können Namensverunstaltung oder Klassenstrukturen aufweisen, die von C FFI nicht direkt konsumiert werden können. Um mit Rust zu interagieren, müssen Sie C++-Logik in
extern "C"
-Funktionen einpacken. Dies weist den C++-Compiler an, die Funktion unter Verwendung der C-Aufrufkonvention zu kompilieren und so die Namensverunstaltung zu verhindern. - Definieren übereinstimmender Signaturen in Rust: Auf der Rust-Seite deklarieren Sie diese C-kompatiblen Funktionen mit
extern "C"
-Blöcken und stellen sicher, dass die Funktionssignaturen (Parametertypen, Rückgabetyp) zwischenrust und C/C++ exakt übereinstimmen. - Daten-Typen behandeln: Primitive Typen (Ganzzahlen, Gleitkommazahlen) werden im Allgemeinen direkt zugeordnet. Komplexere Typen wie Zeichenketten, Arrays oder benutzerdefinierte Strukturen erfordern eine sorgfältige Behandlung, um eine konsistente Speicherlayout und Besitz semantik zu gewährleisten. Rusts
std::ffi
-Modul bietet nützliche Typen wieCStr
undCString
für die sichere C-Zeichenkettenbearbeitung. Zeiger werden typischerweise Rusts rohen Zeigern (*const T
,*mut T
) zugeordnet.
Praktisches Beispiel: C von Rust aufrufen
Lassen Sie uns dies anhand eines einfachen Beispiels veranschaulichen. Angenommen, wir haben eine C-Bibliothek, die eine grundlegende arithmetische Operation durchführt.
C-Code (my_math.h
und my_math.c
)
Zuerst definiert unsere C-Headerdatei die Funktionssignatur:
// my_math.h #ifndef MY_MATH_H #define MY_MATH_H // Eine einfache Funktion zum Addieren zweier Ganzzahlen int add_integers(int a, int b); #endif // MY_MATH_H
Und die Implementierung:
// my_math.c #include "my_math.h" #include <stdio.h> int add_integers(int a, int b) { printf("C function: Adding %d and %d\n", a, b); return a + b; }
Um dies in eine statische Bibliothek (.a
unter Linux/macOS, .lib
unter Windows) zu kompilieren, können Sie gcc
verwenden:
gcc -c my_math.c -o my_math.o ar rcs libmy_math.a my_math.o
Dadurch wird libmy_math.a
erstellt.
Rust-Code (src/main.rs
)
Nun deklarieren wir in unserem Rust-Projekt die C-Funktion und rufen sie auf:
// src/main.rs // Dieser Block teilt Rust mit, dass die Funktion 'add_integers' // extern in einer C-kompatiblen Bibliothek definiert ist. // 'link_name' gibt den Namen der Bibliotheksdatei an (ohne 'lib'-Präfix und '.a/.so/.dll'-Suffix) // 'link' gibt an, ob es sich um einen statischen oder dynamischen Link handelt (Standard ist statisch, wenn für einen gängigen Fall nicht angegeben) #[link(name = "my_math", kind = "static")] // Linkt gegen libmy_math.a extern "C" { // Deklarieren Sie die C-Funktionssignatur. // Stellen Sie sicher, dass die Typen exakt mit C's int (i32 in Rust) übereinstimmen. fn add_integers(a: i32, b: i32) -> i32; } fn main() { let x = 10; let y = 20; // FFI-Aufrufe sind durchweg unsicher, da Rust die Sicherheit des in einer Fremdsprache ausgeführten Codes nicht gewährleisten kann. // Es liegt am Programmierer, sicherzustellen, dass der C-Funktionsaufruf sicher ist. let sum = unsafe { add_integers(x, y) }; println!("Rust: The sum from C is: {}", sum); }
Um diesen Rust-Code zu kompilieren und auszuführen, müssen Sie rustc
mitteilen, wo die C-Bibliothek zu finden ist. Sie können dies oft tun, indem Sie libmy_math.a
im Stammverzeichnis Ihres Rust-Projekts platzieren oder den Pfad mit Build-Skripten angeben. Ein gängiger Ansatz für einfache Fälle ist die Verwendung von cargo build
und dann das manuelle Verknüpfen. Wenn sich libmy_math.a
im selben Verzeichnis wie Ihre Cargo.toml
befindet, müssen Sie möglicherweise den Suchpfad angeben.
Ein build.rs
-Skript in Ihrem Rust-Projekt kann dies automatisieren:
// build.rs fn main() { println!("cargo:rustc-link-search=native=/path/to/your/lib"); // Diesen Pfad anpassen! println!("cargo:rustc-link-lib=static=my_math"); }
Alternativ können Sie, wenn sich libmy_math.a
im Projektstammverzeichnis befindet, es direkt beim Kompilieren verknüpfen:
# Kompilieren Sie den Rust-Code und verknüpfen Sie ihn mit der C-Bibliothek rustc src/main.rs -L . -l static=my_math -o rust_app # Führen Sie die Anwendung aus ./rust_app
Erwartete Ausgabe:
C function: Adding 10 and 20
Rust: The sum from C is: 30
C++-Code in Rust behandeln
Der direkte Aufruf von C++-Funktionen aus Rust ist aufgrund der Namensverunstaltung, virtueller Funktionen und Objektmodelle von C++ komplexer. Der Standardansatz besteht darin, eine C-kompatible API-Schicht (oft als „C-Wrapper“ bezeichnet) um Ihren C++-Code zu erstellen.
C++-Wrapper-Beispiel
Angenommen, Sie haben eine C++-Klasse:
// my_cpp_class.hpp #ifndef MY_CPP_CLASS_HPP #define MY_CPP_CLASS_HPP #include <string> class MyCppClass { public: MyCppClass(int value); void greet(const std::string& name); int get_value() const; private: int m_value; }; #endif // MY_CPP_CLASS_HPP
// my_cpp_class.cpp #include "my_cpp_class.hpp" #include <iostream> MyCppClass::MyCppClass(int value) : m_value(value) { std::cout << "C++: MyCppClass constructed with value: " << m_value << std::endl; } void MyCppClass::greet(const std::string& name) { std::cout << "C++: Hello, " << name << "! My internal value is " << m_value << std::endl; } int MyCppClass::get_value() const { return m_value; }
Um dies von Rust aufrufbar zu machen, erstellen Sie einen extern "C"
-Wrapper:
// my_cpp_class_wrapper.cpp #include "my_cpp_class.hpp" #include <cstring> // Für strlen, strcpy #include <string> // Für std::string extern "C" { // Opaque Zeiger für die MyCppClass-Instanz // Dies verbirgt die C++-Klassen-Details vor Rust typedef void MyCppClassOpaque; // Konstruktor-Wrapper MyCppClassOpaque* my_cpp_class_new(int value) { return reinterpret_cast<MyCppClassOpaque*>(new MyCppClass(value)); } // Methoden-Wrapper: greet void my_cpp_class_greet(MyCppClassOpaque* ptr, const char* name_ptr) { MyCppClass* instance = reinterpret_cast<MyCppClass*>(ptr); if (instance && name_ptr) { instance->greet(std::string(name_ptr)); } } // Methoden-Wrapper: get_value int my_cpp_class_get_value(MyCppClassOpaque* ptr) { MyCppClass* instance = reinterpret_cast<MyCppClass*>(ptr); if (instance) { return instance->get_value(); } return -1; // Oder Fehler entsprechend behandeln } // Destruktor-Wrapper void my_cpp_class_free(MyCppClassOpaque* ptr) { MyCppClass* instance = reinterpret_cast<MyCppClass*>(ptr); delete instance; } } // extern "C"
Kompilieren Sie den C++-Code und den Wrapper zu einer Bibliothek:
g++ -c my_cpp_class.cpp my_cpp_class_wrapper.cpp -o my_cpp_class.o -o my_cpp_class_wrapper.o ar rcs libmy_cpp_class.a my_cpp_class.o my_cpp_class_wrapper.o
Rust-Code für C++-Wrapper
// src/main.rs use std::ffi::{CStr, CString}; use std::os::raw::c_char; // Deklarieren Sie die C-kompatiblen Funktionen aus unserem C++-Wrapper #[link(name = "my_cpp_class", kind = "static")] extern "C" { // Ein opaker Zeigertyp für die C++-Klasseninstanz type MyCppClassOpaque; fn my_cpp_class_new(value: i32) -> *mut MyCppClassOpaque; fn my_cpp_class_greet(ptr: *mut MyCppClassOpaque, name_ptr: *const c_char); fn my_cpp_class_get_value(ptr: *mut MyCppClassOpaque) -> i32; fn my_cpp_class_free(ptr: *mut MyCppClassOpaque); } fn main() { let initial_value = 42; let instance_ptr = unsafe { // Erstellen Sie das C++-Objekt my_cpp_class_new(initial_value) }; if instance_ptr.is_null() { eprintln!("Failed to create C++ object."); return; } let rust_name = "Rustacean"; let c_name = CString::new(rust_name).expect("CString conversion failed"); unsafe { // Rufen Sie eine Methode auf dem C++-Objekt auf my_cpp_class_greet(instance_ptr, c_name.as_ptr()); // Holen Sie einen Wert aus dem C++-Objekt let value = my_cpp_class_get_value(instance_ptr); println!("Rust: Retrieved value from C++ object: {}", value); // Geben Sie das C++-Objekt frei my_cpp_class_free(instance_ptr); } }
Kompilieren und führen Sie dies ähnlich aus und stellen Sie sicher, dass libmy_cpp_class.a
verknüpft ist.
Sicherheitsaspekte
Das unsafe
-Schlüsselwort in Rust dient als leistungsstarke Erinnerung an die Verantwortlichkeiten, die mit FFI einhergehen. Beim Aufrufen von unsafe
-Code müssen Sie manuell die Sicherheitsinvarianten von Rust einhalten:
- Speichersicherheit: Stellen Sie sicher, dass an C/C++ übergebene Zeiger gültig sind und dass Speicher, der von C/C++ zugewiesen wurde (falls vorhanden), ordnungsgemäß freigegeben wird, um Lecks oder Use-after-free-Fehler zu vermeiden.
- Datenkonsistenz: Stellen Sie sicher, dass zwischen Rust und C/C++ gemeinsam genutzte Datenstrukturen kompatible Layouts haben.
- Thread-Sicherheit: Wenn C/C++-Code nicht threadsicher ist, stellen Sie sicher, dass er nicht ohne ordnungsgemäße Synchronisierung von mehreren Rust-Threads gleichzeitig aufgerufen wird.
- Fehlerbehandlung: C/C++-Funktionen verwenden oft Rückgabecodes,
errno
oder Ausnahmen zur Fehlerberichterstattung. Ordnen Sie diese demResult
-Typ von Rust oder entsprechenden Fehlerbehandlungsmechanismen zu. - Null-Zeiger: Behandeln Sie Null-Zeiger, die von C/C++-Funktionen zurückgegeben werden, sicher. Rusts
Option<T>
kann verwendet werden, um rohe Zeiger zu umschließen, um Nullprüfungen bereitzustellen.
Tools wie bindgen
können die Erstellung von Rust FFI-Bindings aus C-Headerdateien automatisieren und so Boilerplate-Code und potenzielle Fehler reduzieren. Für C++-Projekte bietet autocxx
eine fortschrittlichere Lösung zur automatischen Generierung von Rust-Bindings und C++-Brücken.
Anwendungsfälle
Rust FFI mit C/C++ ist in mehreren Szenarien von unschätzbarem Wert:
- Interaktion mit OS-APIs: Die meisten Betriebssystemfunktionen werden über C-APIs exponiert (z. B. Win32 API, POSIX-Funktionen).
- Nutzung von Hochleistungsbibliotheken: Wissenschaftliches Rechnen, Grafik, maschinelles Lernen und Kryptographie basieren oft auf hochoptimierten C/C++-Bibliotheken (z. B. BLAS, LAPACK, OpenCV, TensorFlow, OpenSSL).
- Portierung von Altsystemen: Schrittweise Migration von Teilen einer Altsystem-C/C++-Codebasis nach Rust, wodurch neue Rust-Komponenten mit bestehender C/C++-Logik interagieren können.
- Entwicklung eingebetteter Systeme: Interaktion mit Low-Level-Hardwaretreibern, die in C geschrieben sind.
- Leistungskritische Abschnitte: Umschreiben von Leistungsengpässen in C/C++, während der Großteil der Anwendung in Rust aus Gründen der Sicherheit und Wartbarkeit beibehalten wird.
Fazit
Rusts FFI-Funktionen bieten eine robuste und überraschend sichere Möglichkeit, die Lücke zwischen Ihren Rust-Anwendungen und dem riesigen Ökosystem von C/C++-Code zu schließen. Obwohl sorgfältige Aufmerksamkeit für Details, insbesondere in Bezug auf Speicherverwaltung und Datenrepräsentation, erforderlich ist, sind die Vorteile der Nutzung bestehender, hochentwickelter C/C++-Bibliotheken immens. Durch die strategische Verwendung von extern "C"
-Wrappern und unsafe
-Blöcken in Verbindung mit sorgfältiger Typzuordnung können Rust-Entwickler leistungsstarke Anwendungen erstellen, die Rusts moderne Sicherheitsgarantien mit der unübertroffenen Leistung und etablierten Funktionalität von C/C++-Codebasen kombinieren und so die Reichweite und Nützlichkeit von Rust in neue und aufregende Domänen erweitern.