Wie konvertiere ich einen C-String in einen Rust-String und zurück über FFI?

Lesezeit: 7 Minuten

Benutzeravatar von Dirk
Dolch

Ich versuche, einen C-String von einer C-Bibliothek zurückzugeben und ihn über FFI in einen Rust-String zu konvertieren.

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?
}

Benutzeravatar von Vladimir Matveev
Wladimir Matwejew

Der beste Weg, um mit C-Saiten in Rust zu arbeiten, ist die Verwendung von Strukturen aus der std::ffi Modul, nämlich CStr und CString.

CStr ist ein Typ mit dynamischer Größe und kann daher nur über einen Zeiger verwendet werden. Dadurch ist es dem regulären sehr ähnlich str Typ. Sie können eine konstruieren &CStr aus *const c_char mit einem unsicheren CStr::from_ptr statische Methode. Diese Methode ist unsicher, da es keine Garantie dafür gibt, dass der rohe Zeiger, den Sie übergeben, gültig ist, dass er wirklich auf einen gültigen C-String zeigt und dass die Lebensdauer des Strings korrekt ist.

Sie können eine bekommen &str von einem &CStr mit seiner to_str() Methode.

Hier ist ein Beispiel:

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
}

Sie müssen die Lebensdauer Ihrer berücksichtigen *const c_char Zeiger und wem sie gehören. Abhängig von der C-API müssen Sie möglicherweise eine spezielle Freigabefunktion für die Zeichenfolge aufrufen. Sie müssen Konvertierungen sorgfältig arrangieren, damit die Slices den Zeiger nicht überleben. Die Tatsache, dass CStr::from_ptr gibt a zurück &CStr mit beliebiger Lebensdauer hilft hier (obwohl es an sich gefährlich ist); Beispielsweise können Sie Ihren C-String in eine Struktur kapseln und eine bereitstellen Deref Konvertierung, damit Sie Ihre Struktur verwenden können, als wäre es ein String-Slice:

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

Es gibt auch einen anderen Typ in diesem Modul namens CString. Es hat die gleiche Beziehung zu CStr wie String mit strCString ist eine eigene Version von CStr. Dies bedeutet, dass es das Handle zur Zuweisung der Bytedaten “hält” und verwirft CString würde den bereitgestellten Speicher freigeben (im Wesentlichen CString wickelt Vec<u8>, und letzteres wird fallen gelassen). Folglich ist es nützlich, wenn Sie die in Rust zugewiesenen Daten als C-String verfügbar machen möchten.

Leider enden C-Strings immer mit dem Null-Byte und können keins enthalten, während Rust &[u8]/Vec<u8> sind genau das Gegenteil – sie enden nicht mit null Byte und können eine beliebige Anzahl davon enthalten. Das bedeutet, dass ab Vec<u8> zu CString ist weder fehlerfrei noch allokationsfrei – die CString Der Konstruktor sucht in den von Ihnen bereitgestellten Daten nach Nullen, gibt einen Fehler zurück, wenn er welche findet, und fügt ein Null-Byte an das Ende des Byte-Vektors an, das möglicherweise neu zugewiesen werden muss.

Wie Stringdie implementiert Deref<Target = str>, CString implementiert Deref<Target = CStr>sodass Sie definierte Methoden aufrufen können CStr direkt an CString. Dies ist wichtig, weil die as_ptr() Methode, die die zurückgibt *const c_char die für die C-Interoperation erforderlich ist, wird definiert CStr. Sie können diese Methode direkt aufrufen CString Werte, was praktisch ist.

CString kann aus allem erstellt werden, was konvertiert werden kann Vec<u8>. String, &str, Vec<u8> und &[u8] sind gültige Argumente für die Konstruktorfunktion, CString::new(). Wenn Sie einen Byte-Slice oder einen String-Slice übergeben, wird natürlich eine neue Zuordnung erstellt, während Vec<u8> oder String wird verbraucht.

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

Wenn Sie das Eigentum an der übertragen müssen CString zu C-Code können Sie anrufen CString::into_raw. Sie müssen dann den Zeiger zurückholen und in Rust freigeben; Es ist unwahrscheinlich, dass der Rust-Zuordner mit dem von verwendeten Zuordner identisch ist malloc und free. Alles, was Sie tun müssen, ist anzurufen CString::from_raw und lassen Sie dann die Zeichenfolge normal fallen.

  • Tolle Antwort, das hat mir sehr geholfen. Besteht die Unsicherheit in der Lebensdauer des cstr immer noch, wenn eine Schnittstelle mit einer GC-Sprache wie c# besteht?

    – Landschaft

    17. Februar 2017 um 20:24 Uhr

  • @scape ja, natürlich tut es das. Ich würde sagen, es ist dort sogar noch wichtiger, weil die Garbage Collection jederzeit laufen kann, besonders wenn sie gleichzeitig ist. Wenn Sie nicht darauf achten, dass der String auf der GC-Seite irgendwo verwurzelt bleibt, können Sie plötzlich auf ein freigegebenes Stück Speicher auf der Rust-Seite zugreifen.

    – Wladimir Matwejew

    18. Februar 2017 um 11:00 Uhr

Benutzeravatar von Des Nerger
Des Nerger

Zusätzlich zu dem, was @vladimir-matveev gesagt hat, können Sie auch ohne die Hilfe von zwischen ihnen konvertieren CStr oder 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);
    }
}

Stellen Sie nur sicher, dass bei der Konvertierung von einem &str in einen C-String Ihr &str mit endet '\0'. Beachten Sie, dass ich im obigen Code verwende strlen(c_s)+1 Anstatt von strlen(c_s)Also s ist "Hello World!\0"nicht nur "Hello World!".
(Natürlich funktioniert es in diesem speziellen Fall auch nur mit strlen(c_s). Aber mit einem frischen &str konnten Sie nicht garantieren, dass der resultierende C-String wie erwartet enden würde.)
Hier ist das Ergebnis der Ausführung des Codes:

s == "Hello World!\u{0}"
Hello World!

  • Sie können konvertieren aus ohne CStr, aber es hat keinen Grund, es zu vermeiden. Ihre Umwandlung zurück ist falsch als Rost &str ist nicht NUL-terminiert, also kein gültiger C-String.

    – Schäfer

    19. Januar 2018 um 14:31 Uhr

  • @Shepmaster, Ja, ein Rust &str ist im Allgemeinen nicht NUL-terminiert, aber da dieser aus einer C-Zeichenfolge erstellt wurde, funktioniert es gut, wenn Sie dies tun s.as_ptr(). Zur Verdeutlichung habe ich das jetzt korrigiert strlen(c_s) zu strlen(c_s)+1.

    – DesNerger

    19. Januar 2018 um 14:58 Uhr

  • Sie haben also jetzt die Funktionalität aus der Standardbibliothek repliziert? Bitte bearbeiten Sie Ihre Frage, um zukünftigen Lesern zu erklären, warum sie diese Lösung im Gegensatz zu der vorhandenen Antwort auswählen sollten.

    – Schäfer

    19. Januar 2018 um 15:00 Uhr

  • Ein Grund dafür ist, dass Sie in einer no_std-Umgebung entwickeln.

    – Myk Melez

    2. August 2019 um 16:59 Uhr

  • Wenn Sie CStr in einer no_std-Umgebung benötigen, ist die github.com/Amaneu/cstr_core Kiste ist eine gute Wahl. Das einzige Manko ist, dass es von cty abhängt, das eine offene Zusammenführungsanforderung hat, um die AVR-Unterstützung zu beheben.

    – Mutant Bob

    7. Februar um 15:48 Uhr

1420710cookie-checkWie konvertiere ich einen C-String in einen Rust-String und zurück über FFI?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy