共计 4524 个字符,预计需要花费 12 分钟才能阅读完成。
FFI
存在背景
FFI(Foreign Function Interface)
能够用来与其它语言进行交互,然而并不是所有语言都这么称说,例如 Java
称之为 JNI(Java Native Interface)
FFI
之所以存在是因为事实中很多代码库都是由不同语言编写的,如果咱们须要应用某个库,然而它是由其它语言编写的,那么往往只有两个抉择:
- 对该库进行重写或者移植
- 应用
FFI
FFI
实现须要面对的挑战
- 调用方语言是否涵盖了被调用语言的数据类型,
rust
作为调用方语言,涵盖了所有C
语言的数据类型,比方C
当中的int,double
对应了rust
中的i32,f64
类型 - 是否可能解决
C
的裸指针,包含指向被看作是字符串的数组指针,包含构造体指针
在 Rust
中调用 C
的办法
- 在
Rust
代码中应用extern
关键字申明要调用的C
函数 - 应用
unsafe
块调用它
留神点:须要手动解决参数和返回值的转换,可能产生异样报错
FFI
实现调用简略 C
函数
试验平台Ubuntu22.04 amd64 Desktop
调用了 C
规范库当中的数学库函数,abs
求绝对值,pow
求幂,sqrt
求平方根
use std::os::raw::{c_double, c_int}; | |
// 从规范库 libc 中引入三个函数。// 此处是 Rust 对三个 C 函数的申明:extern "C" {fn abs(num: c_int) -> c_int; | |
fn sqrt(num: c_double) -> c_double; | |
fn pow(num: c_double, power: c_double) -> c_double; | |
} | |
fn main() { | |
let x: i32 = -123; | |
// 每次调用都必须产生在一个 unsafe 区域内, 表明 Rust 对外部调用中可能存在的不平安行为不负责 | |
println!("{x}的绝对值是: {}.", unsafe { abs(x) }); | |
let n: f64 = 9.0; | |
let p: f64 = 3.0; | |
println!("{n}的 {p} 次方是: {}.", unsafe { pow(n, p) }); | |
let mut y: f64 = 64.0; | |
println!("{y}的平方根是: {}.", unsafe { sqrt(y) }); | |
y = -3.14; | |
println!("{y}的平方根是: {}.", unsafe { sqrt(y) }); //** NaN = NotaNumber(不是数字)} |
上述程序的输入是
-123 的绝对值是: 123. | |
9 的 3 次方是: 729. | |
64 的平方根是: 8. | |
-3.14 的平方根是: NaN. |
FFI
调用自定义地位的 C
库
我的项目构造如下
. | |
├── build.rs | |
├── Cargo.lock | |
├── Cargo.toml | |
├── hello | |
│ └── hello.c | |
└── src | |
└── main.rs |
编辑 hello/hello.c
文件
#include <stdio.h> | |
void hello() {printf("Hello, build script!!!!\n"); | |
} |
在 Cargo.toml
退出构建时依赖
[build-dependencies] | |
cc = "1.0" |
批改build.rs
fn main() { | |
// 示意在 hello/hello.c 文件产生批改的时候须要从新运行 build 脚本 | |
println!("cargo:rerun-if-changed=hello/hello.c"); | |
let mut builder: cc::Build = cc::Build::new(); | |
builder | |
.file("./hello/hello.c") | |
.compile("hello"); | |
} |
批改src/main.rs
// 应用 extern 申明 hello 是 C 外面的函数 | |
extern "C" {fn hello(); | |
} | |
fn main() { | |
// rust 调用的时候须要应用 unsafe 包裹 | |
unsafe {hello(); | |
} | |
println!("Hello, world!"); | |
} |
运行
$ cargo run | |
Hello, build script!!!! | |
Hello, world! |
FFI
调用简单函数
本示例蕴含构造体指针的传递
当初应用 C
库中的函数asctime
header
文件地位是 /usr/include/time.h
,函数定义如下,通过传入一个tm
类型的构造体指针,返回一个日期格局为 Day Mon dd hh:mm:ss yyyy\n
的字符串
/* Return a string of the form "Day Mon dd hh:mm:ss yyyy\n" | |
that is the representation of TP in this format. */ | |
extern char *asctime (const struct tm *__tp) __THROW; |
因为波及到大量的模板代码和类型转换,须要应用 bindgen
工具从 C 语言的头文件生成 rust
代码放慢开发速度,缩小低级谬误,提高效率
工具由 rust
语言官网保护,地址
https://github.com/rust-lang/rust-bindgen
Debian/Ubuntu
系列的装置依赖
$ sudo apt install llvm-dev libclang-dev clang
其余零碎的装置依赖参考
https://rust-lang.github.io/rust-bindgen/requirements.html
装置命令行工具
$ cargo install bindgen-cli
比方当初转换 /usr/include/time.h
文件
在 rust
的我的项目根门路下执行命令
$ bindgen /usr/include/time.h > src/mytime.rs
之后比照 /usr/include/time.h
和src/mytime.rs
查看 C
语言的原始代码,找到函数 asctime
和构造体 tm
的定义
/* 源码地位 /usr/include/time.h */ | |
/* Return a string of the form "Day Mon dd hh:mm:ss yyyy\n" | |
that is the representation of TP in this format. */ | |
extern char *asctime (const struct tm *__tp) __THROW; | |
/* 源码地位 /usr/include/x86_64-linux-gnu/bits/types/struct_tm.h | |
尽管执行的命令是 bindgen /usr/include/time.h > src/mytime.rs | |
然而工具依然会主动扫描转换相干头文件定义的构造体类型 | |
*/ | |
/* ISO C `broken-down time' structure. */ | |
struct tm | |
{int tm_sec; /* Seconds. [0-60] (1 leap second) */ | |
int tm_min; /* Minutes. [0-59] */ | |
int tm_hour; /* Hours. [0-23] */ | |
int tm_mday; /* Day. [1-31] */ | |
int tm_mon; /* Month. [0-11] */ | |
int tm_year; /* Year - 1900. */ | |
int tm_wday; /* Day of week. [0-6] */ | |
int tm_yday; /* Days in year.[0-365] */ | |
int tm_isdst; /* DST. [-1/0/1]*/ | |
# ifdef __USE_MISC | |
long int tm_gmtoff; /* Seconds east of UTC. */ | |
const char *tm_zone; /* Timezone abbreviation. */ | |
# else | |
long int __tm_gmtoff; /* Seconds east of UTC. */ | |
const char *__tm_zone; /* Timezone abbreviation. */ | |
# endif | |
}; | |
#endif |
查看 src/mytime.rs
文件外面的代码,找到 asctime
函数以及 tm
构造体的定义
extern "C" {pub fn asctime(__tp: *const tm) -> *mut ::std::os::raw::c_char; | |
} | |
#[repr(C)] | |
#[derive(Debug, Copy, Clone)] | |
pub struct tm { | |
pub tm_sec: ::std::os::raw::c_int, | |
pub tm_min: ::std::os::raw::c_int, | |
pub tm_hour: ::std::os::raw::c_int, | |
pub tm_mday: ::std::os::raw::c_int, | |
pub tm_mon: ::std::os::raw::c_int, | |
pub tm_year: ::std::os::raw::c_int, | |
pub tm_wday: ::std::os::raw::c_int, | |
pub tm_yday: ::std::os::raw::c_int, | |
pub tm_isdst: ::std::os::raw::c_int, | |
pub tm_gmtoff: ::std::os::raw::c_long, | |
pub tm_zone: *const ::std::os::raw::c_char, | |
} |
批改 src/main.rs
文件内容
use std::ffi::{c_char, CStr, CString}; | |
// mytime.rs 作为 main.rs 的一个 mod | |
mod mytime; | |
fn main() { | |
// 因为 mytime::mm 构造体外面的 tm_zone 字段是一个字符串指针 | |
// 先创立一个字符串 | |
let timezone = CString::new("UTC").unwrap(); | |
// 从 mytime 中导入一个 tm 构造体,填写参数如下 | |
let mut time_value = mytime::tm { | |
tm_sec: 1, | |
tm_min: 1, | |
tm_hour: 1, | |
tm_mday: 1, | |
tm_mon: 1, | |
tm_year: 1, | |
tm_wday: 1, | |
tm_yday: 1, | |
tm_isdst: 1, | |
tm_gmtoff: 1, | |
// 此处转换为指针类型 | |
tm_zone: timezone.as_ptr()}; | |
unsafe { | |
// 裸指针 | |
let c_time_value_ptr = &mut time_value; | |
// 获取一个 c 字符串指针 | |
let asctime_result_ptr = mytime::asctime(c_time_value_ptr); | |
// 从指针再转变为 CStr 类型 | |
let c_str = CStr::from_ptr(asctime_result_ptr); | |
// 最初再转变为 rust 的 &str 类型,返回的是一个 Result 类型,可能会产生 utf- 8 编码谬误 | |
println!("{:?}", c_str.to_str()); | |
} | |
} |
运行
$ cargo run | |
Ok("Mon Feb 1 01:01:01 1901\n") |
因为 bindgen
是把整个 /usr/include/time.h
文件外面的所有函数和构造体都转换到 rust
中,所以编译 rust
的时候会产生很多的 function *** is never used,constant **** should have an upper case name
这种告警,这个是失常的
参考浏览
从 Rust
调用 C
库函数 – linux cn
Rust
调用 C
程序的实现步骤
C
规范库
rust
语言圣经 – FFI