Website-Suche

Einführung in Rust-Aufrufe für C-Bibliotheksfunktionen


Das Rust FFI und das Dienstprogramm bindgen sind gut dafür konzipiert, Rust-Aufrufe an C-Bibliotheken durchzuführen. Rust kommuniziert problemlos mit C und damit auch mit jeder anderen Sprache, die mit C kommuniziert.

Warum C-Funktionen von Rust aus aufrufen? Die kurze Antwort lautet: Softwarebibliotheken. Eine längere Antwort befasst sich mit der Position von C unter den Programmiersprachen im Allgemeinen und gegenüber Rust im Besonderen. C, C++ und Rust sind Systemsprachen, die Programmierern Zugriff auf Datentypen und Operationen auf Maschinenebene ermöglichen. Unter diesen drei Systemsprachen bleibt C die dominierende Sprache. Die Kernel moderner Betriebssysteme sind hauptsächlich in C geschrieben, der Rest entfällt auf Assembler. Die Standardsystembibliotheken für Ein- und Ausgabe, Zahlenverarbeitung, Kryptografie, Sicherheit, Netzwerk, Internationalisierung, String-Verarbeitung, Speicherverwaltung und mehr sind ebenfalls größtenteils in C geschrieben. Diese Bibliotheken stellen eine umfangreiche Infrastruktur für Anwendungen dar, die in jeder anderen Sprache geschrieben sind. Rust ist auf dem besten Weg, eigene hervorragende Bibliotheken bereitzustellen, aber C-Bibliotheken, die es schon seit den 1970er Jahren gibt und die immer noch wachsen, sind eine Ressource, die man nicht ignorieren sollte. Schließlich ist C immer noch die Verkehrssprache unter den Programmiersprachen: Die meisten Sprachen können mit C und über C mit jeder anderen Sprache kommunizieren, die dies kann.

Zwei Proof-of-Concept-Beispiele

Rust verfügt über ein FFI (Foreign Function Interface), das Aufrufe von C-Funktionen unterstützt. Ein Problem für jedes FFI besteht darin, ob die aufrufende Sprache die Datentypen in der aufgerufenen Sprache abdeckt. Beispielsweise ist ctypes ein FFI für Aufrufe von Python in C, aber Python deckt die in C verfügbaren vorzeichenlosen Ganzzahltypen nicht ab. Daher muss auf ctypes zurückgegriffen werden Problemumgehungen.

Im Gegensatz dazu deckt Rust alle primitiven (d. h. auf Maschinenebene) Typen in C ab. Beispielsweise stimmt der Rust-Typ i32 mit dem C-Typ int überein. C gibt lediglich an, dass der Typ char ein Byte groß sein muss und andere Typen, wie z. B. int, mindestens diese Größe haben müssen; aber heutzutage unterstützt jeder vernünftige C-Compiler ein vier Byte langes int, ein acht Byte langes double (in Rust der Typ f64) und so weiter An.

Es gibt eine weitere Herausforderung für ein an C gerichtetes FFI: Kann das FFI mit den Rohzeigern von C umgehen, einschließlich Zeigern auf Arrays, die in C als Zeichenfolgen gelten? C hat keinen String-Typ, sondern implementiert Strings als Zeichenarrays mit einem nicht druckbaren Abschlusszeichen, dem Null-Terminator von legend. Im Gegensatz dazu gibt es in Rust zwei String-Typen: String und &str (String-Slice). Die Frage ist also, ob das Rust-FFI einen C-String in einen Rust-String umwandeln kann – und die Antwort lautet ja.

Auch in C sind Zeiger auf Strukturen üblich. Der Grund dafür ist die Effizienz. Standardmäßig wird eine C-Struktur nach Wert übergeben (d. h. durch eine byteweise Kopie), wenn eine Struktur entweder ein an eine Funktion übergebenes Argument oder ein von einer Funktion zurückgegebener Wert ist. C-Strukturen können wie ihre Rust-Gegenstücke Arrays enthalten und andere Strukturen verschachteln und daher beliebig groß sein. Die beste Vorgehensweise in beiden Sprachen besteht darin, Strukturen per Referenz zu übergeben und zurückzugeben, d. h. durch Übergabe oder Rückgabe der Adresse der Struktur und nicht einer Kopie der Struktur. Auch hier ist das Rust FFI der Aufgabe gewachsen, C-Zeiger auf Strukturen zu verarbeiten, die in C-Bibliotheken üblich sind.

Das erste Codebeispiel konzentriert sich auf Aufrufe relativ einfacher C-Bibliotheksfunktionen wie abs (absoluter Wert) und sqrt (Quadratwurzel). Diese Funktionen nehmen skalare Argumente ohne Zeiger entgegen und geben einen skalaren Wert ohne Zeiger zurück. Das zweite Codebeispiel, das Strings und Zeiger auf Strukturen behandelt, stellt das Dienstprogramm bindgen vor, das Rust-Code aus C-Schnittstellendateien (Header) wie math.h und time.h generiert. . C-Header-Dateien geben die Aufrufsyntax für C-Funktionen an und definieren Strukturen, die bei solchen Aufrufen verwendet werden. Die beiden Codebeispiele sind auf meiner Homepage verfügbar.

Aufruf relativ einfacher C-Funktionen

Das erste Codebeispiel enthält vier Rust-Aufrufe von C-Funktionen in der Standard-Mathematikbibliothek: jeweils einen Aufruf von abs (absoluter Wert) und pow (Potenzierung) sowie zwei Aufrufe von sqrt (Quadratwurzel). Das Programm kann direkt mit dem Compiler rustc oder bequemer mit dem Befehl cargo build erstellt werden:

use std::os::raw::c_int;    // 32 bits
use std::os::raw::c_double; // 64 bits

// Import three functions from the standard library libc.
// Here are the Rust declarations for the C functions:
extern "C" {
    fn abs(num: c_int) -> c_int;
    fn sqrt(num: c_double) -> c_double;
    fn pow(num: c_double, power: c_double) -> c_double;
}

fn main() {
    let x: i32 = -123;
    println!("\nAbsolute value of {x}: {}.",
	     unsafe { abs(x) });

    let n: f64 = 9.0;
    let p: f64 = 3.0;
    println!("\n{n} raised to {p}: {}.",
	     unsafe { pow(n, p) });

    let mut y: f64 = 64.0;
    println!("\nSquare root of {y}: {}.",
	     unsafe { sqrt(y) });
    y = -3.14;
    println!("\nSquare root of {y}: {}.",
	     unsafe { sqrt(y) }); //** NaN = NotaNumber
}

Die beiden use-Deklarationen oben gelten für die Rust-Datentypen c_int und c_double, die mit den C-Typen int übereinstimmen bzw. double. Das Standard-Rust-Modul std::os::raw definiert vierzehn solcher Typen für C-Kompatibilität. Das Modul std::ffi verfügt über dieselben vierzehn Typdefinitionen sowie Unterstützung für Zeichenfolgen.

Der extern "C"-Block oberhalb der Funktion main deklariert dann die drei C-Bibliotheksfunktionen, die in der Funktion main unten aufgerufen werden. Jeder Aufruf verwendet den Namen der Standard-C-Funktion, aber jeder Aufruf muss innerhalb eines unsafe-Blocks erfolgen. Wie jeder Programmierer, der neu bei Rust ist, feststellt, setzt der Rust-Compiler die Speichersicherheit mit aller Macht durch. Andere Sprachen (insbesondere C und C++) bieten nicht die gleichen Garantien. Der unsafe-Block besagt also: Rust übernimmt keine Verantwortung für unsichere Vorgänge, die im externen Aufruf auftreten könnten.

Die Ausgabe des ersten Programms ist:

Absolute value of -123: 123.
9 raised to 3: 729
Square root of 64: 8.
Square root of -3.14: NaN.

In der letzten Ausgabezeile steht NaN für Not a Number: Die C-Bibliotheksfunktion sqrt erwartet einen nicht negativen Wert als Argument, was bedeutet, dass das Argument -3,14 ist generiert NaN als zurückgegebenen Wert.

Aufrufen von C-Funktionen mit Zeigern

C-Bibliotheksfunktionen in den Bereichen Sicherheit, Netzwerk, String-Verarbeitung, Speicherverwaltung und anderen Bereichen verwenden aus Effizienzgründen regelmäßig Zeiger. Beispielsweise erwartet die Bibliotheksfunktion asctime (Zeit als ASCII-String) als einzelnes Argument einen Zeiger auf eine Struktur. Ein Rust-Aufruf einer C-Funktion wie asctime ist daher schwieriger als ein Aufruf von sqrt, der weder Zeiger noch Strukturen beinhaltet.

Die C-Struktur für den Funktionsaufruf asctime ist vom Typ struct tm. Ein Zeiger auf eine solche Struktur wird auch an die Bibliotheksfunktion mktime (Erstellen eines Zeitwerts) übergeben. Die Struktur unterteilt eine Zeit in Einheiten wie Jahr, Monat, Stunde usw. Die Felder der Struktur sind vom Typ time_t, einem Alias für entweder int (32 Bit) oder long (64 Bit). Die beiden Bibliotheksfunktionen kombinieren diese zerlegten Zeiteinheiten zu einem einzigen Wert: asctime gibt eine Zeichenfolgendarstellung der Zeit zurück, während mktime einen time_t zurückgibt Wert, der die Anzahl der seit der Epoche verstrichenen Sekunden darstellt. Hierbei handelt es sich um eine Zeit, relativ zu der die Uhr und der Zeitstempel eines Systems bestimmt werden. Typische Epocheneinstellungen sind der 1. Januar 00:00:00 (null Stunden, Minuten und Sekunden) von 1900 oder 1970.

Das folgende C-Programm ruft asctime und mktime auf und verwendet eine andere Bibliotheksfunktion strftime, um den von mktime zurückgegebenen Wert in umzuwandeln eine formatierte Zeichenfolge. Dieses Programm dient als Aufwärmprogramm für die Rust-Version:

#include <stdio.h>
#include <time.h>

int main () {
  struct tm sometime;  /* time broken out in detail */
  char buffer[80];
  int utc;

  sometime.tm_sec = 1;
  sometime.tm_min = 1;
  sometime.tm_hour = 1;
  sometime.tm_mday = 1;
  sometime.tm_mon = 1;
  sometime.tm_year = 1;
  sometime.tm_hour = 1;
  sometime.tm_wday = 1;
  sometime.tm_yday = 1;

  printf("Date and time: %s\n", asctime(&sometime));

  utc = mktime(&sometime);
  if( utc < 0 ) {
    fprintf(stderr, "Error: unable to make time using mktime\n");
  } else {
    printf("The integer value returned: %d\n", utc);
    strftime(buffer, sizeof(buffer), "%c", &sometime);
    printf("A more readable version: %s\n", buffer);
  }

  return 0;
}

Das Programm gibt aus:

Date and time: Fri Feb  1 01:01:01 1901
The integer value returned: 2120218157
A more readable version: Fri Feb  1 01:01:01 1901

Zusammenfassend müssen sich die Rust-Aufrufe der Bibliotheksfunktionen asctime und mktime mit zwei Problemen befassen:

  • Übergabe eines Rohzeigers als einzelnes Argument an jede Bibliotheksfunktion.

  • Konvertieren des von asctime zurückgegebenen C-Strings in einen Rust-String.

Rust-Aufrufe an asctime und mktime

Das Dienstprogramm bindgen generiert Rust-Unterstützungscode aus C-Header-Dateien wie math.h und time.h. In diesem Beispiel reicht eine vereinfachte Version von time.h aus, jedoch mit zwei Änderungen gegenüber dem Original:

  • Anstelle des Alias-Typs time_t wird der integrierte Typ int verwendet. Das bindgen-Dienstprogramm kann mit dem Typ time_t umgehen, erzeugt dabei jedoch einige störende Warnungen, da time_t nicht den Rust-Namenskonventionen folgt: in time_t ein Unterstrich trennt das t am Ende von der time, die zuerst kommt; Rust würde einen CamelCase-Namen wie TimeT bevorzugen.

  • Der Typ struct tm erhält aus dem gleichen Grund StructTM als Alias.

Hier ist die vereinfachte Header-Datei mit Deklarationen für mktime und asctime unten:

typedef struct tm {
    int tm_sec;    /* seconds */
    int tm_min;    /* minutes */
    int tm_hour;   /* hours */
    int tm_mday;   /* day of the month */
    int tm_mon;    /* month */
    int tm_year;   /* year */
    int tm_wday;   /* day of the week */
    int tm_yday;   /* day in the year */
    int tm_isdst;  /* daylight saving time */
} StructTM;

extern int mktime(StructTM*);
extern char* asctime(StructTM*);

Mit installiertem bindgen, % als Befehlszeilenaufforderung und mytime.h als Header-Datei oben generiert der folgende Befehl das erforderliche Rust code und speichert ihn in der Datei mytime.rs:

% bindgen mytime.h > mytime.rs

Hier ist der relevante Teil von mytime.rs:

/* automatically generated by rust-bindgen 0.61.0 */

#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct tm {
    pub tm_sec: ::std::os::raw::c_int,
    pub tm_min: ::std::os::raw::c_int,
    pub tm_hour: ::std::os::raw::c_int,
    pub tm_mday: ::std::os::raw::c_int,
    pub tm_mon: ::std::os::raw::c_int,
    pub tm_year: ::std::os::raw::c_int,
    pub tm_wday: ::std::os::raw::c_int,
    pub tm_yday: ::std::os::raw::c_int,
    pub tm_isdst: ::std::os::raw::c_int,
}

pub type StructTM = tm;

extern "C" {
    pub fn mktime(arg1: *mut StructTM) -> ::std::os::raw::c_int;
}

extern "C" {
    pub fn asctime(arg1: *mut StructTM) -> *mut ::std::os::raw::c_char;
}

#[test]
fn bindgen_test_layout_tm() {
    const UNINIT: ::std::mem::MaybeUninit<tm> =
       ::std::mem::MaybeUninit::uninit();
    let ptr = UNINIT.as_ptr();
    assert_eq!(
        ::std::mem::size_of::<tm>(),
        36usize,
        concat!("Size of: ", stringify!(tm))
    );
    ...

Die Rust-Struktur struct tm enthält wie das C-Original neun 4-Byte-Integer-Felder. Die Feldnamen sind in C und Rust gleich. Die extern "C"-Blöcke deklarieren die Bibliotheksfunktionen asctime und mktime als jeweils ein Argument, einen Rohzeiger auf ein veränderliches StructTM Instanz. (Die Bibliotheksfunktionen können die Struktur über den als Argument übergebenen Zeiger verändern.)

Der verbleibende Code testet unter dem Attribut #[test] das Layout der Rust-Version der Zeitstruktur. Der Test kann mit dem Befehl cargo test ausgeführt werden. Das Problem besteht darin, dass C nicht angibt, wie der Compiler die Felder einer Struktur anordnen muss. Beispielsweise beginnt das C-struct tm mit dem Feld tm_sec für das zweite; C erfordert jedoch nicht, dass die kompilierte Version dieses Feld als erstes enthält. In jedem Fall sollten die Rust-Tests erfolgreich sein und die Rust-Aufrufe an die Bibliotheksfunktionen sollten wie erwartet funktionieren.

Das zweite Beispiel zum Laufen bringen

Der von bindgen generierte Code enthält keine main-Funktion und ist daher ein natürliches Modul. Nachfolgend finden Sie die Funktion main mit der Initialisierung StructTM und den Aufrufen von asctime und mktime:

mod mytime;
use mytime::*;
use std::ffi::CStr;

fn main() {
    let mut sometime  = StructTM {
        tm_year: 1,
        tm_mon: 1,
        tm_mday: 1,
        tm_hour: 1,
        tm_min: 1,
        tm_sec: 1,
        tm_isdst: -1,
        tm_wday: 1,
        tm_yday: 1
    };

    unsafe {
        let c_ptr = &mut sometime; // raw pointer

        // make the call, convert and then own
        // the returned C string
        let char_ptr = asctime(c_ptr);
        let c_str = CStr::from_ptr(char_ptr);
        println!("{:#?}", c_str.to_str());

        let utc = mktime(c_ptr);
        println!("{}", utc);
    }
}

Der Rust-Code kann kompiliert werden (entweder direkt mit rustc oder mit cargo) und dann ausgeführt werden. Die Ausgabe ist:

Ok(
    "Mon Feb  1 01:01:01 1901\n",
)
2120218157

Die Aufrufe der C-Funktionen asctime und mktime müssen wiederum innerhalb eines unsafe-Blocks erfolgen, da der Rust-Compiler nicht für speicherbedingte Speicherverluste verantwortlich gemacht werden kann. Sicherheitsmängel bei diesen externen Funktionen. Fürs Protokoll: asctime und mktime verhalten sich gut. In den Aufrufen beider Funktionen ist das Argument der Rohzeiger ptr, der die (Stapel-)Adresse der sometime-Struktur enthält.

Der Aufruf von asctime ist der schwierigere der beiden Aufrufe, da diese Funktion einen Zeiger auf ein C-Zeichen char zurückgibt, das Zeichen M in Mon der Textausgabe. Dennoch weiß der Rust-Compiler nicht, wo der C-String (das nullterminierte Array von char) gespeichert ist. Im statischen Bereich der Erinnerung? Auf dem Haufen? Das von der Funktion asctime zum Speichern der Textdarstellung der Zeit verwendete Array befindet sich tatsächlich im statischen Bereich des Speichers. In jedem Fall erfolgt die C-zu-Rust-String-Konvertierung in zwei Schritten, um Fehler bei der Kompilierung zu vermeiden:

  1. Der Aufruf Cstr::from_ptr(char_ptr) wandelt den C-String in einen Rust-String um und gibt eine in der Variablen c_str gespeicherte Referenz zurück.

  2. Der Aufruf von c_str.to_str() stellt sicher, dass c_str der Eigentümer ist.

Der Rust-Code generiert keine für Menschen lesbare Version des von mktime zurückgegebenen Ganzzahlwerts, was dem Interessierten als Übung überlassen bleibt. Das Rust-Modul chrono::format enthält eine strftime-Funktion, die wie die gleichnamige C-Funktion verwendet werden kann, um eine Textdarstellung der Zeit zu erhalten.

Aufruf von C mit FFI und Bindung

Das Rust FFI und das Dienstprogramm bindgen sind gut dafür konzipiert, Rust-Aufrufe an C-Bibliotheken durchzuführen, egal ob Standard- oder Drittanbieter-Bibliotheken. Rust kommuniziert problemlos mit C und damit mit jeder anderen Sprache, die mit C kommuniziert. Für den Aufruf relativ einfacher Bibliotheksfunktionen wie sqrt ist das Rust-FFI unkompliziert, da die primitiven Datentypen von Rust ihre C-Gegenstücke abdecken.

Für kompliziertere Austausche – insbesondere Rust-Aufrufe von C-Bibliotheksfunktionen wie asctime und mktime, die Strukturen und Zeiger beinhalten – das bindgen Nutzen ist der richtige Weg. Dieses Dienstprogramm generiert den Supportcode zusammen mit entsprechenden Tests. Natürlich kann der Rust-Compiler nicht davon ausgehen, dass C-Code in puncto Speichersicherheit den Rust-Standards entspricht; Daher müssen Aufrufe von Rust an C in unsicheren Blöcken erfolgen.

Verwandte Artikel: