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 Typint
verwendet. Das bindgen-Dienstprogramm kann mit dem Typtime_t
umgehen, erzeugt dabei jedoch einige störende Warnungen, datime_t
nicht den Rust-Namenskonventionen folgt: intime_t
ein Unterstrich trennt dast
am Ende von dertime
, die zuerst kommt; Rust würde einen CamelCase-Namen wieTimeT
bevorzugen.Der Typ
struct tm
erhält aus dem gleichen GrundStructTM
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:
Der Aufruf
Cstr::from_ptr(char_ptr)
wandelt den C-String in einen Rust-String um und gibt eine in der Variablenc_str
gespeicherte Referenz zurück.Der Aufruf von
c_str.to_str()
stellt sicher, dassc_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.