rust调用ffi函数
下文提到的ffi皆指cffi。
Rust作为一门系统级语言,自带对ffi调用的支持。
Getting Start
引入libc库
由于cffi
的数据类型与rust
不完全相同,我们需要引入libc
库来表达对应ffi
函数中的类型。
在Cargo.toml
中添加以下行:
在你的rs文件中引入库:
在以前libc
库是和rust
一起发布的,后来libc被移入了crates.io
通过cargo安装。
声明你的ffi
函数
ffi
函数就像c语言
需要#include
声明了对应函数的头文件一样,rust
中调用ffi
也需要对对应函数进行声明。
声明一个ffi
库需要一个标记有#[link(name = "yourlib")]
的extern
块。name
为对应的库(so
/dll
/dylib
/a
)的名字。 如:如果你需要snappy
库(libsnappy.so
/libsnappy.dll
/libsnappy.dylib
/libsnappy.a
), 则对应的name
为snappy
。 在一个extern块
中你可以声明任意多的函数和变量。
调用ffi函数
声明完成后就可以进行调用了。 由于此函数来自外部的c库,所以rust并不能保证该函数的安全性。因此,调用任何一个ffi
函数需要一个unsafe
块。
封装unsafe
,暴露安全接口
unsafe
,暴露安全接口作为一个库作者,对外暴露不安全接口是一种非常不合格的做法。在做c库的rust binding
时,我们做的最多的将是将不安全的c接口封装成一个安全接口。 通常做法是:在一个叫ffi.rs
之类的文件中写上所有的extern块
用以声明ffi函数。在一个叫wrapper.rs
之类的文件中进行包装:
对外暴露(pub use) your_func_wrapper
函数即可。
数据结构对应
libc
为我们提供了很多原始数据类型,比如c_int
, c_float
等,但是对于自定义类型,如结构体,则需要我们自行定义。
结构体
rust
中结构体默认的内存表示和c并不兼容。如果要将结构体传给ffi函数,请为rust
的结构体打上标记:
此外,如果使用#[repr(C, packed)]
将不为此结构体填充空位用以对齐。
Union
比较遗憾的是,rust到目前为止(2016-03-31)还没有一个很好的应对c的union的方法。只能通过一些hack来实现。(对应rfc)
Enum
和struct
一样,添加#[repr(C)]
标记即可。
回调函数
和c库打交道时,我们经常会遇到一个函数接受另一个回调函数的情况。将一个rust
函数转变成c可执行的回调函数非常简单:在函数前面加上extern "C"
:
对应c库代码:
字符串
rust为了应对不同的情况,有很多种字符串类型。其中CStr
和CString
是专用于ffi
交互的。
CStr
对于产生于c的字符串(如在c程序中使用malloc
产生),rust使用CStr
来表示,和str
类型对应,表明我们并不拥有这个字符串。
在这里get_string
使用CStr::from_ptr
从c的char*
获取一个字符串,并且转化成了一个String.
注意to_string_lossy()的使用:因为在rust中一切字符都是采用utf8表示的而c不是,
因此如果要将c的字符串转换到rust字符串的话,需要检查是否都为有效
utf-8
字节。to_string_lossy
将返回一个Cow<str>
类型,即如果c字符串都为有效
utf-8
字节,则将其0开销地转换成一个&str
类型,若不是,rust会将其拷贝一份并且将非法字节用U+FFFD
填充。
CString
和CStr
表示从c中来,rust不拥有归属权的字符串相反,CString
表示由rust分配,用以传给c程序的字符串。
注意c字符串中并不能包含\0
字节(因为\0
用来表示c字符串的结束符),因此CString::new
将返回一个Result
, 如果输入有\0
的话则为Error(NulError)
。
不透明结构体
C库存在一种常见的情况:库作者并不想让使用者知道一个数据类型的具体内容,因此常常提供了一套工具函数,并使用void*
或不透明结构体传入传出进行操作。 比较典型的是ncurse
库中的WINDOW
类型。
当参数是void*
时,在rust中可以和c一样,使用对应类型*mut libc::c_void
进行操作。如果参数为不透明结构体,rust中可以使用空白enum
进行代替:
C代码:
空指针
另一种很常见的情况是需要一个空指针。请使用0 as *const _
或者 std::ptr::null()
来生产一个空指针。
内存安全
由于ffi
跨越了rust边界,rust编译器此时无法保障代码的安全性,所以在涉及ffi操作时要格外注意。
析构问题
在涉及ffi调用时最常见的就是析构问题:这个对象由谁来析构?是否会泄露或use after free? 有些情况下c库会把一类类型malloc
了以后传出来,然后不再关系它的析构。因此在做ffi操作时请为这些类型实现析构(Drop Trait
).
可空指针优化
当rust
的一个enum
为一种特殊结构:它有两种实例,一种为空,另一种只有一个数据域的时候,rustc会开启空指针优化将其优化成一个指针。 比如Option<extern "C" fn(c_int) -> c_int>
会被优化成一个可空的函数指针。
ownership处理
在rust中,由于编译器会自动插入析构代码到块的结束位置,在使用owned
类型时要格外的注意。
由于as_ptr
接受一个&self
作为参数(fn as_ptr(&self) -> *const c_char
),as_ptr
以后ownership
仍然归rust所有。因此rust会在函数退出时进行析构。 正确的做法是使用into_raw()
来代替as_ptr()
。由于into_raw
的签名为fn into_raw(self) -> *mut c_char
,接受的是self
,产生了ownership
转移, 因此danger
函数就不会将cstring
析构了。
panic
由于在ffi
中panic
是未定义行为,切忌在cffi
时panic
包括直接调用panic!
,unimplemented!
,以及强行unwrap
等情况。 当你写cffi
时,记住:你写下的每个单词都可能是发射核弹的密码!
静态库/动态库
前面提到了声明一个外部库的方式--#[link]
标记,此标记默认为动态库。但如果是静态库,可以使用#[link(name = "foo", kind = "static")]
来标记。 此外,对于osx的一种特殊库--framework
, 还可以这样标记#[link(name = "CoreFoundation", kind = "framework")]
.
调用约定
前面看到,声明一个被c调用的函数时,采用extern "C" fn
的语法。此处的"C"
即为c调用约定的意思。此外,rust还支持:
stdcall
aapcs
cdecl
fastcall
vectorcall //这种call约定暂时需要开启abi_vectorcall feature gate.
Rust
rust-intrinsic
system
C
win64
bindgen
是不是觉得把一个个函数和全局变量在extern块
中去声明,对应的数据结构去手动创建特别麻烦?没关系,rust-bindgen
来帮你搞定。 rust-bindgen
是一个能从对应c头文件自动生成函数声明和数据结构的工具。创建一个绑定只需要./bindgen [options] input.h
即可。 项目地址
Last updated