こんにちは、プログラマーのイシドです。
最近巷では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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import sys import os sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/bindings/python') from clang.cindex import Config, Index # Clangの設定 Config.set_library_path(r'C:\Program Files\LLVM\bin') # パース translation_unit = Index.create().parse('test.cpp') # 出力 def dump(cursor, indent=0): text = cursor.kind.name print('\t' * indent + text) for child in cursor.get_children(): dump(child, indent+1) dump(translation_unit.cursor) |
・test.cpp
1 2 3 4 5 6 7 8 9 10 11 |
class TestClass { using My = TestClass; int a; My* pMy; int Foo(int a) { return a; } }; |
・結果
1 2 3 4 5 6 7 8 9 10 11 12 13 |
TRANSLATION_UNIT CLASS_DECL TYPE_ALIAS_DECL TYPE_REF FIELD_DECL FIELD_DECL TYPE_REF CXX_METHOD PARM_DECL COMPOUND_STMT RETURN_STMT UNEXPOSED_EXPR DECL_REF_EXPR |
いい感じにパースできていますね!
もう少し詳細に
下記プロパティを使ってもう少し詳細に出力してみます。特にusingなどしている場合は元の型を知りたい場合が多いと思いますので、それも出力するようにしています。
- cursor.spelling 変数名や関数名
- cursor.type.spelling 型
- cursor.type.get_canonical().spelling 元の型
ただし、関数の中身に関しては今回は必要ありませんので、test.pyの11行目で「TranslationUnit.PARSE_SKIP_FUNCTION_BODIES」を指定して関数の中身はスキップしています。
・test.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
import sys import os sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/bindings/python') from clang.cindex import Config, Index, TranslationUnit # Clangの設定 Config.set_library_path(r'C:\Program Files\LLVM\bin') # パース translation_unit = Index.create().parse('test.cpp', options = TranslationUnit.PARSE_SKIP_FUNCTION_BODIES) # 出力 def dump(cursor, indent=0): text = cursor.kind.name # 名前 text += ' 名前[' + cursor.spelling + ']' # 型 text += ' 型[' + cursor.type.spelling + ']' # 元の型 if cursor.type.spelling != cursor.type.get_canonical().spelling: text += ' 元型[' + cursor.type.get_canonical().spelling + ']' print('\t' * indent + text) for child in cursor.get_children(): dump(child, indent+1) dump(translation_unit.cursor) |
・結果
1 2 3 4 5 6 7 8 9 |
TRANSLATION_UNIT 名前[test.cpp] 型[] CLASS_DECL 名前[TestClass] 型[TestClass] TYPE_ALIAS_DECL 名前[My] 型[TestClass::My] 元型[TestClass] TYPE_REF 名前[class TestClass] 型[TestClass] FIELD_DECL 名前[a] 型[int] FIELD_DECL 名前[pMy] 型[TestClass::My *] 元型[TestClass *] TYPE_REF 名前[TestClass::My] 型[TestClass::My] 元型[TestClass] CXX_METHOD 名前[Foo] 型[int (int)] PARM_DECL 名前[a] 型[int] |
他にも関数の戻り型は「cursor.result_type.spelling」で取得できたりします、「bindings\python\clang\cindex.py」のCursorクラスを眺めると、様々な情報が取れるのがわかります。
ここまでの情報が取れれば、後はFIELD_DECLをリストアップしてジェネレーターコードを書けば目的は果たせます。非常にお手軽ですね!
ちなみに #include していたりして目的以外のものも出力されて困る場合があります。そういう場合は「cursor.location.file.name」に現在パースしている名前が入っているのでフィルタリングすればOKです。
パース引数
パースするときに以下のようにすると、パースする際にdefineなどが定義できます。
1 2 |
args = ["-D__CLANG_PARSE__",] translation_unit = Index.create().parse('test.cpp', args = args, options = TranslationUnit.PARSE_SKIP_FUNCTION_BODIES) |
ここに書いてある引数にvcxprojに書かれてあるものが使えれば、同じdefineでパースすることができます。
・MSBuild
いろいろ悩んだ結果、ゴリ押しでパースする方法を試してみました。ただし、vcxprojにはプロパティシートが適応されていますので、それらをMSBuildのプリプロセスを使って展開させておくことで、XMLをパースするだけで済むようにしています。
・sample.py
今回のテストではMSBuildのインストール場所は決め打ちしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import sys import os import subprocess sys.path.append(os.path.dirname(os.path.abspath(__file__))) import vcxproj_parser MSBUILD_EXE = "C:\\Program Files (x86)\\MSBuild\\14.0\\Bin\\MSBuild.exe" MSBUILD_OPTION = '/p:Configuration=Debug;Platform="x64" ' # プリプロセス cmd = MSBUILD_EXE + " " + MSBUILD_OPTION + 'sample.vcxproj' + " /preprocess:" + 'sample.vcxproj.preprocess' subprocess.call(cmd) # vcxprojパース options = vcxproj_parser.GenerateCompileOption('sample.sln', 'sample.vcxproj', 'sample.vcxproj.preprocess') print(options) |
・vcxproj_parser.py(GenerateCompileOption関数)
実際にパースしているのがこの関数です。必要なxmlの場所を探してガリッと引っ張って来ています。その際にSolutionDirなどの変数やConditionを解決する必要がありますが、試したvcxprojにて必要なもののみ対応しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
def GenerateCompileOption(sln_name, vcxproj_name, preprocess_vcxproj_name): with open(preprocess_vcxproj_name, "r", encoding="utf-8") as f: root = ET.fromstring(f.read()) ENVIRONMENT.update( os.environ.items() ) ENVIRONMENT["Configuration"] = "Debug" ENVIRONMENT["Platform"] = "x64" ENVIRONMENT["SolutionDir"] = os.path.dirname(os.path.abspath(sln_name)) ENVIRONMENT["ProjectDir"] = os.path.dirname(os.path.abspath(vcxproj_name)) ENVIRONMENT["IncludePath"] = "$(VC_IncludePath);$(WindowsSDK_IncludePath)" ENVIRONMENT["VC_IncludePath"] = "" # システムはいらないので空白にしてしまう。 ENVIRONMENT["WindowsSDK_IncludePath"] = "" # システムはいらないので空白にしてしまう。 includes = [] definitions = [] forceIncludeFiles = [] for ItemDefinitionGroup in root.iter('{http://schemas.microsoft.com/developer/msbuild/2003}ItemDefinitionGroup'): if not ValidElement( ItemDefinitionGroup ): continue for AdditionalIncludeDirectories in ItemDefinitionGroup.iter('{http://schemas.microsoft.com/developer/msbuild/2003}AdditionalIncludeDirectories'): includes = ConvertText(includes, "AdditionalIncludeDirectories", AdditionalIncludeDirectories.text) # 追加ではなく代入 for PreprocessorDefinitions in ItemDefinitionGroup.iter('{http://schemas.microsoft.com/developer/msbuild/2003}PreprocessorDefinitions'): definitions = ConvertText(definitions, "PreprocessorDefinitions", PreprocessorDefinitions.text) # 追加ではなく代入 for ForcedIncludeFiles in ItemDefinitionGroup.iter('{http://schemas.microsoft.com/developer/msbuild/2003}ForcedIncludeFiles'): forceIncludeFiles = ConvertText(forceIncludeFiles, "ForcedIncludeFiles", ForcedIncludeFiles.text) # 追加ではなく代入 for PropertyGroup in root.iter('{http://schemas.microsoft.com/developer/msbuild/2003}PropertyGroup'): if not ValidElement( PropertyGroup ): continue for IncludePath in PropertyGroup.iter('{http://schemas.microsoft.com/developer/msbuild/2003}IncludePath'): includePaths = ConvertText([], "IncludePath", IncludePath.text) # 追加ではなく代入 text = "" includes = includePaths + includes # システムインクルードパスをくっつける for include in includes: text += "-I" + include + "\n" for definition in definitions: text += "-D" + definition + "\n" for forceIncludeFile in forceIncludeFiles: text += "-include" + forceIncludeFile + "\n" return text |
ConvertTextで変数の展開や、ValidElementでconditionのチェックを行っています。一応やりたかったことは果たせましたが、実はこうやれば普通に引っ張ってこれるなどがあればぜひ教えていただきたいです。だいぶ大雑把な実装ですが、使ったvcxprojのソースは下記にアップロードしてあります。
vcxproj_parser.py
・結果
1 2 3 4 5 6 |
-D_DEBUG -D_CONSOLE -D_UNICODE -DUNICODE -D_UNICODE -DUNICODE |
これでvcxprojに設定されているオプションが引っ張ってこれましたので、これをClangのパーサに渡してあげれば、マクロによる分岐をしていても問題なくパースすることができます。
応用
test.cppに下記のようにアノテーションマクロをつけることによって、対象クラスの一つ上にダミー関数が定義されます。
・test.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#define UNIQUE_NAME_IMPL(SYMBOL_, LINE_) SYMBOL_ ## LINE_ #define UNIQUE_NAME(SYMBOL_, LINE_) UNIQUE_NAME_IMPL(SYMBOL_, LINE_) #define ANNOTATION_IMPL(func_name_, ARGS_) void UNIQUE_NAME(func_name_, __LINE__)(const char* arg = ARGS_); #define ANNOTATION(...) ANNOTATION_IMPL(annotation_, #__VA_ARGS__) ANNOTATION(Param1=1, Param2=2) class TestClass { using My = TestClass; int a; My* pMy; int Foo(int a) { return a; } }; |
・結果
1 2 3 4 5 6 7 8 9 10 11 12 13 |
TRANSLATION_UNIT 名前[test.cpp] 型[] FUNCTION_DECL 名前[annotation_7] 型[void (const char *)] PARM_DECL 名前[arg] 型[const char *] UNEXPOSED_EXPR 名前[] 型[const char *] STRING_LITERAL 名前["Param1=1, Param2=2"] 型[const char [19]] 元型[char const[19]] CLASS_DECL 名前[TestClass] 型[TestClass] TYPE_ALIAS_DECL 名前[My] 型[TestClass::My] 元型[TestClass] TYPE_REF 名前[class TestClass] 型[TestClass] FIELD_DECL 名前[a] 型[int] FIELD_DECL 名前[pMy] 型[TestClass::My *] 元型[TestClass *] TYPE_REF 名前[TestClass::My] 型[TestClass::My] 元型[TestClass] CXX_METHOD 名前[Foo] 型[int (int)] PARM_DECL 名前[a] 型[int] |
ジェネレータを作る際に「CLASS_DECL」の一つ前に「annotation_*」という名前の関数があったら「STRING_LITERAL」を探しに行き、文字列の部分を分解してあげればジェネレータに対象クラス(や関数)のオプションを渡すことができます。
このマクロは通常コンパイル時には空に置き換わるようにしておけばランタイム時に害はありません。
最後に
C++をパースする・・・と聞いただけで、初めはなんだか気が重い感じがしていたんですが、実際やってみるとさっくりとパースできてしまい、こんなことを試してみたい、などとやってみたいことが増えました。実運用させるにはビルド後イベントに仕込んだり、makeなどを使って日付チェックをしたりなど、整えないといけないことは多いですが、これだけ手軽にパースできるのであればいろいろな箇所で使えそうです。
それでは今日はこのへんで。