FFI
存在背景
FFI(Foreign Function Interface)
可以用来与其它语言进行交互,是用一种编程语言写的程序能调用另一种编程语言写的函数,但是并不是所有语言都这么称呼,例如 Java
称之为 JNI(Java Native Interface)
FFI
之所以存在是由于现实中很多代码库都是由不同语言编写的,如果我们需要使用某个库,但是它是由其它语言编写的,那么往往只有两个选择:
- 对该库进行重写或者移植
- 使用
FFI
FFI
调用原理
- 前提可能性:计算机的运算,最底层的数据/代码都是以二进制的形式存在,所有的语言在编译后,都会以二进制的形式去执行(即使编译后的代码为字节码,虚拟机在运行的时候,也会继续翻译成 CPU 认识的二进制指令,比如
java
)这就为不同语言间的调用提供了可能性 ABI
的诞生:二进制太底层了,没有大家一致认可的调用约定,程序之间不可能互通的,于是诞生了ABI
(二进制接口),是调用约定,类型表示和名称修饰这三者的统称,计算机的发展中出现了各种ABI
规范,详情查看https://zh.wikipedia.org/wiki/X86%E8%B0%83%E7%94%A8%E7%BA%A6%E5%AE%9A#cdecl
大致规范包含如下
cdecl
:起源于C
语言的一种调用约定syscall
: 与cdecl
类似,参数被从右到左推入堆栈中。EAX, ECX和EDX
不会保留值。参数列表的大小被放置在AL寄存器中syscall
是32位OS/2 API
的标准optlink
: 在IBM VisualAge
编译器中被使用pascal
:基于Pascal
语言的调用约定,参数从左至右入栈register
:Borland fastcall
的别名stdcall
:stdcall
是由微软创建的调用约定,是Windows API
的标准调用约定。非微软的编译器并不总是支持该调用协议fastcall
: 此约定还未被标准化,不同编译器的实现也不一致thiscall
: 在调用C++
非静态成员函数时使用此约定,基于所使用的编译器和函数是否使用可变参数,有两个主流版本的thiscall
winapi
:WINAPI
是平台的缺省调用约定,Windows
操作系统上默认是StdCall
;Windows CE
上默认是Cdecl
Intel ABI
: 根据Intel ABI,EAX、EDX及ECX
可以自由在过程或函数中使用,不需要保留System V
: 主要在Solaris,GNU/Linux,FreeBSD
和其他非微软OS
上使用
目前IT 工业的基石,绝大部分是由
C
语言写成,所以绝大多数库都遵循cdecl
(或C
)规范,rust
支持主流的ABI
规范,包括cdecl/C
详情参考
https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions
FFI
实现需要面对的挑战
- 调用方语言是否涵盖了被调用语言的数据类型,
rust
作为调用方语言,涵盖了所有C
语言的数据类型,比如C
当中的int,double
对应了rust
中的i32,f64
类型 - 是否能够处理
C
的裸指针,包括指向被看作是字符串的数组指针,包括结构体指针 - 两边要同时引用一个可变对象的时候,可能会遇到问题
- 复杂对象或类型,在映射到两边的时候,可能会有一些不协调甚至失真的现象
- 类型系统/对象组合模型/继承机制等其它细节,可能在跨语言的时候,成为障碍
在 Rust
中调用 C
的方法
- 在
Rust
代码中使用extern
关键字声明要调用的C
函数 - 使用
unsafe
块调用它
注意点:需要手动处理参数和返回值的转换,可能发生异常报错
调用简单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.
调用自定义位置的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!
调用复杂函数
本示例包含结构体指针的传递
现在使用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
这种告警,这个是正常的
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。