Как конвертировать C-строку (char*) в Rust String и обратно через FFI?
Основы: 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 недоступен. В остальных случаях придерживайтесь первого ответа.