ネストした関数呼出しについて
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 のクロージャとは違うのだが。