ClangでC++をパースしてしてみよう

こんにちは、プログラマーのイシドです。

最近巷ではC#だったりブループリントだったりが人気のようですが、私はまだC++を使うことが多いです。開発をしているとたまに構造体を出力して、他の言語で読みたくなったりします。大げさなシステムは・・・と思いながらつい手動でシリアライズ関数を作ったり、別言語で書いたりしてしまいがちですが、ミスもありますし自動で生成できるならそれに越したことはありません。

シリアライズ生成でよくあるのが、専用のフォーマットで構造体を定義して、それをジェネレータに入力して各種言語の定義を生成するものです。しかし普段の開発では先にC++でランタイムの実装をしてしまうこともあるため、C++の構造体の定義から自動でシリアライザの定義やC#の定義を生成したいなと思いました。実際にClangによるC++パーサを試してみましたところ、思った以上に手軽にできましたので、そのやり方を載せたいと思います。

ちなみにその際にvcxprojからincludeパスやdefineオプションを引っ張ってこれると嬉しいので、試しに引っ張ってきたものも載せたいと思います。ただ、vcxprojのパースはもっといいやり方がありそうな気がしますが、調べてもわからなかったのでちょっとゴリ押しで試してみました。

インストール

試した環境はWindow10 x64です。

・Python

https://www.python.org/downloads/windows/
このページのPython3.7.3の「Windows x86-64 embeddable zip file」を落として、任意の場所に展開させました。

・Clang

http://releases.llvm.org/download.html
このページのLLVM8.0.0のPre-Built Binariesにある「Window(64-bit)」をダウンロード&インストールしました。

(C:\Program Files\LLVM にインストールしました)

・Python binding

Pythonから呼べるようにするモジュールはソースの方に入っているので、同じページにある Sourcesの「Clang source code」も落とします。解凍した中に入っている「bindings」フォルダをテストするフォルダに置きました。

ただしPython bindingはpipによるインストールが可能なので、そちらを使えばわざわざ自分でソースをとってくる必要はありませんが、今回は自分でダウンロードしました。

パース

では実際にパースしてみましょう。

・test.py

・test.cpp

・結果

いい感じにパースできていますね!

もう少し詳細に

下記プロパティを使ってもう少し詳細に出力してみます。特にusingなどしている場合は元の型を知りたい場合が多いと思いますので、それも出力するようにしています。

  • cursor.spelling 変数名や関数名
  • cursor.type.spelling 型
  • cursor.type.get_canonical().spelling 元の型

ただし、関数の中身に関しては今回は必要ありませんので、test.pyの11行目で「TranslationUnit.PARSE_SKIP_FUNCTION_BODIES」を指定して関数の中身はスキップしています。

・test.py

・結果

他にも関数の戻り型は「cursor.result_type.spelling」で取得できたりします、「bindings\python\clang\cindex.py」のCursorクラスを眺めると、様々な情報が取れるのがわかります。

ここまでの情報が取れれば、後はFIELD_DECLをリストアップしてジェネレーターコードを書けば目的は果たせます。非常にお手軽ですね!

ちなみに #include していたりして目的以外のものも出力されて困る場合があります。そういう場合は「cursor.location.file.name」に現在パースしている名前が入っているのでフィルタリングすればOKです。

パース引数

パースするときに以下のようにすると、パースする際にdefineなどが定義できます。

ここに書いてある引数にvcxprojに書かれてあるものが使えれば、同じdefineでパースすることができます。

・MSBuild

いろいろ悩んだ結果、ゴリ押しでパースする方法を試してみました。ただし、vcxprojにはプロパティシートが適応されていますので、それらをMSBuildのプリプロセスを使って展開させておくことで、XMLをパースするだけで済むようにしています。

・sample.py

今回のテストではMSBuildのインストール場所は決め打ちしています。

・vcxproj_parser.py(GenerateCompileOption関数)

実際にパースしているのがこの関数です。必要なxmlの場所を探してガリッと引っ張って来ています。その際にSolutionDirなどの変数やConditionを解決する必要がありますが、試したvcxprojにて必要なもののみ対応しました。

ConvertTextで変数の展開や、ValidElementでconditionのチェックを行っています。一応やりたかったことは果たせましたが、実はこうやれば普通に引っ張ってこれるなどがあればぜひ教えていただきたいです。だいぶ大雑把な実装ですが、使ったvcxprojのソースは下記にアップロードしてあります。
vcxproj_parser.py

・結果

これでvcxprojに設定されているオプションが引っ張ってこれましたので、これをClangのパーサに渡してあげれば、マクロによる分岐をしていても問題なくパースすることができます。

応用

test.cppに下記のようにアノテーションマクロをつけることによって、対象クラスの一つ上にダミー関数が定義されます。

・test.cpp

・結果

ジェネレータを作る際に「CLASS_DECL」の一つ前に「annotation_*」という名前の関数があったら「STRING_LITERAL」を探しに行き、文字列の部分を分解してあげればジェネレータに対象クラス(や関数)のオプションを渡すことができます。

このマクロは通常コンパイル時には空に置き換わるようにしておけばランタイム時に害はありません。

最後に

C++をパースする・・・と聞いただけで、初めはなんだか気が重い感じがしていたんですが、実際やってみるとさっくりとパースできてしまい、こんなことを試してみたい、などとやってみたいことが増えました。実運用させるにはビルド後イベントに仕込んだり、makeなどを使って日付チェックをしたりなど、整えないといけないことは多いですが、これだけ手軽にパースできるのであればいろいろな箇所で使えそうです。

それでは今日はこのへんで。

(Visited 8,168 times, 1 visits today)

コメント投稿は締め切りました。