Rust FFI 编程

Stella981
• 阅读 878

当我们拥有一组具有良好声明的头文件时,自己定义 C 库的 Rust FFI 绑定函数是毫无意义的。我们可以使用  bindgen  这种工具从 C 库的头文件生成 Rust FFI 绑定函数。然后,我们运行一些测试代码以验证其是否正常运行,并对它们进行调整,直到正确为止。

本文我们将通过一个示例,讨论如何使用  bindgen  将 C 库中的函数公开给 Rust。我们的目标是创建一个 crate 项目,其中包含一个 bindings.rs 文件,该文件代表 C 库的公共 API(包括函数,结构体,枚举等),然后通过将该 crate 导入其它项目中来调用原 C 库的功能。

上一篇我们介绍了使用 bindgen 为 C 库创建 Rust FFI 绑定有两种方式:使用  bindgen  命令行和使用  build.rs 。本文我们使用 build.rs 这种方式作为示例进行说明。

1. 设置 crate 项目

一般 Rust FFI 绑定的 crate 项目会包含构建和导出 C 库的 unsafe 函数, crate 的 Rust 标准命名约定为 lib<XXXX>-sys ,我们本次示例,针对 C 实现的 secp256k1 库生成 Rust FFI 绑定。

首先是设置 Cargo.toml ,添加 bindgen 作为构建时的依赖项,如下所示:

    [build-dependencies]bindgen = "0.55.1"

   
   
   

Cargo.toml 文件的 [build-dependencies] 部分,这样就声明了对  bindgen 的构建时依赖并使用了最新版本 v0.55.1,可随时通过  crates.io bindgen  页面获取最新的版本信息。

其次在 crate 项目的根目录下创建一个 build.rs 文件,用来编译和链接 bindgen 的导出。我们可以通过 C 库的源代码,也可以直接通过链接库,本文选择通过链接库的方式。创建  wrapper.h  文件内容如下:

    #include <secp256k1.h>

   
   
   

创建  build.rs 文件内容如下:

    fn main() {    println!("cargo:rustc-link-lib=secp256k1");    println!("cargo:rerun-if-changed=wrapper.h");    let bindings = bindgen::Builder::default()        .header("wrapper.h")        .parse_callbacks(Box::new(bindgen::CargoCallbacks))        .generate()        .expect("Unable to generate bindings");    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());    bindings        .write_to_file(out_path.join("bindings.rs"))        .expect("Couldn't write bindings!");}

   
   
   

其中: rustc-link-lib = [KIND =] NAME 用来指定 C 库,传递给 cargo 告知 Rust 编译器 rustc 链接 secp256k1 共享库,可选的  KIND  可以是  staticdylib ,默认值是动态库 dylib,有关更多详细信息,请参见  rustc --help

bindgen::Builderbindgen 的主要入口点,可让为生成的绑定配置各种选项。 .header 用来指定要生成绑定的头文件。 .parse_callbacks 是指当更改包含的任何头文件时,生成的 crate 无效。

可以通过 bindings.write_to_file 将绑定写入指定的文件,比如: $OUT_DIR/bindings.rs

2. 生成绑定

现在直接运行 cargo build ,将立即生成与 secp256k1 的 Rust FFI 绑定。生成的绑定文件位于 OUT_DIR/bindings.rs ,其中 $OUT_DIR 由 cargo 根据 build.rs 确定,默认类似于 ./target/debug/build/crate-package-name-afc7747d7eafd720/out/

bindings.rs 中有如下内容:

    #[repr(C)]#[derive(Debug, Copy, Clone)]pub struct secp256k1_context_struct {    _unused: [u8; 0],}pub type secp256k1_context = secp256k1_context_struct;#[repr(C)]#[derive(Copy, Clone)]pub struct secp256k1_pubkey {    pub data: [::std::os::raw::c_uchar; 64usize],}

   
   
   

由于 Rust 与 C 不同,不允许对结构体进行单独的声明和定义。我们可以看到 bindgen 用了一个私有的大小为零的类型字段,这是其默认执行的操作。

同时, bindgen 会将 C 中的 const 指针转换为Rust 中的  const * ,并将没有修饰符的 C 指针转换为 mut * 。如下所示:

    extern "C" {    pub fn secp256k1_context_create(flags: ::std::os::raw::c_uint) -> *mut secp256k1_context;}extern "C" {    pub fn secp256k1_ec_pubkey_create(        ctx: *const secp256k1_context,        pubkey: *mut secp256k1_pubkey,        seckey: *const ::std::os::raw::c_uchar,    ) -> ::std::os::raw::c_int;}

   
   
   

3. 使用生成的绑定,测试

我们可以使用 include!  宏将生成的绑定直接转储到 crate 项目的入口中 src/lib.rs

    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

   
   
   

然后,我们可以编写测试,以验证生成的 Rust FFI 是否可以正常工作:

    #[test]fn test_create_pubkey() {    // secp256k1返回公钥    let mut pubkey: secp256k1_pubkey = secp256k1_pubkey {        data: [0; 64],    };    let prikey: u8 = 1;    unsafe {        let context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);        assert!(!context.is_null());        let ret = secp256k1_ec_pubkey_create(& *context, &mut pubkey, &prikey);        assert_eq!(ret, 1);    }}

   
   
   

完整代码:https://github.com/lesterli/rust-practice/tree/master/ffi/secp256k1-sys

自定义生成的绑定

如果生成的绑定,我们可以通过以下几种方式对结构体,枚举等进行调整:

  • 使用 build.rs 时,通过 bindgen::Builder 的配置方法。

  • 使用 bindgen 命令行时,通过使用其它命令行选项。

  • 也可以直接在C/C++源代码中添加注释。

具体可以参考:https://rust-lang.github.io/rust-bindgen/

与此同时,直接使用 bindgen 生成的 Rust FFI 绑定函数,需要通过  unsafe  的方式访问 C 库中的函数,这不符合人体工程学,实际项目中,我们通常会提供一个安全的包装库。 rust-secp256k1 就是这样的一个包装 crate,它为 libsecp256k1 的所有函数提供类型安全的 Rust 绑定,Github链接:https://github.com/rust-bitcoin/rust-secp256k1。

本文分享自微信公众号 - Rust语言中文社区(rust-china)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
2年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Java修道之路,问鼎巅峰,我辈代码修仙法力齐天
<center<fontcolor00FF7Fsize5face"黑体"代码尽头谁为峰,一见秃头道成空。</font<center<fontcolor00FF00size5face"黑体"编程修真路破折,一步一劫渡飞升。</font众所周知,编程修真有八大境界:1.Javase练气筑基2.数据库结丹3.web前端元婴4.Jav
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
2年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Stella981 Stella981
2年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
京东云开发者 京东云开发者
7个月前
Vitess全局唯一ID生成的实现方案 | 京东云技术团队
为了标识一段数据,通常我们会为其指定一个唯一id,比如利用MySQL数据库中的自增主键。但是当数据量非常大时,仅靠数据库的自增主键是远远不够的,并且对于分布式数据库只依赖MySQL的自增id无法满足全局唯一的需求。因此,产生了多种解决方案,如UUID,Sn
小万哥 小万哥
1个月前
深入理解 C++ 语法:从基础知识到高级应用
C语法让我们将以下代码分解以更好地理解它:示例cppincludeusingnamespacestd;intmain()cout<<"HelloWorld!";return0;示例解释第1行:include是一个头文件库,它让我们可以使用输入和输出对象