C言語でclosureを実現する

C言語は、仕様上closureをサポートしていない。GCC拡張によるサポートはあるけど、clangではエラーになる。ただライブラリレベルでclosureの機能を実現することはできるようで、libffcallというライブラリがその機能を提供している。この記事では、libffcallを使ってみた感触と、libffcallを参考に簡易closureライブラリを実装してみた感想をメモしておく。

libffcallを使ってみる

libffcallは名前の通りforeign functionを呼ぶためのライブラリで、その機能の一つとしてclosureを実現するためのcallbackという関数群を提供している。

例えば以下のように利用する。

#include <callback.h>
#include <stdio.h>

void add(void *data, va_alist alist) {
  int arg1 = *(int*)data;

  va_start_int(alist);
  int arg2 = va_arg_int(alist);

  *(int*)data = arg1 + arg2;

  va_return_int(alist, *(int*)data);
}

int main() {
  int in1 = 10, in2 = 10;
  int (*add1)() = alloc_callback(&add, &in1);
  int (*add2)() = alloc_callback(&add, &in2);

  int out1 = add1(1);
  add2(1);
  int out2 = add2(1);
  printf("out1: %d\n", out1);
  printf("out2: %d\n", out2);

  free_callback(add1);
  free_callback(add2);
}

実行してみる。

$ gcc -o callback callback_main.c -lcallback
$ ./callback
out1: 11
out2: 12

alloc_callback()は、環境の準備+関数へのジャンプをする関数を実行時に生成する。この例では、add関数にin1変数をバインドして、add1という関数を生成している。add1を呼ぶと、add関数の引数を準備した上でadd関数を呼ぶ。引数dataにはalloc_callback()に与えたデータが入る。引数alistにはadd1に与えられた引数が入る。add1に与える引数は複数でもいい。戻り値はalist変数を通して返す。

機能面では近いように思うけど、よくあるclosureとは見た目がだいぶ違う。バインドする変数を明示的に指定しているし、その変数を関数の引数として明示的に受け取っている。また、バインドする変数のメモリ管理はしてくれない。やや読みやすいとは言えないコードになってしまうが、仕組みとしては面白いと思った。実行時コード生成には夢がある。

自前で実装してみる

libffcallのclosure周りの実装を見た感じでは、OSとCPUを限定すれば簡易的なものなら実装できそうだったのでやってみた。OSはLinux、CPUはx86_64を想定した。

以下が実装。

実行してみる。

$ gcc -fPIC -shared -o libmycallback.so mycallback.c
$ gcc -L. -o mycallback_main mycallback_main.c -lmycallback
$ LD_LIBRARY_PATH=. ./mycallback_main
sum1: 10
sum2: 15

動いているようだ。最低限の実装なら意外と短い行数で済む。本家libffcallでは、OSごとのページサイズ、mmap/mprotectの違いや、CPUごとの呼び出し規約の違いに地道に対応している。また、上記の自前実装には関数とバインドする変数のアドレスが32 bitsまでという制限があるが、本家には当然そのような制限はない。

まとめ

C言語でclosureを実現する方法として、libffcallによる方法と、自前実装の方法を紹介した。C言語でも(構文はかなり違うけど)機能は近いものができることがわかった。