C言語マクロもCPS化して合成可能にする

yuniFFIのAPI記述フォーマットであるstubIR( http://d.hatena.ne.jp/mjt/20141127/p2 )の処理系を作っていく。ひとまず、処理系に期待するCソース出力を手書きしてみることにした。

手書きしたコードは、stubIRで記述される:

  • 通常のC APIをnccc呼び出し規約に適合させるブリッジコード
static
YUNIFFI_FUNC_BEGIN(test_outint_forward0,in,in_size,out,out_size)
    /* out[0] : int [return] */
    int out0;
    /* out[1] : int [thevalue] */
    int out1;

    /* call */
    out0 = test_outint(&out1); /* 通常のC API test_outint() を呼び出す */

    /* output */ /* C APIは1in-1outだが、ncccでは0in-2outに変換されている */
    YUNIWORD_SET_SINT(out,0,out0);
    YUNIWORD_SET_SINT(out,1,out1);
YUNIFFI_FUNC_END(test_outint_forward0)
  • 型のsizeofと構造体レイアウトの抽出
/* someblob_t のサイズの抽出 */
#define EXPORT_someblob_t(k)\
    YUNIFFI_EXPORTCLASS_TYPE_BLOB(k, "someblob_t", someblob_t)
/* someblob_t のメンバ a のレイアウトの抽出 */
#define EXPORT_someblob_t_a(k)\
    YUNIFFI_EXPORTCLASS_AGGREGATE_MEMBER(k, "someblob_t/a", someblob_t, a)
  • 定数の抽出
#define EXPORT_REAL_2(k)\
    YUNIFFI_EXPORTCLASS_CONST_REAL(k, "REAL_2", REAL_2)

の要素を含む。
ギリギリまで機械的に書けるようにするため、Cマクロをかなり活用している。人間が書かされるAPIならば悪夢のようだが、どうせ機械が生成したコードでしか使わない。

CPS化によるCマクロの合成

上記の EXPORT_someblob_t のようなエクスポートオブジェクトはCPS(継続渡し)マクロとして定義している。
SchemeでのCPSマクロは以前書いた( http://d.hatena.ne.jp/mjt/20141011/p1 )が、今回使用しているのはこれのC言語版で、SchemeでのCPSマクロと同じ目的で活用している。
Cマクロは関数と異なり合成可能でない。例えば、以下のように関数showをONE〜FOURマクロによって可変長引数関数のように扱いたいとする。

#define INVALID -1

#define ARG1(x)       x, INVALID, INVALID, INVALID
#define ARG2(x,y)     x, y, INVALID, INVALID
#define ARG3(x,y,z)   x, y, z, INVALID
#define ARG4(x,y,z,w) x, y, z, w

#define ONE   ARG1(1)
#define TWO   ARG2(1,2)
#define THREE ARG3(1,2,3)
#define FOUR  ARG4(1,2,3,4)

#define SHOW(x,y,z,w) show(x,y,z,w)

#include <stdio.h>

static void
show(int a, int b, int c, int d){
    printf("a = %d, b = %d, c = %d, d = %d\n",
           a, b, c, d);
}

int
main(int ac, char* av){
    SHOW(ONE); /* ← ★ エラー。SHOWマクロは引数を1つしか取れない */
    SHOW(TWO);
    SHOW(THREE);
    SHOW(FOUR);
    return 0;
}

SHOWやONEが関数であれば、ONEが最初に評価されSHOWが評価されるので上記のようなコードは正しく実行されるように見える。実際には、マクロは常に左から展開されるためSHOWのマクロ呼び出しがエラーになってしまう。
そこで、ONE〜FOURを引数に継続 k を取るCPSスタイルのマクロに変換し、継続としてSHOWを渡すことで無理矢理合成可能にする。

#define INVALID -1

/* ARGマクロは継続に4値を渡す。 */
#define ARG1(k,x)       k(x, INVALID, INVALID, INVALID)
#define ARG2(k,x,y)     k(x, y, INVALID, INVALID)
#define ARG3(k,x,y,z)   k(x, y, z, INVALID)
#define ARG4(k,x,y,z,w) k(x, y, z, w)

#define ONE(k)   ARG1(k,1)
#define TWO(k)   ARG2(k,1,2)
#define THREE(k) ARG3(k,1,2,3)
#define FOUR(k)  ARG4(k,1,2,3,4)

#define SHOW(x,y,z,w) show(x,y,z,w)

#include <stdio.h>

static void
show(int a, int b, int c, int d){
    printf("a = %d, b = %d, c = %d, d = %d\n",
           a, b, c, d);
}

int
main(int ac, char* av){
    ONE(SHOW);
    TWO(SHOW);
    THREE(SHOW);
    FOUR(SHOW);
    return 0;
}

stubIR処理系では、複雑なエクスポートパラメタをシンボリックに記述するためにこのテクニックを使用している。

/*  Export datum templates ::
 *
 *   A raw export will be a CPS macro like this;
 *
 *    #define RAWEXPORT(k) k(r0,r1,r2,r3,r4,r5,r6,r7)
 *
 *  Current mapping is:
 *
 *    r0 : flags
 *    r1 : name (C string)
 *    r2 : value_setter such as YUNIWORD_SET_SINT
 *    r3 : value (argument to value_setter macro)
 *    r4 : size
 *    r5 : offset */

エクスポートされるオブジェクトは1つにつき6つもプロパティが有るが、殆んどのオブジェクトでは3個程度しか必要としていないので、これらを手書きしていては見づらくなってしまう。そこで、マクロによってタプルに展開し、さらにその展開マクロをCPSマクロとすることでCマクロを使用して個々のプロパティにアクセスできるようにしている。

必要?

もっとも、この手のテクニックは現代的にはあまり必要とされていない。yuniFFIの場合は、VisualC++ 2005のCコンパイラ等悪夢のようなコンパイラをサポートする必要性からCマクロを使ってこの手の処理を実現している。
開発環境はCマクロのデバッグ環境を殆んど提供していない。C++テンプレートのデバッグ環境は急速に改善しつつあるが、Cマクロのデバッグ環境が今後充実することはほぼ期待できないので、可能な限りコンパイラを信頼し、プリプロセサではなく言語を直接使用するようにすべきと言える。
C99以降であれば、言語標準として可変長マクロが使用できるため、arityが異なるマクロを合成したいだけであればそちらを使用する手もある。
robustnessを犠牲にするならば、関数呼び出しのカッコをマクロ側で生成することでCPSマクロを使用せずに実現することもできる。ただ、このようなコードは野蛮で、複雑なことをすると壊れる可能性がある - 例えば、式全体をカッコで括っても意味は変わらないと信じているような場合。

#define INVALID -1

#define ARG1(x)       (x, INVALID, INVALID, INVALID)
#define ARG2(x,y)     (x, y, INVALID, INVALID)
#define ARG3(x,y,z)   (x, y, z, INVALID)
#define ARG4(x,y,z,w) (x, y, z, w)

#define ONE   ARG1(1)
#define TWO   ARG2(1,2)
#define THREE ARG3(1,2,3)
#define FOUR  ARG4(1,2,3,4)

#define SHOW show

#include <stdio.h>

static void
show(int a, int b, int c, int d){
    printf("a = %d, b = %d, c = %d, d = %d\n",
           a, b, c, d);
}

int
main(int ac, char* av){
    SHOW ONE;
    SHOW TWO;
    SHOW THREE;
    SHOW FOUR;
    return 0;
}