Misc Change Log

`OpenBSD で scheme のアプリ開発' みたいなことをやってます。

2004-11-17

ネストした関数呼出しについて

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 のクロージャとは違うのだが。

Posted at 19:09 | Permalink | Category | Comments