跳过正文

外部函数调用:FFI

··1916 字
Rust
rust-lang - 这篇文章属于一个选集。
§ 18: 本文

外部函数和外部全局 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 的 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 部分内容:

  1. C 库(动态或静态) 文件;
  2. 在 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。

rust-lang - 这篇文章属于一个选集。
§ 18: 本文

相关文章

不安全:unsafe
··824 字
Rust
Rust
借用:refer/borrow
··3127 字
Rust
Rust 引用类型和借用
函数、方法和闭包:function/method/closure
··7032 字
Rust
Rust 函数、方法和闭包
包和模块:package/crate/module
··2066 字
Rust
Rust 项目的包和模块组织结构