読者です 読者をやめる 読者になる 読者になる

テストステ論

高テス協会会長が, テストステロンに関する情報をお届けします.

(macro-of-inline report) _fake_defines.hが有害

さきほど

(macro-of-inline report) 変換の過程をロギングするrecordオプションの実装 - テストステ論

の最後で, バグに気づいた. ファイルレベルの変換の最後の方がよくないと思ったが, よく考えると問題はもっと本質的である.

pycparserは, 入力がプリプロセスされてることを前提とする. これは, pycparserがマクロをASTノードとして認識出来ないからだ. しかしこれは妥当な仕様だと思う.

pycparserは, あらゆるコードを偽にパースするために, fake_libc_includeを用意している. これをインクルードすることで, 偽のtypedefなどを使い, とりあえずパースは出来るようにするというものだ.

macro-of-inlineは, ひとまず偽のパースを行ったあとに, インクルード展開されてしまったtypedefなどを取り除いて, もともとあったinclude directivesを戻している. こうして生成されたファイルは, このコードを実際にコンパイルするコンパイラにとって, 本来渡されるコードが単にinline関数がマクロ化されただけのコードになる. というのが, macro-of-inlineが拠り所にしている仕組みである.

ここで原点に戻ると, 最初にfake_libc_includeを使ってプリプロセスしてしまうと, 例えばそこに書いてあるNULLだとかが具体的に置換されてしまう. このコードは, もうもとに戻すことが出来ない. NULLくらいならかわいいが, もっと致命的な置換が行われた場合, 動作環境において破滅的な動作を引き起こす可能性がある. 置換された値がやたら大きいとかいう理由からだ.

というわけで, 現状では, 以下の_fake_defines.hに書いてあるdefine文に強く依存したソフトウェアは破滅することとなる.

#ifndef _FAKE_DEFINES_H
#define _FAKE_DEFINES_H

#define    NULL   0
#define    BUFSIZ     1024
#define    FOPEN_MAX  20
#define    FILENAME_MAX   1024

#ifndef SEEK_SET
#define    SEEK_SET   0  /* set file offset to offset */
#endif
#ifndef SEEK_CUR
#define    SEEK_CUR   1  /* set file offset to current plus offset */
#endif
#ifndef SEEK_END
#define    SEEK_END   2  /* set file offset to EOF plus offset */
#endif


#define EXIT_FAILURE 1
#define EXIT_SUCCESS 0

#define RAND_MAX 32767
#define INT_MAX 32767

/* C99 stdbool.h defines */
#define __bool_true_false_are_defined 1
#define false 0
#define true 1

#endif

この条件を満たしたソフトウェアはたぶんあまりない. しかし可能性はゼロではない.

この問題に対する私の解は, _fake_define.hを使わないことである.

そもそも論でいうと, pycparserが必要なのはtypedefだとかstructだけであり, defineはどうでもいいのだ. 例えば, 以下のコードにおいて, t1はパース出来るが, t2はパース出来ない. t2の問題がtypedefである点が本質的なのだ(myintが定義されていない). t1において, Nが定義されていないだとかは関係がない. NULLなども同様である. 型でなければよいのだ.

def p(t):
        parser = c_parser.CParser()
        parser.parse(t).show()

t1 = r"""
void f()
{
  int x = N;
  int x = true;
  f(1);
}
"""
p(t1)

t2 = r"""
void f()
{
  myint x = 0;
}
"""
p(t2)
FileAST:
  FuncDef:
    Decl: f, [], [], []
      FuncDecl:
        TypeDecl: f, []
          IdentifierType: ['void']
    Compound:
      Decl: x, [], [], []
        TypeDecl: x, []
          IdentifierType: ['int']
        ID: N
      Decl: x, [], [], []
        TypeDecl: x, []
          IdentifierType: ['int']
        ID: true
      FuncCall:
        ID: f
        ExprList:
          Constant: int, 1
Traceback (most recent call last):
  File "pycparser-attic.py", line 23, in <module>
    p(t2)
  File "pycparser-attic.py", line 5, in p
    parser.parse(t).show()
  File "/usr/local/lib/python2.7/dist-packages/pycparser/c_parser.py", line 138, in parse
    debug=debuglevel)
  File "/usr/local/lib/python2.7/dist-packages/pycparser/ply/yacc.py", line 265, in parse
    return self.parseopt_notrack(input,lexer,debug,tracking,tokenfunc)
  File "/usr/local/lib/python2.7/dist-packages/pycparser/ply/yacc.py", line 1047, in parseopt_notrack
    tok = self.errorfunc(errtoken)
  File "/usr/local/lib/python2.7/dist-packages/pycparser/c_parser.py", line 1631, in p_error
    column=self.clex.find_tok_column(p)))
  File "/usr/local/lib/python2.7/dist-packages/pycparser/plyparser.py", line 54, in _parse_error
    raise ParseError("%s: %s" % (coord, msg))
pycparser.plyparser.ParseError: :4:9: before: x

最後の問題は, プリプロセッサである. 例えば以下のようなコードは, コンパイル出来ない. しかしプリプロセッシングは出来る. もしここで「Nが見つからないからプリプロセシング出来ません」と言われると, pycparserよりも制約きついことになるから, pycparserに食わせる前にプリプロセシング出来ない. 制約のきつさが, pycparser以下ならば問題がない. ではどのくらいきついかというと, ゆるゆるである. 例えば, intをmyintに変更しても, プリプロセシングだけは通る. プリプロセシングは本当に文字列しか見ていないということがよく分かる(本来, pycparserも構文しか気にすべきでなく, 型の定義未定義は気にすべきでないが, 実装が糞なのでそうなってる. 修正出来ると思うが, やってもupstreamに入らない).

main()
{
        int x = N;
}
# 1 "a.c"
# 1 "<command-line>"
# 1 "a.c"
main()
{
 int x = N;
}

以上の考察を以って, 私のとる道は, 最低でも, macro-of-inlineでは_fake_define.hを使わないこと. 最善は, upstreamに提案して消すこと. 「pycparserがパースするためにdefine文は必要ない」ということを言えばいい.

マクロオブインラインを使いましょう!!!