Как преобразовать строку C в строку Rust и обратно через FFI?

Я пытаюсь получить строку C, возвращаемую библиотекой C, и преобразовать ее в строку Rust через 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?
}

person Dirk    schedule 10.06.2014    source источник


Ответы (2)


Лучший способ работать со строками C в Rust - использовать структуры из модуля std::ffi , а именно CStr и _ 3_.

CStr - это тип с динамическим размером, поэтому его можно использовать только через указатель. Это делает его очень похожим на обычный тип str. Вы можете построить &CStr из *const c_char, используя небезопасный _8 _ статический метод. Этот метод небезопасен, потому что нет гарантии, что переданный ему необработанный указатель действителен, что он действительно указывает на допустимую строку C и что время жизни строки правильное.

Вы можете получить &str из &CStr, используя его _11 _ метод.

Вот пример:

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
}

Вы должны учитывать время жизни ваших *const c_char указателей и того, кому они принадлежат. В зависимости от C API вам может потребоваться вызвать специальную функцию освобождения строки. Вам нужно тщательно организовать преобразования, чтобы фрагменты не пережили указатель. Здесь помогает тот факт, что CStr::from_ptr возвращает &CStr с произвольным временем жизни (хотя это опасно само по себе); например, вы можете инкапсулировать свою строку C в структуру и обеспечить преобразование Deref, чтобы вы могли использовать свою структуру, как если бы это был фрагмент строки:

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()
    }
}

В этом модуле также есть другой тип, называемый CString. Он имеет те же отношения с CStr, что и String с str - CString является собственной версией CStr. Это означает, что он «удерживает» дескриптор распределения байтовых данных, и удаление CString освободит память, которую он предоставляет (по сути, CString обертывает Vec<u8>, и последний будет удален). Следовательно, это полезно, когда вы хотите предоставить данные, выделенные в Rust, в виде строки C.

К сожалению, строки C всегда заканчиваются нулевым байтом и не могут содержать один внутри себя, в то время как Rust _27 _ / _ 28_ - с точностью до наоборот - они не заканчиваются нулевым байтом и могут содержать произвольное их количество внутри. Это означает, что переход от Vec<u8> к CString не является ни безошибочным, ни без выделения - конструктор CString проверяет наличие нулей внутри предоставленных вами данных, возвращая ошибку, если обнаруживает их, и добавляет нулевой байт в конец байта. вектор, который может потребовать его перераспределения.

Подобно String, который реализует Deref<Target = str>, CString реализует Deref<Target = CStr>, поэтому вы можете вызывать методы, определенные в CStr, непосредственно в CString. Это важно, потому что метод as_ptr(), который возвращает *const c_char, необходимый для взаимодействия с C, определен на CStr. Вы можете вызвать этот метод непосредственно для CString значений, что удобно.

CString можно создать из всего, что можно преобразовать в Vec<u8>. String, &str, Vec<u8> и &[u8] - допустимые аргументы для функции-конструктора, CString::new(). Естественно, если вы передадите байтовый или строковый сегмент, будет создано новое выделение, а Vec<u8> или String будут использованы.

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),
    }  
}

Если вам нужно передать право владения CString коду C, вы можете вызвать _ 53_. Затем вам необходимо вернуть указатель и освободить его в Rust; распределитель Rust вряд ли будет таким же, как распределитель, используемый malloc и free. Все, что вам нужно сделать, это вызвать CString::from_raw и затем разрешите сбросить строку в обычном режиме.

person Vladimir Matveev    schedule 10.06.2014
comment
Отличный ответ, это мне очень помогло. Сохраняется ли небезопасность времени жизни cstr при взаимодействии с языком сборщика мусора, таким как c #? - person scape; 17.02.2017
comment
@scape да, конечно, есть. Я бы сказал, что там это даже более важно, потому что сборка мусора может запускаться в любое время, особенно если она параллельна. Если вы не позаботитесь о том, чтобы строка на стороне GC оставалась где-то укорененной, вы можете внезапно получить доступ к освобожденной части памяти на стороне Rust. - person Vladimir Matveev; 18.02.2017

В дополнение к тому, что сказал @ vladimir-matveev, вы также можете конвертировать между ними без помощи CStr или 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);
    }
}

Просто убедитесь, что при преобразовании из & str в строку C ваш & str оканчивается на '\0'. Обратите внимание, что в приведенном выше коде я использую strlen(c_s)+1 вместо strlen(c_s), поэтому s равно "Hello World!\0", а не только "Hello World!".
(Конечно, в этом конкретном случае он работает даже с strlen(c_s). Но с новым & str вы не смогли бы ' t гарантировать, что результирующая строка C завершится там, где ожидалось.)
Вот результат выполнения кода:

s == "Hello World!\u{0}"
Hello World!
person Des Nerger    schedule 19.01.2018
comment
Вы можете преобразовать из без CStr, но для этого нет причин. Ваше обратное преобразование неверно, так как Rust &str не завершается NUL и, следовательно, не является допустимой строкой C. - person Shepmaster; 19.01.2018
comment
@Shepmaster, Да, Rust & str обычно не завершается NUL, но поскольку этот был сделан из строки C, он отлично работает, когда вы делаете s.as_ptr(). Чтобы было понятнее, я исправил strlen(c_s) на strlen(c_s)+1. - person Des Nerger; 19.01.2018
comment
Итак, теперь вы скопировали функциональность стандартной библиотеки? Пожалуйста, отредактируйте свой вопрос, чтобы объяснить будущим читателям, почему им следует выбрать именно это решение, а не существующий ответ. - person Shepmaster; 19.01.2018
comment
Одна из причин для этого заключается в том, что вы разрабатываете в среде no_std. - person Myk Melez; 02.08.2019