C言語で部分適用したい!(実は、できるアーキテクチャがあるんです)

通常、C言語の関数ポインタは、クロージャではない。したがって、関数を部分適用したり、カリー化したり、ローカル変数をキャプチャーした関数ポインタを返したりすることはできない。しかし、実際にC言語が動作する環境のなかには、そのようなことが実現できるものがある。PowerPC64 System V ABIは、そのひとつである。

PowerPC64 System V ABIは、Linux等において高級言語のコードをPowerPC64機械語に翻訳するさいの取り決めである。

多くのABIでは、関数ポインタは関数の最初の命令のアドレスに翻訳されるが、PowerPC64 System V ABIはそれとは異なる定義をしている。具体的には、関数ポインタは以下のような構造体

struct Funptr {
  void *jump_target; /* ジャンプ先 */
  void *initial_r2; /* TOCベース。ジャンプ前に %r2 レジスタに代入される値 */
  void *initial_r11; /* 環境ポインタ。ジャンプ前に %r11 レジスタに代入される値 */
};

へのポインタ (すなわち struct Funptr*) である。

この3番目の要素はプログラミング言語の要請に応じて使ってよい。C言語自体は環境ポインタを使わないが、関数ポインタを呼ぶさいは環境ポインタが考慮される。

そのため、アセンブリを用いて以下のようなコードを書くことができる。以下は、乗算演算子を部分適用する multiply 関数を用いてかけ算九九を出力するコードである。

#include <stdio.h>
#include <stdlib.h>

int (*multiply(int x))(int);

int main() {
  int (*f[10])(int);
  for(int x = 0; x < 10; ++x) {
    f[x] = multiply(x);
  }
  for(int x = 0; x < 10; ++x) {
    for(int y = 0; y < 10; ++y) {
      printf("%d * %d = %d\n", x, y, f[x](y));
    }
  }
  for(int x = 0; x < 10; ++x) {
    free(f[x]);
  }
  return 0;
}

この multiplyアセンブリで以下のように書ける。

	.globl multiply
multiply:
	mflr %r0
	std %r0, 16(%r1)
	stdu %r1, -96(%r1)
	std %r3, 88(%r1)
	li %r3, 24
	bl malloc
	nop
	ld %r5, 88(%r1)
	addis %r4, %r2, multiply2@toc@ha
	addi %r4, %r4, multiply2@toc@l
	std %r4, 0(%r3)
	std %r2, 8(%r3)
	std %r5, 16(%r3)
	addi %r1, %r1, 96
	ld %r0, 16(%r1)
	mtlr %r0
	blr
multiply2:
	mulld %r3, %r3, %r11
	blr

これを実行すると、以下のようになる。

$ powerpc64-linux-gnu-gcc -std=c99 -static main.c multiply.s
$ qemu-ppc64 ./a.out
0 * 0 = 0
0 * 1 = 0
0 * 2 = 0
0 * 3 = 0
0 * 4 = 0
0 * 5 = 0
0 * 6 = 0
0 * 7 = 0
0 * 8 = 0
0 * 9 = 0
1 * 0 = 0
1 * 1 = 1
1 * 2 = 2
1 * 3 = 3
1 * 4 = 4
1 * 5 = 5
1 * 6 = 6
1 * 7 = 7
1 * 8 = 8
1 * 9 = 9
2 * 0 = 0
2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
2 * 4 = 8
2 * 5 = 10
2 * 6 = 12
2 * 7 = 14
2 * 8 = 16
2 * 9 = 18
3 * 0 = 0
3 * 1 = 3
3 * 2 = 6
3 * 3 = 9
3 * 4 = 12
3 * 5 = 15
3 * 6 = 18
3 * 7 = 21
3 * 8 = 24
3 * 9 = 27
4 * 0 = 0
4 * 1 = 4
4 * 2 = 8
4 * 3 = 12
4 * 4 = 16
4 * 5 = 20
4 * 6 = 24
4 * 7 = 28
4 * 8 = 32
4 * 9 = 36
5 * 0 = 0
5 * 1 = 5
5 * 2 = 10
5 * 3 = 15
5 * 4 = 20
5 * 5 = 25
5 * 6 = 30
5 * 7 = 35
5 * 8 = 40
5 * 9 = 45
6 * 0 = 0
6 * 1 = 6
6 * 2 = 12
6 * 3 = 18
6 * 4 = 24
6 * 5 = 30
6 * 6 = 36
6 * 7 = 42
6 * 8 = 48
6 * 9 = 54
7 * 0 = 0
7 * 1 = 7
7 * 2 = 14
7 * 3 = 21
7 * 4 = 28
7 * 5 = 35
7 * 6 = 42
7 * 7 = 49
7 * 8 = 56
7 * 9 = 63
8 * 0 = 0
8 * 1 = 8
8 * 2 = 16
8 * 3 = 24
8 * 4 = 32
8 * 5 = 40
8 * 6 = 48
8 * 7 = 56
8 * 8 = 64
8 * 9 = 72
9 * 0 = 0
9 * 1 = 9
9 * 2 = 18
9 * 3 = 27
9 * 4 = 36
9 * 5 = 45
9 * 6 = 54
9 * 7 = 63
9 * 8 = 72
9 * 9 = 81

FAQ

Q. つまりPowerPC64がすごいということか?

A. そうではない。これは「C言語のコードを機械語にどのように翻訳するか」という決まり事、すなわちABIに関する話である。LinuxのPowerPC64版においてたまたま上記のようであったというだけであり、この特徴はPowerPC64というCPUアーキテクチャ自身の性質とは関係ない。したがって、同様にPowerPC64のCPU上で動作するシステムであっても、上で述べたようなことが成り立たない可能性もある。

Q. 関数ポインタをそのような特殊な形で実装してしまって、非互換性は生じないのか。

A. 少なくともC言語(ISO C)の定める範囲では、関数ポインタの中身に関しての規定はほぼない。それどころか、関数ポインタとオブジェクトへのポインタは明確に区別され、バイト数が異なっていてもよい。(16bitのx86には実際にそのようなメモリモデルがある)

Q. これはどう見ても関数ポインタではなくてクロージャなのではないか。

A. もちろん誰がどう見てもクロージャだが、C言語からは完璧に関数ポインタとして見えている。