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


  • Français


  • 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.

    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 intun 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_doublequi 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.

    Programmation et développement

    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 à sqrtqui 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_tun 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 mktimeet 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’alias time_t. L’utilitaire bindgen peut gérer les time_t type mais génère des avertissements gênants en cours de route car time_t ne respecte pas les conventions de nommage de Rust : dans time_t un trait de soulignement sépare le t à la fin de la time qui vient en premier; Rust préférerait un nom CamelCase tel que TimeT.

    • 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 ptrqui 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 charle 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 :

    1. 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 le c_str variable.

    2. L’appel à c_str.to_str() s’assure que c_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 sqrtle 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.

    Source

    Houssen Moshinaly

    Pour contacter personnellement le taulier :

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

    Copy code