C
rust
callback
ffi

C FFIでのコールバックの扱い(RustにおけるNullableな関数ポインタ)

Cの関数ポインタとRustの関数ポインタ

#include <stdio.h>

typedef void (*rust_callback)(int);
rust_callback cb;

int register_callback(rust_callback callback) {
    cb = callback;
    return 1;
}

void trigger_callback() {
  if (cb != NULL) {
    cb(7);
  } else {
    printf("callback function is not set!");
  }
}

(他言語関数インターフェイスのものを改変)
このようなコールバックを扱うCのライブラリを呼び出すRustのFFIを作りましょう。

他言語関数インターフェイスでは以下のように作るように書いてあります:

extern fn callback(a: i32) {
    println!("I'm called from C with value {0}", a);
}

#[link(name = "ext")]
extern {
   fn register_callback(cb: extern fn(i32)) -> i32;
   fn trigger_callback();
}

fn main() {
    unsafe {
        register_callback(callback);
        trigger_callback(); // Triggers the callback
    }
}

これは勿論動作します。しかし、これは本当にCのAPIを正しく踏襲できているでしょうか?

元のCのプログラムにおいて、cbにはNULLを入れてもいいはずです。CのAPIでは、コールバックが必要ない場合は関数ポインタとしてNULLを入れることはよく行われます。

register_callback(NULL);  // callbackは登録しない

さてここで問題になるのはRust側のregister_callbackの定義です。NULLに相当するのはstd::ptr::null_mut()なのでこれを代入してみましょう:

use std::ptr::null_mut;
extern "C" {
    fn register_callback(cb: extern "C" fn(i32)) -> i32;
}
fn main() {
    unsafe { register_callback(null_mut()) };
}

これはコンパイルできません:

   |
12 |     unsafe { register_callback(null_mut()) };
   |                                ^^^^^^^^^^ expected fn pointer, found *-ptr
   |
   = note: expected type `extern "C" fn(i32)`
              found type `*mut _`

これはasで明示的に変換しようとしてもできません。

Nullableな関数ポインタ

ところでCからRustのバインディングを生成できるrust-bindgenはどうやっているのでしょうか?上のCのコードをbindgenしてみましょう

pub type rust_callback = ::std::option::Option<unsafe extern "C" fn(arg1: ::std::os::raw::c_int)>;
extern "C" {
    #[link_name = "\u{1}cb"]
    pub static mut cb: rust_callback;
}
extern "C" {
    pub fn register_callback(callback: rust_callback) -> ::std::os::raw::c_int;
}

#include<stdio.h>があるので余計な部分がたくさん出てきますが、該当部分はこれだけです。
Option<extern fn(i32)>を使えば良いようですね。

extern "C" fn callback(a: i32) {
    println!("I'm called from C with value {0}", a);
}

#[link(name="ext")]
extern "C" {
    fn register_callback(cb: Option<extern "C" fn(i32)>) -> i32;
    fn trigger_callback();
}

fn main() {
    unsafe {
        register_callback(Some(callback));
        trigger_callback();
    }
    unsafe {
        register_callback(None);
        trigger_callback();
    }
}
$ cargo run
I'm called from C with value 7
callback function is not set!

上手くできました(/・ω・)/

参考

以上のソースコードは https://github.com/termoshtt/nullable_fn_ptr にあります