简单介绍 Rust 和 C 之间的相互代码调用。
简介
首先简单介绍如何通过动态库、静态库进行相互调用,除了单独使用之外,还可以通过 Rust 的 build.rs
管理编译过程。
自动编译
Cargo
提供了 build.rs
机制,工程目录下存在 build.rs
文件时,则先编译并执行 build.rs
,并将其输出的作为 rustc
编译参数,然后再编译 Rust
工程,这样就可以先编译 C 代码,并将相关参数传递给 rustc
编译。
简单示例可以使用 Makefile
,复杂工程建议使用 CMake
类似管理,如下以 CMake
为例进行介绍。
CMake
对应的 CMakeLists.txt
文件为。
CMAKE_MINIMUM_REQUIRED(VERSION 3.9)
PROJECT(sysx)
SET(SRC_LIST src/math.c)
SET(LIB_STATIC_NAME ${PROJECT_NAME}-static)
SET(LIB_SHARED_NAME ${PROJECT_NAME}-shared)
ADD_LIBRARY(${LIB_STATIC_NAME} STATIC ${SRC_LIST})
SET_TARGET_PROPERTIES(${LIB_STATIC_NAME} PROPERTIES OUTPUT_NAME ${PROJECT_NAME})
ADD_LIBRARY(${LIB_SHARED_NAME} SHARED ${SRC_LIST})
SET_TARGET_PROPERTIES(${LIB_SHARED_NAME} PROPERTIES OUTPUT_NAME ${PROJECT_NAME})
INSTALL(TARGETS ${LIB_STATIC_NAME} DESTINATION .)
INSTALL(TARGETS ${LIB_SHARED_NAME} DESTINATION .)
项目会依赖 cmake-rs
三方库,可以通过 cargo add --build cmake
添加,同时需要确保在 CMakeLists.txt
文件中包含 install
目标,对应的 build.rs
代码如下,此时会在 target/debug
目录下生成对应二进制文件。
fn main() {
// 通过 CMake 生成对应的库,可以时动态或者静态库
use cmake::Config;
let dst = Config::new("ansic").build();
// 若 build.rs 有任何修改,则重新编译
println!("cargo:rerun-if-changed=build.rs");
// 搜索生成的函数库
println!("cargo:rustc-link-search=native={}", dst.display());
// 链接生成的函数库,可以是动态或者静态库,这里选择静态库
println!("cargo:rustc-link-lib=static=sysx");
//println!("cargo:rustc-link-lib=dylib=sysx");
}
如果不想引入依赖,那么就可以通过命令编译,对应的 build.rs
示例如下。
use std::process::Command;
fn main() {
let libs = "libsysx";
let target = "target/libsysx";
std::fs::create_dir(target).unwrap();
Command::new("cmake")
.args(&["-S", libs, "-B", target])
.status()
.unwrap();
Command::new("cmake")
.args(&["--build", target])
.status()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rustc-link-search=native={}", target);
println!("cargo:rustc-link-lib=static=sysx");
}
注意,当前两种方式并不会检查 C
库的修改,所以,修改后需要先 cargo clean
然后再编译。
调用实践
常见的一些场景就是参数传递、内存管理。
基础类型参数
C 调用 Rust
最常见的是基础类型,例如 int
bool
等,只需要注意两种不同类型语言之间的差异即可。
#[no_mangle]
pub extern "C" fn add(left: u64, right: u64) -> u64 {
left + right
}
#include <stdio.h>
#include <stdint.h>
uint64_t add(uint64_t, uint64_t);
int main(void) {
printf("%ld\n", add(10, 20));
return 0;
}
另外,在 Rust 的 std::os::raw
中包含很多 c_xxx
的类型声明,其对应了 C 中的类型,例如 c_int
就是 C 语言中的 int
类型。
数组参数
C 调用 Rust
这里只展示 Rust 的代码。
#[no_mangle]
pub extern fn sum(arr: *const c_int, length: c_int) -> c_int {
let nums = unsafe {
assert!(!arr.is_null());
slice::from_raw_parts(arr, length as usize)
}
let sum = nums.iter().fold(0, |acc, v| acc + v);
sum as c_int
}
结构体参数
C 调用 Rust
在 Rust 需要指定与 C 相同的布局方式,或者 #[repr(C, packed)]
不对齐。
#[repr(C)]
pub struct Rectangle {
width: u64,
height: u64,
}
#[no_mangle]
pub extern "C" fn area(rect: Rectangle) -> u64 {
rect.width * rect.height
}
#include <stdint.h>
#include <stdio.h>
struct Rectangle {
uint64_t width, height;
};
uint64_t area(struct Rectangle);
int main(void) {
struct Rectangle rect = {10, 20};
printf("%ld\n", area(rect));
return 0;
}
另外,如果使用的是引用,而 C 语言中不存在,可以通过 Rust 中的原始指针,包括了 *const T
*mut T
两种,分别对应了 &T
和 &mut T
两种类型。在 Rust 中的引用会进行一系列的检查,而 raw pointer 则会忽略,使用时需要 unsafe
关键字。
示例如下。
#[repr(C)]
pub struct Rectangle {
width: u64,
height: u64,
}
#[no_mangle]
pub extern "C" fn area(rect: *const Rectangle) -> u64 {
if rect.is_null() {
return 0;
}
//unsafe { (*rect).width * (*rect).height }
let rec: &Rectangle = unsafe { &(*rect) };
rec.height * rec.width
}
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
struct Rectangle {
uint64_t width, height;
};
uint64_t area(struct Rectangle *);
int main(void) {
struct Rectangle *rect = malloc(sizeof(struct Rectangle));
rect->width = 10;
rect->height = 20;
printf("%ld\n", area(rect));
free(rect);
return 0;
}
在上述的 Rust 代码中,可以直接将 *const
修改为 *mut
然后进行修改。
字符串
C 调用 Rust
C 语言中的字符串为 char *
类型,而且是 \0
结尾,而 Rust 中则保存了长度,所以,无法直接使用 String
或者 str
类型,而是需要通过 CString
和 CStr
进行转接。
其中 CString
用于 Rust->C
的转换,而 CStr
反之。另外,String::new()
会复制底层数据,而 CStr::new()
则不会。
如下是最简单的 C 调用 Rust 代码。
use std::ffi::CStr;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn hey(ptr: *const c_char) {
let name = unsafe { CStr::from_ptr(ptr).to_str().expect("bad encoding") };
println!("Hello {}", name); // NOTE: not owned now.
let owned = name.to_owned();
println!("Hello {}", owned); // Will auto drop.
}
#include <stdlib.h>
#include <string.h>
void hey(char *);
int main(void) {
char *name = malloc(6);
strcpy(name, "World");
hey(name);
free(name);
return 0;
}
注意,这里使用的是 CStr
而不是 CString
类型,而且不要通过 CString::from_raw()
构造,该函数只接受 CString::into_raw()
的返回值。
特征调用
Rust 中的 trait object
采用的是胖指针,一般会在 Rust 中定义对应的特征,通过 C 实现对应的函数,如果需要,可以用 Rust 实现相关的结构对函数进行封装,例如 Rust Trait 中的示例。
另外,还有一种场景,在 Rust 中提供了函数,不过其入参是 dyn
类型,例如 Log 的实现,此时需要通过 Rust 的回调函数调用,这里就以此为例介绍如何处理。
异步调用
这一部分会比较复杂,单独拆出来,通过 python3 -m http.server 9090
命令,在本地启动一个静态文件服务器,如下是通过 tokio
和 reqwest
简单读取某个文件内容。
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let body = reqwest::Client::new()
.get("http://127.0.0.1:9090/test.md")
.send()
.await?
.text()
.await?;
println!("{}", body);
Ok(())
}
如下是简单实现。
use once_cell::sync::Lazy;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::sync::{Arc, Mutex};
use tokio::runtime::Runtime;
static RUNTIME: Lazy<Runtime> = Lazy::new(|| Runtime::new().unwrap());
pub struct ReqContext {
complete: bool,
response: Option<String>,
}
#[no_mangle]
pub extern "C" fn request_send(ptr: *const c_char) -> *mut Arc<Mutex<ReqContext>> {
let url = unsafe { CStr::from_ptr(ptr).to_str().expect("bad encoding") };
let req_ctx_raw = Arc::new(Mutex::new(ReqContext {
complete: false,
response: None,
}));
println!("Rust handle send for {}", url);
let req_ctx_clone = req_ctx_raw.clone();
let future = async move {
let body = reqwest::Client::new()
.get(url)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
println!("Rust {}", body);
let mut res = req_ctx_raw.lock().unwrap();
res.complete = true;
res.response = Some(body);
};
RUNTIME.spawn(future);
Box::into_raw(Box::new(req_ctx_clone))
}
#[no_mangle]
pub extern "C" fn request_is_complete(ptr: *mut Arc<Mutex<ReqContext>>) -> bool {
let ctx = unsafe {
assert!(!ptr.is_null());
&*ptr
};
let res = ctx.try_lock();
if res.is_ok() {
res.unwrap().complete
} else {
false
}
}
#[no_mangle]
pub extern "C" fn request_get_result(ptr: *mut Arc<Mutex<ReqContext>>) -> *const c_char {
let ctx = unsafe {
assert!(!ptr.is_null());
&*ptr
};
let res = ctx.try_lock();
if res.is_ok() {
if let Some(body) = res.unwrap().response.as_ref() {
return CString::new(body.as_str()).unwrap().into_raw();
}
}
ptr::null()
}
#[no_mangle]
pub extern "C" fn request_free_body(ptr: *mut c_char) {
if ptr.is_null() {
return;
}
unsafe { drop(CString::from_raw(ptr)) };
}
#[no_mangle]
pub extern "C" fn request_free(ptr: Box<Mutex<ReqContext>>) {
drop(ptr);
}
#include <stdio.h>
#include <unistd.h>
struct reqwest *request_send(const char *);
int request_is_complete(struct reqwest *);
const char *request_get_result(struct reqwest *);
void request_free_body(const char *);
void request_free(struct reqwest *);
int main(void) {
struct reqwest *req = request_send("http://127.0.0.1:9090/data.txt");
while (!request_is_complete(req)) {
printf("Waiting for fetching data.\n");
sleep(1);
}
const char *body = request_get_result(req);
if (body != NULL) {
printf("Got data: %s\n", body);
request_free_body(body);
} else {
printf("Got empty data\n");
}
request_free(req);
return 0;
}
编译过程中建议使用静态库,同时添加 OpenSSL 的依赖 -lssl -lcrypto -lm
库。
其它
libc
libc 是对各个平台库的原始 FFI 绑定,用于与最基础的 C 库交互,支持 std/no-std
场景,提供了 C 相关的类型、常量、函数、宏等内容,使用时需要通过 unsafe
方式调用,
在使用 FFI
时,在 std::os::*::raw
中也提供了很多类似的类型定义等,可以用于一些简单的 C 代码交互,只要不涉及底层 API 的调用就不需要 libc
库。另外,类似 std::os::unix::raw
模块已经 Deprecated
了,使用时需要注意。
Rust 封装库
因为历史原因,有很多优秀的 C 库实现,可以参考 LMDB-RS 的实现。
总结
内存谁申请谁释放。不要假设使用何种内存分配器,可能是 tcmalloc
jemalloc
等,所以,不要将 malloc
申请的转换为 Box
然后 drop
掉,反之,通过 Box::into_raw()
获取的指针也不要通过 free
释放。
所有权管理,常见的 Box
Arc
CStr
CString
类型提供了 as_ptr()
into_raw()
from_raw()
几种与原始指针相关的函数,前者仍然持有对象,会按照 Rust 的规则执行 Drop 操作,而后者,则将所有权进行了转移。
参考
- Foreign Function Interface Rust 死灵书中的相关介绍。
- Quiche 一个 QUIC/HTTP3 协议库,同时提供了 C 接口,不过均采用同步调用,没有异步接口实现。