ネストした関数呼出しについて
gcc に興味深い修正があったのでメモのようなもの。
openbsd-tech にて、 次のようなコードで呼出しに失敗する、との報告(gcc nested function trampoline doesn't seem to work)
$ cat a.c
static void test_trampoline(void (*p)(void))
{
p();
}
int main( int argc, char *argv[] )
{
void dummy(void) {}
test_trampoline(dummy);
return 0;
}
もちろん ANSI 違反だが、gcc では許容されるコードではある。 上のメールにもあるように、OpenBSD の売りのひとつである W^X がまずいらしい。 実際、
$ gcc -Z a.c
と W^X をオフにすれば動作する。 最終的に以下のように修正された:config.gcc.diff。 trampoline コードの include が早過ぎたのが原因のようだ。
ところで、話はそれるが、 このようなコードは lisp/scheme ではごくあたりまえのように使われてる。 scheme では上のコードは以下のように書けるだろう*1。
(define (test_trampoline p)
(p))
(define (foo)
(define (dummy) #t)
(test_trampoline dummy))
(write (foo))
scheme が読めなくても、なにをやっているのかはすぐに解ると思う。
こういったコードはごくあたりまえといったが、 標準関数ですら高階関数(関数を引数にとる関数)呼出しであることがある。 例として、よく挙げられるのが call-with-input-file だ。
(call-with-input-file filename proc)
filename のオープンに成功すると proc が呼ばれる。 proc は入力 port を引数にとる関数である。簡単な例をあげると、
(define (hexdump)
(define (dump-char c port l)
(cond ((eof-object? c)
#t)
(else
(if (eq? (modulo l 16) 0)
(display (format #f "\n~8,'0x" l)))
(display (format #f " ~2,'0x" (char->integer c)))
(dump-char (read-char port) port (+ l 1)))))
(call-with-input-file "foo.txt"
(lambda (port)
(dump-char (read-char port) port 0))))
(hexdump)
foo.txt を 16 進数でダンプするスクリプトである。 もし foo.txt のオープンに失敗しても、きちんとエラーを表示して終了するはずだ。 これを C でむりやり書くと、
#include <stdio.h>
void*
call_with_input_file(char *fn, void* (*p)(FILE *))
{
FILE *fp;
if ((fp = fopen(fn, "r")) == NULL) {
perror("call_with_input_file");
return NULL;
}
return p(fp);
}
int
main(int argc, char *argv[])
{
int l = 0;
void* dump_char(char c, FILE *fp) {
if (feof(fp))
return NULL;
if ((l % 16) == 0)
printf("\n%08x", l);
printf(" %02x", c);
l++;
return dump_char(getc(fp), fp);
}
void* dummy(FILE *fp) {
return dump_char(getc(fp), fp);
}
call_with_input_file("foo.txt", dummy);
return 0;
}
といった感じであろう。 dump_char の引数の数が scheme の例と違うが、 main の中の変数を参照できる例としてこのようにしてみた。
よく C でネストした関数は可読性が低い、と言われているが、どうだろうか。
386-elf で OpenBSD を使っているひとは、 gcc を -current にアップデートしてリビルドすることをお忘れなく。
いろいろ話が広がりすぎたので、まとまってないまとめ。 OpenBSD が elf の環境に移ってから 1 年半ほど経過したのだが、 いままでこのバグ報告がなかったことを見ると、 ネストした関数の gcc 拡張はあまり使われていない、 ということだろうか。よくわからないが。
*1: 親関数のスコープを出てしまうと、ネストした関数のアドレスも失われるので、 lisp/scheme のクロージャとは違うのだが。