Rust FFI 編程 - 手動綁定 C 庫入門 03

2021-02-20 Rust語言中文社區
所有權是Rust中最核心的關注點之一。在Rust中,變量有嚴格的所有權關係,並於此之上建立了一整套上層建築。本篇,我們對Rust調用C場景下的一種數據所有權場景進行編程。之前例子為什麼不需要關心所有權上一篇的兩個示例,實際是將Rust中的數據傳到C中執行。為什麼沒有涉及所有權的問題呢?這裡就來分析一下。


int sum(const int* my_array, int length) { int total = 0;
for(int i = 0; i < length; i++) { total += my_array[i]; } return total;}


use std::os::raw::c_int;
extern "C" { fn sum(my_array: *const c_int, length: c_int) -> c_int;}
fn main() { let numbers: [c_int; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
unsafe { let total = sum(numbers.as_ptr(), numbers.len() as c_int); println!("The total is {}", total);
assert_eq!(total, numbers.iter().sum()); }}

Rust這邊,將數組中的 int 元素傳到C函數中執行相加運算。int本身這種基礎類型,默認按值傳遞(copy一份傳遞)。

fn main() {    // 初始化    let mut v: Vec<u8> = vec![0; 80];    // 初始化結構體    let mut t = time::tm {        tm_sec: 15,        tm_min: 09,        tm_hour: 18,        tm_mday: 14,        tm_mon: 04,        tm_year: 120,        tm_wday: 4,        tm_yday: 135,        tm_isdst: 0,    };    // 期望的日期格式    let format = b"%Y-%m-%d %H:%M:%S\0".as_ptr();        unsafe {        // 調用        time::strftime_in_rust(v.as_mut_ptr(), 80, format, &mut t);
let s = match str::from_utf8(v.as_slice()) { Ok(r) => r, Err(e) => panic!("Invalid UTF-8 sequence: {}", e), }; println!("result: {}", s); }}

將Rust中初始化的結構體,轉換成指針,傳遞到C函數中進行調用。本身只是借用讀一下(不寫)。這個結構體的所有權一直在 Rust 這邊,由 t 掌控(表述不完全準確,但基本上是這個意思。原因是抽象降級到C這一層的時候,就不再自動分辨所有權了)。生命期結束時,由Rust的RAII規則,自動銷毀。以後,我們對於int這種自帶 Copy(或按值傳遞)的類型,就不重點關注了,兩邊對照寫就行了,沒有什麼有難度的地方在裡面。Rust 調用 C,內存在 C 這邊分配,在Rust中進行填充為了分析清楚這個場景,我們設計了一個例子。在實現的過程中,遇到了相當多的坑。這方面的資料,中英文都非常缺乏。好在,經過一番摸索,最後算是找到了正確的方法。在C端,設計一個結構體,欄位有整型,字符串,浮點型在C端,malloc一塊內存,是一個n個結構體實例組成的數組C端,導出三個函數。create, print, release在Rust中,調用C的create函數,創建一個資源,並拿到指針在Rust中,利用這個指針,填充C中管理的結構體數組假如我們創建了一個名為 rustffi 的cargo工程。


#include<stdio.h>#include<stdlib.h>#include<malloc.h>
typedef struct Students { int num; int total; char name[20]; float scores[3];} Student;
Student* create_students(int n) { if (n <= 0) return NULL; Student *stu = NULL; stu = (Student*) malloc(sizeof(Student)*n);
return stu;}
void release_students(Student *stu) { if (stu != NULL) free(stu);}
void print_students(Student *stu, int n) { int i; for (i=0; i<n; i++) { printf("C side print: %d %s %d %.2f %.2f %.2f\n", stu[i].num, stu[i].name, stu[i].total, stu[i].scores[0], stu[i].scores[1], stu[i].scores[2]); }}

gcc -fPIC -shared -o libcfoo.so cfoo.c

use std::os::raw::{c_int, c_float};use std::ffi::CString;use std::slice;
#[repr(C)]#[derive(Debug)]pub struct Student { pub num: c_int, pub total: c_int, pub name: [u8; 20], pub scores: [c_float; 3],}
#[link(name = "cfoo")]extern "C" { fn create_students(n: c_int) -> *mut Student; fn print_students(p_stu: *mut Student, n: c_int); fn release_students(p_stu: *mut Student);}
fn main() { let n = 3; unsafe { let p_stu = create_students(n as c_int); assert!(!p_stu.is_null());
let s: &mut [Student] = slice::from_raw_parts_mut(p_stu, n as usize); for elem in s.iter_mut() { elem.num = 1 as c_int; elem.total = 100 as c_int;
let c_string = CString::new("Mike").expect("CString::new failed"); let bytes = c_string.as_bytes_with_nul(); elem.name[..bytes.len()].copy_from_slice(bytes);
elem.scores = [30.0 as c_float, 40.0 as c_float, 30.0 as c_float]; }
println!("rust side print: {:?}", s);
print_students(p_stu, n as c_int);
release_students(p_stu); } println!("Over.");}

RUSTFLAGS='-L .' cargo build

編譯。這裡,RUSTFLAGS='-L .' 指定要連結的 so 的目錄。我把上面生成的 libcfoo.so 放到了工程根目錄,因此,指定路徑為 .,其它類推。

LD_LIBRARY_PATH="." target/debug/rustffi

rust side print: [Student { num: 1, total: 100, name: [77, 105, 107, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [30.0, 40.0, 30.0] }, Student { num: 1, total: 100, name: [77, 105, 107, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [30.0, 40.0, 30.0] }, Student { num: 1, total: 100, name: [77, 105, 107, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [30.0, 40.0, 30.0] }]C side print: 1 Mike 100 30.00 40.00 30.00C side print: 1 Mike 100 30.00 40.00 30.00C side print: 1 Mike 100 30.00 40.00 30.00Over.

可以看到,達到了我們的預期目標:在Rust中,修改C中創建的結構體數組內容。完整可運行代碼在:https://github.com/daogangtang/learn-rust/tree/master/08rustffi要點(踩坑)分析C和Rust的結構體定義,兩邊要保持一致。

typedef struct Students {  int num;  int total;  char name[20];  float scores[3];} Student;

pub struct Student {    pub num: c_int,    pub total: c_int,    pub name: [u8; 20],    pub scores: [c_float; 3],}

pub struct Student {    pub num: c_int,    pub total: c_int,    pub name: *mut c_char,    pub scores: [c_float; 3],}

結果可以編譯通過,但是一運行就發生段錯誤。讀者可以想一想為什麼?:D關於C中數組指針的翻譯問題

fn create_students(n: c_int) -> *mut Student;

*mut Student 感覺只是指向一個實例的指針,或者說分不清是一個實例還是一個實例數組。對,發現這點就對了,C語言裡面,這個就是這樣的,也不分(。。。從現在來看這個設計,其實有點奇葩)。所以C裡面,在知道指針的情況下,還需要一個長度數據才能準確界定一個數組。既然這樣,那我們就這樣寫就行了。另外兩個接口中的參數也是類似情況,不再說明。神器 slice
Rust的slice提供的兩個方法:slice::from_raw_parts() 和 slice::from_raw_parts_mut()。這個東西是神器。實現了我們這個場景下的核心要求,資源在C那邊管理,Rust這邊只是借用。但是填數據又是在Rust這邊。搜索標準庫,我們會發現,Vec也有這兩個方法。這其實是對應的。slice的這兩個方法,不獲取數據的所有權。Vec的這兩個方法,獲取數據的所有權(必要的時候,會進行完全Copy一份)。於是可以看到,Rust中的所有權基礎,直接影響到了API的設計和使用。C字符串的細節

let c_string = CString::new("Mike").expect("CString::new failed");            let bytes = c_string.as_bytes_with_nul();

這裡這個 as_bytes_with_nul() 就是轉成字節的時候,帶上後面的 '\0'。

elem.name[..bytes.len()].copy_from_slice(bytes);

這個目的就是把我們生成的數據源slice,填充到目標slice,也就是成員的 name 字符中去。

elem.name[0] = b'M';elem.name[1] = b'i';elem.name[2] = b'k';elem.name[3] = b'e';elem.name[4] = b'\0';

c_charc_char 內部定義為 i8,我們這裡用的 u8,關係不大,用 c_char 的話,用 as 操作符轉一下就好了。所有權分析整個Rust代碼,實際就是調用了C導出的函數。C那邊的數據資源,完全由C自己掌控,分配和釋放都是C函數自己做的(這點非常重要)。Rust這邊只是可變借用,然後填充了數據。因為在這種跨FFI邊界調用的情況下,內存的分配,完全可能不是同一個分配器做的,混用會出各種 undefined behaviour。所以,這些細節一定要注意。

同時也可以看到,Rust和C竟然可以這樣玩兒?Rust太強大了。除了C++,我暫時還想不到其它有什麼語言能直接與C這樣互操作的。

Rust 調 C,數據在 Rust 這邊生成,在C中進行處理

相關焦點

  • Rust FFI 編程 - 手動綁定 C 庫入門 05
    其實我們遇到的問題,在 C 的領域,早就是一種常見的問題(比如一個 GUI 庫的回調函數),所以其實也早就有對應的解決方案,比如,使用 C 中的魔幻主義的 void * 攜帶一個數據塊傳遞。了解過 void * 的就知道,它和 C 中的其它指針一起,幾乎把 C 變成了一門動態語言(所以有一種說法認為 C 其實是弱類型語言?)。
  • Rust 語言新人入門指南
    如果你抱著之前 1 天上手 Python, 2 天入門 Go 的經驗和優越感來學習 Rust 的話,你可能會遭遇嚴重的失敗感。如果你來自 Haskell/Ocaml 等函數式語言社區,你會有相當的親切感。對於有豐富 C++ 開發經驗的同學來說,上手可能相對比較容易。
  • ​半小時入門Rust,這是一篇Rust代碼風暴
    本文介紹的就是 Rust,作者表示,通過解析大量代碼,「半個小時」就能入門 Rust。Rust 是一門系統程式語言,專注於安全,尤其是並發安全。它支持函數式和命令式以及泛型等編程範式的多範式語言,且 TensorFlow 等深度學習框架也把它作為一個優秀的前端語言。
  • 【Rust日報】 2019-08-28:Rust異步代碼的優勢:相比於其他語言更加容易調試
    「系列文章」在Rust中使用C庫#C #FFi兩篇文章介紹了如何綁定C庫,並且將其抽象為安全的方法調用。
  • 學習編程道路上的入門書籍之C篇
    學習編程專欄連載編程學習編程道路上的入門書籍之C篇,此篇內容將包含一些算法以及數據結構相關內容,文章中的所有推薦的書籍均來自知乎社區大牛力薦書籍、豆瓣評分較高書籍、各語言社區比較熱門書籍以及京東、亞馬遜、噹噹熱銷書籍的重合書籍。
  • Rust 語言入門
    該關鍵字在編寫與非 Rust 函數的綁定時最常見。此特性使得 Rust 對作業系統開發或嵌入式(裸機)編程等領域很有用。更好的錯誤處理無論您使用何種程式語言,都會發生錯誤。在 Rust 中,錯誤分為兩個陣營:不可恢復的錯誤(壞的種類)和可恢復的錯誤(不太壞的種類)。不可恢復的錯誤Rust panic!
  • 【Rust日報】2020-03-10 - A C# programmer 嘗試 Rust - Part 1
    licensed basis if necessary on the same basisSupports assets all common languages and formats (Rust and Cargo being just one of dozens of supported formats)https://blog.cloudsmith.io/2020/03
  • C程序編程四步走
    本文使用的測試代碼是經典入門程序 "Hello World!"。測試環境為探究預處理,編譯,彙編和連結的功能,我們在 Ubuntu 系統中使用 Gcc 編譯器( version=4.8.4 ),用簡單的也是最經典的入門程序 "Hello World!" 作為測試代碼。
  • 繼C/C+之後,微軟如何全面採用 Rust 的?
    由於C ++不是一種內存安全的語言,因此在其代碼庫中會彈出很多內存錯誤,並且需要花費大量時間來修復它們。 去年,Microsoft開始研究可以幫助解決其內存安全問題的替代程式語言。作為這些努力的結果,Microsoft已開始嘗試在某些情況下將Rust集成到其代碼庫中。
  • Electron 調用 原生代碼的正確打開方式,不是ffi!
    我們知道,nodejs 主要使用 ffi 實現調用原生代碼,這點毋庸置疑,而 Electron 你想使用 ffi 卻不行,實際上,electron 需要較高版本的 nodejs 而 ffi 卻不支持高版本 nodejs,筆者這裡是 nodejs 12 這個版本。
  • 半小時入門Rust,這是一篇Rust代碼風暴
    本文介紹的就是 Rust,作者表示,通過解析大量代碼,「半個小時」就能入門 Rust。Rust 是一門系統程式語言,專注於安全,尤其是並發安全。它支持函數式和命令式以及泛型等編程範式的多範式語言,且 TensorFlow 等深度學習框架也把它作為一個優秀的前端語言。
  • 為什麼選擇Rust?
    在很長一段時間內,像Haskell這樣的函數式程式語言的一個殺手級特性就是可以形式證明代碼,而傳統的命令式程式語言由於廣泛使用了共享可變性、不安全的指針運算和無法控制的副作用,仍然無法應用形式證明。但是Rust可以改變這一點,儘管它是一種命令式語言,但它已然「在進行形式化證明的路上」。
  • 零基礎學編程,加工中心編程入門指導
    對於數控加工來說,編程至關重要,直接影響到加工的質量與效率,由於編程較為複雜,不少加工中心的從業者也沒有經過製造業相關學院的學習的系統學習,相信大家也是對編程又愛又恨吧。那麼如何迅速掌握數控加工中心的編程技巧呢?下面與成海小編一起學習一下吧!
  • Rust Cookbook 中文版 - Rust 生態中使用各類 crate 來完成編程任務的良好實踐
    《Rust Cookbook 中文版》是 Rust 程序設計語言(Rust 2018 簡體中文版文檔)的簡要實例示例集合:展示了在 Rust 生態系統中,使用各類 crate 來完成常見編程任務的良好實踐
  • Rust是最有前途的區塊鏈程式語言?
    各程式語言PK:Solidity、Python最易學,Rust難度最高對於『哪種語言最難學』這個問題,每個人都有自己的想法,衡量的標準也不太一樣,那如果按開發者頭髮濃密度的標準來看,那麼我們可初步地排個名
  • 【譯】對Rust中的std::io::Error的研究
    首先 ,從底層庫暴露出的錯誤會其成為公開 API 的一部分。如果你的依賴庫出現重大變更,那麼你也需要進行大量修改。其次,它規定了所有的實現細節。例如,如果你留意到ConnectionDiscovery很大,對其進行 boxing 將會是一個破壞性的改變。
  • 蘋果Apple Swift程式語言中文版入門教程
    蘋果Apple Swift程式語言中文版入門教程 來源:www.18183.com作者:集落時間:2014-06-03 蘋果在WWDC第一天早晨的kyenote中說道全新的開發者程式語言
  • 和C++ 相比,我為什麼要選擇 Rust 來開發軟體?
    作為一門系統程式語言,Rust 一直致力於解決高並發和高安全性系統等問題。和老牌的 C++ 相比,Rust 的性能也毫不遜色。但曾幾何時,因為上手難、用戶量少、社區不活躍等諸如問題讓想要入門的開發者感到迷茫,如今新的一年已經開始,我們是否真的有必要學習 Rust?
  • 我們為什麼選擇Rust開發頂尖實時通信產品?|應用程式|代碼|編譯器|...
    https://doc.rust-lang.org/stable/book/  https://doc.rust-lang.org/book/ch08-02-strings.html  與其他許多語言不同,在 Rust 中通常有一種顯而易見的「正確方法」來做各種事情。
  • 如何入門中學生編程
    相信很多同學對編程很感興趣,但是卻不知道如何入門以及入門難度如何,廢話少說,直接進入主題。首先,學習編程不難,難的是自己的想法(如果有同學學習過就知道了)。其次,如何入門。對安卓開發有興趣的同學,入門學習 java ,這是因為安卓開發用到的程式語言就是 java。(聽說谷歌出了 kotlin...)