Présentation des appels Rust aux fonctions de la bibliothèque C

Pourquoi appeler des fonctions C depuis Rust ? La réponse courte est les bibliothèques de logiciels. Une réponse plus longue concerne la position de C parmi les langages de programmation en général et vis-à-vis de Rust en particulier. C, C++ et Rust sont des langages système qui permettent aux programmeurs d’accéder aux types de données et aux opérations au niveau de la machine. Parmi ces trois langages systèmes, C reste le langage dominant. Les noyaux des systèmes d’exploitation modernes sont écrits principalement en C, le langage d’assemblage représentant le reste. Les bibliothèques système standard pour l’entrée et la sortie, le traitement des nombres, la cryptographie, la sécurité, la mise en réseau, l’internationalisation, le traitement des chaînes, la gestion de la mémoire, etc., sont également écrites principalement en C. Ces bibliothèques représentent une vaste infrastructure pour les applications écrites dans n’importe quel autre langage. Rust est sur la bonne voie pour fournir ses propres bibliothèques de qualité, mais les bibliothèques C, qui existent depuis les années 1970 et qui continuent de croître, sont une ressource à ne pas ignorer. Enfin, C est toujours le lingua franca parmi les langages de programmation : la plupart des langages peuvent parler à C et, via C, à tout autre langage qui le fait.
Contents
Deux exemples de preuve de concept
Rust a une FFI (Foreign Function Interface) qui prend en charge les appels aux fonctions C. Un problème pour toute FFI est de savoir si la langue d’appel couvre les types de données dans la langue appelée. Par exemple, ctypes
est un FFI pour les appels de Python vers C, mais Python ne couvre pas les types entiers non signés disponibles en C. Par conséquent, ctypes
doit recourir à des solutions de contournement.
En revanche, Rust couvre tous les types primitifs (c’est-à-dire au niveau de la machine) en C. Par exemple, le Rust i32
le type correspond au C int
taper. C précise seulement que le char
type doit avoir une taille d’un octet et d’autres types, tels que int
, doit être au moins de cette taille ; mais de nos jours, chaque compilateur C raisonnable prend en charge un quatre octets int
un huit octets double
(à Rust, le f64
types), etc.
Il existe un autre défi pour une FFI dirigée vers C : la FFI peut-elle gérer les pointeurs bruts de C, y compris les pointeurs vers des tableaux qui comptent comme des chaînes en C ? C n’a pas de type chaîne, mais implémente plutôt des chaînes sous forme de tableaux de caractères avec un caractère de fin non imprimable, le terminateur nul de légende. En revanche, Rust a deux types de chaînes : String
et &str
(tranche de corde). La question est donc de savoir si le FFI Rust peut transformer une chaîne C en une chaîne Rust – et la réponse est oui.
Les pointeurs vers des structures sont également courants en C. La raison en est l’efficacité. Par défaut, une structure C est passée par value (c’est-à-dire par une copie octet par octet) lorsqu’une structure est soit un argument passé à une fonction, soit une valeur renvoyée par une fonction. Les structures C, comme leurs homologues Rust, peuvent inclure des tableaux et imbriquer d’autres structures et ainsi avoir une taille arbitrairement grande. La meilleure pratique dans l’un ou l’autre langage consiste à transmettre et à renvoyer des structures par référence, c’est-à-dire en transmettant ou en renvoyant l’adresse de la structure plutôt qu’une copie de la structure. Une fois de plus, le Rust FFI est à la hauteur de la tâche de gérer les pointeurs C vers les structures, qui sont courantes dans les bibliothèques C.
Le premier exemple de code se concentre sur les appels à des fonctions de bibliothèque C relativement simples telles que abs
(valeur absolue) et sqrt
(racine carrée). Ces fonctions acceptent des arguments scalaires non pointeurs et renvoient une valeur scalaire non pointeur. Le deuxième exemple de code, qui couvre les chaînes et les pointeurs vers des structures, introduit le lier utilitaire, qui génère du code Rust à partir de fichiers d’interface C (en-tête) tels que math.h
et time.h
. Les fichiers d’en-tête C spécifient la syntaxe d’appel des fonctions C et définissent les structures utilisées dans de tels appels. Les deux exemples de code sont disponible sur ma page d’accueil.
Appel de fonctions C relativement simples
Le premier exemple de code a quatre appels Rust aux fonctions C dans la bibliothèque mathématique standard : un appel chacun à abs
(valeur absolue) et pow
(exponentiation), et deux appels à sqrt
(racine carrée). Le programme peut être construit directement avec le rustc
compilateur, ou plus commodément avec le cargo build
commande:
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
}
Les deux use
les déclarations en haut sont pour les types de données Rust c_int
et c_double
qui correspondent aux types C int
et double
, respectivement. Le module Rust standard std::os::raw
définit quatorze types de ce type pour la compatibilité C. Le module std::ffi
a les mêmes quatorze définitions de type ainsi que la prise en charge des chaînes.
La extern "C"
bloc au-dessus du main
fonction déclare alors les trois fonctions de la bibliothèque C appelées dans la main
fonction ci-dessous. Chaque appel utilise le nom de la fonction C standard, mais chaque appel doit se produire dans un unsafe
bloquer. Comme le découvre tout programmeur débutant dans Rust, le compilateur Rust applique la sécurité de la mémoire avec vengeance. Les autres langages (notamment C et C++) n’offrent pas les mêmes garanties. La unsafe
block dit donc : Rust n’assume aucune responsabilité pour les opérations dangereuses qui pourraient se produire dans l’appel externe.
La sortie du premier programme est :
Absolute value of -123: 123.
9 raised to 3: 729
Square root of 64: 8.
Square root of -3.14: NaN.
Dans la dernière ligne de sortie, le NaN
signifie Not a Number : le C sqrt
la fonction de bibliothèque attend une valeur non négative comme argument, ce qui signifie que l’argument -3.14 génère NaN
comme valeur renvoyée.
Appeler des fonctions C impliquant des pointeurs
Les fonctions de la bibliothèque C dans les domaines de la sécurité, de la mise en réseau, du traitement des chaînes, de la gestion de la mémoire et d’autres domaines utilisent régulièrement des pointeurs pour plus d’efficacité. Par exemple, la fonction bibliothèque asctime
(time en tant que chaîne ASCII) attend un pointeur vers une structure comme argument unique. Un appel Rust à une fonction C telle que asctime
est donc plus délicat qu’un appel à sqrt
qui n’implique ni pointeurs ni structures.
La structure C pour le asctime
l’appel de fonction est de type struct tm
. Un pointeur vers une telle structure est également passé à la fonction de bibliothèque mktime
(faire une valeur de temps). La structure décompose une heure en unités telles que l’année, le mois, l’heure, etc. Les champs de la structure sont de type time_t
un alias pour soit int
(32 bits) ou long
(64 bits). Les deux fonctions de la bibliothèque combinent ces pièces d’horlogerie séparées en une seule valeur : asctime
renvoie une représentation sous forme de chaîne de l’heure, alors que mktime
renvoie un time_t
valeur qui représente le nombre de secondes écoulées depuis le époque, qui est une heure par rapport à laquelle l’horloge et l’horodatage d’un système sont déterminés. Les paramètres d’époque typiques sont le 1er janvier 00:00:00 (zéro heure, minute et seconde) de 1900 ou 1970.
Le programme C ci-dessous appelle asctime
et mktime
et utilise une autre fonction de bibliothèque strftime
pour convertir le mktime
valeur renvoyée dans une chaîne formatée. Ce programme sert d’échauffement pour la version Rust :
#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;
}
Le programme affiche :
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
En résumé, les appels Rust aux fonctions de la bibliothèque asctime
et mktime
doit faire face à deux problèmes :
Rust appelle asctime
et mktime
La bindgen
L’utilitaire génère du code de support Rust à partir de fichiers d’en-tête C tels que math.h
et time.h
. Dans cet exemple, une version simplifiée de time.h
fera l’affaire mais avec deux changements par rapport à l’original :
-
Le type intégré
int
est utilisé à la place du type d’aliastime_t
. L’utilitaire bindgen peut gérer lestime_t
type mais génère des avertissements gênants en cours de route cartime_t
ne respecte pas les conventions de nommage de Rust : danstime_t
un trait de soulignement sépare let
à la fin de latime
qui vient en premier; Rust préférerait un nom CamelCase tel queTimeT
. -
Le type
struct tm
le type est donnéStructTM
comme alias pour la même raison.
Voici le fichier d’en-tête simplifié avec les déclarations pour mktime
et asctime
au fond:
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*);
Avec bindgen
installée, %
comme invite de ligne de commande, et mytime.h
comme fichier d’en-tête ci-dessus, la commande suivante génère le code Rust requis et l’enregistre dans le fichier mytime.rs
:
% bindgen mytime.h > mytime.rs
Voici la partie pertinente de 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))
);
...
La structure Rouille struct tm
, comme l’original C, contient neuf champs d’entiers de 4 octets. Les noms de champ sont les mêmes en C et Rust. La extern "C"
les blocs déclarent les fonctions de la bibliothèque asctime
et mktime
comme prenant un argument chacun, un pointeur brut vers un mutable StructTM
exemple. (Les fonctions de la bibliothèque peuvent muter la structure via le pointeur passé en argument.)
Le code restant, sous le #[test]
, teste la mise en page de la version Rust de la structure temporelle. Le test peut être exécuté avec le cargo test
commande. Le problème est que C ne spécifie pas comment le compilateur doit disposer les champs d’une structure. Par exemple, le C struct tm
commence par le terrain tm_sec
pour le second; mais C n’exige pas que la version compilée ait ce champ comme premier. Dans tous les cas, les tests Rust devraient réussir et les appels Rust aux fonctions de la bibliothèque devraient fonctionner comme prévu.
Obtenir le deuxième exemple opérationnel
Le code généré à partir de bindgen
ne comprend pas un main
fonction et, par conséquent, est un module naturel. Ci-dessous le main
fonction avec le StructTM
l’initialisation et les appels à asctime
et 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);
}
}
Le code Rust peut être compilé (en utilisant soit rustc
directement ou cargo
) puis exécutez. La sortie est :
Ok(
"Mon Feb 1 01:01:01 1901\n",
)
2120218157
Les appels aux fonctions C asctime
et mktime
doit à nouveau se produire à l’intérieur d’un unsafe
block, car le compilateur Rust ne peut être tenu responsable de tout problème de sécurité de la mémoire dans ces fonctions externes. Pour mémoire, asctime
et mktime
se comportent bien. Dans les appels aux deux fonctions, l’argument est le pointeur brut ptr
qui contient l’adresse (de pile) du sometime
structure.
L’appel à asctime
est le plus délicat des deux appels car cette fonction renvoie un pointeur vers un C char
le personnage M
dans Mon
de la sortie de texte. Pourtant, le compilateur Rust ne sait pas où se trouve la chaîne C (le tableau à terminaison nulle de char
) est stocké. Dans la zone statique de la mémoire ? Sur le tas ? Le tableau utilisé par le asctime
fonction pour stocker la représentation textuelle de l’heure est, en fait, dans la zone statique de la mémoire. Dans tous les cas, la conversion de chaîne C-to-Rust se fait en deux étapes pour éviter les erreurs de compilation :
-
L’appel
Cstr::from_ptr(char_ptr)
convertit la chaîne C en une chaîne Rust et renvoie une référence stockée dans lec_str
variable. -
L’appel à
c_str.to_str()
s’assure quec_str
est le propriétaire.
Le code Rust ne génère pas de version lisible par l’homme de la valeur entière renvoyée par mktime
, qui est laissé en exercice aux intéressés. Le module Rouille chrono::format
comprend un strftime
fonction, qui peut être utilisée comme la fonction C du même nom pour obtenir une représentation textuelle de l’heure.
Appeler C avec FFI et bindgen
Le Rust FFI et le bindgen
sont bien conçus pour effectuer des appels Rust vers les bibliothèques C, qu’elles soient standard ou tierces. Rust parle facilement à C et donc à tout autre langage qui parle à C. Pour appeler des fonctions de bibliothèque relativement simples telles que sqrt
le Rust FFI est simple car les types de données primitifs de Rust couvrent leurs homologues C.
Pour les échanges plus compliqués, en particulier, Rust appelle les fonctions de la bibliothèque C telles que asctime
et mktime
qui impliquent des structures et des pointeurs bindgen
l’utilité est la voie à suivre. Cet utilitaire génère le code de support avec les tests appropriés. Bien sûr, le compilateur Rust ne peut pas supposer que le code C est à la hauteur des normes Rust en matière de sécurité de la mémoire ; par conséquent, les appels de Rust à C doivent se produire dans unsafe
blocs.