導入:クロージャーについて
昨今ではクロージャーを使えるプログラミング言語は珍しくなくなった。クロージャーとは、関数の引数だけではなく、外のスコープにある変数を参照できる関数のことである。
例えば、次のJavaScriptコードでは、関数を返す関数 f
を定義している。 f
の中で定義された無名関数は、外側の変数 x
を参照できている。
function f(x) { return function(y) { return x + y; }; }
JavaScriptではネストした関数を使わなくても、bind
メソッドによって同等の処理を記述することができる:
function g(x, y) { return x + y; } function f(x) { return g.bind(null, x); // gの第1引数を束縛する }
残念ながらC言語にはクロージャーはない。関数の中に関数を書けないからだ。
しかし、クロージャーと同等のこと、つまり関数に追加の引数を渡すことはできないのだろうか?
目次
ライブラリーレベルでの解決策
ライブラリーレベルで古典的に行われてきた手法は、追加の引数(contextと呼ばれることが多い)を明示的に引き渡すことだ。追加の引数の型としては典型的にはポインター型が使われる。
// 与えられた関数に42を与えて呼び出す関数、のつもり int make_call(int (*f)(void *, int), void *context) { return f(context, 42) + 1; } // 追加の引数を受け取る関数 int g(void *context, int y) { int x = *(int *)context; return x + y; } int main(void) { int x = 3; int z = make_call(g, &x); printf("%d\n", z); }
このパターンを採用している関数の例としては、C標準の thrd_create
関数がある。
// <threads.h> typedef void (*thrd_start_t)(void *); int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);
thrd_create
関数は新しく作ったスレッドで func(arg)
を呼び出す。
そのほか、C標準では qsort_s
や bsearch_s
もこのパターンを採用している。
ライブラリーレベルではない解決が欲しい
残念ながら、C言語には先述のパターンに従っていない関数も結構ある。C標準では qsort
や atexit
がそれだ。
また、パターンに従っていたとしても「追加の引数」の位置は関数によってバラバラだ。ひどい例として、MSVCに実装された後で標準に取り込まれた bsearch_s
がある。
// C標準のbsearch_s: void *bsearch_s(const void *key, const void *base, rsize_t nmemb, rsize_t size, int (*compar)(const void *k, const void *y, void *context), void *context); // MSVCのbsearch_s: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/bsearch-s?view=msvc-170 void *bsearch_s(const void *key, const void *base, size_t number, size_t width, int (*compare)(void *context, const void *key, const void *datum), void *context);
C標準の bsearch_s
に与える比較関数とMSVCの bsearch_s
に与える比較関数で、context引数の位置が違うことにお気づきだろうか?
どうしてこのような非互換性が生まれてしまったのかは筆者にはよくわからないが、C言語がネイティブのクロージャーを持つか、あるいは「追加の引数」の位置についてC言語的な慣習が確立していればこのような悲劇は起きなかったはずだ。
「追加の引数」を標準化する方向性としてはC23に向けてN2862(関数ポインターと追加の引数を組にしたデータ型を導入する)が提案されていたが、採択されなかったようだ。
言語拡張による解決
C言語にネイティブなクロージャー、関数内関数を追加する試みはいくつかある。ここでは2つを紹介しよう。
まず、GCCは伝統的に関数内関数を言語拡張として実装してきた。
コードの例は次の通りだ:
#include <stdio.h> void h(int (*g)(int)) { printf("g(42) = %d\n", g(42)); } int f(int x) { int g(int y) { return x + y; } printf("g(37) = %d\n", g(37)); h(g); return g(7); } int main(void) { printf("f(3) = %d\n", f(3)); printf("f(-10) = %d\n", f(3)); }
内側の関数 g
から外側の変数 x
を参照できていることがわかる。注目すべきは、内側の関数のアドレスを取れる(関数ポインターに変換できる)ことだ。実現方法については後述する。
残念ながらGCCの関数内関数は定義された関数の外に持ち出すことはできない。「関数を返す関数」には使えないのだ。
別の拡張として、AppleによるBlocks拡張がある。BlocksはObjective-Cの機能と思われる方もいるかもしれないが、実はC言語でも使えるのだ。C標準に提案されていたこともあるとか(結局入らなかったけど)。
コード例は次のようになる:
#include <stdio.h> #include <Block.h> void h(int (^g)(int)) { printf("g(42) = %d\n", g(42)); } int (^f(const int x))(int) { return Block_copy(^int (int y) { return x + y; }); } int main(void) { int (^g1)(int) = f(2); int (^g2)(int) = f(-7); h(g1); h(g2); Block_release(g1); // Block_copyで作った複製はBlock_releaseする Block_release(g2); }
Blocks拡張の下では、特別な関数型(関数ポインターの *
の代わりに ^
を使う)と、 ^
から始まる関数式を使えるようになる。デフォルトでは関数内関数はスタックに確保され、定義された関数からエスケープできないが、 Block_copy
というプリミティブを使うとエスケープできるようになる。
Blocks拡張では従来の関数ポインターとは異なる特別な関数型(クロージャー型)を使う。そのため、従来の関数ポインターを期待する関数にクロージャーを渡すことはできない。
Blocks拡張について詳しくは
を参照されたい。
このほか、C23に向けた提案でC++ライクなラムダ式を入れるものがあったが、採択には至らなかった。
実行時にコード生成するライブラリー
GCCの関数内関数は、実は実行時に機械語を生成することによって実現している(この際に生成される機械語はトランポリンと呼ばれる)。そのおかげで関数ポインターが取得できるのだ。(一方で実行時に機械語を生成することが難しいプラットフォームへの移植が困難になるというデメリットもある。)
実行時に機械語を生成すれば何でもできるのはそれはそうだが、一般人にはハードルが高い。GCC以外のコンパイラーでも手軽にクロージャーっぽいものが実現できないだろうか?具体的には、「追加の引数」を受け渡ししつつ、それが見えない形の関数ポインターを取得できるようなライブラリーが欲しい。
そのようなことができる既存のライブラリーとして、 libffi と libffcall がある。
libffiを使った例は次のようになる:
#include <stdio.h> #include <ffi.h> // libffi void h(int (*fun)(int)) { printf("fun(42) = %d\n", fun(42)); } void g(ffi_cif *cif, void *ret, void *args[], void *context) { int x = *(int *)context; int y = *(int *)args[0]; *(int *)ret = x + y; } int main() { ffi_closure *closure; void *fun_p; closure = ffi_closure_alloc(sizeof(ffi_closure), &fun_p); if (closure) { ffi_cif cif; ffi_type *args[1]; args[0] = &ffi_type_sint; if (ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 1, &ffi_type_sint, args) == FFI_OK) { int x = -5; if (ffi_prep_closure_loc(closure, &cif, g, &x, fun_p) == FFI_OK) { int (*fun)(int) = (int (*)(int))fun_p; printf("fun(77) = %d\n", fun(77)); h(fun); } } } ffi_closure_free(closure); }
libffcallを使った例は次のようになる:
#include <stdio.h> #include <callback.h> // libffcall void h(int (*fun)(int)) { printf("fun(42) = %d\n", fun(42)); } void g(void *context, va_alist alist) { int x = *(int *)context; va_start_int(alist); int y = va_arg_int(alist); va_return_int(alist, x + y); } int main() { int x = -5; int (*fun)(int) = (int (*)(int))alloc_callback(&g, &x); printf("fun(77) = %d\n", fun(77)); h(fun); free_callback(fun); }
libffiやlibffcallは呼び出される側の関数の引数が void *args[]
やら va_alist
やらになっており、普通に値が渡ってくる感じではなくなっているが、「スクリプト言語で作った関数からCの関数を作る」というような用途ではこっちの方が都合が良い。
C言語以外での例
いくつかの言語は、その言語の関数からC言語の関数ポインターを取得できる。取得というか、生成と言った方が的確かもしれない。
HaskellはFFIの仕様でそういうことができることを定めている。例は次のとおりだ:
import Foreign.C.Types (CInt(..)) import Foreign.Ptr (FunPtr) foreign import ccall h :: FunPtr (CInt -> CInt) -> IO () foreign import ccall "wrapper" mkCallback :: (CInt -> CInt) -> IO (FunPtr (CInt -> CInt)) f :: CInt -> CInt -> CInt f x y = x + y main = do let g = f (-5) g' <- mkCallback g h g' {- こういう内容のCファイルとリンクする: #include <stdio.h> void h(int (*fun)(int)) { printf("fun(42) = %d\n", fun(42)); } -}
Haskellの実装者は実行時に機械語を書き込むなり何なりしてCの関数を生成しなければならない。
SML#も同じような機構、つまりMLの関数をCの世界へ関数ポインターとして渡せるような機構を持っている。
自作してみた
自分でもlibffiやlibffcallみたいなやつの簡易版を作ってみた。対象はAArch64で、関数の追加の引数は引数リストの先頭に追加される。使用例は次の通りだ:
#include <stdio.h> #include "lib.h" void h(int (*fun)(int)) { printf("fun(42) = %d\n", fun(42)); } int g(void *context, int y) { int x = *(int *)context; return x + y; } int main() { int x = -5; int (*fun)(int) = (int (*)(int))make_closure((FP)g, &x, /* 引数の個数 */ 1, /* 引数の型 */ (const enum arg_type [1]){I32}); printf("fun(77) = %d\n", fun(77)); h(fun); // 解放処理は未実装 }
コードは
- minoki/c-closure-test の aarch64/
に置いた。
実行時のコード生成やAArch64のアセンブリーについてはこのブログにも何回か書いている:
追加の引数を何らかの形で渡すこと自体はそんなに難しくない。破壊しても良いレジスター、今回はX16に「追加の引数」を設定する機械語を書き込んでやればいいのだ。しかしC言語で書く関数からはX16に簡単にはアクセスできない。そこで引数を詰め替えて最初の引数が「追加の引数」になるようにする。
AArch64の呼び出し規約では、最初のいくつかの引数はレジスターで、レジスターがいっぱいになったらスタックを使って渡すようになっている。生成した機械語からはアセンブリーで書いたコード(thunk.S
の thunk
関数)に飛ぶ。thunk
関数ではレジスターに入っている内容(引数の数が少ない場合はゴミが入っているかもしれないが、ともかく)をスタックのメモリー領域に詰め込んでやる。そこからCで書かれた関数 adjust_args
を呼び出して引数の規定に従って詰め直す。それで thunk
関数に戻って詰め替えられたデータをレジスターに書き戻す。そうするとターゲットとなる(最初の引数が「追加の引数」であるような)関数を呼べる。
AArch64の呼び出し規約は色々書いてあるが、標準的なスカラーデータ型(64ビット以下の整数およびポインター、32ビットおよび64ビットの浮動小数点数)だけならそんなに難しくないと思う。ベクトル型とか構造体の値渡し・返却とかをやり出すと大変そうだ。
今回これを実装したのはSML#をAArch64に移植するための実験としてなのだが、それはまた気が向いた時にやりたい。