< 返回版块

Mike Tang 发表于 2020-06-27 19:26

Tags:rust,ffi

本篇,咱们一起来研究 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 端:

// 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 端:

// 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 中定义:

typedef void (*SumSquareCB)(int result);

Rust 中定义:

pub type SumSquareCB = unsafe extern fn(c_int);

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

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

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

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

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

运行

RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r01

输出:

The result in callback function is: 25

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

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

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

那我们这样行不行呢?

// 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);
}

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

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

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

那我们用闭包试试看:

// 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);
}

编译,提示:

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端:

// 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 端:

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);
}

运行

RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r01

输出:

The sum is 25

要点:

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 指针就像一个万能的桥一样,让我们能够到处任意传递数据块。

  • 回调函数中的类型强制转换
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 也是对应的,也可以与任意指针类型强制转换。

let data = &mut *(user_data as *mut c_int);

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

然后,

*data += result;

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

  • 传参时的
    unsafe {
        sum_square_cb02(
            3,
            4,
            cb_func,
            &mut sum as *mut c_int as *mut c_void);
    }

这里:

&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 端:

#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 端:

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);
}

运行:

RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r03

输出:

The sum is SumRecord { sum: 25, elem_number: 1 }

解析:

  • C 中代码没变

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

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

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

  • 魔法在哪里?

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

    let data = &mut *(user_data as *mut SumRecord);
    data.sum += result;
    data.elem_number += 1;

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

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

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

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

总结

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

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

评论区

写评论

还没有评论

1 共 0 条评论, 1 页