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

8 minutos de lectura

avatar de usuario
Puñal

Estoy tratando de obtener una cadena C devuelta por una biblioteca C y convertirla en una cadena Rust a través de FFI.

mylib.c

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

principal.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?
}

avatar de usuario
vladimir matveev

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

CStr es un tipo de tamaño dinámico y, por lo tanto, solo se puede usar a través de un puntero. Esto lo hace muy similar al regular. str escribe. Puedes 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 sin procesar que le pasa sea válido, que realmente apunte a una cadena C válida y que la vida útil de la cadena sea correcta.

puedes conseguir un &str a partir de una &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 su *const c_char punteros y quién los posee. Dependiendo de la API de C, es posible que deba llamar a una función de desasignación especial 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 la vida útil arbitraria ayuda aquí (aunque es peligroso por sí mismo); por ejemplo, puede encapsular su cadena C en una estructura y proporcionar una Deref conversión para que pueda usar su estructura como si fuera una porción 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 como String con strCString es una versión propia de CStr. Esto significa que “mantiene” el identificador de la asignación de los datos de bytes y descarta 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 en su interior. Esto significa que pasando de Vec<u8> para CString no está libre de errores ni de asignaciones: el CString El constructor verifica los ceros dentro de los datos que proporciona, devuelve un error si encuentra alguno y agrega un byte cero al final del vector de bytes que puede requerir su reasignación.

Me gusta Stringque implementa Deref<Target = str>, CString implementos 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 necesaria para la interoperación C se define en CStr. Puede llamar a este método directamente en CString valores, lo cual es conveniente.

CString se puede crear a partir de todo lo que se puede convertir en Vec<u8>. String, &str, Vec<u8> y &[u8] son argumentos válidos para la función constructora, 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 será consumido.

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 del CString al código C, puede llamar CString::into_raw. Luego debe recuperar el puntero y liberarlo en Rust; es poco probable que el asignador de Rust sea el mismo que el asignador utilizado por malloc y free. Todo lo que necesitas hacer es llamar CString::from_raw y luego permita que la cuerda se suelte normalmente.

  • Gran respuesta, esto me ayudó mucho. ¿Todavía existe la inseguridad en la vida útil del cstr cuando se interactúa con un lenguaje GC como c #?

    – escapar

    17 de febrero de 2017 a las 20:24

  • @scape sí, por supuesto que sí. Diría que es aún más importante allí, porque la recolección de elementos no utilizados puede ejecutarse en cualquier momento, especialmente si es concurrente. Si no tiene cuidado de mantener la cadena en el lado de GC arraigada en algún lugar, puede acceder repentinamente a una parte de la memoria liberada en el lado de Rust.

    – Vladimir Matveyev

    18 de febrero de 2017 a las 11:00

avatar de usuario
des nerger

Además de lo que ha dicho @vladimir-matveev, también puede 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 un &str a una cadena C, su &str termine con '\0'. Tenga en cuenta que en el código anterior uso strlen(c_s)+1 en lugar de strlen(c_s)entonces 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 nuevo no podría garantizar que la cadena C resultante terminaría donde se esperaba.)
Aquí está el resultado de ejecutar el código:

s == "Hello World!\u{0}"
Hello World!

  • Puedes convertir desde sin CStr, pero evitarlo no tiene por qué. Tu conversión es incorrecto como un óxido &str no termina en NUL, por lo que no es una cadena C válida.

    – Maestro de pastores

    19 de enero de 2018 a las 14:31

  • @Shepmaster, sí, un Rust &str generalmente no termina en NUL, pero dado que este se hizo a partir de una cadena C, funciona bien cuando lo hace s.as_ptr(). Para que quede más claro, ahora he corregido strlen(c_s) para strlen(c_s)+1.

    – Des Nerger

    19 de enero de 2018 a las 14:58

  • Entonces, ¿ahora ha replicado la funcionalidad de la biblioteca estándar? Edite su pregunta para explicar a los futuros lectores por qué deberían elegir esta solución en lugar de la respuesta existente.

    – Maestro de pastores

    19/01/2018 a las 15:00

  • Una razón para hacerlo es que está desarrollando en un entorno no_std.

    – Myk Mélez

    2 de agosto de 2019 a las 16:59

  • Si necesita CStr en un entorno no_std, el github.com/Amanieu/cstr_core caja es una buena opción. El único inconveniente es que depende de cty, que tiene una solicitud de fusión abierta para corregir la compatibilidad con AVR.

    – Bob mutante

    7 de febrero a las 15:48

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad