Rust 可以使用和链接其它语言开发的外部库中的函数和外部全局 item(常量、变量、类型定义等),但在使用前必须在 extern block( extern "ABI" {}
) 中声明。
extern block 中只能声明 static
变量(对应 C 的全局变量或常量)和函数(只是函数签名),需要在 unsafe 中使用它们。
编译器根据 ABI 或 #[link(name="crypto", kind="dylib")]
链接到指定的库:
- name 指定不带 lib 前缀的库名称,如 crypto 表示 libcrypto;
- 链接类型(
link kind
)类型:dylib、static、framework (MacOS)、raw-dylib(windows)
; - 对于 static kind 链接类型,bindgen 缺省使用 bundle 机制来将依赖的 source code 打包到项目中;
#[cfg(target_family = "unix")]
#[link(name = "readline", kind = "dylib")]
// extern block 中只能使用 static 变量和函数签名。
extern {
// 静态常量
static rl_readline_version: libc::c_int;
// 静态变量
static mut rl_prompt: *const libc::c_char;
// 函数签名
fn with_name(format: *const u8, args: ...);
// 变长参数函数签名( 只有 extern block 中的函数签名的最后一个参数支持变参)
fn foo(x: i32, ...);
}
fn main() {
// 在 unsafe block 中使用 exten block 中的 static 变量和函数。
println!("You have readline version {} installed.", unsafe { rl_readline_version as i32 });
let prompt = CString::new("[my-awesome-shell] $").unwrap();
unsafe {
rl_prompt = prompt.as_ptr();
println!("{:?}", rl_prompt);
rl_prompt = ptr::null();
}
unsafe {
foo(10, 20, 30, 40, 50);
}
}
extern "C" {
fn foo(...);
fn bar(x: i32, ...);
fn with_name(format: *const u8, args: ...);
}
FFI 调用出现 excception/panic 时, Rust 不会进行栈展开, 可能会直接退出。
- 作为对比,如果 Rust 代码的非主线程发生 panic, 默认是栈展开, 而不会导致程序直接退出;
Rust 的 std::ffi 和 libc crate
都提供了 C 类型的 Rust 类型别名,优选标准库 std::ffi
:
- Rust 的
usize/isize
对应 C 的size_t/ptrdiff_t
; - Rust 的裸指针
*mut T
和*const T
, 对应 C 指针类型; - Rust 的
*const c_void
相当于 C 的const void *
,*mut c_void
相当于 C 的void *
;
C type | Corresponding std::ffi type |
---|---|
short | c_short |
int | c_int |
long | c_long |
long long | c_longlong |
unsigned short | c_ushort |
unsigned, unsigned int | c_uint |
unsigned long | c_ulong |
unsigned long long | c_ulonglong |
char | c_char |
signed char | c_schar |
unsigned char | c_uchar |
float | c_float |
double | c_double |
void *, const void * | *mut c_void, *const c_void |
type_alias! { "c_char.md", c_char = c_char_definition::c_char; #[doc(cfg(all()))] } // u8 或 i8
type_alias! { "c_schar.md", c_schar = i8; }
type_alias! { "c_uchar.md", c_uchar = u8; }
type_alias! { "c_short.md", c_short = i16; }
type_alias! { "c_ushort.md", c_ushort = u16; }
type_alias! { "c_int.md", c_int = c_int_definition::c_int; #[doc(cfg(all()))] } // i32
type_alias! { "c_uint.md", c_uint = c_int_definition::c_uint; #[doc(cfg(all()))] } // u32
type_alias! { "c_long.md", c_long = c_long_definition::c_long; #[doc(cfg(all()))] } // i64
type_alias! { "c_ulong.md", c_ulong = c_long_definition::c_ulong; #[doc(cfg(all()))] } // u64
type_alias! { "c_longlong.md", c_longlong = i64; }
type_alias! { "c_ulonglong.md", c_ulonglong = u64; }
type_alias! { "c_float.md", c_float = f32; }
type_alias! { "c_double.md", c_double = f64; }
pub type c_size_t = usize;
pub type c_ptrdiff_t = isize;
pub type c_ssize_t = isize;
extern block 使用 C ABI
惯例来传参和返回值, 都是 unsafe 函数,需要在 unsafe block 中调用:
use std::ffi::c_char;
// 默认链接 C 库
extern {
// 使用 raw pointer 来表示 C 函数的指针
fn strlen(s: *const c_char) -> usize;
// C 库中的全局变量 extern char **environ;
static environ: *mut *mut c_char;
}
// 从 Rust &str 创建 CString,然后转换为 *const c_char 类型,传递给 C 函数
let rust_str = "I'll be back";
let null_terminated = CString::new(rust_str).unwrap();
unsafe {
assert_eq!(strlen(null_terminated.as_ptr()), 12);
if !environ.is_null() && !(*environ).is_null() {
let var = CStr::from_ptr(*environ);
println!("first environment variable: {}", var.to_string_lossy())
}
}
// 使用 C struct 的内部布局
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int
}
通过 #[link(name = "xx")]
指定 extern block 中全局变量/常量、函数签名所在的库名称。该属性是可选的,例如:
- 通过 cc crate 生成的库会自动链接到 Rust 程序,这时不需要指定该属性。
- 在
.cargo/config.toml
中指定 rustc 编译器参数,添加 -l 和 -L 来指定要链接到 Rust 程序的库;
use std::ffi::c_int;
#[link(name = "git2")]
extern {
pub fn git_libgit2_init() -> c_int;
pub fn git_libgit2_shutdown() -> c_int;
}
fn main() {
unsafe {
git_libgit2_init();
git_libgit2_shutdown();
}
}
Rust 使用系统链接器, 如 linux 的 ld, 传递 -lgit2
参数。如果 git2 库被安装到非系统库目录, 则 Rust 编译器调用 ld 时可能会找不到该库。
解决办法:使用 build script
机制,在 Cargo.toml 同级目录下创建名为 build.rs 文件
, 该文件是一个含 main 函数的可执行程序,cargo build
时会先编译&执行该程序,它的输出为后续 cargo build
提供相关配置参数,例如输出链接 C 库时搜索的目录。
- build.rs 使用 Cargo.toml 文件中 build-dependencies 声明的依赖。
fn main() {
println!(r"cargo:rustc-link-search=native=/home/alizj/libgit2-0.25.1/build");
}
上面只解决了链接时查找共享库的问题, 在运行时有可能还是找不到动态库。解决办法: 设置环境变量 LD_LIBRARY_PATH
:
export LD_LIBRARY_PATH=/home/alizj/libgit2-0.25.1/build:$LD_LIBRARY_PATH
注:对于 MacOS 是设置 DYLD_LIBRARY_PATH
环境变量。
示例:
#![allow(non_camel_case_types)]
use std::os::raw::{c_int, c_char, c_uchar};
#[link(name = "git2")]
extern {
pub fn git_libgit2_init() -> c_int;
pub fn git_libgit2_shutdown() -> c_int;
pub fn giterr_last() -> *const git_error;
pub fn git_repository_open(out: *mut *mut git_repository, path: *const c_char) -> c_int;
pub fn git_repository_free(repo: *mut git_repository);
pub fn git_reference_name_to_id(out: *mut git_oid, repo: *mut git_repository, reference: *const c_char) -> c_int;
pub fn git_commit_lookup(out: *mut *mut git_commit, repo: *mut git_repository, id: *const git_oid) -> c_int;
pub fn git_commit_author(commit: *const git_commit) -> *const git_signature;
pub fn git_commit_message(commit: *const git_commit) -> *const c_char;
pub fn git_commit_free(commit: *mut git_commit);
}
#[repr(C)]
pub struct git_repository {
_private: [u8; 0]
}
#[repr(C)]
pub struct git_commit {
_private: [u8; 0]
}
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int
}
pub const GIT_OID_RAWSZ: usize = 20;
#[repr(C)]
pub struct git_oid {
pub id: [c_uchar; GIT_OID_RAWSZ]
}
pub type git_time_t = i64;
#[repr(C)]
pub struct git_time {
pub time: git_time_t,
pub offset: c_int
}
#[repr(C)]
pub struct git_signature {
pub name: *const c_char,
pub email: *const c_char,
pub when: git_time
}
use std::ffi::CStr;
use std::os::raw::c_int;
fn check(activity: &'static str, status: c_int) -> c_int {
if status < 0 {
unsafe {
let error = &*raw::giterr_last();
println!("error while {}: {} ({})",
activity,
CStr::from_ptr(error.message).to_string_lossy(),
error.klass);
std::process::exit(1);
}
}
status
}
check("initializing library", raw::git_libgit2_init());
unsafe fn show_commit(commit: *const raw::git_commit) {
let author = raw::git_commit_author(commit);
let name = CStr::from_ptr((*author).name).to_string_lossy();
let email = CStr::from_ptr((*author).email).to_string_lossy();
println!("{} <{}>\n", name, email);
let message = raw::git_commit_message(commit);
println!("{}", CStr::from_ptr(message).to_string_lossy());
}
// 主程序
use std::ffi::CString; use std::mem;
use std::ptr;
use std::os::raw::c_char;
fn main() {
let path = std::env::args().skip(1).next().expect("usage: git-toy PATH");
let path = CString::new(path).expect("path contains null characters");
unsafe {
check("initializing library", raw::git_libgit2_init());
let mut repo = ptr::null_mut();
check("opening repository", raw::git_repository_open(&mut repo, path.as_ptr()));
let c_name = b"HEAD\0".as_ptr() as *const c_char;
let oid = {
let mut oid = mem::MaybeUninit::uninit(); // 创建一个未初始化的内存区域
check("looking up HEAD", repo, c_name));
};
raw::git_reference_name_to_id(oid.as_mut_ptr(), oid.assume_init()
};
let mut commit = ptr::null_mut();
check("looking up commit", raw::git_commit_lookup(&mut commit, repo, &oid));
show_commit(commit);
raw::git_commit_free(commit);
raw::git_repository_free(repo);
check("shutting down library", raw::git_libgit2_shutdown());
}
除了通过 FFI 封装调用 C 库中的函数外,Rust 也支持将 Rust 代码编译为 C 动态库,供其它 C/C++ 程序调用和链接:
- 在导出的 Rust 函数签名前添加:
extern "C"
以及#[no_mangle]
属性; #[no_mangle]
用于指示 Rust 编译器不要对该函数改名,从而让其它语言链接时能从库中正确找到该 Rust 函数;- 该 Rust 函数不需要标记为 unsafe;
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
如果是 Rust 程序调用 C 库函数,还可以使用 dlopen2 crate。
bindgen #
A best practice is to offer a vendored feature (enabled by default) that builds the C/C++ code you ship in your repo (often as a git submodule). If someone wants to link against a system library instead, they can disable the feature.
Likewise, a bindgen feature lets you regenerate bindings if you want, but by default, you should check in the generated Rust bindings. This way, users don’t need to install LLVM or deal with bindgen unless they’re hacking on the FFI. One thing to note: you’ll most likely need differently generated bindgen files for different platforms. Some library’s won’t expose their platform-specific APIs in all platforms (for good reason), and very often you end up mismatching types (enums are notorious—on some platforms they’re represented as signed ints, and others they’re represented as unsigned ints, etc).
std::mem::MaybeUninit
#
C 函数常见的情况:先分配一块内容,然后将内存地址传递给函数,函数内的逻辑来修改指针指向的内容。
一般来说,Rust 要求借用/指针指向的内存必须有效,也就是内存一般需要先初始化再使用。
但 Rust 也提供了 std::mem::MaybeUninit<T>
类型, 它告诉编译器为 T 分配内存, 但是不做任何处理, 直到后续明确告诉他可以安全地操作这一块内存区域。
MaybeUninit<T>
拥有这一块内存区域, 编译器不会操作该区域, 从而避免非预期的行为:
MaybeUninit.as_mut_ptr()
返回这个内存区域的 *mut T 指针, 可以传递给 FFI 函数使用;- 然后调用
MaybeUninit.assume_init()
来将内存区域标记为已初始化;