Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ein bisschen C zu Ihrem Rust

Die Verwendung von C oder C++ innerhalb eines Rust-Projekts umfasst zwei wesentliche Aspekte:

  • Einkapselung der bereitgestellten C-API für die Verwendung mit Rust
  • Erstellen Ihres C- oder C++-Codes zur Integration mit dem Rust-Code

Da C++ über keine stabile ABI verfügt, auf die der Rust-Compiler abzielen könnte, wird empfohlen, bei der Kombination von Rust mit C oder C++ die C-ABI zu verwenden.

Definition der Schnittstelle

Bevor C- oder C++-Code aus Rust heraus verwendet werden kann, muss (in Rust) definiert werden, welche Datentypen und Funktionssignaturen im verlinkten Code vorhanden sind. In C oder C++ würde man eine Header-Datei (.h oder .hpp) einbinden, die diese Daten definiert. In Rust ist es erforderlich, diese Definitionen entweder manuell nach Rust zu übertragen oder ein Werkzeug zu ihrer Generierung zu verwenden.

Zunächst behandeln wir die manuelle Übertragung dieser Definitionen von C/C++ nach Rust.

Einbinden von C-Funktionen und -Datentypen

Typischerweise stellen in C oder C++ geschriebene Bibliotheken eine Header-Datei bereit, die alle in öffentlichen Schnittstellen verwendeten Typen und Funktionen definiert. Eine Beispieldatei könnte wie folgt aussehen:

/* File: cool.h */
typedef struct CoolStruct {
    int x;
    int y;
} CoolStruct;

void cool_function(int i, char c, CoolStruct* cs);

Nach Rust übertragen, sähe diese Schnittstelle folgendermaßen aus:

/* File: cool_bindings.rs */
#[repr(C)]
pub struct CoolStruct {
    pub x: cty::c_int,
    pub y: cty::c_int,
}

extern "C" {
    pub fn cool_function(
        i: cty::c_int,
        c: cty::c_char,
        cs: *mut CoolStruct
    );
}

Schauen wir uns diese Definition Schritt für Schritt an, um die einzelnen Bestandteile zu erläutern.

#[repr(C)]
pub struct CoolStruct { ... }

Standardmäßig garantiert Rust weder die Reihenfolge noch das Padding oder die Größe der in einer struct enthaltenen Daten. Um die Kompatibilität mit C-Code zu gewährleisten, verwenden wir das Attribut #[repr(C)]; dieses weist den Rust-Compiler an, für die Anordnung der Daten innerhalb der Struktur stets dieselben Regeln wie C anzuwenden.

pub x: cty::c_int,
pub y: cty::c_int,

Aufgrund der Flexibilität, wie C oder C++ ein „int“ oder „char” definiert, wird empfohlen, in „cty“ definierte primitive Datentypen zu verwenden, die Typen von C auf Typen in Rust abbilden.

extern "C" { pub fn cool_function( ... ); }

Diese Anweisung definiert die Signatur einer Funktion namens cool_function, die die C-ABI verwendet. Da die Signatur ohne den Funktionsrumpf definiert wird, muss die eigentliche Funktionsdefinition an anderer Stelle bereitgestellt oder aus einer statischen Bibliothek in die endgültige Bibliothek bzw. das fertige Binärprogramm eingebunden werden.

    i: cty::c_int,
    c: cty::c_char,
    cs: *mut CoolStruct

Ähnlich wie bei unserem obigen Datentyp definieren wir die Datentypen der Funktionsargumente mithilfe von C-kompatiblen Definitionen. Der Übersichtlichkeit halber behalten wir zudem die ursprünglichen Argumentnamen bei.

Hier begegnet uns ein neuer Typ: *mut CoolStruct. Da C das Konzept der Rust-Referenzen (die etwa so aussehen: &mut CoolStruct) nicht kennt, verwenden wir stattdessen einen sogenannten „Raw Pointer“ (Rohzeiger). Da das Dereferenzieren dieses Zeigers als unsafe gilt und es sich tatsächlich um einen null-Zeiger handeln kann, ist bei der Interaktion mit C- oder C++-Code besondere Sorgfalt geboten, um die für Rust typischen Garantien zu wahren.

Automatische Generierung der Schnittstelle

Anstatt diese Schnittstellen manuell zu erstellen – was mühsam und fehleranfällig sein kann –, gibt es ein Werkzeug namens bindgen, das diese Konvertierungen automatisch durchführt. Hinweise zur Verwendung von bindgen finden Sie im bindgen-Benutzerhandbuch; der typische Ablauf sieht jedoch folgendermaßen aus:

  1. Sammeln Sie alle C- oder C++-Header, die Schnittstellen oder Datentypen definieren, die Sie mit Rust verwenden möchten.
  2. Erstelle eine Datei namens bindings.h, die mittels #include "..." jede der Dateien einbindet, die du in Schritt eins zusammengetragen hast.
  3. Übergeben Sie diese bindings.h-Datei zusammen mit den für die Kompilierung Ihres Codes verwendeten Flags an bindgen. Tipp: Verwenden Sie Builder.ctypes_prefix("cty") / --ctypes-prefix=cty und Builder.use_core() / --use-core, um den generierten Code #![no_std]-kompatibel zu machen.
  4. bindgen gibt den generierten Rust-Code direkt im Terminal aus. Diese Ausgabe lässt sich in eine Datei Ihres Projekts umleiten, beispielsweise bindings.rs. Sie können diese Datei in Ihrem Rust-Projekt verwenden, um mit C/C++-Code zu interagieren, der als externe Bibliothek kompiliert und gelinkt wurde. Tipp: Vergessen Sie nicht, das cty-Crate zu verwenden, falls die Typen in den generierten Bindings das Präfix cty aufweisen.

Erstellen Ihres C/C++-Codes

Da der Rust-Compiler nicht direkt weiß, wie man C- oder C++-Code (oder Code einer anderen Sprache mit C-Schnittstelle) kompiliert, muss der Nicht-Rust-Code vorab kompiliert werden.

Bei Embedded-Projekten bedeutet dies meist, dass der C/C++-Code zu einem statischen Archiv (z. B. cool-library.a) kompiliert wird, welches dann im abschließenden Link-Schritt mit dem Rust-Code zusammengeführt werden kann.

Wenn die gewünschte Bibliothek bereits als statisches Archiv vorliegt, ist eine erneute Kompilierung des Codes nicht erforderlich. Es genügt, die bereitgestellte Header-Datei für die Schnittstelle wie oben beschrieben umzuwandeln und das statische Archiv beim Kompilieren bzw. Linken einzubinden.

Liegt der Code als Quellcode-Projekt vor, muss der C/C++-Code in eine statische Bibliothek kompiliert werden. Dies kann entweder durch Aufruf des vorhandenen Build-Systems (z. B. make, CMake usw.) oder durch Portierung der erforderlichen Kompilierungsschritte auf das sogenannte cc-Crate erfolgen. Für beide Vorgehensweisen ist die Verwendung eines build.rs-Skripts erforderlich.

Rust-build.rs-Build-Skripte

Ein build.rs-Skript ist eine in Rust-Syntax verfasste Datei, die auf dem Kompilierrechner ausgeführt wird – und zwar nachdem die Abhängigkeiten Ihres Projekts erstellt wurden, aber bevor Ihr Projekt selbst kompiliert wird.

Die vollständige Referenz finden Sie hier. build.rs-Skripte eignen sich beispielsweise zur Code-Generierung (etwa mittels bindgen), zum Aufruf externer Build-Systeme wie Make oder zur direkten Kompilierung von C/C++-Code unter Verwendung des cc-Crates.

Auslösen externer Build-Systeme

Bei Projekten, die komplexe externe Projekte oder Build-Systeme einbinden, ist es oft am einfachsten, std::process::Command zu verwenden, um andere Build-Systeme aufzurufen (sogenanntes „Shelling-out“). Dabei navigieren Sie über relative Pfade, führen einen festen Befehl aus (wie etwa make library) und kopieren anschließend die erzeugte statische Bibliothek an den entsprechenden Ort im target-Build-Verzeichnis.

Auch wenn Ihre Crate für eine eingebettete Plattform ohne Standardbibliothek (no_std) gedacht ist, wird die build.rs ausschließlich auf dem Rechner ausgeführt, der die Crate kompiliert. Das bedeutet, dass Sie beliebige Rust-Crates verwenden können, die auf Ihrem Kompilier-Host lauffähig sind.

Kompilieren von C/C++-Code mit dem cc-Crate

Bei Projekten mit geringen Abhängigkeiten oder überschaubarer Komplexität – oder wenn es schwierig ist, das Build-System so anzupassen, dass eine statische Bibliothek (statt einer fertigen Binärdatei oder eines ausführbaren Programms) erzeugt wird – kann es einfacher sein, stattdessen das cc-Crate zu verwenden; dieses bietet eine idiomatische Rust-Schnittstelle zu dem vom Host bereitgestellten Compiler.

Im einfachsten Fall, bei dem eine einzelne C-Datei als Abhängigkeit für eine statische Bibliothek kompiliert wird, sähe ein Beispiel für ein build.rs-Skript, das das cc-Crate verwendet, folgendermaßen aus:

fn main() {
    cc::Build::new()
        .file("src/foo.c")
        .compile("foo");
}

Die Datei build.rs befindet sich im Wurzelverzeichnis des Pakets. cargo build kompiliert und führt sie dann vor dem eigentlichen Build-Vorgang des Pakets aus. Dabei wird ein statisches Archiv namens libfoo.a erstellt und im Verzeichnis target abgelegt.