Acheter version papier

Rust

Fork me on GitHub

III. Aller plus loin

2. Utiliser du code compilé en C avec les FFI

Rust permet d'exécuter du code compilé en C au travers des Foreign Function Interface (aussi appelée FFI). Ce chapitre va vous montrer comment faire.

Les bases

La première chose à faire est d'ajouter une dépendance à la crate libc :

Cargo.toml :

[dependencies]
libc = "0.2"

Bien que cette étape ne soit pas obligatoire, libc fournit un grand nombre de type C sur un grand nombre de plateformes/architectures. Il serait bête de s'en passer et de devoir le refaire soi-même !

Toute fonction que vous voudrez utiliser doit être déclarée ! Par exemple, utilisons la fonction rename :

Runuse std::ffi::CString;

extern "C" {
    fn rename(
        old: *const libc::c_char,
        new_p: *const libc::c_char,
    ) -> libc::c_int;
}

fn main() {
    if unsafe {
        rename(
            CString::new("old").unwrap().as_ptr(),
            CString::new("new").unwrap().as_ptr(),
        )
    } != 0 {
        println!("Rename failed");
    } else {
        println!("successfully renamed !");
    }
}

À noter qu'il est tout à fait possible de ne pas passer par les types fournis par la libc :

Runextern "C" {
    fn rename(old: *const i8, new_p: *const i8) -> i32;
}

Cependant je vous le déconseille fortement. Les types fournis par la libc ont l'avantage d'être plus clairs et surtout de correspondre au type C. Dans ce code, char n'est pas nécessairement un entier signé, ni même de 8 bits.

Regardons maintenant comment utiliser des fonctions d'une bibliothèque C.

Interfaçage avec une bibliothèque C

Tout d'abord, il va falloir linker notre code avec la bibliothèque C que l'on souhaite utiliser :

Run// Dans le fichier principal.

#[cfg(target_os = "linux")]
mod platform {
    #[link(name = "nom_de_la_bibliotheque")] extern {}
}

Dans le cas présent j'ai mis linux, mais sachez que vous pouvez aussi mettre win32, macos, etc.... Il est aussi possible de préciser l'architecture de cette façon :

Run#[cfg(target_os = "linux")]
mod platform {
    #[cfg(target_arch = "x86")]
    #[link(name = "nom_de_la_bibliotheque_en_32_bits")] extern{}
    #[cfg(target_arch = "x86_64")]
    #[link(name = "nom_de_la_bibliotheque_en_64_bits")] extern{}
}

Nous avons donc maintenant les bases.

Interfacer les fonctions

Tout comme je vous l'ai montré précédemment, il va falloir redéclarer les fonctions que vous souhaitez utiliser. Il est recommandé de les déclarer dans un fichier ffi.rs (c'est ce qui généralement fait). Vous allez aussi enfin voir les structures unitaires en action !

On va dire que la bibliothèque en C ressemble à ça :

#define NOT_OK 0
#define OK 1

// On ne sait pas ce que la structure contient.
struct Handler;

Handler *new();
int do_something(Handler *h);
int add_callback(Handler *h, int (*pointeur_sur_fonction)(int, int););
void destroy(Handler *h);

Nous devons écrire son équivalent en Rust, ce que nous allons faire dans le fichier ffi.rs :

Runuse libc::{c_int, c_void, c_char};

enum Status {
    NotOk = 0,
    Ok = 1,
}

// Cette metadata n'est pas obligatoire mais il est recommandé de la mettre
// quand on manipule des objets venant du C.
#[repr(C)]
pub struct FFIHandler; // La structure unitaire.

extern "C" {
    pub fn new() -> *mut FFIHandler;
    pub fn do_something(handler: *mut FFIHandler) -> c_int;
    pub fn add_callback(
        handler: *mut FFIHandler,
        fonction: *mut c_void,
    ) -> c_int;
    pub fn set_name(handler: *mut FFIHandler, name: *const c_char);
    pub fn get_name(handler: *mut FFIHandler) -> *const c_char;
    pub fn destroy(handler: *mut FFIHandler);
}

Voilà pour les déclarations du code C. Nous pouvons attaquer le portage à proprement parler. Comme l'objet que l'on va binder s'appelle Handler, on va garder le nom en Rust :

Run// Dans le fichier handler.rs :
use libc::{c_int, c_void, c_char};
use ffi::{self, FFIHandler};

pub struct Handler {
    pointer: *mut FFIHandler,
}

impl Handler {
    pub fn new() -> Result<Handler, ()> {
        let tmp = unsafe { ffi::new() };

        if tmp.is_null() {
            Ok(Handler { pointer: tmp })
        } else {
            Err(())
        }
    }

    pub fn do_something(&self) -> Status {
        unsafe { ffi::do_something(self.pointer) }
    }

    pub fn add_callback(&self, fonction: fn(isize, isize) -> isize) -> Status {
        unsafe { ffi::add_callback(self.pointer, fonction as *mut c_void) }
    }

    pub fn set_name(&self, name: &str) {
        unsafe { ffi::set_name(self.pointer, name.as_ptr() as *const c_char) }
    }

    pub fn get_name(&self) -> String {
        let tmp unsafe { ffi::get_name(self.pointer) };

        if tmp.is_null() {
            String::new()
        } else {
            unsafe {
                String::from_utf8_lossy(
                    std::ffi::CStr::from_ptr(tmp).to_bytes(),
                ).to_string()
            }
        }
    }
}

impl Drop for Handler {
    fn drop(&mut self) {
        if !self.pointer.is_null() {
            unsafe { ffi::destroy(self.pointer); }
            self.pointer = std::ptr::null_mut();
        }
    }
}

Voilà, vous devriez maintenant pouvoir vous en sortir avec ces bases. Nous avons vu comment ajouter un callback, convertir une String entre C et Rust et nous avons surtout pu voir les structures unitaires en action !