デバッグモードでカスタムクラスを視覚化するVisual Studio拡張機能

良い一日、



この記事では、アプリケーションのデバッグの過程で複雑なカスタムクラスを視覚化するのに役立つVisual Studioの拡張機能の作成について説明します。



背景



このプロジェクトでは、国内のジオメトリックコアC3Dカーネルを積極的に使用します。 このライブラリは、曲線、ボディ、サーフェスなどを操作するための多数のクラスを提供します。 これらのクラスは複雑な構造を持ち、標準のVisual Studio視覚化ツールを使用してアプリケーションをデバッグするプロセスでは、たとえば、表面が特定の変数に格納されているかどうかを理解することは困難です。 また、複雑なアルゴリズムをデバッグする場合、アルゴリズムの各ステップでオブジェクトに何が起こるかを理解することが非常に重要です。







この問題をさまざまな方法で回避しようとしました。 たとえば、単純な2次元曲線の場合は、紙の上の点の座標を書きました。 そして、この曲線を点で描きました。 問題の2番目の解決策:オブジェクトを適切なタイミングでファイルに保存し、ライブラリからテストユーティリティでこのファイルを開きます。 これは本当にデバッグに役立ちますが、かなりの手作業が必要です。 オブジェクトをファイルに保存するコードを挿入し、アプリケーションを再コンパイルし、特定のアルゴリズムを実行するためにアプリケーションで必要なアクションを実行し、ユーティリティで保存されたファイルを開き、結果を確認し、必要に応じてアルゴリズムを修正し、手順全体を繰り返します。 一般的には耐えられますが、デバッグモードのVisual Studioで目的の変数を直接ポイントし、そこに格納されているオブジェクトがどのように見えるかを簡単に確認できるようにしたかったのです。



Visual Studio拡張



この問題の解決策を探して、OpenCource OpenCVライブラリのMicrosoftからVisual Studio Image Watchの拡張機能に出会いました。 この拡張により、デバッグ中にcv :: Matタイプの変数の内容を表示し、ビットマップを読み取ることができます。 それから、同様の拡張機能を記述するようになりましたが、私たちのタイプ用です。 残念ながら、パブリックドメインでこの拡張機能のソースコードを見つけることはできませんでした。 Visual Studioのこのような拡張機能を記述する方法について、少しずつ情報を収集する必要がありました。 msdnのこのトピックに関するドキュメントは、すべてが悲しいものです。 また、多くの例はなく、1つのstd :: vector visualizerがあります。 見つけるのはそれほど簡単ではありません。 例の本質:デバッグモードでのstd :: vector <int>にあるint数のグラフの視覚化:







拡張機能を作成



拡張機能を作成するには、Visual Studio SDKをインストールする必要があります。 インストール後、プロジェクトウィザードに新しいプロジェクトタイプが表示されます。







新しいプロジェクトの作成ウィザードは、必要なファイルをすべて作成し、プロジェクトを構成します。

Microsoftの例の説明は繰り返さず、拡張機能の作成手順については簡単に説明しています。 この例の説明をご覧になりたい方はお勧めします。 この記事では、この例で説明されていない点に触れたいと思います。



変数の値を取得する



内容を確認したい変数、および拡張機能自体は異なるプロセスにあります。 この例から、より複雑なユーザータイプからデータを取得する方法はまだ明確ではありませんでした。 この例は、IDebugProperty3インターフェイスを使用して、ベクターの最初の要素のアドレスと最後の要素のアドレスを見つけるときのトリックを示しています。 アドレスを減算することにより、メモリのサイズを見つけ、このメモリをプロセスにコピーします。 例からのコードは次のとおりです。

オブジェクトからデータを取得する
public int DisplayValue(uint ownerHwnd, uint visualizerId, IDebugProperty3 debugProperty) { int hr = VSConstants.S_OK; DEBUG_PROPERTY_INFO[] propertyInfo = new DEBUG_PROPERTY_INFO[1]; hr = debugProperty.GetPropertyInfo( enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_ALL, 10 /* Radix */, 10000 /* Eval Timeout */, new IDebugReference2[] { }, 0, propertyInfo); Debug.Assert(hr == VSConstants.S_OK, "IDebugProperty3.GetPropertyInfo failed"); // std::vector internally keeps pointers to the first and last elements of the dynamic array // First get the values of those members. We are going to use them later for reading vector elements. // An std::vector<int> variable has the following nodes in raw view: // myVector // + std::_Vector_alloc<0,std::_Vec_base_types<int,std::allocator<int> > > // + std::_Vector_val<std::_Simple_types<int> > // + std::_Container_base12 // + _Myfirst // + _Mylast // + _Myend // This is the underlying base class of std::vector (std::_Vector_val<std::_Simple_types<int> > node above) DEBUG_PROPERTY_INFO vectorBaseClassNode = GetChildPropertyAt(0, GetChildPropertyAt(0, propertyInfo[0])); // myFirstInfo member points to the first element DEBUG_PROPERTY_INFO myFirstInfo = GetChildPropertyAt(1, vectorBaseClassNode); // myLastInfo member points to the last element DEBUG_PROPERTY_INFO myLastInfo = GetChildPropertyAt(2, vectorBaseClassNode); // Vector length can be calculated by the difference between myFirstInfo and myLastInfo pointers ulong startAddress = ulong.Parse(myFirstInfo.bstrValue.Substring(2), System.Globalization.NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); ulong endAddress = ulong.Parse(myLastInfo.bstrValue.Substring(2), System.Globalization.NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); uint vectorLength = (uint)(endAddress - startAddress) / elementSize; // Now that we have the address of the first element and the length of the vector, // we can read the vector elements from the debuggee memory. IDebugMemoryContext2 memoryContext; hr = myFirstInfo.pProperty.GetMemoryContext(out memoryContext); Debug.Assert(hr == VSConstants.S_OK, "IDebugProperty.GetMemoryContext failed"); IDebugMemoryBytes2 memoryBytes; hr = myFirstInfo.pProperty.GetMemoryBytes(out memoryBytes); Debug.Assert(hr == VSConstants.S_OK, "IDebugProperty.GetMemoryBytes failed"); // Allocate buffer on our side for copied vector elements byte[] vectorBytes = new byte[elementSize * vectorLength]; uint read = 0; uint unreadable = 0; hr = memoryBytes.ReadAt(memoryContext, elementSize * vectorLength, vectorBytes, out read, ref unreadable); Debug.Assert(hr == VSConstants.S_OK, "IDebugMemoryBytes.ReadAt failed"); // Create data series that will be needed by the plotter window and add vector elements to the series Series series = new Series(); series.Name = propertyInfo[0].bstrName; for (int i = 0; i < vectorLength; i++) { series.Points.AddXY(i, BitConverter.ToUInt32(vectorBytes, (int)(i * elementSize))); } // Invoke plotter window to show vector contents PlotterWindow plotterWindow = new PlotterWindow(); WindowInteropHelper helper = new WindowInteropHelper(plotterWindow); helper.Owner = (IntPtr)ownerHwnd; plotterWindow.ShowModal(series); return hr; } /// <summary> /// Helper method to return the child property at the given index /// </summary> /// <param name="index">The index of the child property</param> /// <param name="debugPropertyInfo">The parent property</param> /// <returns>Child property at index</returns> public DEBUG_PROPERTY_INFO GetChildPropertyAt(int index, DEBUG_PROPERTY_INFO debugPropertyInfo) { int hr = VSConstants.S_OK; DEBUG_PROPERTY_INFO[] childInfo = new DEBUG_PROPERTY_INFO[1]; IEnumDebugPropertyInfo2 enumDebugPropertyInfo; Guid guid = Guid.Empty; hr = debugPropertyInfo.pProperty.EnumChildren( enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE | enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_PROP | enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE_RAW, 10, /* Radix */ ref guid, enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_CHILD_ALL, null, 10000, /* Eval Timeout */ out enumDebugPropertyInfo); Debug.Assert(hr == VSConstants.S_OK, "GetChildPropertyAt: EnumChildren failed"); if (enumDebugPropertyInfo != null) { uint childCount; hr = enumDebugPropertyInfo.GetCount(out childCount); Debug.Assert(hr == VSConstants.S_OK, "GetChildPropertyAt: IEnumDebugPropertyInfo2.GetCount failed"); Debug.Assert(childCount > index, "Given child index out of bounds"); hr = enumDebugPropertyInfo.Skip((uint)index); Debug.Assert(hr == VSConstants.S_OK, "GetChildPropertyAt: IEnumDebugPropertyInfo2.Skip failed"); uint fetched; hr = enumDebugPropertyInfo.Next(1, childInfo, out fetched); Debug.Assert(hr == VSConstants.S_OK, "GetChildPropertyAt: IEnumDebugPropertyInfo2.Next failed"); } return childInfo[0]; }
      
      







すべて問題ありませんが、このデータが単一のメモリに格納されている場合、オブジェクトからデータを取得する方法を示しています。 どうやら、MS自体がImage Watch拡張機能で同様のアプローチを使用しています。 そこには、画像も単一のメモリに保存され、この部分の先頭へのポインタがあります。

しかし、ユーザータイプが複雑な階層構造を持ち、通常のデータ配列のように見えない場合はどうでしょうか? クラスが他のクラスの基本クラスへのポインタを格納している場合はさらに悪化します。 このようなオブジェクトを断片的に復元することは、非現実的なタスクのようです。 さらに、この設計は非常に脆弱です。新しいメンバーが中間クラスに追加されると、拡張機能が停止します。 理想的には、オブジェクト自体またはそのコピーを取得したかったのです。 残念ながら、私はこれを行う方法を見つけられませんでした。 しかし、必要なクラスがファイルまたはメモリ内のバッファにシリアル化できることを知って、共有メモリとベクトルを使用したハイブリッドアプローチを使用できると判断しました。 このソリューションはあまり洗練されておらず、クラスを編集する必要がありますが、非常に機能しています。 それに加えて、これ以上優れたものはありません。



実装



メソッドの本質:

各クラス(デビューしたい)に、1つのフィールドstd :: vector <char>を含む特別なクラスが追加されます。 ベクターには、マーカー行を保存します。これにより、共有メモリでシリアル化されたオブジェクトを見つけることができます。 次に、各非定数クラスメソッドで、関数への呼び出しを追加して、共有メモリにクラスを保存します。 これで、クラスが変更されるたびに、共有メモリに保存されます。

拡張機能自体では、MSの例のメソッドを使用して、オブジェクトからマーカーラインを取得します。 次に、マーカーによって、共有メモリからシリアル化されたオブジェクトを取得し、それを逆シリアル化します。 その結果、拡張機能にオブジェクトのコピーがあります。 さて、それは技術の問題です。 オブジェクトから、私たちにとって有用なデータを取得し、何らかの形で便利な形式で表示します。



HabraLineデバッグビジュアライザー



拡張機能の例は、この考えを示すために書かれました。 また、拡張機能の動作を示すために、簡単なライブラリが作成されました。 このライブラリには、HabraPointとHabraLineの2つのクラスしかありません。 さらに、共有メモリのシリアル化と操作に必要ないくつかのクラス。 HabraLineクラスは単なるセグメントです。 シリアル化と共有メモリブーストの使用には、ブーストが使用されます。 拡張機能をインストールした後、HabraLineタイプの変数の値を視覚化する機会があります。



拡張機能の動作を短いビデオで見ることができます:







拡張ソースへのリンク: TYNTS

デモプロジェクトへのリンク: TYNTS



この記事が誰かにとって有用であり、Visual Studioの有用な拡張機能を作成するきっかけになることを願っています。



皆さん、頑張ってください。



All Articles