外部函数和外部全局 item(常量、变量、类型定义等)必须在 extern “ABI” {} block 中声明。
external block 中只能声明 static 变量(对应 C 的全局变量或常量)和函数(只是函数签名接口),编译器再根据 ABI 或 #[link(name="crypto")]
attr macro 链接到指定的库(如 name 指定的 libcrypto 库)。
link kind 支持:dylib、static、framework (MacOS)、raw-dylib(windows)。如果是 static 链接类型,则缺省使用 bundle 机制来将依赖的 static lib 打包到二进制中。
#[cfg(target_family = "unix")]
#[link(name = "readline", kind = "dylib")]
extern {
// extern block 中只能使用 static 变量和函数签名。
// 静态常量
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() {
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, 默认是栈展开, 而不会导致程序直接退出的;
- 参考: https://doc.rust-lang.org/nomicon/ffi.html
Rust 的 std::ffi 和 libc crate
都提供了 C 类型的 Rust 类型别名,选择使用一个均可:
- 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 函数:
use std::ffi::c_char;
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())
}
}
#[repr(C)]
pub struct git_error {
pub message: *const c_char,
pub klass: c_int
}
通过 #[link] 指定 extern block 中全局变量/常量、函数签名所在的库名称:
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 库时搜索的目录。
fn main() {
println!(r"cargo:rustc-link-search=native=/home/jimb/libgit2-0.25.1/build");
}
上面只解决了链接时查找共享库的问题, 在运行时有可能还是找不到动态库, 解决办法是设置环境变量:
export LD_LIBRARY_PATH=/home/jimb/libgit2- 0.25.1/build:$LD_LIBRARY_PATH
注:对于 MacOS 是设置 DYLD_LIBRARY_PATH
环境变量。
如果一个 Rust crate 是专用于调用 C 库的 Rust 接口封装, 则该 crate 的命名惯例是 LIB-sys
, 其中 LIB
是 C 库的名称,该 crate 一般包含 2 部分内容:
- C 库(动态或静态) 文件;
- 在 extern block 中声明的 C 库中的变量或函数的 Rust 封装表示;
bindgen
crate 提供了根据 C 库 header 文件自动生成 Rust extern block 封装表示的功能:
#![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());
}
C 函数常见的情况:先分配一块内容,然后将内存地址传递给函数,函数内的逻辑来修改指针指向的内容。
一般来说,Rust 要求借用/指针指向的内存必须有效,也就是内存一般需要先初始化再使用。但 Rust 也提供了
std::mem::MaybeUninit<T>
类型, 他告诉编译器为 T 分配内存, 但是不做任何处理, 直到后续明确告诉他可以安全地操作这一块内存区域.
MaybeUninit<T>
拥有这一块内存区域, 编译器不会做一些优化和操作, 从而避免非预期的行为:
- MaybeUninit.as_mut_ptr() 返回这个内存区域的 *mut T 指针, 可以传递给 FFI 函数使用;
- 然后调用 MaybeUninit.assume_init() 来将内存区域标记为已初始化;
除了通过 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。