ヘキサ日記 Blog

 

2017年6月22日

優しいC++解析(その3 そろそろ優しくない)

お久しぶりです。
台湾で印象的だった食べ物が鳥の頭と鳥の脚、あとは臭豆腐なgood sunこと山口です。

 

さて前回のシリーズから半年ぶりですが、その実全く進歩していないという噂のC++ソース解析第三弾です。

 

今回は純粋にシンプルにVisual Studioのプロジェクトファイル内で使用されている型情報をザックリ拾い上げてみます。

 

このシリーズ、libclangの仕様を十分に理解していない為かそろそろ優しくなくなってきました。

 

思えばこのシリーズを始めてから長い月日がたったので大分C#も馴染みの言語になってきました。 
特筆してReflection(Attributes含む)とLINQとプロパティが素敵です。

 

早速ソースコードを掲載しながらやっていきましょう。

まずはVisual StudioのC++プロジェクトファイルであるvcxprojを読み込みます。
もしかしたら解析してくれるツールがあるのかもしれませんが、 見つけられなかったのでサクッと作ります

C#というか.Netさん素敵です。 目標はビルド対象のc++のピックアップとインクルードディレクトリの取得、プリプロセッサ定義の取得です。

 

ということでXmlTextReaderを利用してこんな感じで書きます

public class BuildInfo
{
	public String ProjPath { get; set; }
	public List SrcFiles { get; set; } = new List();
	public List IncludePathes { get; set; } = new List();
	public List PreprocessorDefs { get; set; } = new List();
}

public class AnalyzeVcxproj
{
	private Regex buildCondition;
	public BuildInfo Result{ get; private set; }
	public void Analyze(string vcxproj, string condition)
	{
		buildCondition = new Regex(condition, RegexOptions.IgnoreCase);
		Result = new BuildInfo();
		var currentElement = new Stack();
		currentElement.Push("");
		Result.ProjPath = Path.GetDirectoryName(vcxproj);
		// ビルド構成チェック
		bool isTargetCondition = false;
		bool bodyReadable = false;

		using (var reader = new XmlTextReader(vcxproj))
		{
			while (reader.Read())
			{
				switch (reader.NodeType)
				{
					case XmlNodeType.Element:
						{
							var lastElement = currentElement.Peek();
							if (!reader.IsEmptyElement)
							{
								currentElement.Push(reader.Name);
							}
							switch (lastElement)
							{
								case "ItemGroup":
									{
										if (currentElement.Peek() == "ClCompile")
										{
											while (reader.MoveToNextAttribute())
											{
												if (reader.Name == "Include")
												{
													Result.SrcFiles.Add(reader.Value);
													break;
												}
											}
										}
									}
									break;
								case "PropertyGroup":
									{
										if (currentElement.Peek() == "IncludePath")
										{
											bodyReadable = isTargetCondition;
										}
									}
									break;
								case "ClCompile":
									{
										if (currentElement.Peek() == "PreprocessorDefinitions")
										{
											bodyReadable = isTargetCondition;
										}
									}
									break;
								default:
									{
										if (currentElement.Peek() == "PropertyGroup")
										{
											if (!reader.IsEmptyElement)
											{
												// Conditionの簡易チェック
												while (reader.MoveToNextAttribute())
												{
													if (reader.Name == "Condition")
													{
														isTargetCondition = buildCondition.IsMatch(reader.Value);
														break;
													}
												}
											}
										}
										else if (currentElement.Peek() == "ItemDefinitionGroup")
										{
											if (!reader.IsEmptyElement)
											{
												// Conditionの簡易チェック
												while (reader.MoveToNextAttribute())
												{
													if (reader.Name == "Condition")
													{
														isTargetCondition = buildCondition.IsMatch(reader.Value);
														break;
													}
												}
											}
										}
									}
									break;
							}
						}
						break;
					case XmlNodeType.Text:
						if (bodyReadable)
						{
							switch (currentElement.Peek())
							{
								case "IncludePath":
									if (Result.IncludePathes.Count == 0)
									{
										Result.IncludePathes = reader.Value.Split(';').Where(x => x[0] != '$').ToList();
									}
									break;
								case "PreprocessorDefinitions":
									if (Result.PreprocessorDefs.Count == 0)
									{
										Result.PreprocessorDefs = reader.Value.Split(';').Where(x => x[0] != '%').ToList();
									}
									break;
							}
							bodyReadable = false;
						}
						break;
					case XmlNodeType.EndElement:
						currentElement.Pop();
						break;
					default:
						break;
				}
			}
		}
	}
}

サクッとvcxprojの中で必要な情報を抜き出しました。
必要な情報を限定するとそこまで複雑では無いですね。
(VS2017を利用したので他のバージョンはダメかもです)

 

問題は型情報の収集です。
一番の苦労ポイントは単純に型のtypedefやnamespaceを解決した名前を取得する事でした。
もしかしたらもっとしっかりした解析方法があるかもしれませんが 今回はgetCanonicalType()を使ってみたところいい感じに取得できました。

なかなか参考ページにたどり着くまで苦労しますね

	public class AnalyzedField
	{
		public String TypeName { get; set; }
		public String VariableName { get; set; }
		public int ArraySize { get; set; }
		public bool IsPtr { get; set; }
	}
	public class AnalyzedClass
	{
		public bool IsTemplate { get; set; }
		public String NSpace{ get; set; }
		public String Name { get; set; }
		public List Member { get; set; } = new List();
		public List Bases { get; set; } = new List();
	}
	public class AnalyzedEnum
	{
		public String NSpace { get; set; }
		public String Name { get; set; }
		public List Member { get; set; } = new List();
	}
	public class SubAnalyzer
	{
		private List nspace = new List();
		public List Classes { get; private set; } = new List();
		public List Enums { get; private set; } = new List();
		private AnalyzedEnum currentEnum;
		private AnalyzedClass currentClass;
		private CX_CXXAccessSpecifier currentAccess = CX_CXXAccessSpecifier.CX_CXXPrivate;
		public SubAnalyzer(CXCursor cursor)
		{
			clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
		}
		private String ConnectedNameSpace
		{
			get { return nspace.Count == 0 ? "" : (nspace.Aggregate((x,y)=>x+"::"+y)); }
		}
		private CXChildVisitResult ScanChild(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			switch (cursor.kind)
			{
				case CXCursorKind.CXCursor_Namespace:
					pushNamespace(cursor);
					clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
					popNamespace();
					break;
				case CXCursorKind.CXCursor_StructDecl:
					if (currentClass == null)
					{
						var prevClass = currentClass;
						currentClass = new AnalyzedClass()
						{
							IsTemplate = false,
							Name = clang.getCursorSpelling(cursor).ToString(),
							NSpace = ConnectedNameSpace,
						};
						clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
						Classes.Add(currentClass);
						currentClass = prevClass;
					}
					break;
				case CXCursorKind.CXCursor_ClassDecl:
					// クラスのネストは未対応で...
					if(currentClass == null){
						var prevClass = currentClass;
						currentClass = new AnalyzedClass()
						{
							IsTemplate = false,
							Name = clang.getCursorSpelling(cursor).ToString(),
							NSpace = ConnectedNameSpace,
						};
						clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
						Classes.Add(currentClass);
						currentClass = prevClass;
					}
					break;
				case CXCursorKind.CXCursor_CXXBaseSpecifier:
					{
						// 基底クラス情報
						var typ = clang.getCursorType(cursor);
						// typedefとかの解除
						while (typ.kind == CXTypeKind.CXType_Typedef)
						{
							typ = clang.getCanonicalType(typ);
						}
						currentClass.Bases.Add(clang.getTypeSpelling(typ).ToString());
					}
					break;
				case CXCursorKind.CXCursor_EnumDecl:
					{
						// enumを作成してリストに追加する
						currentEnum = new AnalyzedEnum()
						{
							Name = clang.getCursorSpelling(cursor).ToString(),
							NSpace = ConnectedNameSpace,
						};
						clang.visitChildren(cursor, new CXCursorVisitor(ScanEnum), new CXClientData());
						Enums.Add(currentEnum);
						currentEnum = null;
					}
					break;
				case CXCursorKind.CXCursor_CXXAccessSpecifier:
					{
						currentAccess = clang.getCXXAccessSpecifier(cursor);
					}
					break;
				case CXCursorKind.CXCursor_FieldDecl:
					// クラスのメンバのみをスキャンする
					if(currentClass != null)
					{
						var type = clang.getCursorType(cursor);
						var arrCount = clang.getArraySize(type);
						var field = new AnalyzedField();
						field.ArraySize = (int)arrCount;
						CXType vtype;
						if (arrCount <= 0)
						{
							vtype = type;
						}
						else
						{
							vtype = clang.getArrayElementType(type);
						}
						while (vtype.kind == CXTypeKind.CXType_Typedef)
						{
							vtype = clang.getCanonicalType(vtype);
						}
						// ちょっと良く分からない
						if (vtype.kind == CXTypeKind.CXType_Unexposed)
						{
							break;
						}
						if (vtype.kind == CXTypeKind.CXType_Pointer)
						{
							field.IsPtr = true;
							vtype = clang.getPointeeType(vtype);
							while (vtype.kind == CXTypeKind.CXType_Typedef)
							{
								vtype = clang.getCanonicalType(vtype);
							}
						}
						else
						{
							field.IsPtr = false;
						}
						field.TypeName = clang.getTypeSpelling(vtype).ToString();
						field.VariableName = clang.getCursorSpelling(cursor).ToString();
						currentClass.Member.Add(field);
					}
					break;
				case CXCursorKind.CXCursor_ClassTemplate:
					{
						// クラスのネストは未対応で...
						if (currentClass == null)
						{
							var prevClass = currentClass;
							currentClass = new AnalyzedClass()
							{
								IsTemplate = true,
								Name = clang.getCursorSpelling(cursor).ToString(),
								NSpace = ConnectedNameSpace,
							};
							clang.visitChildren(cursor, new CXCursorVisitor(ScanChild), new CXClientData());
							Classes.Add(currentClass);
							currentClass = prevClass;
						}
					}
					break;
				case CXCursorKind.CXCursor_CXXMethod:
					break;
				default:
					break;
			}
			return CXChildVisitResult.CXChildVisit_Continue;
		}
		private CXChildVisitResult ScanEnum(CXCursor cursor, CXCursor parent, IntPtr client_data)
		{
			var itype = clang.getEnumDeclIntegerType(cursor);
			if (itype.kind == CXTypeKind.CXType_UInt)
			{
				currentEnum.Member.Add($"{clang.getCursorSpelling(cursor)},{clang.getEnumConstantDeclUnsignedValue(cursor)}");
			}
			else
			{
				currentEnum.Member.Add($"{clang.getCursorSpelling(cursor)},{clang.getEnumConstantDeclValue(cursor)}");
			}
			return CXChildVisitResult.CXChildVisit_Continue;
		}

		private void pushNamespace(CXCursor cursor)
		{
			var cxName = clang.getCursorSpelling(cursor);
			nspace.Add(cxName.ToString());
		}
		private void popNamespace()
		{
			nspace.RemoveAt(nspace.Count -1);
		}
	}
	public class CppAnalyzer
	{
		public void Analyze(BuildInfo info)
		{
			// インクルードパスオプション
			List cmdOptions = new List();
			cmdOptions.Add("-std=c++11");
			cmdOptions.AddRange(info.IncludePathes.Select(x=>"-I" + (Path.IsPathRooted(x) ? x : Path.Combine(info.ProjPath, x))).ToList());
			cmdOptions.AddRange(info.PreprocessorDefs.Select(x=>"-D" + x).ToList());

			foreach (var src in info.SrcFiles) {
				// ハッシュと依存関係から更新チェックする
				var dummy = new CXUnsavedFile();
				var index = clang.createIndex(0, 0);
				var trans = new CXTranslationUnit();
				// 第二回と違ってバッチあてるの面倒だったので
				var err = clang.parseTranslationUnit2(index, Path.Combine(info.ProjPath, src), cmdOptions.ToArray(), cmdOptions.Count, out dummy, 0, 0, out trans);
				if (err != CXErrorCode.CXError_Success)
				{
					// TODO:エラー出力
					continue;
				}
				// 解析する
				var cursor = clang.getTranslationUnitCursor(trans);
				var kind = clang.getCursorKind(cursor);
				var sub = new SubAnalyzer(cursor);
				// TODO異なる情報のみを収集
			}
		}
	}

と比較的短いコードでVisual Studioのプロジェクト内の型情報がある程度の粒度で集められました。
あとはこの情報を利用して外部からデータを変更するコードを書いたり、 シリアライズ、デシリアライズする仕組みを作ってレベルエディタ等に活用したりですね
今回未完成で申し訳ないですが翻訳単位1個だけピックアップしていて、 複数の翻訳単位でのマージなどは行っていません
同一クラス名を省く形でリストアップすると良いかもしれません。

 

次回まで気力が続くときっとシリアライズとかその辺のコードが書かれて本連載も終了となるハズです。

 

ではまた


ヘキサブログ ピックアップ



過去の日記はこちら

2017年11月
« 10月    
 12345
6789101112
13141516171819
20212223242526
27282930