Avatar ·

Как конвертировать C-строку (char*) в Rust String и обратно через FFI?

Всем привет! Я пишу небольшую программу на Rust, которая использует библиотеку на C через FFI. Столкнулся с проблемой преобразования строк между C и Rust. У меня есть C-функция, которая возвращает `char*`: ```c // C code char* get_greeting(void) { return "Hello from C!"; } ``` И другая C-функция, которая принимает `const char*`: ```c void print_message(const char* msg); ``` Я пытаюсь вызвать их из Rust: ```rust extern "C" { fn get_greeting() -> *const c_char; fn print_message(msg: *const c_char); } fn main() { unsafe { // Получаем C-строку let c_str = get_greeting(); // Как преобразовать *const c_char в Rust String? // Пытаемся передать Rust строку в C let rust_string = String::from("Hello from Rust!"); // Как преобразовать String в *const c_char? print_message(???); } } ``` Я понимаю, что C-строки — это нуль-терминированные последовательности байтов, а Rust строки — это UTF-8 с известной длиной. Но как правильно и безопасно конвертировать их друг в друга? Какие крейты или стандартные методы использовать? Нужно ли освобождать память после использования C-строки? Буду благодарен за подробный пример с объяснением всех нюансов, включая управление памятью и обработку ошибок. <<>>

Основы: C-строки vs Rust строки

Ключевое отличие: C-строки — это нуль-терминированные ('\0' в конце) последовательности байтов, а Rust строки (String и &str) — это UTF-8 с явно хранящейся длиной. При FFI нужно аккуратно преобразовывать их, не забывая про владение памятью.

Лучший подход: использование крейта std::ffi

Стандартная библиотека Rust предоставляет всё необходимое. Вот полное решение вашей задачи:

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

// Объявление внешних C-функций
extern "C" {
    fn get_greeting() -> *const c_char;
    fn print_message(msg: *const c_char);
    fn free_c_string(ptr: *mut c_char); // если C требует освобождения
}

fn main() {
    unsafe {
        // === 1. Преобразование C-строки (char*) в Rust String ===
        let c_ptr: *const c_char = get_greeting();
        
        // Проверка на NULL (C может вернуть NULL)
        if c_ptr.is_null() {
            panic!("C function returned null pointer");
        }
        
        // CStr::from_ptr — "одалживает" указатель, не копируя данные
        let c_str: &CStr = CStr::from_ptr(c_ptr);
        
        // Преобразуем в &str (если это валидный UTF-8)
        let rust_str: &str = c_str.to_str().expect("C string is not valid UTF-8");
        
        // Если нужна owned String:
        let owned_string: String = rust_str.to_string();
        
        println!("Got from C: {}", owned_string);
        
        // ВАЖНО: если C-функция выделила память (malloc), её нужно освободить
        // Если get_greeting возвращает статическую строку — не нужно
        // Если динамическую — вызываем C-функцию free:
        // free_c_string(c_ptr as *mut c_char);
        
        // === 2. Преобразование Rust String в C-строку (char*) ===
        let rust_string = String::from("Привет из Rust! (с кириллицей)");
        
        // CString::new() добавляет нуль-терминатор и проверяет, что внутри нет '\0'
        let c_string: CString = CString::new(rust_string)
            .expect("Rust string contained null byte");
        
        // Получаем *const c_char
        let c_ptr_for_c: *const c_char = c_string.as_ptr();
        
        // Передаём в C-функцию
        print_message(c_ptr_for_c);
        
        // Память освободится автоматически при выходе c_string из области видимости
        // НЕ вызывайте free_c_string на этом указателе!
    } // здесь c_string.drop() освобождает память
}

Ключевые моменты

  • CStr::from_ptr() — создаёт ссылку на C-строку. Не копирует данные, просто "смотрит" на память. Требует unsafe.
  • CString::new() — создаёт новую нуль-терминированную копию строки. Паникует, если исходная строка содержит '\0' (используйте expect()).
  • Владение памятью: Если C-функция выделила строку через malloc, Rust должен освободить её через C-функцию free. Если строка статическая — не нужно. Всегда читайте документацию C-библиотеки.
  • Кодировка: Всегда предполагайте UTF-8. Если C использует другую кодировку (например, Latin-1), используйте крейты вроде encoding для преобразования.

Продвинутый пример с обработкой ошибок

use std::ffi::{CStr, CString, NulError};
use std::str::Utf8Error;

fn c_ptr_to_string(ptr: *const c_char) -> Result<String, Box<dyn std::error::Error>> {
    if ptr.is_null() {
        return Err("Null pointer".into());
    }
    let c_str = unsafe { CStr::from_ptr(ptr) };
    let rust_str = c_str.to_str()?; // Utf8Error
    Ok(rust_str.to_string())
}

fn string_to_c_ptr(s: String) -> Result<CString, NulError> {
    CString::new(s)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    unsafe {
        let c_ptr = get_greeting();
        let rust_string = c_ptr_to_string(c_ptr)?;
        println!("{}", rust_string);
        
        let c_string = string_to_c_ptr("Hello".to_string())?;
        print_message(c_string.as_ptr());
    }
    Ok(())
}

Рекомендация

Для сложных FFI сценариев используйте крейт cstr (макрос cstr!) для константных C-строк или cstr_core для no_std. Но для 95% случаев достаточно std::ffi.

<<>>

Альтернативный подход: крейт libc и ручное управление

Если вы не хотите использовать std::ffi (например, в no_std среде), можно работать напрямую через крейт libc:

use libc::{c_char, strlen};
use std::slice;

unsafe fn c_str_to_string(ptr: *const c_char) -> String {
    if ptr.is_null() {
        return String::new();
    }
    // Вычисляем длину через strlen (ищет \0)
    let len = strlen(ptr);
    // Создаём слайс байтов
    let bytes = slice::from_raw_parts(ptr as *const u8, len);
    // Преобразуем в String (предполагаем UTF-8)
    String::from_utf8_lossy(bytes).to_string()
}

fn string_to_c_str(s: &str) -> Vec<u8> {
    // Создаём Vec с нуль-терминатором
    let mut bytes = s.as_bytes().to_vec();
    bytes.push(0); // добавляем \0
    bytes
}

// Использование:
fn main() {
    unsafe {
        let c_str = get_greeting();
        let rust_str = c_str_to_string(c_str);
        println!("{}", rust_str);
        
        let c_bytes = string_to_c_str("Hello");
        print_message(c_bytes.as_ptr() as *const c_char);
    }
}

Недостатки этого подхода:

  • Ручное управление памятью — легко ошибиться.
  • from_utf8_lossy заменяет невалидные UTF-8 последовательности на �, что может скрыть ошибки.
  • Нет проверки на внутренние нуль-байты в Rust строке (может привести к обрезанию).

Используйте этот метод только если std::ffi недоступен. В остальных случаях придерживайтесь первого ответа.