¿Cómo convierto una cadena C en una cadena Rust y viceversa a través de FFI?


Estoy tratando de obtener una cadena C devuelta por una biblioteca de C y convertirla en una cadena Rust a través de 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?
}
 33
Author: Shepmaster, 2014-06-10

2 answers

La mejor manera de trabajar con cadenas de C en Rust es usar estructuras de la std::ffi módulo, a saber CStr y CString.

CStr es un tipo de tamaño dinámico, por lo que solo se puede usar a través de un puntero. Esto lo hace muy similar al tipo regular str. Puede construir un &CStr desde *const c_char usando un inseguro CStr::from_ptr método estático. Este método no es seguro porque no hay garantía de que el puntero raw que le pase sea válido, que realmente apunta a una cadena C válida y que el tiempo de vida de la cadena es correcto.

Puedes obtener un &str de un &CStr usando su to_str() método.

Aquí hay un ejemplo:

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
}

Debe tener en cuenta la vida útil de sus *const c_char punteros y quién los posee. Dependiendo de la API de C, es posible que necesite llamar a una función especial de desasignación en la cadena. Debe organizar cuidadosamente las conversiones para que los segmentos no sobrevivan al puntero. El hecho de que CStr::from_ptr devuelve un &CStr con vida útil arbitraria ayuda aquí (aunque es peligroso por sí mismo); por ejemplo, puede encapsular su cadena C en una estructura y proporcionar una conversión Deref para que pueda usar su estructura como si fuera un segmento de cadena:

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()
    }
}

También Hay otro tipo en este módulo llamado CString. Tiene la misma relación con CStr que String con str - CString es una versión propia de CStr. Esto significa que "mantiene" el handle a la asignación del byte datos, y soltar CString liberaría la memoria que proporciona (esencialmente, CString envuelve Vec<u8>, y es este último el que se eliminará). En consecuencia, es útil cuando desea exponer los datos asignados en Rust como una cadena C.

Desafortunadamente, las cadenas C siempre terminan con el byte cero y no pueden contener uno dentro de ellas, mientras que Rust &[u8]/Vec<u8> son exactamente lo contrario: no terminan con cero bytes y pueden contener números arbitrarios de ellos dentro. Esto significa que yendo desde Vec<u8> a CString no está libre de errores ni de asignación: el constructor CString comprueba si hay ceros dentro de los datos que proporciona, devolviendo un error si encuentra algunos, y añade un byte cero al final del vector de bytes que puede requerir su reasignación.

Como String, que implementa Deref<Target = str>, CString implementa Deref<Target = CStr>, por lo que puede llamar a los métodos definidos en CStr directamente en CString. Esto es importante porque el as_ptr() método que devuelve el *const c_char necesario para C la interoperación se define en CStr. Puede llamar a este método directamente en los valores CString, lo cual es conveniente.

CString se puede crear a partir de todo lo que se puede convertir a Vec<u8>. String, &str, Vec<u8> y &[u8] son argumentos válidos para la función de constructor, CString::new(). Naturalmente, si pasa un segmento de byte o un segmento de cadena, se creará una nueva asignación, mientras que Vec<u8> o String se consumirá.

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 necesita transferir la propiedad de CString a C código, puedes llamar CString::into_raw. A continuación, debe recuperar el puntero y liberarlo en Rust; es poco probable que el asignador Rust sea el mismo que el utilizado por malloc y free. Todo lo que necesitas hacer es llamar CString::from_raw y luego permita que la cadena se suelte normalmente.

 65
Author: Vladimir Matveev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-08 03:32:21

Además de lo que @vladimir-matveev ha dicho, también puedes convertir entre ellos sin la ayuda de CStr o CString:

#![feature(link_args)]

extern crate libc;
use libc::{c_char, puts, strlen};
use std::{slice, str};

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

fn main() {
    //converting a C string into a Rust string:
    let s = unsafe {
        let c_s = hello();
        str::from_utf8_unchecked(slice::from_raw_parts(c_s as *const u8, strlen(c_s)+1))
    };
    println!("s == {:?}", s);
    //and back:
    unsafe {
        puts(s.as_ptr() as *const c_char);
    }
}

Solo asegúrese de que al convertir de una &str a una cadena C, su &str termina con '\0'. Observe que en el código anterior uso strlen(c_s)+1 en lugar de strlen(c_s), por lo que s es "Hello World!\0", no solo "Hello World!".
( Por supuesto, en este caso particular funciona incluso con solo strlen(c_s). Pero con un &str fresco no se podía garantizar que la cadena C resultante terminaría donde se esperaba.)
Este es el resultado de ejecutar el código:

s == "Hello World!\u{0}"
Hello World!
 -4
Author: Des Nerger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-20 22:45:50