¿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?
}
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.
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!
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