前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust FFI 编程 - 手动绑定 C 库入门 05

Rust FFI 编程 - 手动绑定 C 库入门 05

作者头像
MikeLoveRust
发布2020-07-02 15:05:14
1.5K0
发布2020-07-02 15:05:14
举报
文章被收录于专栏:Rust语言学习交流

本篇,咱们一起来研究 Rust 与 C 之间的回调函数传递。本篇的目标如下:

  1. 被调函数在 C 端,接收一个函数指针作为回调函数,并调用;
  2. 主函数在 Rust 中,在 Rust 中调用 C 端的这个函数;
  3. 在 Rust 中,传递一个 Rust 中定义的函数,到这个 C 端的被调函数中作为回调函数。

为什么要研究跨 FFI 的回调函数,因为

  1. 有可能想在底层事件(异步)框架中,注册一个函数,事件触发的时候,调用;
  2. 底层采用注册一个路由表的形式,在程序开始的时候,注册一堆函数操作进去;
  3. 其它。

这是一种常见需求,也是一种设计模式。

基础示例

话不多说,我们来设计一个示例流程:

  1. C 端,设计一个函数,sum_square_cb01, 接收两个整型参数 a, b,和一个函数指针,计算 a2 + b2 的值,并且将值传递进第三个参数(函数中),进行打印;
  2. Rust 端,定义一个回调函数 cb_func,在这个回调函数中,打印上述平方和;
  3. Rust 端,引入 C 中定义的 sum_square_cb01;
  4. 在 Rust 的 main 中,调用 sum_square_cb01。

好,直接上代码。C 端:

代码语言:javascript
复制
// csrc/ccode01.c

#include<stdio.h>

typedef void (*SumSquareCB)(int result);

void sum_square_cb01(int a, int b, SumSquareCB cb) {
    int result = a*a + b*b;
    cb(result);
}

Rust 端:

代码语言:javascript
复制
// src/r01.rs

use std::os::raw::c_int;

pub type SumSquareCB = unsafe extern fn(c_int);

#[link(name = "ccode01")]
extern {
    pub fn sum_square_cb01(a: c_int, b: c_int, cb: SumSquareCB);
}

pub unsafe extern fn cb_func(result: c_int) {
    println!("The result in callback function is: {}", result);
}

fn main() {
    unsafe {
        sum_square_cb01(3, 4, cb_func);
    }
}

两边代码其实挺简洁。不过也有要注意的一些地方。要点提醒:

  • 两边都需要定义回调函数的类型(签名),而且定义要一致。

C 中定义:

代码语言:javascript
复制
typedef void (*SumSquareCB)(int result);

Rust 中定义:

代码语言:javascript
复制
pub type SumSquareCB = unsafe extern fn(c_int);

fn 是 Rust 中的函数指针类型。具体可参见标准库文档 fn,解释得非常详尽。

函数指针的功能就是指向函数代码片断,可以用函数指针来调用函数,效果跟函数名一样,如上面 C 代码中的 cb(result)

  • Rust 中的回调函数定义
代码语言:javascript
复制
pub unsafe extern fn cb_func(result: c_int) {
    println!("The result in callback function is: {}", result);
}

是 Rust 中定义回调函数的代码,注意前面加的 unsafe 和 extern 修饰关键字。回调函数签名,要与前面定义的回调函数类型完全一致(此处接受一个整型参数,并且没有返回值)。

  • 代码的编译方式,见前一篇,此不赘述。

运行

代码语言:javascript
复制
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r01

输出:

代码语言:javascript
复制
The result in callback function is: 25

在回调函数中,更新外部数据

我们的上述代码(目前只有一条打印语句),可以适用于在回调函数中不需要改变外界数据的情况。而在实际情况下,我们使用的回调的逻辑,要求用回调更新一些程序中其它地方持有的数据,这种需求,使用上面的代码,就不能满足要求了。

我们很自然地想到了 C 中常用的全局变量大法。非常方便,无脑引用,并且这确实是可以实现的。但是,在 Rust 中,我们严重不推荐使用全局变量,故不举出全局变量的例子(防止只看片断的人,抄出不良风气)。

那我们这样行不行呢?

代码语言:javascript
复制
// src/r01-1.rs

use std::os::raw::c_int;

pub type SumSquareCB = unsafe extern fn(c_int);

#[link(name = "ccode01")]
extern {
    pub fn sum_square_cb01(a: c_int, b: c_int, cb: SumSquareCB);
}

fn main() {
    let mut sum = 0;

    pub unsafe extern fn cb_func(result: c_int) {
        sum += result;
    }

    unsafe {
        sum_square_cb01(3, 4, cb_func);
    }

    println!("The result in callback function is: {}", sum);
}

肯定是不行的。报如下错:

代码语言:javascript
复制
error[E0434]: can't capture dynamic environment in a fn item
  --> src/r01-1.rs:14:9
   |
14 |         sum += result;
   |         ^^^
   |
   = help: use the `|| { ... }` closure form instead

error: aborting due to previous error

提示这里应该用闭包。闭包跟函数还是不同的。闭包简单来说,由函数+被捕获的数据两大块儿组成。

那我们用闭包试试看:

代码语言:javascript
复制
// src/r01-2.rs

use std::os::raw::c_int;

pub type SumSquareCB = unsafe extern fn(c_int);

#[link(name = "ccode01")]
extern {
    pub fn sum_square_cb01(a: c_int, b: c_int, cb: SumSquareCB);
}

fn main() {
    let mut sum = 0;

    unsafe {
        sum_square_cb01(3, 4, |r| sum += r );
    }

    println!("The result in callback function is: {}", sum);
}

编译,提示:

代码语言:javascript
复制
error[E0308]: mismatched types
  --> src/r01-1.rs:14:31
   |
14 |         sum_square_cb01(3, 4, |r| sum += r );
   |                               ^^^^^^^^^^^^ expected fn pointer, found closure
   |
   = note: expected fn pointer `unsafe extern "C" fn(i32)`
                 found closure `[closure@src/r01-1.rs:14:31: 14:43 sum:_]`

error: aborting due to previous error

说这里类型不匹配。使用闭包,解决我们的问题,是肯定可以的。但是,需要有更多知识,我们专门放在下一节中讲解。本节,我们专注于用函数指针解决问题。

其实我们遇到的问题,在 C 的领域,早就是一种常见的问题(比如一个 GUI 库的回调函数),所以其实也早就有对应的解决方案,比如,使用 C 中的魔幻主义的 void * 携带一个数据块传递。了解过 void * 的就知道,它和 C 中的其它指针一起,几乎把 C 变成了一门动态语言(所以有一种说法认为 C 其实是弱类型语言?)。

void * 是一种通用指针,意思是”指向某个东西的指针“,它的灵活和强大之处在于,可以强制转换到任何指针类型。这里,我们也可以使用 void * 来传递我们的”数据块“。

有同学要问,为何不让回调函数直接返回一个值来达到我们想要实现的效果呢?所谓回调函数,一般处于调用链的末端,在这个函数里,实现对外部数据的更新。如果对返回值进行处理,则破坏了逻辑封装的抽象(需要在回调函数外写对应的逻辑代码,而回调函数外往往是框架代码)。

于是,我们继续规划一下我们的示例更新。在前面的基础之上:

  1. 在 Rust 的 main 函数中,定义一个变量 sum;
  2. 在 Rust 中定义的回调函数中,更新这个变量 sum;
  3. 由于需要传递数据块地址,需要修改回调函数的签名定义;

那我们直接上代码。

C端:

代码语言:javascript
复制
// csrc/ccode02.c

#include<stdio.h>

typedef void (*SumSquareCB)(int result, void *user_data);

void sum_square_cb02(int a, int b, SumSquareCB cb, void *user_data) {
    int result = a*a + b*b;
    cb(result, user_data);
}

Rust 端:

代码语言:javascript
复制
use std::os::raw::c_int;
use std::ffi::c_void;

pub type SumSquareCB = unsafe extern fn(c_int, *mut c_void);

#[link(name = "ccode02")]
extern {
    pub fn sum_square_cb02(a: c_int, b: c_int, cb: SumSquareCB, user_data: *mut c_void);
}

pub unsafe extern fn cb_func(result: c_int, user_data: *mut c_void) {
    let data = &mut *(user_data as *mut c_int);
    *data += result;
}

fn main() {
    let mut sum = 0;

    unsafe {
        sum_square_cb02(
            3,
            4,
            cb_func,
            &mut sum as *mut c_int as *mut c_void);
    }

    println!("The sum is {}", sum);
}

运行

代码语言:javascript
复制
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r01

输出:

代码语言:javascript
复制
The sum is 25

要点:

  • std::ffi::c_void

Rust 端引入了 std::ffi::c_void;。这是 Rust 给我们提供的强大的基础设施,不然我们真要愁眉苦脸了。从标准库页面可以学习到,Rust 中的 *const c_void 等于 C 的 const void*,Rust 中的 *mut c_void 等于 C 的 void*。(C 中的 void 函数返回值本身,与 Rust 的空值类型 () 相等)

请仔细体会上述代码中的各处 void **mut c_void 的写法和对应关系。

可以看到,void 指针就像一个万能的桥一样,让我们能够到处任意传递数据块。

  • 回调函数中的类型强制转换
代码语言:javascript
复制
pub unsafe extern fn cb_func(result: c_int, user_data: *mut c_void) {
    let data = &mut *(user_data as *mut c_int);
    *data += result;
}

前面提过,void * 的强大就在于可以与任意指针类型进行强制转换。在 Rust 也是对应的,也可以与任意指针类型强制转换。

代码语言:javascript
复制
let data = &mut *(user_data as *mut c_int);

这一句,先是把 *mut c_void 指针转换成 *mut c_int 指针,然后用 * 取它的数据块,然后用 &mut 取这个数据块的可变引用,进入 Rust 的常规领域(标准使用模式)。

然后,

代码语言:javascript
复制
*data += result;

就是更新数据块中的值了。

  • 传参时的
代码语言:javascript
复制
    unsafe {
        sum_square_cb02(
            3,
            4,
            cb_func,
            &mut sum as *mut c_int as *mut c_void);
    }

这里:

代码语言:javascript
复制
&mut sum as *mut c_int as *mut c_void

是上述转换过程的逆过程。先将 &mut sum(sum 的可变引用),转换成 *mut c_int(c_int 类型的指针),进而转换成 *mut c_void(通用指针)。

  • 打印语句

本身中的打印语句,是在 Rust 的 main 函数中,打印的是 main 函数中定义的 sum(而第一例是在回调中打印的)。因此,可以看到,sum 的值,确实是在回调函数中,被修改过了。达到了我们的目的。

好了,我们的想法其实已经实现了。但是本例仅仅更新了一个整数,貌似没多大用。真实世界中,一般是更新一个结构体。那我们就更进一步,研究一下,怎么更新结构体。

其实非常简单。

更新结构体

同样直接上代码,然后再讲要点。

C 端:

代码语言:javascript
复制
#include<stdio.h>

typedef void (*SumSquareCB)(int result, void *user_data);

void sum_square_cb03(int a, int b, SumSquareCB cb, void *user_data) {
    int result = a*a + b*b;
    cb(result, user_data);
}

Rust 端:

代码语言:javascript
复制
use std::os::raw::c_int;
use std::ffi::c_void;

pub type SumSquareCB = unsafe extern fn(c_int, *mut c_void);

#[link(name = "ccode03")]
extern {
    pub fn sum_square_cb03(a: c_int, b: c_int, cb: SumSquareCB, user_data: *mut c_void);
}

pub unsafe extern fn cb_func(result: c_int, user_data: *mut c_void) {
    let data = &mut *(user_data as *mut SumRecord);
    data.sum += result;
    data.elem_number += 1;
}

#[derive(Debug, Default, Clone, PartialEq)]
struct SumRecord {
    sum: c_int,
    elem_number: usize,
}


fn main() {
    let mut sum = SumRecord::default();

    unsafe {
        sum_square_cb03(
            3,
            4,
            cb_func,
            &mut sum as *mut SumRecord as *mut c_void);
    }

    println!("The sum is {:?}", sum);
}

运行:

代码语言:javascript
复制
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r03

输出:

代码语言:javascript
复制
The sum is SumRecord { sum: 25, elem_number: 1 }

解析:

  • C 中代码没变

可以看到,其实 C 中代码没有变化。

  • Rust 中加了结构体定义
代码语言:javascript
复制
#[derive(Debug, Default, Clone, PartialEq)]
struct SumRecord {
    sum: c_int,
    elem_number: usize,
}

就是一个普通的 Rust 结构体定义。

  • 魔法在哪里?

Rust 中的回调函数签名都没有变化。变化在下面这里:

代码语言:javascript
复制
    let data = &mut *(user_data as *mut SumRecord);
    data.sum += result;
    data.elem_number += 1;

可以看到,就是把例 2 中的 *mut c_int 变成了 *mut SumRecord 了。然后,更新数据的时候,按 Rust 结构体更新的方式操作就可以了。

代码语言:javascript
复制
    unsafe {
        sum_square_cb03(
            3,
            4,
            cb_func,
            &mut sum as *mut SumRecord as *mut c_void);
    }

同样,这个逆过程也变化了,仔细体会。

就这样,我们就实现了在回调函数中,更新外部结构体。达成我们的理想要求。

总结

在本篇,我们研究了 Rust 与 C 如何跨 FFI 边界实现回调函数的调用,以及在回调中更新外部数据。全篇内容,主要参考:

  • http://adventures.michaelfbryan.com/posts/rust-closures-in-ffi/ 感谢作者的精彩分享
  • Rust 标准库文档

文章中的代码在:https://github.com/daogangtang/learn-rust/tree/master/09rustffi2

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust语言学习交流 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础示例
  • 在回调函数中,更新外部数据
  • 更新结构体
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档