Python と C++ の同時デバッグ

ほとんどの標準的 Python デバッガーは、Python コードのみのデバッグをサポートします。 ただし、実際には、高パフォーマンスやプラットフォーム API の直接呼び出しが必要なところで Python と、C または C++ が併用されています (例については、「Python 向け C++ 拡張機能の作成」をご覧ください)。 Python プロジェクトが読み込まれるとき、Visual Studio には、Python とネイティブ C/C++ のデバッグを統合して同時に実行する混合モードのデバッグ機能があります。このモードでは、結合された呼び出し履歴、Python とネイティブ コード間でのステップ実行機能、両方の種類のコード内のブレークポイント、Python のオブジェクト表現をネイティブ フレームに表示する機能 (逆も可能) が提供されます。

混合モードのデバッグ

Visual Studio でのネイティブ C モジュールのビルド、テスト、およびデバッグの概要については「Deep Dive: Creating Native Modules (詳細情報: ネイティブ モジュールの作成)」(youtube.com、9 分 9 秒) をご覧ください。

注意

混合モードのデバッグは、Python Tools for Visual Studio 1.x では使用できません。

混合モードのデバッグの有効化

  1. ソリューション エクスプローラーでプロジェクトを右クリックし、[プロパティ] を選択し、[デバッグ] タブを選択します。[ネイティブ コードのデバッグを有効にする] オプションをオンにします。 これで、すべてのデバッグ セッションで、混合モードが有効になります。

    ネイティブ コードのデバッグの有効化

    ヒント

    ネイティブ コードのデバッグを有効にすると、プログラムが通常の [続行するには、任意のキーを押してください] で一時停止せずに完了した場合に、Python の出力ウィンドウがすぐに消えることがあります。 強制的に一時停止するには、ネイティブ コードのデバッグを有効にするときに、[デバッグ] タブの [実行] > [インタープリターの引数] フィールドに、-i オプションを追加します。 このようにすると、Python インタープリターはコード終了後に対話モードになり、この時点でユーザーが Ctrl + Z キー、Enter キーの順に押して終了するのを待機します。

  2. 混合モードのデバッガーを既存のプロセスにアタッチ ([デバッグ] メニューの [プロセスにアタッチ] を選択) したら、[選択] ボタンをクリックして [コードの種類の選択] ダイアログを開きます。[次のコードの種類をデバッグする] オプションをオンにし、一覧の [ネイティブ][Python] の両方を選択します。

    コードの種類 ([ネイティブ] と [Python]) の選択

    コードの種類の設定は永続的であるため、後で別のプロセスにアタッチするときに混合モードのデバッグを無効にする場合は、これらの手順を繰り返して Python コードの種類の選択を解除する必要があります。

    [ネイティブ] に加えて、またはその代わりとして他のコードの種類を選択できます。 たとえば、CPython をホストしてネイティブ拡張モジュールを使用するマネージ アプリケーションで、3 つすべてをデバッグする場合は、[Python][ネイティブ]、および [マネージ * *] をまとめて選択すると、結合された呼び出し履歴と 3 つすべてのランタイム間でのステップ実行を含む統合されたデバッグ エクスペリエンスを得ることができます。

  3. 混合モードのデバッグを初めて開始したときに、Python シンボルが必要であることを示すダイアログが表示される可能性があります。 詳細については、混合モードのデバッグ用のシンボルに関するページを参照してください。 どの Python 環境でも、シンボルは 1 回だけインストールする必要があります。 Visual Studio 2017 のインストーラーで Python のサポートをインストールすると、シンボルが自動的に組み込まれます。

  4. Python のソース コード自体を手元に置きたい場合もあります。 標準の Python の場合は、https://www.python.org/downloads/source/ からソース コードを入手できます。 バージョンに合ったアーカイブをダウンロードし、フォルダーに抽出します。 要求されたら、そのフォルダーの特定のファイルを Visual Studio で参照します。

注意

ここで説明する混合モードのデバッグは、Python プロジェクトが Visual Studio に読み込まれている場合にのみ有効です。 プロジェクトは Visual Studio のデバッグ モードを特定し、それによって混合モードのオプションを使用できるようにします。 ただし、C++ プロジェクトが読み込まれている場合は (python.org で説明されているように別のアプリケーションに Python を埋め込んでいるとき)、Visual Studio は混合モード デバッグをサポートしていないネイティブ C++ デバッガーを使います。

この場合は、デバッグなしで C++ プロジェクトを開始し ([デバッグ] > [デバッグなしで開始] または Ctrl + F5 キー)、その後で [デバッグ] > [プロセスにアタッチ...] を使います。 表示されるダイアログで適切なプロセスを選んだ後、[選択...] ボタンを使って [コードの種類の選択] ダイアログを開き、次に示すように Python を選びます。 [OK] を選んでダイアログを閉じた後、[アタッチ] を選んでデバッガーを起動します。 デバッガーをアタッチする前にデバッグ対象の Python が呼び出されないように、適切な一時停止または遅延を C++ アプリに組み込むことが必要になる場合があることに注意してください。

デバッガーをアタッチするときにデバッグの種類として Python を選ぶ

混合モード固有の機能

結合された呼び出し履歴

[呼び出し履歴] ウィンドウには、ネイティブと Python のスタック フレームの両方が、2 つの間の遷移情報を挟んで交互に表示されます。

結合された呼び出し履歴

注意

[ツール]、[オプション]、[デバッグ]、[全般] の順に選択し、[マイ コードのみを有効にする] オプションをオンにした場合、遷移情報は "[外部コード]" として表示され、遷移の方向は示されません。

呼び出しフレームをダブルクリックすると、それがアクティブになり、適切なソース コードが開きます (可能な場合)。 ソース コードが入手できない場合でも、フレームはアクティブになり、ローカル変数を調べることができます。

Python とネイティブ コード間のステップ実行

ステップ イン コマンドまたは ステップ アウト コマンドを使用したとき、混合モードのデバッガーは、コードの種類の変更を正しく処理します。 たとえば、C で実装されている型のメソッドをPython で呼び出しているときに、そのメソッドへの呼び出しにステップ インすると、実行は、メソッドを実装しているネイティブ関数の先頭で停止します。 同様に、ネイティブ コードが Python API 関数を呼び出しているときは、呼び出されている Python コードで停止します。 たとえば、Python で定義された関数値の PyObject_CallObject にステップ インすると、Python 関数の先頭で停止します。 Python からネイティブへのステップ インは、Python から ctypes 経由で呼び出されるネイティブ関数でもサポートされています。

ネイティブ コード内の PyObject 値の表示

ネイティブ (C または C++) フレームがアクティブのときは、そのローカル変数がデバッガーの [ローカル] ウィンドウに表示されます。 ネイティブの Python 拡張モジュールでは、これらの多くは PyObject 型 (_object の typedef) であり、いくつかがその他の基本的な Python 型です (下の一覧を参照してください)。 混合モードのデバッグでは、これらの値は、[Python view (Python ビュー)] というラベルが付いた追加の子ノードに表示されます。 このノードを展開すると、変数の Python 表現が表示されます。これは、同じオブジェクトを参照しているローカル変数が Python フレームに存在している場合に表示されるものと同じです。 このノードの子は編集可能です。

Python ビュー

この機能を無効にするには、[ローカル] ウィンドウ内を右クリックし、[Python]、[Show Python View Nodes (Python ビュー ノードの表示)] メニュー オプションを切り替えます。

Python ビューの有効化

"[Python View (Python ビュー)]" ノードを表示する C の型 (ビューが有効な場合):

  • PyObject
  • PyVarObject
  • PyTypeObject
  • PyByteArrayObject
  • PyBytesObject
  • PyTupleObject
  • PyListObject
  • PyDictObject
  • PySetObject
  • PyIntObject
  • PyLongObject
  • PyFloatObject
  • PyStringObject
  • PyUnicodeObject

自分で作成した型は、"[Python View (Python ビュー)]" に自動的に表示されることはありません。 Python 3.x での拡張機能の作成では、すべてのオブジェクトに最終的には上記のいずれかの型の ob_base フィールドが存在し、それによって "[Python View (Python ビュー)]" に表示されるため、通常はこれは問題ではありません。

ただし、Python 2.x では、通常は、各オブジェクト型でヘッダーをインライン フィールドのコレクションとして宣言するため、カスタム作成された型と PyObject の間には、C/C++ コードの型システム レベルでの関連付けがありません。 このようなカスタム型に対して "[Python View (Python ビュー)]" ノードを有効にするには、Python ツールのインストール ディレクトリ PythonDkm.natvis を編集して、C 構造体または C++ クラスの XML に別の要素を追加します。

別の (より優れた) 方法は、PEP 3123 に従って、PyObject_HEAD の代わりに明示的な PyObject ob_base; フィールドを使用することです。ただし、旧バージョンとの互換性の点で、この方法は常に可能であるとは限りません。

Python コード内のネイティブ値の表示

前のセクションに似ていますが、Python フレームがアクティブのときに、ネイティブ値を [ローカル] ウィンドウに表示する "[C++ View (C++ ビュー)]" を有効にできます。 この機能は既定では有効になっていないため、[ローカル] ウィンドウ内を右クリックし、[Python] メニューの [Show C++ View Nodes ([C++ ビュー] ノードの表示)] オプションをオンにすることで有効にします。

C++ ビューの有効化

"[C++ View (C++ ビュー)]" ノードは、値の基になる C/C++ 構造体の表現を提供します (これはネイティブ フレームに表示されるものと同じです)。 たとえば、Python の長整数型の _longobject インスタンス (その PyLongObject は typedef です) が表示され、カスタム作成されたネイティブ クラスの型の推測が試行されます。 このノードの子は編集可能です。

C++ ビュー

オブジェクトの子フィールドが PyObject 型であるか、サポートされているその他の型のいずれかである場合は、"[Python View (Python ビュー)]" ノードが表示され (有効な場合)、リンクが Python に直接公開されていないオブジェクト グラフに移動することができます。

Python オブジェクトのメタデータを使用してオブジェクトの型を特定する "[Python ビュー]" ノードとは異なり、"[C++ ビュー]" には同じように信頼性の高いメカニズムはありません。 一般的に言えば、Python 値 (つまり PyObject 参照) が与えられた場合、そのバックにある C/C++ 構造体はどれかを確実に判断することはできません。 混合モードのデバッガーは、関数ポインターの型があるオブジェクトの型のさまざまなフィールドを調べて、型を推測しようとします (たとえば ob_type フィールドによって参照されている PyTypeObject)。 関数ポインターのいずれかが解決可能な関数を参照し、その関数に PyObject* よりも型が明確な self パラメーターがあれば、その型はバッキング型であるとみなされます。 たとえば、特定のオブジェクトの ob_type->tp_init が次の関数をポイントしている場合、

static int FobObject_init(FobObject* self, PyObject* args, PyObject* kwds) {
    return 0;
}

デバッガーは、オブジェクトの C の型は FobObject であることを正しく推測できます。 tp_init から正確な型を判別できない場合は、他のフィールドに移動します。 どのフィールドからも型を推測できない場合、オブジェクトは "[C++ View (C++ オブジェクト)]" ノードに PyObject インスタンスとして表示されます。

カスタム作成した型の有用な表現を常に取得する最善の方法は、型を登録するときに少なくとも 1 つの特殊な関数を登録し、厳密に型指定された self パラメーターを使用することです。 ほとんどの型は問題なくこの要件を満たしますが、当てはまらない場合、この目的で使用するための最も便利なエントリは、通常は tp_init です。 デバッガーの型推測を可能にするためにのみ存在する tp_init 型のダミー実装は、上のコード サンプルに示すように、すぐにゼロを返すことができます。

Python の標準的なデバッグとの違い

混合モードのデバッガーは、追加機能がいくつか導入されていますが、Python に関連するいくつかの機能が欠けているという点で、標準的な Python のデバッガーとは区別されます。

  • サポートされていない機能: 条件付きブレークポイント、デバッグの対話型ウィンドウ、およびプラットフォーム間のリモート デバッグ。
  • イミディエイト ウィンドウ: 使用できますが、その機能はサブセットに制限され、ここに記載されている制限もすべて適用されます。
  • サポートされている Python のバージョン: CPython 2.7 と 3.3+ のみ。
  • Visual Studio Shell: Visual Studio Shell で Python を使用する場合 (たとえば、統合インストーラーを使用してインストールした場合)、Visual Studio では C++ プロジェクトを開くことができません。また、C++ ファイルの編集方法は、基本的なテキスト エディターでの編集と同様です。 ただし、C/C++ のデバッグと混合モードでのデバッグは Shell で完全にサポートされ、ソース コード、ネイティブ コードのステップ イン、およびデバッガー ウィンドウでの C++ 式の評価を実行できます。
  • オブジェクトの表示と展開: デバッガー ツールの [ローカル] ウィンドウと [ウォッチ] ウィンドウに Python オブジェクトを表示するとき、混合モードのデバッガーでは、オブジェクトの構造のみが表示されます。 プロパティの自動評価や計算される属性の表示は行われません。 コレクションでは、組み込みコレクション型 (tuplelistdictset) の要素のみを表示します。 カスタム コレクション型は、組み込みコレクション型から継承される場合を除き、コレクションとして視覚化されません。
  • 式の評価: 下記を参照してください。

式の評価

標準的な Python デバッガーでは、デバッグ対象のプロセスは、I/O 操作またはその他の同様のシステム呼び出しでブロックされていない限り、コード内のどのポイントで一時停止された場合でも、[ウォッチ] ウィンドウと [イミディエイト] ウィンドウですべての Python 式を評価することができます。 混合モードのデバッグでは、任意の式は、ブレークポイントまたはコードにステップ インした後、Python コード内で停止した場合にのみ評価することができ、ブレークポイントまたはステップ イン操作が行われたスレッドでのみ、式を評価できます。

ネイティブコード内、または上記の条件が適用されない Python コード内で停止した (ステップ アウト操作の後や異なるスレッド上などで停止した) 場合、式の評価は、現在選択されているフレームのスコープ内のローカル変数とグローバル変数へのアクセス、それらのフィールドへのアクセス、および組み込みコレクション型のリテラルによるインデックス作成に限定されます。 たとえば、次の式は、すべてのコンテキストで評価できます (すべての識別子が既存の変数と適切な型のフィールドを参照していることを条件とします)。

foo.bar[0].baz['key']

混合モードのデバッガーも、このような式を異なる方法で解決します。 すべてのメンバー アクセス操作は、直接的にオブジェクトの一部であるフィールド (__dict__ または __slots__ 内のエントリや、tp_members 経由で Python に公開されているネイティブ構造体のフィールドなど) のみを検索し、すべての __getattr____getattribute__、または記述子ロジックを無視します。 同様に、すべてのインデックス作成操作は __getitem__ を無視して、コレクションの内部データ構造に直接アクセスします。

整合性を保つため、任意の式が現在の停止ポイントで評価できるかどうかに関係なく、制限された式の評価に対する制約と一致するすべての式に対して、この名前解決スキームが使用されます。 フル機能のエバリュエーターを使用できるときに Python の適切なセマンティクスを適用するには、式をかっこで囲みます。

(foo.bar[0].baz['key'])