J'expérimente actuellement la fonctionnalité FFI de Rust et j'ai implémenté une simple requête HTTP en utilisant libcurl comme exercice. Prenons l'exemple autonome suivant :

use std::ffi::c_void;

#[repr(C)]
struct CURL {
    _private: [u8; 0],
}

// Global CURL codes
const CURL_GLOBAL_DEFAULT: i64 = 3;
const CURLOPT_WRITEDATA: i64 = 10001;
const CURLOPT_URL: i64 = 10002;
const CURLOPT_WRITEFUNCTION: i64 = 20011;

// Curl types
type CURLcode = i64;
type CURLoption = i64;

// Curl function bindings
#[link(name = "curl")]
extern "C" {
    fn curl_easy_init() -> *mut CURL;
    fn curl_easy_setopt(handle: *mut CURL, option: CURLoption, value: *mut c_void) -> CURLcode;
    fn curl_easy_perform(handle: *mut CURL) -> CURLcode;
    fn curl_global_init(flags: i64) -> CURLcode;
}

// Curl callback for data retrieving
extern "C" fn callback_writefunction(
    data: *mut u8,
    size: usize,
    nmemb: usize,
    user_data: *mut c_void,
) -> usize {
    let slice = unsafe { std::slice::from_raw_parts(data, size * nmemb) };

    let mut vec = unsafe { Box::from_raw(user_data as *mut Vec<u8>) };
    vec.extend_from_slice(slice);
    Box::into_raw(vec);
    nmemb * size
}

type Result<T> = std::result::Result<T, CURLcode>;

// Our own curl handle
pub struct Curl {
    handle: *mut CURL,
    data_ptr: *mut Vec<u8>,
}

impl Curl {
    pub fn new() -> std::result::Result<Curl, CURLcode> {
        let ret = unsafe { curl_global_init(CURL_GLOBAL_DEFAULT) };
        if ret != 0 {
            return Err(ret);
        }

        let handle = unsafe { curl_easy_init() };
        if handle.is_null() {
            return Err(2); // CURLE_FAILED_INIT according to libcurl-errors(3)
        }

        // Set data callback
        let ret = unsafe {
            curl_easy_setopt(
                handle,
                CURLOPT_WRITEFUNCTION,
                callback_writefunction as *mut c_void,
            )
        };
        if ret != 0 {
            return Err(2);
        }

        // Set data pointer
        let data_buf = Box::new(Vec::new());
        let data_ptr = Box::into_raw(data_buf);
        let ret = unsafe {
            curl_easy_setopt(handle, CURLOPT_WRITEDATA, data_ptr as *mut std::ffi::c_void)
        };
        match ret {
            0 => Ok(Curl { handle, data_ptr }),
            _ => Err(2),
        }
    }

    pub fn set_url(&self, url: &str) -> Result<()> {
        let url_cstr = std::ffi::CString::new(url.as_bytes()).unwrap();
        let ret = unsafe {
            curl_easy_setopt(
                self.handle,
                CURLOPT_URL,
                url_cstr.as_ptr() as *mut std::ffi::c_void,
            )
        };
        match ret {
            0 => Ok(()),
            x => Err(x),
        }
    }

    pub fn perform(&self) -> Result<String> {
        let ret = unsafe { curl_easy_perform(self.handle) };
        if ret == 0 {
            let b = unsafe { Box::from_raw(self.data_ptr) };
            let data = (*b).clone();
            Box::into_raw(b);
            Ok(String::from_utf8(data).unwrap())
        } else {
            Err(ret)
        }
    }
}

fn main() -> Result<()> {
    let my_curl = Curl::new().unwrap();
    my_curl.set_url("https://www.example.com")?;
    my_curl.perform().and_then(|data| Ok(println!("{}", data)))
    // No cleanup code in this example for the sake of brevity.
}

Bien que cela fonctionne, j'ai trouvé surprenant que my_curl n'ait pas besoin d'être déclaré mut, car aucune des méthodes n'utilise &mut self, même si elles passent un pointeur mut* vers la fonction FFI s.

Dois-je modifier la déclaration de perform pour utiliser &mut self au lieu de &self (par sécurité), car le tampon interne est modifié ? Rust ne l'applique pas, mais bien sûr Rust ne sait pas que le tampon est modifié par libcurl.

Ce petit exemple fonctionne bien, mais je ne sais pas si je serais confronté à des problèmes dans des programmes plus volumineux, lorsque le compilateur pourrait optimiser l'accès non modifiable sur la structure Curl, même si l'instance de la structure est être modifié - ou au moins les données vers lesquelles le pointeur pointe.

0
Philipp Ludwig 3 nov. 2020 à 21:05

1 réponse

Meilleure réponse

Contrairement à la croyance populaire, il n'y a absolument aucune restriction induite par le vérificateur d'emprunt dans Rust sur le passage des pointeurs *const/*mut. Cela n'est pas nécessaire, car le déréférencement des pointeurs est intrinsèquement dangereux et ne peut être effectué que dans de tels blocs, le programmeur vérifiant manuellement tous les invariants nécessaires. Dans votre cas, vous devez indiquer au compilateur qu'il s'agit d'une référence modifiable, comme vous le soupçonniez déjà.

Le lecteur intéressé doit absolument lire la section ffi du nomicon, pour découvrez des façons intéressantes de vous tirer une balle dans le pied avec.

1
L. Riemer 3 nov. 2020 à 21:25