HEXA BLOG

プログラム

HEXA BLOGプログラム2016.9.7

優しいC++解析(その1)

お久しぶりです。

 

夏は暑いのでお年寄りに交じって早朝ジョギングをしているgood sunこと山口です
5:30にはすでにお年寄りは散歩中という現実に驚きです。

 

以前の悪ダイコンChanで立体造形も一区切りついたので、
次は編みぐるみの話でも始めようかと思ったのですが、
そろそろプログラマらしいネタをやらないといけないのではないか?
そんな使命感に駆られたので簡単なテーマを決めて、
シリーズ化して紹介していきたいと思います。

 

今回選んだテーマはC++のソース解析についてです

 

motivation:(<=論文のこの表現が好き)

ヘキサエンジンで採用しているリフレクション,シリアライズ機能を今風にしたい。

 

ヘキサエンジンではC++のソースのみで完結できる形で
リフレクションとシリアライズ機能を実装しています。
(特にソースを自動生成とかせずに頑張ってメタを埋め込む感じ)
これはこれでコンパイラさえあれば機能が実現できるので良いのですが、
機能拡張が困難だったり書式的に冗長性が高かったりするので、
少し前から巷で噂のClangを利用したアノテーションを埋め込むスタイルを実現してみたい。

 

Clang:

クランって何?って方の為に簡単な説明をwikipediaさんがしてくれているのでリンクを紹介
https://ja.wikipedia.org/wiki/Clang
LLVMという比較的新しめのコンパイラを作る仕組みを利用した、
C言語系のコンパイラらしいです。

 

因みにClangそのものを利用するとコンパイル結果を取得する事になったりして面倒ぽいので、
今回はlibclangというC言語から利用するライブラリを利用して、
解析結果を利用したいと思います。

 

ClangSharp:

libclangを使うと言いましたが、
正直C言語で文字列扱ったりするのは正気の沙汰ではありません。
ネットを徘徊するとPythonのバインディング等があるのですが、
デバッグが面倒だなぁと感じたりしたので
(VisualStudioで良い感じにデバッグは出来るんですが…)
C#経由で使ってみたいと思います。

 

今回紹介するのはClangSharpというパッケージです。
他にもバインディングはあるのですが、
NuGetから導入可能というお手軽さだったのでこれを選択しました。
https://www.nuget.org/packages/ClangSharp/

 

demonstration:

今回は簡単にC++のクラスの内容を取得する事をやってみたいと思います。

 

ではまずはC#で適当にプロジェクトを作成します。
プロジェクトを作成したら、
ソリューションにNuGetからClangSharp3.8.Xをインストールします。
20160907_01
※ClangSharpは3.6.X系がありますが、ネームスペース以外全く別物なので要注意!

 

さらに一つ問題がありまして、
ClangSharp3.8.XはAnyCPUでビルドできないのでx64かx86のターゲットを作成します。
20160907_02
20160907_03

そうしましたら今回解析するソースはこんな感じです。

sample.cpp

typedef short s16;

namespace test{

class Sample{
public:
	enum class EnumType : s16{
		E0,
		E1=10,
		E2,
	};
	Sample(){}
	~Sample(){}
private:
	long		_value;
	EnumType	_e;
	s16			_array[3];
};

template
class TSample
{
public:
	TSample(){}
	~TSample(){}
private:
	T			_v;
};

}

 

最低限なC++の構成としてネームスペースとクラス定義と各種変数が定義されています。
このようなソースにlibclangを使うと上記が解析されて各要素が木にぶら下がった状態で取得が出来るらしいです

 

ではこれを解析するC#のコードをザックリとこんな感じで用意します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ClangSharp;

namespace ClangTest
{
	class Program
	{
		static void Main(string[] args)
		{
			var index = clang.createIndex(0, 0);
			var ufile = new CXUnsavedFile();
			var trans = new CXTranslationUnit();
			var erro = clang.parseTranslationUnit2(index, "sample.cpp", new String[] { "-std=c++11" }, 1, out ufile, 0, 0, out trans);
			// 本当はここでエラーチェックが好ましい
			var cursor = clang.getTranslationUnitCursor(trans);
			var pg = new Program();
			clang.visitChildren(cursor, new CXCursorVisitor(pg.visitChild), new CXClientData());
		}
		protected List _namespace = new List();
		protected String _namespaceStr = "";
		CXChildVisitResult visitChild(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			switch (cursor.kind) {
			case CXCursorKind.CXCursor_Namespace:
				readNamespace(cursor);
				clang.visitChildren(cursor, new CXCursorVisitor(visitChild), new CXClientData());
				removeNamespace();
				break;
			case CXCursorKind.CXCursor_ClassDecl:
				readClass(cursor);
				break;
			case CXCursorKind.CXCursor_EnumDecl:
				readEnum(cursor);
				break;
			case CXCursorKind.CXCursor_FieldDecl:
				readField(cursor);
				break;
			case CXCursorKind.CXCursor_ClassTemplate:
				readTClass(cursor);
				break;
			default:
				break;
			}
			return CXChildVisitResult.CXChildVisit_Continue;
		}
		CXChildVisitResult visitEnum(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			var itype = clang.getEnumDeclIntegerType(cursor);
			if (itype.kind == CXTypeKind.CXType_UInt) {
				Console.WriteLine("{0} {1}", clang.getCursorSpelling(cursor), clang.getEnumConstantDeclUnsignedValue(cursor));
			}
			else {
				Console.WriteLine("{0} {1}", clang.getCursorSpelling(cursor), clang.getEnumConstantDeclValue(cursor));
			}
			return CXChildVisitResult.CXChildVisit_Continue;
		}
		void readNamespace(CXCursor cursor)
		{
			String name = clang.getCursorSpelling(cursor).ToString();
			_namespace.Add(name);
			_namespaceStr = String.Join("::", _namespace);
		}
		void removeNamespace()
		{
			_namespace.RemoveAt(_namespace.Count - 1);
			_namespaceStr = String.Join("::", _namespace);
		}
		void readClass(CXCursor cursor)
		{
			Console.WriteLine("クラス定義発見 ({1}){0}", clang.getCursorSpelling(cursor), _namespaceStr);
			// さらに子を検索する
			clang.visitChildren(cursor, new CXCursorVisitor(visitChild), new CXClientData());
		}
		void readTClass(CXCursor cursor)
		{
			Console.WriteLine("テンプレートクラス定義発見 {0}", clang.getCursorSpelling(cursor));
			// さらに子を検索する
			clang.visitChildren(cursor, new CXCursorVisitor(visitChild), new CXClientData());
		}
		void readEnum(CXCursor cursor)
		{
			Console.WriteLine("enum定義発見 {0}", clang.getCursorSpelling(cursor));
			// さらに子を検索する
			clang.visitChildren(cursor, new CXCursorVisitor(visitEnum), new CXClientData());
		}
		void readField(CXCursor cursor)
		{
			var type = clang.getCursorType(cursor);
			String typeName;
			var arrCount = clang.getArraySize(type);
			if (arrCount > 0) {
				var arrType = clang.getArrayElementType(type);
				typeName = String.Format("{0}=>({1})", arrType, arrCount);
			}
			else {
				typeName = type.ToString();
			}
			Console.WriteLine("変数宣言発見 {0} {1}", clang.getCursorSpelling(cursor), typeName);
		}
	}
}

※急いで書いたコードなので汚い…
最初ClangSharp3.6.X使っていて情報取れないなぁ…って状態からClangSharp3.8.Xにしたら 様変わりしている上にどうやって使っていいかドキュメントが無くて、
最初のclang.parseTranslationUnit2がどうしてもinvalid argmentしてしまったりで心折れ掛かりました

 

因みにClangSharpの3.8.Xはほぼlibclangなのでlibclangのドキュメントさえあれば何とかなります。
http://clang.llvm.org/doxygen/group__CINDEX.html

 

そしてこれを実行するとこんな感じになります。

20160907_04
結構すんなり取得出来てしまいました。

 

こんな感じで構文木状態のデータ構造を読み取っていると、
自作スクリプトのコンパイラを作ってるみたいでワクワクしますよね。

 

尚cppを解析させていますが、
これはネットの情報によるとClangが解釈するのは翻訳単位なので ヘッダでは十分な解析が行えないという情報に基づきます。
ClangSharp3.6.Xで試した時は確かに情報は取得できませんでした。
将来的にやりたい事はヘッダにアノテーションを追加して
それを解析できれば十分なのでこの辺の対応方法は今後の課題ですね。

 

とは言えとても簡単にC++の構成を取得できる事が分かりましたので、
今後はこの情報を利用して色々な機能を実現していく事でシリーズ化していこうと思います。

 

という事で一応プログラマアピールをするブログその1はこれまでです。

では次回も続くことを祈って

RECRUIT

大阪・東京共にスタッフを募集しています。
特にキャリア採用のプログラマー・アーティストに興味がある方は下のボタンをクリックしてください

RECRUIT SITE 

NEWS