最近在研究Rust,目前大多數項目都可以使用Rust開發,但是涉及到和其他語言交互,比如用Rust開發一個SDK,一般還是需要導出C接口。
那如何將Rust導出C接口?
Rust的FFI就是專門做這件事的。一個正常的Rust public接口長這樣:
pub fn hello_world() -> i32 {
20
}
如果要把一個Rust函數導出為C接口,需要對它進行改造:
pub extern "C" fn hello_world() -> i32 {
20
}
它相比于純Rust函數有兩點不同:
一個是extern "C":表示導出C接口
一個是#[no_mangle]:正常一個C++或者Rust函數相關的符號都特別長且難以理解,使用它表示導出的符號是C符號,即沒有任何多余的修飾,函數名是啥樣,相關的符號大概就是啥樣,如圖:
如何導出C的動態庫或者靜態庫?
如果想要編譯出動態庫或者靜態庫,可以在Cargo.toml中配置crate-type:
[lib]
crate-type = ["cdylib"] # Creates dynamic lib
# crate-type = ["staticlib"] # Creates static lib
cylib表示導出動態庫,staticlib表示導出靜態庫。
如何生成對應的C頭文件?
一個C庫一般都有個頭文件,那Rust怎么生成一個C的頭文件呢?可以使用cbindgen:
cbindgen --config cbindgen.toml --crate hello_world --output hello_world.h
其中cbindgen.toml是一個template文件,我后面鏈接中列了具體地址。
上面的hello_world函數,我使用cbindgen就可以自動生成一個頭文件:
extern "C" {
int32_t hello_world(void);
} // extern "C"
至于如何使用更復雜的類型和C交互,比如我想導出和傳入一個結構體的指針,比如我想設置const char*,以及怎么管理對應的內存?
可以直接看這段代碼:
use std::boxed::Box;
use std::ffi::CStr;
use std::ffi::CString;
use std::os::raw::c_char;
pub struct Manager {
path: CString,
}
fn get_default_cstring() -> CString {
CString::new("").expect("new string failed")
}
#[no_mangle]
pub extern "C" fn manager_create() -> *mut Manager {
println!("{}", "create_manager().".to_string());
Box::into_raw(Box::new(Manager {
path: get_default_cstring(),
}))
}
#[no_mangle]
pub extern "C" fn manager_destroy(ptr: *mut Manager) {
if ptr.is_null() {
return;
}
// safe
unsafe {
let _b = Box::from_raw(ptr);
}
}
impl Manager {
#[no_mangle]
pub extern "C" fn manager_set_path(&mut self, p: *const c_char) {
unsafe {
self.path = CString::from(CStr::from_ptr(p));
}
}
#[no_mangle]
pub extern "C" fn manager_get_function(&self) -> *const c_char {
self.path.as_ptr()
}
}
#[no_mangle]
pub extern "C" fn hello_world() -> i32 {
20
}
也許有人不太理解Rust代碼的含義,那可以直接看它對應的C Header:
typedef struct Manager Manager;
extern "C" {
int32_t hello_world(void);
struct Manager *manager_create(void);
void manager_destroy(struct Manager *ptr);
const char *manager_get_function(const struct Manager *self);
void manager_set_path(struct Manager *self, const char *p);
} // extern "C"
通過manager_create創建的內存,需要通過manager_destroy銷毀。
但是在對應的Rust代碼沒有用到申請或者銷毀內存相關的代碼,而是使用Box來管理內存,它可以理解為C++中的unique_ptr。
內存都通過對象來管理,避免Rust申請的內存,讓C這邊來釋放,違反代碼的開發準則。
當然,如果你非要在Rust層想malloc和free內存,可以使用libc的crate。
如果想在Rust中調用C或者C++代碼,可以使用cxx crate,也很方便,比如Rust中的String、Vec都在cxx中有對應的類型。
但是我的目的是使用Rust開發一個C的動態庫的SDK,兩種方法都嘗試了下,感覺還是直接使用FFI更方便些。
我這里特意整理了一些Rust FFI相關資料,感興趣的可以看看
Rust如何調用C接口、Rust如何導出C接口、C的callback如何傳給Rust:https://doc.rust-lang.org/nomicon/ffi.html
The Embedded Rust Book FFI:https://docs.rust-embedded.org/book/interoperability/rust-with-c.html
使用cbindgen可以自動分析,將Rust接口導出為C接口:https://github.com/eqrion/cbindgen
cbindgen的default template:https://github.com/eqrion/cbindgen/blob/master/template.toml
cbindgen的doc:https://github.com/eqrion/cbindgen/blob/master/docs.md
Rust FFI的 example blog,主要是string如何傳出去并銷毀相關內存:https://snacky.blog/en/string-ffi-rust.html
cxx專用于Rust和C++之間的橋梁:https://github.com/dtolnay/cxx
cxx:https://cxx.rs/
Complex data types and Rust FFI blog:http://kmdouglass.github.io/posts/complex-data-types-and-the-rust-ffi/
Rust std ffi:https://doc.rust-lang.org/std/ffi/index.html
與C交互時,可以使用libc在Rust層做C的malloc和free
涉及ptr的地方需要了解:https://doc.rust-lang.org/std/ptr/index.html
還有些Rust入門資料:
https://www.zhihu.com/question/31038569
https://doc.rust-lang.org/rust-by-example/
https://doc.rust-lang.org/cargo/getting-started/installation.html
https://github.com/rustlang-cn/Rustt
https://github.com/sunface/rust-course
更多內容在 一個優質的C++學習圈 里,來一起鉆研C++和Rust吧。