Rustの関数ポインタの落とし穴

概要: Rustの関数ポインタの落とし穴について

その1: 関数ポインタはクロージャとは異なる

これはC/C++に慣れている人には当たり前ですが、関数ポインタ型 (fn()) とクロージャ型 (Fn()) には重大な違いがあります。それは、関数ポインタは環境をキャプチャーしないということです。大雑把にいうと、

  • 関数ポインタは、ある機械語コードのアドレス
  • クロージャは、関数ポインタと、キャプチャーした環境の対

なので、関数ポインタは、ひとつのプログラムにつき原則として有限個しかないのに対し、クロージャは、キャプチャーする環境によって無限にたくさんのクロージャを作ることができます。例えば、

fn main() {
    let closures = [3, 7, 1, 5, 8, 9, 2].iter().map(|&i| {
        move |j| i + j
    }).collect::<Vec<_>>();
    println!("{}", closures[3](14));
}

というコードでは、「3を足す関数」「7を足す関数」「1を足す関数」「5を足す関数」…… のようにたくさんの「関数」を動的に生成していますが、こういうのはクロージャでないとできません。

関数ポインタは基本的には fn で定義した関数からしか作ることができません。例外として、キャプチャーしていないクロージャは関数ポインタとして使うことができます。

fn main() {
    let f : fn(i32) -> i32 = |x| x + 1;
    println!("{}", f(3));
}

その2: fn と書いたらそれ自体がポインタ型

C言語では

int (*f)(void) = getchar;

のように、関数ポインタ型には最低1個の * がつきますが、対応するRustの記法では

let f : fn() -> i32 = i32::max_value;

のように、 * が0個で関数ポインタです。したがって、

let f : *const fn() -> i32; // 二重ポインタ!

は、関数ポインタへのポインタになってしまいます。ここを間違えるとFFIで未知のsegfaultに悩まされる可能性があります。

その3: 関数の型は関数ポインタ型ではない

例えば、以下のコードはコンパイルエラーになります。

fn foo() { println!("foo"); }
fn bar() { println!("bar"); }

fn main() {
    let mut f = foo;
    if true {
        f = bar;
    }
    f();
}
   Compiling playground v0.0.1 (file:///playground)
error[E0308]: mismatched types
 --> src/main.rs:7:13
  |
7 |         f = bar;
  |             ^^^ expected fn item, found a different fn item
  |
  = note: expected type `fn() {foo}`
             found type `fn() {bar}`

error: aborting due to previous error

error: Could not compile `playground`.

ここでは fn() ではなく、 fn() {foo}fn() {bar} という型が表示されています。これは関数定義型とよばれ、関数ポインタ型とは異なります

関数ポインタ型と関数定義型のわかりやすい違いとしてバイト数が挙げられます。以下でわかるように、関数定義型は実は0バイトです。

use std::mem;
fn main() {
    println!("{}", mem::size_of_val(&main)); // 0
    println!("{}", mem::size_of_val(&(main as fn()))); // 8
}

関数定義型は、関数ごとに異なる型がついているので、それ自体は情報を持っていなくても呼び出せるようになっています。C++でいえば、各関数ごとにファンクタオブジェクトが一個割り当てられている、以下のような状態とみることができます。

void foo_funptr() {
  printf("foo!\n");
}
struct foo {
  int dummy; //C++にはZSTがないので
  // 直接呼び出し
  void operator()() const {
    printf("foo!\n");
  }
  // 関数ポインタへの変換
  void (*to_funptr())() const {
    return foo_funptr;
  }
};

関数がジェネリクスを持っている場合は、関数定義型にも対応するジェネリクスが与えられます。なお、コンパイラは便宜上 fn() {foo} のような表示をしますが、関数定義型を名指しで指定することはできません。これはクロージャ型 ([closure@src/main.rs:3:20: 3:25] などと表示される) と同様です。

いずれにせよ、通常は必要なタイミングで関数ポインタ型に自動で変換されるので、変なコンパイルエラーなどに遭遇しない限り気にする必要はないでしょう。

その4: 関数ポインタ型にはABIと安全性フラグがつけられる

特に、C/C++とのFFIをするときは、ABIを間違えないように注意が必要です。通常のRust関数は extern "Rust" fn() なのに対して、C/C++と相互呼び出しする関数は extern "C" fn() です。それぞれ fn(), extern fn() と省略できます。また unsafe をつけて unsafe fn() のようにもできます。

なお、 C++extern "C" とは異なり、Rustの extern "C" はABIを指定するだけで、マングリング規則を変更しません。マングリングを無効化するには別途 #[no_mangle] をつけます。

また、これまたC++に慣れているとわかりづらいですが、

extern "C" fn foo() {} // ここに実体がある

extern "C" {
  fn foo(); // 別の場所にある実体とリンクする
}

は意味が異なります。 extern { .. } は、実体が他の場所にあるときに使います。