tft每日頭條

 > 科技

 > rust語言實現雙向鍊表

rust語言實現雙向鍊表

科技 更新时间:2025-01-10 11:30:55
摘要:Rust 得以在編程語言中火速崛起的原因之一,就是它能夠與 C 語言進行互操作。因此在本文中,作者介紹了在 Rust 與 C 之間傳遞字符串的七種方式,這些方法不僅可用于傳遞字符串,也可用于其他數據。

原文鍊接

:https://dev.to/kgrech/7-ways-to-pass-a-string-between-rust-and-c-4ieb

聲明:本文為 CSDN 翻譯,未經授權,禁止轉載。

作者 | Konstantin Grechishchev

譯者 | 彎月

出品 | CSDN(ID:CSDNnews)

Rust和C語言能夠互操作,這恐怕是Rust最不可思議的功能之一。能夠在C語言中調用安全地Rust代碼,能夠在Rust中通過C接口使用一些流行庫,正是Rust能夠在整個行業迅速流行的原因之一。此外,我們還可以通過C接口,用不同的語言編寫代碼,這樣凡是能夠調用C的語言都可以使用這些代碼。

FFI接口的編寫難度很高,新手很難成功。如何處理 into_raw 和 as_ptr 方法,才不會導緻内存洩漏或引發安全漏洞?在編寫代碼時,我們難免會使用一些不安全的關鍵字,這會令我們不安。

我将在本文中介紹有關FFI接口的内存處理,并提供一些我在項目中使用過的有效模式。

(注意:這裡我以字符串作為例進行說明,實際上這些技術也适用于将字節數組或指針傳輸到 Box 或 Arc 類型的堆結構上。)

rust語言實現雙向鍊表(Rust與C)1

rust語言實現雙向鍊表(Rust與C)2

基本規則

在學習如何實現FFI函數之前,我想先介紹一些基本的規則。你應該在設計的過程中牢記這些規則,因為缺少其中任何一個都可能引發各種bug,最終導緻函數全面崩潰或内存洩漏。

規則1:一個指針,一個分配器

你可能會認為内存分配隻不過是調用一些操作系統API。然而實際上,獲取一大塊内存,寫入緩存區是一項複雜且開銷很大的操作。編譯器和庫開發人員很想應用各種優化,比如獲得更大的内存塊以避免頻繁調用操作系統API,而且實現方式也各異。

你不應該假設調用庫的人會使用某種類型的内存分配器。他們不一定會使用malloc,而且也不會受限于libc。換句話說,Rust代碼分配的内存應該由Rust代碼删除,越過FFI邊界獲取的指針應該交還給創建者去釋放。如果使用malloc分配内存,請不要将其轉換為Box,然後drop。我們應該通過調用Box::into_raw獲取的指針,不應該通過調用free來釋放。

規則2:所有權

Rust是一種内存安全語言,會明确指出所有權。如果在代碼中看到Box<dyn Any>,你就知道在你drop Box之後,存儲Any的内存會被立即釋放。相反,如果看到void*,則無法判斷是應該調用free釋放内存,還是由其他人來釋放這些内存(或許這些内存壓根不需要被釋放,因為它指向堆棧)。

在Rust中,将結構轉化為原始指針的方法有一種命名約定。标準的庫,比如Box、Arc、CStr和CString提供了as_ptr,還有一對into_raw和from_raw方法。并非每個結構都提供這三種方法,因此實際情況更加混亂。

我們來具體讨論一下這些庫。首先是CString,它提供以上三種方法,as_ptr和into_raw方法都提供了相同類型的指針。然而,就像上面提到的void*一樣,這些指針的所有權略有不同。

as_ptr方法以引用的形式接受&self。這意味着,在as_ptr返回後,CString實例依然會留在棧上,而數據的所有權也會保留。換句話說,返回的指針指向的數據仍歸CString實例所有。一旦删除實例,指針就會懸空。在删除CString實例後,你永遠不應再使用此指針。在安全的Rust中,指針的此屬性由引用的生命周期(類似于指針)表示,并由編譯器控制,但如果使用原始指針,一切都将變成未知。

與as_ptr不同,into_raw會通過值接受并銷毀self。那麼,會不會破壞釋放内存?事實證明,into_raw不會調用drop方法。它會創建一個自己擁有的指針,然後将Rust分配器提供的内存塊“洩漏”出來,脫離Rust編譯器的控制範圍。如果你隻删除該指針,而不調用from_raw方法,就會引發内存洩漏。但是,它永遠不會懸空(除非在調用from_raw之前修改或克隆它)。

如果你想讓C暫時“借用”Rust的内存,則應該使用as_ptr。它有一個巨大的優勢,因為 C 代碼不必釋放這塊内存,而且還會限制指針的生命周期。但請不要将這個指針保存到某個全局結構中,或将其傳遞給另一個線程,也不應該将這樣的指針作為函數調用的結果返回。

into_raw方法會将數據的所有權轉移到C中。隻要代碼需要,它就可以保留指針,但請務必記得将它轉移回Rust删除。

字符串的内存表示

不幸的是,在Rust和語言C中,字符串的表示方式不同。C的字符串通常是char*指針,指向以 /0 結尾的char數組。而Rust則會保存字符數組及其長度。

由于這個原因,Rust的String和str類型與原始指針之間不應該互相轉換。你應該使用CString和CStr中間類型來實現。通常,我們使用CString将Rust字符串傳遞給C代碼,使用CStr将C的字符串轉換為Rust的&str。請注意,這種轉換并不一定會複制底層的數據。因此,通過CStr獲得的&str會指向C分配的數組,而且它的生命周期與指針綁定。

注意:String:new會複制數據,但CStr::new不會。

rust語言實現雙向鍊表(Rust與C)3

項目設置

如何将Rust和C連接起來

網上有很多關于如何構建C代碼,以及使用build.rs将C連接到Rust crate的資料,但是如何将Rust代碼添加到C項目的文章卻很少。相比之下,我更喜歡用C語言實現主要功能,并使用CMake作為構建系統。我希望CMake項目将Rust crate作為庫,并根據Rust代碼生成C的頭文件。

通過CMake運行Cargo

我建立了一個簡單的CMake 3控制台應用程序。

首先,我們需要定義構建Rust庫的命令和保存Rust成果物的位置:

if (CMAKE_BUILD_TYPE STREQUAL "Debug")set(CARGO_CMD RUSTFLAGS=-Zsanitizer=address cargo build -Zbuild-std --target x86_64-unknown-linux-gnu)set(TARGET_DIR "x86_64-unknown-linux-gnu/debug")else set(CARGO_CMD cargo build --release)set(TARGET_DIR "release")endif SET(LIB_FILE "${CMAKE_CURRENT_BINARY_DIR}/${TARGET_DIR}/librust_lib.a")

對于熟悉Rust的人來說,這個構建crate調試版本的命令可能看起來有點古怪。我們完全可以使用cargo build來代替這個命令,但是我想利用Rust不穩定的地址清理器功能來确保内存不會被洩漏。

其次,我們需要自定義命令和目标,讓它們根據命令輸出結果。然後,我們可以定義一個名為rust_lib的靜态導入庫,并根據目标構建它:

add_custom_command(OUTPUT ${LIB_FILE}COMMENT "Compiling rust module"COMMAND CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} ${CARGO_CMD}WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/rust_lib)add_custom_target(rust_lib_target DEPENDS ${LIB_FILE})add_library(rust_lib STATIC IMPORTED GLOBAL)add_dependencies(rust_lib rust_lib_target)

最後,我們可以使用将二進制文件與Rust庫(以及其他必需的系統庫)鍊接在一起。我們還在C代碼中啟用了地址清理器:

target_compile_options(rust_c_interop PRIVATE -fno-omit-frame-pointer -fsanitize=address)target_link_libraries(rust_c_interop PRIVATE Threads::Threads rust_lib ${CMAKE_DL_LIBS} -fno-omit-frame-pointer -fsanitize=address)

如此一來,運行CMake即可自動構建rust create,并與之鍊接。但是,我們還需要從C代碼中調用Rust的方法。

生成C的頭文件,并将它們添加到CMake項目中

最簡單的在Rust代碼中獲取C頭文件的方法是使用cbingen庫。

我們可以将以下代碼添加到Rust crate的build.rs文件中,以檢測Rust中定義的所有extern "C"函數,為其生成頭文件定義,并保存到include/目錄下:

let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap;let package_name = env::var("CARGO_PKG_NAME").unwrap;let output_file = PathBuf::from(&crate_dir).join("include").join(format!("{}.h", package_name));cbindgen::generate(&crate_dir).unwrap.write_to_file(output_file);

此外,我們還應該在Rust crate的根目錄中創建cbindgen.toml文件,并指明language = "C"。

接下來,CMake需要在Rust crate的include文件夾中查找頭文件:

SET(LIB_HEADER_FOLDER "${CMAKE_CURRENT_SOURCE_DIR}/rust_lib/include")set_target_properties(rust_libPROPERTIESIMPORTED_LOCATION ${LIB_FILE}INTERFACE_INCLUDE_DIRECTORIES ${LIB_HEADER_FOLDER})

rust語言實現雙向鍊表(Rust與C)4

将Rust字符串傳遞到C的五種方式

一切準備就緒。下面,我們來看看如何從Rust的數據中獲取字符串,然後在C中使用。我們怎麼才能安全地傳遞字符串,同時不會造成内存洩漏?

方法1:提供創建和删除方法

如果不知道C代碼需要使用字符串多久,就可以采用這種方式。為了将所有權移交給C,我們可以構建CString對象,并使用into_raw将其轉換為指針。free方法隻需要構建CString,再drop這個對象就可以釋放内存:

#[no_mangle]pub extern fn create_string -> *const c_char {let c_string = CString::new(STRING).expect("CString::new failed");c_string.into_raw // Move ownership to C}/// # Safety/// The ptr should be a valid pointer to the string allocated by rust#[no_mangle]pub unsafe extern fn free_string(ptr: *const c_char) {// Take the ownership back to rust and drop the ownerlet _ = CString::from_raw(ptr as *mut _);}

不要忘記調用free_string,以避免内存洩漏:

const char* rust_string = create_string;printf("1. Printed from C: %s\n", rust_string);free_string(rust_string);

不要調用libc free方法,也不要嘗試修改此類指針指向的數據。

這個方法雖然效果很好,但如果我們想在使用内存時釋放Rust庫,或者在不知道Rust庫的代碼中釋放内存,該怎麼辦?你可以考慮以下三種方法。

方法2:分配緩沖區并複制數據

還記得規則1嗎?如果我們想在C中使用free方法釋放内存,就應該使用malloc分配内存。但是,Rust怎麼會知道malloc呢?一種解決方案是,“問一問”Rust需要多少内存,然後為它分配一個緩沖區:

size_t len = get_string_len;char *buffer = malloc(len);copy_string(buffer);printf("4. Printed from C: %s\n", buffer);free(buffer);

Rust隻需要告訴我們緩沖區的大小,并小心翼翼地将Rust字符串複制到其中(注意不要漏掉末尾的字節0):

#[no_mangle]pub extern fn get_string_len -> usize {STRING.as_bytes.len 1}/// # Safety/// The ptr should be a valid pointer to the buffer of required size#[no_mangle]pub unsafe extern fn copy_string(ptr: *mut c_char) {let bytes = STRING.as_bytes;let len = bytes.len;std::ptr::copy(STRING.as_bytes.as_ptr.cast, ptr, len);std::ptr::write(ptr.offset(len as isize) as *mut u8, 0u8);}

這個方法的優勢在于,我們不必實現free_string,可以直接使用free。還有一個優點是,如有需要C代碼也可以修改緩沖區(這就是我們使用*mut c_char,而不是*const c_char的原因)。

問題在于,我們仍然需要實現額外的方法get_string_len,而且還需要分配一塊新内存,并複制數據(但其實CString::new也需要)。

如果你想将Rust字符串移動到C函數棧上分配的緩沖區,也可以使用此方法,但應該确保有足夠的空間。

方法3:将内存分配器方法傳遞給Rust

我們可以避免使用get_string_len方法嗎?有沒有其他方法在Rust中分配内存?一種簡單的方法是将分配内存函數傳遞給Rust:

type Allocator = unsafe extern fn(usize) -> *mut c_void;/// # Safety/// The allocator function should return a pointer to a valid buffer#[no_mangle]pub unsafe extern fn get_string_with_allocator(allocator: Allocator) -> *mut c_char {let ptr: *mut c_char = allocator(get_string_len).cast;copy_string(ptr);ptr}

上述示例使用了的copy_string,接下來我們可以使用get_string_with_allocator:

char* rust_string_3 = get_string_with_allocator(malloc);printf("3. Printed from C: %s\n", rust_string_3);free(rust_string_3);

這個方法與方法2相同,而且優缺點也一樣。

但是,我們現在必須傳遞額外的參數allocator。其實,我們可以進行一些優化,将其保存到某個全局變量中,就可以避免向每個函數傳遞。

方法4:從Rust調用glibc

如果我們的C代碼會使用malloc/free來分配内存,則可以嘗試在Rust代碼中引入libc crate,盡管這種方式有點冒險:

#[no_mangle]pub unsafe extern fn get_string_with_malloc -> *mut c_char {let ptr: *mut c_char = libc::malloc(get_string_len).cast;copy_string(ptr);ptr}

C代碼不變:

char* rust_string_4 = get_string_with_malloc;printf("4. Printed from C: %s\n", rust_string_4);free(rust_string_4);

在這種方式下,我們不需要提供分配内存的方法,但是C代碼也會受到很多限制。我們最好做好文檔記錄,盡量避免使用這種方式,除非我們确定百分百安全。

方法5:借用Rust字符串

以上這些方法都是将數據的所有權傳遞給C。但如果我們不需要傳遞所有權呢?舉個例子,Rust代碼需要同步調用C方法,并向它傳遞一些數據。這時,可以考慮使用CString的as_ptr:

type callback = unsafe extern fn(*const c_char);#[no_mangle]pub unsafe extern fn get_string_in_callback(callback: Callback) {let c_string = CString::new(STRING).expect("CString::new failed");// as_ptr keeps ownership in rust unlike into_rawcallback(c_string.as_ptr)}

不幸的是,即便在這種情況下,CString:new也會複制數據(因為它需要在末尾添加字節0)。

C代碼如下:

void callback(const char* string) {printf("5. Printed from C: %s\n", string);}int main {get_string_in_callback(callback);return 0;}

如果有一個生命周期已知的C指針,則我們應該優先使用這種方式,因為它可以保證沒有内存洩漏。

rust語言實現雙向鍊表(Rust與C)5

将C字符串傳遞給Rust的兩種方法

下面,我們來介紹兩種反向操作的方法,即将C的字符串轉換為Rust的類型。主要方法有以下兩種:

  • 将C字符串轉換成&str,不複制數據;

  • 複制數據并接收字符串。

這兩種方法的示例相同,因為它們非常相似。實際上,方法2需要先使用方法1。

C代碼如下。我們在堆上分配數據,但實際上我們也可以将指針傳遞給棧:

char *test = (char*) malloc(13*sizeof(char));strcpy(test, "Hello from C");print_c_string(test);free(test);

Rust的實現如下:

#[no_mangle]/// # Safety/// The ptr should be a pointer to valid Stringpub unsafe extern fn print_c_string(ptr: *const c_char) {let c_str = CStr::from_ptr(ptr);let rust_str = c_str.to_str.expect("Bad encoding");// calling libc::free(ptr as *mut _); causes use after free vulnerabilityprintln!("1. Printed from rust: {}", rust_str);let owned = rust_str.to_owned;// calling libc::free(ptr as *mut _); does not cause after free vulnerabilityprintln!("2. Printed from rust: {}", owned);}

注意,此處我們使用了CStr,而不是CString。如果不是CString::into_raw創建的指針,請不要調用CString:from_raw。

這裡還需要注意,&str引用的生命周期不是“靜态”的,而是綁定到了c_str對象方法。Rust編譯器會阻止你在該方法之外返回&str,或将其移動到全局變量/另一個線程,因為一旦C代碼釋放内存,&str引用就會變成非法。

如果需要在Rust中長時間保留數據的所有權,隻需調用to_owned即可獲取字符串的副本。如果不想複制,則可以使用CStr,但我們應該确保C代碼不會在字符串還在使用期間釋放内存。

rust語言實現雙向鍊表(Rust與C)6

總結

在本文中,我們讨論了Rust與C之間的互操作,并介紹了幾種跨FFI邊界傳遞數據的方法。這些方法不僅可用于傳遞字符串,也可用于其他數據,或者利用FFI将Rust連接到其他編程語言。

希望本文能對你有所幫助,如有任何問題或反饋,請在下方留言。

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关科技资讯推荐

热门科技资讯推荐

网友关注

Copyright 2023-2025 - www.tftnews.com All Rights Reserved