La interoperabilidad entre Rust y C presenta un desafío fundamental debido a la representación de las cadenas de texto en memoria. Rust utiliza String y &str, que son buffers de UTF-8 con longitud explícita y no dependen de un terminador para conocer su tamaño. Por el contrario, C utiliza cadenas de caracteres terminadas en un carácter nulo (\0), conocidas como null-terminated strings. Intentar pasar un *const u8 de un &str de Rust directamente a una función de C que espera una cadena de C provocará errores de lectura de memoria, ya que C seguirá leyendo bytes hasta encontrar un \0 inexistente.

Para resolver esto, Rust proporciona dos tipos fundamentales: CString y CStr. CString es una versión “propiedad” (owned) de una cadena de C; es decir, es un String que tiene un byte extra al final conteniendo el terminador nulo. Se utiliza para enviar datos desde Rust hacia C. Es vital notar que CString::new() fallará si la cadena contiene un carácter nulo interno, ya que esto corrompería la estructura de la cadena en C.

CStr es el equivalente a un slice (&str) pero orientado a la interfaz de C. Representa una referencia a una secuencia de bytes terminada en nulo que ya reside en memoria. Se utiliza para recibir o “envolver” punteros que provienen de código C. La conversión de un puntero crudo (*const c_char) a un CStr es una operación unsafe porque Rust no puede garantizar que el puntero sea válido o que realmente termine en un nulo.

Al convertir de CStr a tipos de Rust (&str), existen dos caminos: to_str(), que devuelve un Result porque la cadena proveniente de C podría no ser UTF-8 válida, y to_string_lossy(), que permite recuperar una representación legible sustituyendo bytes inválidos por el carácter de reemplazo .

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

// Simulamos una función en C que recibe un puntero a caracteres
// En un escenario real, esto estaría en una biblioteca externa (.so o .dll)
extern "C" {
    fn c_function_receiver(ptr: *const c_char);
}

// Implementación de la simulación de la función de C en Rust
#[no_mangle]
pub unsafe extern "C" fn c_function_receiver(ptr: *const c_char) {
    if ptr.is_null() { return; }
    
    // CStr::from_ptr es unsafe: asumimos que el puntero es válido y termina en \0
    let c_str_ref = CStr::from_ptr(ptr);
    
    // Intentamos convertir a &str (UTF-8 seguro)
    match c_str_ref.to_str() {
        Ok(s) => println!("[C-Receiver] Recibido string UTF-8 válido: {}", s),
        Err(_) => println!("[C-Receiver] Error: El puntero no contiene UTF-8 válido"),
    }

    // Alternativa para datos no confiables: to_string_lossy
    let lossy = c_str_ref.to_string_lossy();
    println!("[C-Receiver] Representación lossy: {}", lossy);
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Enviar datos de Rust a C usando CString
    let mensaje_rust = "Hola desde Rust!";
    let c_string = CString::new(mensaje_rust)?;
    
    // .as_ptr() devuelve *const c_char para pasar a C
    // IMPORTANTE: c_string debe vivir tanto como la llamada a la función
    unsafe {
        c_function_receiver(c_string.as_ptr());
    }

    // 2. Simulación de recibir datos de C (un puntero a bytes que no es UTF-8)
    // Creamos un array de bytes con un byte inválido para UTF-8 (0xFF)
    let datos_corruptos: [c_char; 5] = [b'H', b'o', b'l', 0xFF, 0];
    let ptr_corrupto = datos_corruptos.as_ptr();

    println!("\n--- Probando datos no UTF-8 ---");
    unsafe {
        c_function_receiver(ptr_corrupto);
    }

    Ok(())
}

Análisis del código

  • CString::new(mensaje_rust)?: Crea una nueva instancia de CString que añade el terminador nulo. El operador ? gestiona el Result en caso de que mensaje_rust contenga un nulo interno.
  • c_string.as_ptr(): Este método de c_string devuelve un puntero crudo (*const c_char). Es crucial entender que la propiedad (ownership) reside en c_string; si c_string se liberara antes de la llamada a c_function_receiver, el puntero sería inválido (dangling pointer).
  • CStr::from_ptr(ptr): Dentro de la función simulada, transformamos el puntero crudo ptr en una referencia segura c_str_ref de tipo &CStr. Esta operación es unsafe porque delegamos la responsabilidad de la validez del puntero al programador.
  • c_str_ref.to_str(): Intenta convertir la referencia de C a un &str de Rust. Devuelve un Result<&str, Utf8Error>. En el primer caso de main, funciona porque “Hola desde Rust!” es UTF-8.
  • c_str_ref.to_string_lossy(): En el segundo caso, donde datos_corruptos contiene 0xFF (inválido en UTF-8), to_str() fallaría, pero to_string_lossy() permite la conversión reemplazando el byte corrupto, garantizando que el programa no colapse al manejar datos externos.
  • #[no_mangle]: Se utiliza en la implementación de la función para evitar que el compilador cambie el nombre de la función, asegurando que sea localizable mediante su nombre exacto durante la simulación de la interoperabilidad.

98