HEXA BLOG

プログラム

HEXA BLOGプログラム2016.12.2

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

ついに今年も最後の月になりましたね。
ここにきて期待のホラー映画ブレアウィッチまで登場してワクワクが止まりません。
お久しぶりです、good sunこと山口です。

さて、今回は前回に引き続いてclangを使ったC++のソース解析の続きを行います。

前回ではクラスや構造体の情報を解析するコードについて簡単に触れました。

今回はそこから発展して、特定の変数について情報を追加してみたいと思います。

具体的には以下の様なコードで

class Sample{
public:
	Sample();
	~Sample();
private:
	PARAM(_SERIALIZE("no"), _HINT("hint text"))
	long		_value;
};

_valueという変数に対して

・シリアライズを行わない

・ヒント情報としてhint textという情報を持つ

という情報を付与する事を目標とします。

その前にNuGetで取得できるClangSharpの不具合を修正します。

clang.parseTranslationUnit2の引数でUnsavedFileをout属性で渡すという処理になっていますが、

こちらは配列を渡すのが本来の挙動になります。

このUnsavedFileはメモリ上のファイルを読み込ませたりする目的で使うのでout属性ではそれが達成出来ません。

まずは開発者のGitHubのページを見てみます。(オープンソースって素晴らしいですね)

https://github.com/Microsoft/ClangSharp

見てみると実はその辺対策済みなコードが上がっています。

のでこちらをcloneするかdownloadします。

(私はGitHub Desktopがエラーになったので男らしくdownloadを選びました)

20161202_00

downloadもしくはcloneしたファイルにはClangSharp.slnが入っているのでコレを開きます。

※公式ページのBuilding ClangSharpの手順に従うと修正されているハズのファイルが上書きされてしまうので、

公式に反旗を翻すアウトローになり切ってください

次にExtensions.csを開いて8行目をコメントアウトします。

※時間的に原因調査していませんが…そのままではここのdisposeによってハングしてしまいます。

コメントアウトが完了したらソリューションエクスプローラから

ClangSharpのプロジェクトを右クリックでビルドします。

20161202_01

出来上がったClangSharp.dllをpdbと共にNuGetで追加したプロジェクトの

package\ClangSharp.3.8.0\lib\net40に上書きします。

念のためClangSharpがclang3.9対応版との事なのでlibclang.dllも3.9のものに差し替えます。

llvmをビルドすると大変なのでClang for Windowsをインストールします。

http://llvm.org/releases/download.html

20161202_02

インストール先のLLVM\binの下のlibclang.dllを

packages\ClangSharp.3.8.0\content\x64に上書きコピーします。

因みにこの状態になるまで主に例のdisposeの所で原因不明の停止を繰り返すことになりましたが

対策としてClangSharpを使うプロジェクトのデバッグ設定で

ネイティブコードデバッグを有効にするのチェックを入れ忘れていて調査に手間取ったという事があったので、

この項目へのチェックは推奨です

20161202_03

はい、準備が整いましたので本題です。

流れとしましてはUnrealEngineでやっているようなマクロを作ってそれを活用することになります。

ちょっと今回手抜きをしてマクロ全部入りのコードで紹介をしますが…

C++のコードはこんなコードを使います。

#define CLASS_DEFS()	\
					virtual void serialize();	\
					virtual void deserialize();	\

#define _HINT(n) const char* hint=n
#define _SERIALIZE(n) const char* serialize=n

#define _COMBINE__(a,b)	a##b
#define _COMBINE_(name,l)	_COMBINE__(name, l)
#define PARAM(...)	void _COMBINE_(__Paramater_, __LINE__)(__VA_ARGS__);

namespace test{

class Sample{
public:
	Sample();
	~Sample();
private:
	PARAM(_SERIALIZE("no"), _HINT("hint text等"))
	long		_value;
};

}

CLASS_DEFS()の定義はゲームとしてビルドするときは空になる様にifdefで分けるようにする必要があります。

今回は仕組みの説明なので…

解析時はこのマクロが出てきたところに関数宣言を展開してキーワードとしてClangから抽出を行おうと考えています。

さらにデフォルト引数を使って入力パラメータの解釈を行います。

前回の解析用コードを少し変更してこんな感じのコードを使って解析します。

using System;
using System.Collections.Generic;
using System.Text;
using ClangSharp;

namespace ClangTest
{
	// パラメータ解析用クラス
	public class Param
	{
		public bool Serialize { get; set; } = true;
		public string Hint { get; set; }
		public void parseFunc(CXCursor cursor)
		{
			var param = clang.getCursorSpelling(cursor).ToString();
			switch (param) {
			case "serialize":
				clang.visitChildren(cursor, new CXCursorVisitor(visitSerialize), new CXClientData());
				break;
			case "hint":
				clang.visitChildren(cursor, new CXCursorVisitor(visitHint), new CXClientData());
				break;
			}
		}
		// シリアライズの有無のチェック
		CXChildVisitResult visitSerialize(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			if (clang.getCursorKind(cursor) == CXCursorKind.CXCursor_StringLiteral)
			{
				var val = clang.getCursorSpelling(cursor).ToString().Replace("\"","");
				switch (val.ToLower()) {
				case "no":
				case "false":
					Serialize = false;
					break;
				default:
					break;
				}
				return CXChildVisitResult.CXChildVisit_Break;
			}
			return CXChildVisitResult.CXChildVisit_Recurse;
		}
		// ヒントテキストを取得
		CXChildVisitResult visitHint(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			if (clang.getCursorKind(cursor) == CXCursorKind.CXCursor_StringLiteral) {
				var val = clang.getCursorSpelling(cursor).ToString();
				val = val.Substring(1, val.Length - 2);
				Hint = val;
				return CXChildVisitResult.CXChildVisit_Break;
			}
			return CXChildVisitResult.CXChildVisit_Recurse;
		}
		public override string ToString()
		{
			return string.Format("serialize={0},hint={1}", Serialize, Hint);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var index = clang.createIndex(0, 0);
			var ufile = new CXUnsavedFile();
			var trans = new CXTranslationUnit();
			ufile.Filename = "dummy.cpp";
			ufile.Contents = "#include \"sample.h\"";
			ufile.Length = ufile.Contents.Length;

			var erro = clang.parseTranslationUnit2(index, "dummy.cpp", new String[] { "-std=c++11" }, 1, new CXUnsavedFile[] { ufile }, 1, (uint)(ClangSharp.CXTranslationUnit_Flags.CXTranslationUnit_SkipFunctionBodies), out trans);
			var cursor = clang.getTranslationUnitCursor(trans);
			var kind = clang.getCursorKind(cursor);
			var pg = new Program();
			clang.visitChildren(cursor, new CXCursorVisitor(pg.visitChild), new CXClientData());

		}
		protected List _namespace = new List();
		protected String _namespaceStr = "";
		protected Dictionary<uint, Param> _params = new Dictionary<uint, Param>();
		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;
			case CXCursorKind.CXCursor_CXXMethod:
				readMethod(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)
		{
			var cxName = clang.getCursorSpelling(cursor);
			String name = cxName.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());
			_params.Clear();
		}
		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 readMethod(CXCursor cursor)
		{
			var name = clang.getCursorSpelling(cursor).ToString();
			// パラメータ解析する為のキーワードを見つけたら
			if (name.StartsWith("__Paramater_"))
			{
				var location = clang.getCursorLocation(cursor);
				CXFile file;
				uint line;
				uint column;
				uint offset;
				// 行番号とセットで情報を覚えさせる
				clang.getSpellingLocation(location, out file, out line, out column, out offset);
				Console.WriteLine("パラメータ宣言発見 {0} {1}", name, line);
				var param = new ClangTest.Param();
				// これで引数を解析
				var n = clang.Cursor_getNumArguments(cursor);
				for (uint i = 0; i < n; ++i)
				{
					var arg = clang.Cursor_getArgument(cursor, i);
					param.parseFunc(arg);
				}
				_params.Add(line + 1, param);
			}
		}
		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();
			}
			var location = clang.getCursorLocation(cursor);
			CXFile file;
			uint line;
			uint column;
			uint offset;
			// フィールドが出てきたときに覚えていた行数とパラメータ情報に一致するものがあったらその情報を適応する
			clang.getSpellingLocation(location, out file, out line, out column, out offset);
			Param pm;
			if (_params.TryGetValue(line, out pm)) {
				Console.WriteLine("パラメタ付変数宣言発見 {0} {1} {2}", clang.getCursorSpelling(cursor), typeName, pm);
			}
			else {
				Console.WriteLine("変数宣言発見 {0} {1}", clang.getCursorSpelling(cursor), typeName);
			}
		}
	}
}

という感じで、

特定の名前の関数が登場した行数とそこから取得できるデフォルト引数を記録して、

フィールドが出てきたときに行数をチェックして該当のパラメータを持っているという解釈をさせるという事で

フィールドに情報の追加を行う事が出来ます。

デフォルト引数の取得にちょっと苦戦しましたが…思いの外すんなり出来てしまうものですね

ここまでくるとそろそろ次回辺りは実践編になるかもですね。

という事で今回はここまでです。

ではまた次回にお会いしましょう

RECRUIT

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

RECRUIT SITE 

NEWS