動機
Unreal EngineのUPPROPETYマクロのようなことがやりたいです。
class MyActor : public ActorBase {
ACTOR_DECL()
PROPETY(Serialize)
s32 mVarA;
}
C#やJavaにはReflectionの機能があるので、こういったものが簡単に作れます。しかし、C++でメンバ変数の一覧や、その隣に書いてあるマクロを得るのは簡単ではありません。言語組み込みの機能を使って強引にやろうとすると、どうしても実行効率が犠牲になってしまいます。
そこで、コードジェネレータを利用することにしました。
このフローを実現するためには、C++のヘッダファイルを正しく解析する必要があります。今回はclangの構文解析器を使用して実装してみました。
環境はMacOS X 10.11、Clang 3.7を使用しました。
#Clang python bindingのセットアップ
MacPortsからclang-mp-3.7をインストールします。
続いて、公式svnからpython-bindingsをダウンロードします。
環境変数にPYTHONPATHに、先ほどダウンロードしたパスを設定しておきます。
export PYTHONPATH="~/cfe-3.7.1.src/bindings/python"
これでめでたくpython bindingsが使えるようになりました。
#coding: utf-8
import clang.cindex
from clang.cindex import Config
from clang.cindex import Index
from clang.cindex import TranslationUnit
# Clangの設定
Config.set_library_path("/opt/local/libexec/llvm-3.7/lib")
clang_args = [
"-std=c++11",
"-I./",
]
# 構文木をダンプ
def dump_ast(cursor, indent = ""):
print("%s%s : %s" % (indent, cursor.kind.name, cursor.displayname))
for child in cursor.get_children():
dump_ast(child, indent + "\t")
index = Index.create()
tu = index.parse("MyActor.h", clang_args, None, TranslationUnit.PARSE_SKIP_FUNCTION_BODIES)
dump_ast(tu.cursor)
ヘッダファイルを読ませる
そもそもclangがパースできるのは翻訳単位(≒cpp)であってヘッダではありません。
上のコードでは何も考えずにヘッダファイルを渡してしまっているため、classなどが正しく読み込まれません。おそらくC言語のヘッダとして解釈されているのだと思います。
そこで、#includeが一つだけ存在する架空のcppを作ることで、C++として認識させます。
また、#includeによって連鎖的に読み込まれる他のファイルの情報は不要ですので、今回は無視します。
buf = "#include \"MyActor.h\""
tu = index.parse("MyActor.cpp", clang_args, [("MyActor.cpp", buf)], TranslationUnit.PARSE_SKIP_FUNCTION_BODIES)
for tu_cursor in tu.cursor.get_children():
for node_cursor in tu_cursor.get_children():
if node_cursor.location.file.name == "./MyActor.h":
dump_ast(node_cursor)
アノテーションを取得する
PROPETY(Serialize)の部分はマクロを使って記述しようと思います。
しかし、構文木の段階ではマクロは展開されているため、そのまま見つけることはできません。
そこで、今回はマクロ内でダミーの関数を定義することにしました。
#define UNIQ_NAME_IMPL(SYMBOL_, LINE_) SYMBOL_ ## LINE_
#define UNIQ_NAME(SYMBOL_, LINE_) UNIQ_NAME_IMPL(SYMBOL_, LINE_)
#define PROPETY(...) PROPETY_IMPL(#__VA_ARGS__)
#define PROPETY_IMPL(ARGS_) void UNIQ_NAME(_dummy_property_, __LINE__)(const char* arg = ARGS_);
こういう感じでマクロを作っておけば、PROPETY()マクロの位置に_dummy_property_XXXという名前のダミー関数が宣言されます。
構文木の内容は以下のようになっているはずです。
CLASS_DECL : MyActor
CXX_METHOD : _dummy_property_4(const char *)
PARM_DECL : args
UNEXPOSED_EXPR :
STRING_LITERAL : "Serialize"
FIELD_DECL : mVarA
ここまで来たら、あとは難しいことはなにもありません。
注意するべき事があるとしたら、名前空間とクラスのネストを正しく扱うことくらいでしょうか。
細かい実装は省略します。
まとめ
libclangを使うのは初めてでしたが、意外と簡単に扱うことができました。
ただ、clangの認識しているのは「翻訳単位」であって「型」ではないので、そこをきちんと考慮しないと、C#やJavaのリフレクションと同じようには使えませんでした。
ダミー関数の存在に関しては、もう少しうまいやりかたがあるのかもしれません。今回はちょっと妥協しています。
C++でリフレクションが使えるとなると、シリアライズの他にも、デバッグ描画を自動で作ったり、アクセサを自動で定義したりと、夢が広がります。
追記
上記のダミー関数を用いた方法では、enumの内部に属性を定義するのが難しいことが分かりました。
それを解決するために、属性マクロのソース上の位置を参照して、宣言と属性を紐付けることにしました。
ソースコードはGistに置いているので、よければご利用ください。