web-dev-qa-db-fra.com

Comment convertir une chaîne C en une chaîne Rust et vice-versa via FFI?

J'essaie d'obtenir une chaîne C retournée par une bibliothèque C et de la convertir en une chaîne Rust via FFI.

mylib.c

const char* hello(){
    return "Hello World!";
}

main.rs

#![feature(link_args)]

extern crate libc;
use libc::c_char;

#[link_args = "-L . -I . -lmylib"]
extern {
    fn hello() -> *c_char;
}

fn main() {
    //how do I get a str representation of hello() here?
}
42
Dirk

La meilleure façon de travailler avec des chaînes C dans Rust est d'utiliser les structures du module std::ffi , à savoir CStr et CString .

CStr est un type de taille dynamique et ne peut donc être utilisé que via un pointeur. Cela le rend très similaire au type normal str. Vous pouvez construire un &CStr À partir de *const c_char En utilisant une méthode statique non sûre CStr::from_ptr . Cette méthode n'est pas sûre car il n'y a aucune garantie que le pointeur brut que vous lui passez est valide, qu'il pointe vraiment vers une chaîne C valide et que la durée de vie de la chaîne est correcte.

Vous pouvez obtenir un &str À partir d'un &CStr En utilisant sa méthode to_str() .

Voici un exemple:

extern crate libc;

use libc::c_char;
use std::ffi::CStr;
use std::str;

extern {
    fn hello() -> *const c_char;
}

fn main() {
    let c_buf: *const c_char = unsafe { hello() };
    let c_str: &CStr = unsafe { CStr::from_ptr(c_buf) };
    let str_slice: &str = c_str.to_str().unwrap();
    let str_buf: String = str_slice.to_owned();  // if necessary
}

Vous devez prendre en compte la durée de vie de vos pointeurs *const c_char Et leur propriétaire. Selon l'API C, vous devrez peut-être appeler une fonction de désallocation spéciale sur la chaîne. Vous devez organiser soigneusement les conversions afin que les tranches ne survivent pas au pointeur. Le fait que CStr::from_ptr Renvoie un &CStr Avec une durée de vie arbitraire aide ici (bien qu'il soit dangereux en soi); par exemple, vous pouvez encapsuler votre chaîne C dans une structure et fournir une conversion Deref afin que vous puissiez utiliser votre structure comme s'il s'agissait d'une tranche de chaîne:

extern crate libc;

use libc::c_char;
use std::ops::Deref;
use std::ffi::CStr;

extern "C" {
    fn hello() -> *const c_char;
    fn goodbye(s: *const c_char);
}

struct Greeting {
    message: *const c_char,
}

impl Drop for Greeting {
    fn drop(&mut self) {
        unsafe {
            goodbye(self.message);
        }
    }
}

impl Greeting {
    fn new() -> Greeting {
        Greeting { message: unsafe { hello() } }
    }
}

impl Deref for Greeting {
    type Target = str;

    fn deref<'a>(&'a self) -> &'a str {
        let c_str = unsafe { CStr::from_ptr(self.message) };
        c_str.to_str().unwrap()
    }
}

Il existe également un autre type dans ce module appelé CString . Il a la même relation avec CStr que String avec str - CString est une version appartenant à CStr. Cela signifie qu'il "détient" le descripteur de l'allocation des données d'octets, et la suppression de CString libérerait la mémoire qu'il fournit (essentiellement, CString encapsule Vec<u8>, Et c'est ce dernier qui sera abandonné). Par conséquent, il est utile lorsque vous souhaitez exposer les données allouées dans Rust sous forme de chaîne C.

Malheureusement, les chaînes C se terminent toujours par l'octet zéro et ne peuvent pas en contenir un à l'intérieur, tandis que Rust &[u8]/Vec<u8> Sont exactement l'inverse - elles ne se terminent pas par zéro octet et peuvent en contenir des nombres arbitraires. Cela signifie que passer de Vec<u8> à CString n'est ni sans erreur ni allocation - le CString Le constructeur vérifie les zéros à l'intérieur des données que vous fournissez, renvoyant une erreur s'il en trouve, et ajoute un octet zéro à la fin du vecteur d'octets qui peut nécessiter sa réallocation.

Comme String, qui implémente Deref<Target = str>, CString implémente Deref<Target = CStr>, Vous pouvez donc appeler des méthodes définies sur CStr directement sur CString. Ceci est important car la méthode as_ptr() qui renvoie le *const c_char Nécessaire à l'interopérabilité C est définie sur CStr. Vous pouvez appeler cette méthode directement sur les valeurs de CString, ce qui est pratique.

CString peut être créé à partir de tout ce qui peut être converti en Vec<u8>. String, &str, Vec<u8> Et &[u8] Sont des arguments valides pour la fonction constructeur, CString::new() . Naturellement, si vous passez une tranche d'octets ou une tranche de chaîne, une nouvelle allocation sera créée, tandis que Vec<u8> Ou String seront consommés.

extern crate libc;

use libc::c_char;
use std::ffi::CString;

fn main() {
    let c_str_1 = CString::new("hello").unwrap(); // from a &str, creates a new allocation
    let c_str_2 = CString::new(b"world" as &[u8]).unwrap(); // from a &[u8], creates a new allocation
    let data: Vec<u8> = b"12345678".to_vec(); // from a Vec<u8>, consumes it
    let c_str_3 = CString::new(data).unwrap();

    // and now you can obtain a pointer to a valid zero-terminated string
    // make sure you don't use it after c_str_2 is dropped
    let c_ptr: *const c_char = c_str_2.as_ptr();

    // the following will print an error message because the source data
    // contains zero bytes
    let data: Vec<u8> = vec![1, 2, 3, 0, 4, 5, 0, 6];
    match CString::new(data) {
        Ok(c_str_4) => println!("Got a C string: {:p}", c_str_4.as_ptr()),
        Err(e) => println!("Error getting a C string: {}", e),
    }  
}

Si vous devez transférer la propriété du code CString au code C, vous pouvez appeler CString::into_raw . Vous devez ensuite récupérer le pointeur et le libérer dans Rust; l'allocateur Rust ne sera probablement pas le même que l'allocateur utilisé par malloc et free. Il vous suffit d'appeler CString::from_raw puis laissez la chaîne être supprimée normalement.

76
Vladimir Matveev