在 Visual Studio 中调试 Python 和C++

大多数常规 Python 调试器仅支持调试 Python 代码,但开发人员通常使用 Python 和 C 或C++。 使用混合代码的一些方案是需要高性能的应用程序,或者直接调用平台 API 的功能通常用 Python 和 C 或C++进行编码。

Visual Studio 为 Python 和本机 C/C++ 代码提供集成的同时混合模式调试。 在 Visual Studio 安装程序中为 Python 开发 工作负载选择 Python 本机开发工具 选项时,支持可用:

显示 Visual Studio 安装程序中选择的 Python 本机开发工具选项的屏幕截图。

本文介绍如何使用以下混合模式调试功能:

  • 组合调用堆栈
  • 在 Python 和本机代码之间的过渡步骤
  • 这两种类型的代码中的断点
  • 查看本机帧中对象的 Python 表示形式,反之亦然
  • 在 Python 项目或C++项目的上下文中进行调试

显示 Visual Studio 中 Python 和C++代码的混合模式调试示例的屏幕截图。

先决条件

  • Visual Studio 2017 及更高版本。 混合模式调试不适用于 Visual Studio 2015 及更早版本中的 Python Tools for Visual Studio 1.x。

  • 安装了支持 Python 工作负载的 Visual Studio。 有关详细信息,请参阅 在 Visual Studio中安装 Python 支持。

在 Python 项目中启用混合模式调试

以下步骤介绍如何在 Python 项目中启用混合模式调试:

  1. 解决方案资源管理器中,右键单击 Python 项目,然后选择“ 属性”。

  2. “属性 ”窗格中,选择 “调试 ”选项卡,然后选择 “调试>启用本机代码调试 ”选项:

    显示如何在 Visual Studio 中设置“启用本机代码调试”属性的屏幕截图。

    此选项为所有调试会话启用混合模式。

    小窍门

    启用本机代码调试时,Python 输出窗口可能会在程序完成后立即关闭,而不会暂停并显示 “按任意键继续 提示”。 若要在启用本机代码调试后强制暂停和提示,请将-i参数添加到“调试”选项卡上的>解释器参数”字段。此参数使 Python 解释器在代码运行后进入交互模式。 程序等待你选择 Ctrl+Z+Enter 关闭窗口。

  3. 选择 “文件>保存 ”(或 Ctrl+S)保存属性更改。

  4. 若要将混合模式调试器附加到现有进程,请选择“ 调试>附加到进程”。 此时会打开一个对话框。

    1. 在“ 附加到进程 ”对话框中,从列表中选择相应的进程。

    2. 对于 “附加到” 字段,请使用 “选择 ”选项打开 “选择代码类型 ”对话框。

    3. “选择代码类型 ”对话框中,选择 “调试这些代码类型 ”选项。

    4. 在列表中,选中 Python(本机) 复选框,然后选择 “确定” :

      显示如何在 Visual Studio 中选择用于调试的 Python(本机)代码类型的屏幕截图。

    5. 选择 “附加 ”以启动调试器。

    代码类型设置是永久性的。 如果以后要禁用混合模式调试并附加到其他进程,请清除 Python(本机) 代码类型复选框,然后选择 “本机 代码类型”复选框。

    除了可以选择“本机”选项以外,还可以选择其他代码类型,亦或是作为对“本机”选项的替代。 例如,如果托管应用程序托管 CPython,而 CPython 又使用本机扩展模块,并且想要调试所有三个代码项目,请选中 PythonNativeManaged 复选框。 此方法提供统一的调试体验,包括组合调用堆栈和所有三个运行时之间的单步执行。

使用虚拟环境

当你对虚拟环境(venvs)使用这种混合模式调试方法时,适用于 Windows 的 Python 使用 python.exe 存根文件,该文件由 Visual Studio 发现并作为子进程加载。

  • 对于 Python 3.8 及更高版本,混合模式不支持多进程调试。 启动调试会话时,会调试存根子进程,而不是应用程序。 对于连接场景,解决方法是附加到正确的 python.exe 文件。 当使用调试(例如通过 F5 键盘快捷方式)启动应用程序时,可以使用命令来创建 venv C:\Python310-64\python.exe -m venv venv --symlinks。 在命令中,插入首选版本的 Python。 默认情况下,只有管理员可以在 Windows 上创建符号链接。

  • 对于低于 3.8 的 Python 版本,混合模式调试应按预期使用 venvs。

在全局环境中运行不会导致任何版本的 Python 出现这些问题。

安装 Python 符号

首次在混合模式下开始调试时,可能会看到 “需要 Python 符号 ”对话框。 对于任何给定的 Python 环境,只需安装一次符号。 如果通过 Visual Studio 安装程序(Visual Studio 2017 及更高版本)安装 Python 支持,则会自动包含符号。 有关详细信息,请参阅 在 Visual Studio 中安装 Python 解释器的调试符号

访问 Python 源代码

可以在调试时使标准 Python 本身的源代码可用。

  1. 转到 https://www.python.org/downloads/source/

  2. 下载适合你的版本的 Python 源代码存档,并将代码提取到文件夹中。

  3. 当 Visual Studio 提示输入 Python 源代码的位置时,指向提取文件夹中的特定文件。

在 C/C++ 项目中启用混合模式调试

Visual Studio 2017 版本 15.5 及更高版本支持从 C/C++ 项目中进行混合模式调试。 此用法的一个示例是想要 将 Python 嵌入另一个应用程序中,如 python.org 所述

以下步骤介绍如何为 C/C++ 项目启用混合模式调试:

  1. 解决方案资源管理器中,右键单击 C/C++ 项目,然后选择“ 属性”。

  2. 在“ 属性页 ”窗格中,选择“ 配置属性>调试 ”选项卡。

  3. 展开 调试器启动 选项的下拉菜单,然后选择 “Python/本机调试”。

    显示如何在 Visual Studio 中选择 C/C++ 项目的 Python 本机调试选项的屏幕截图。

    注释

    如果未看到 Python/本机调试 选项,则需要首先使用 Visual Studio 安装程序安装 Python 本机开发工具 。 本机调试选项在 Python 开发工作负载下可用。 有关详细信息,请参阅 在 Visual Studio中安装 Python 支持。

  4. 选择“确定”以保存更改。

调试程序启动器

使用此方法时,无法调试 py.exe 程序启动器,因为它会生成子 python.exe 子进程。 调试器不会附加到子进程。 对于此方案,解决方法是直接使用参数启动 python.exe 程序,如下所示:

  1. 在 C/C++ 项目的 “属性页 ”窗格中,转到 “配置属性>调试 ”选项卡。

  2. 对于 “命令 ”选项,请指定程序文件的完整路径 python.exe

  3. “命令参数” 字段中指定所需的参数。

附加混合模式调试器

对于 Visual Studio 2017 版本 15.4 及更早版本,仅当在 Visual Studio 中启动 Python 项目时,才启用直接混合模式调试。 支持有限,因为 C/C++ 项目仅使用本机调试器。

对于此方案,解决方法是单独附加调试器:

  1. 通过选择调试>不调试启动或使用键盘快捷键Ctrl+F5来启动C++项目。

  2. 若要将混合模式调试器附加到现有进程,请选择“ 调试>附加到进程”。 此时会打开一个对话框。

    1. 在“ 附加到进程 ”对话框中,从列表中选择相应的进程。

    2. 对于 “附加到” 字段,请使用 “选择 ”选项打开 “选择代码类型 ”对话框。

    3. “选择代码类型 ”对话框中,选择 “调试这些代码类型 ”选项。

    4. 在列表中,选中 Python 复选框,然后选择“ 确定”。

    5. 选择 “附加 ”以启动调试器。

小窍门

可以在C++应用程序中添加暂停或延迟,以确保它不会在附加调试器之前调用要调试的 Python 代码。

探索混合模式特定功能

Visual Studio 提供了多种混合模式调试功能,以便更轻松地调试应用程序:

使用组合调用堆栈

调用堆栈窗口显示本机和 Python 堆栈帧相互交错,并标记两者间的过渡。

Visual Studio 中混合调用堆栈窗口与混合模式调试的屏幕截图。

  • 若要使转换显示为 [外部代码] 而不指定切换方向,请使用 “工具>选项 ”窗格。 展开“所有设置>>”部分,选中“启用仅我的代码”复选框。
  • 若要使转换显示为 [外部代码] 而不指定切换方向,请使用 “工具>选项 ”对话框。 展开 “调试>常规 ”部分,选中“ 启用仅我的代码 ”复选框,然后选择“ 确定”。
  • 若要使任何调用帧处于活动状态,请双击该帧。 此操作会打开相应的源代码(如果可能)。 如果源代码不可用,帧仍处于活动状态,可以检查局部变量。

在 Python 和本机代码之间的过渡步骤

Visual Studio 提供 “单步进入”F11)或 “单步跳出”Shift+F11)命令,使混合模式调试器能够正确处理不同代码类型之间的切换。

  • 当 Python 调用在 C 中实现的类型的方法时,单步执行对该方法的调用会在实现该方法的本机函数的开头停止。

  • 当本机代码调用导致调用 Python 代码的 Python API 函数时,会发生相同的行为。 单步执行对最初在 Python 中定义的函数值的调用 PyObject_CallObject 会在 Python 函数的开头停止。

  • 通过 ctypes 从 Python 调用的本机函数也支持从 Python 单步执行到本机函数。

在本机代码中使用 PyObject 的值视图

当本机 (C 或 C++) 帧处于活动状态时,其局部变量将显示在调试器 “局部变量 ”窗口中。 在本机 Python 扩展模块中,其中许多变量属于类型 PyObject (它是 typedef for _object)或其他一些 基本 Python 类型。 在混合模式调试中,这些值显示另一个标记为 [Python 视图] 的子节点。

  • 若要查看变量的 Python 表示形式,请展开节点。 变量的视图与在 Python 中,局部变量引用同一对象的情况下看到的视图相同。 此节点的子级是可编辑的。

    显示 Visual Studio 中“局部变量”窗口中的 Python 视图的屏幕截图。

  • 若要禁用此功能,请右键单击“局部变量”窗口中的任意位置,并切换“Python> 视图节点”菜单选项:

    显示如何为“局部变量”窗口启用“显示 Python 视图节点”选项的屏幕截图。

显示 Python 视图节点的 C 类型

以下 C 类型显示 [Python 视图] 节点(如果已启用):

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

[Python 视图] 不会自动显示你自己创作的类型。 为 Python 3.x 编写扩展时,这通常不是问题。 任何对象最终都有一个 ob_base 列出的 C 类型的字段,这会导致 [Python 视图] 出现。

在 Python 代码中查看原生值

当 Python 框架处于活动状态时,可以在“局部变量”窗口中为本机值启用[C++ 视图]。 此功能在默认情况下未启用。

  • 若要启用该功能,请在 “局部变量 ”窗口中右键单击并设置 Python>显示C++“查看节点 ”菜单选项。

    显示如何为“局部变量”窗口启用“显示C++视图节点”选项的屏幕截图。

  • [C++ 视图] 节点为值提供基础 C/C++ 结构的表示形式,与您在本机框架中看到的相同。 它显示了一个_longobject的实例(其中PyLongObject是其类型定义)用于 Python 长整数,并尝试推断您自己编写的本机类的类型。 此节点的子级是可编辑的。

    显示 Visual Studio 中“局部变量”窗口中C++视图的屏幕截图。

如果对象的子字段的类型 PyObject或其他受支持的类型,则它具有 [Python 视图] 表示节点(如果已启用这些表示形式)。 通过此行为,可以在不直接向 Python 公开链接的对象图中导航。

[Python 视图] 节点(使用 Python 对象元数据确定对象的类型)不同, [C++视图]没有类似的可靠机制。 一般来说,鉴于 Python 值(即 PyObject 引用),无法可靠地确定支持它的 C/C++ 结构。 混合模式调试器通过查看对象的各个字段(如字段ob_type所引用的PyTypeObject)来尝试猜测类型,其中包括具有函数指针类型的字段。 如果其中一个函数指针引用可解析的函数,并且该函数具有 self 比类型更 PyObject*具体的参数,则假定该类型为后盾类型。

请考虑以下示例,其中 ob_type->tp_init 给定对象的值指向以下函数:

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

在这种情况下,调试器可以正确推断对象的 FobObjectC 类型。 如果调试器无法从中确定更精确的类型 tp_init,则会转到其他字段。 如果无法从其中任一字段推断类型, [C++视图] 节点会将对象呈现为 PyObject 实例。

若要始终获取自定义类型的有用的表示,最好在注册该类型时至少注册一个特殊函数,并使用强类型的self参数。 大多数类型自然地满足该要求。 对于其他类型, tp_init 检测通常是为了此目的而使用的最便捷入口。 仅为了启用调试器类型推理而存在的类型的虚拟实现 tp_init 可以立即返回零,如前面的示例所示。

审查与标准 Python 调试的不同之处

混合模式调试器不同于 标准 Python 调试器。 它引入了一些额外的功能,但缺少一些与 Python 相关的功能,如下所示:

  • 不支持的功能包括条件断点、 调试交互 窗口和跨平台远程调试。
  • 即时 ”窗口可用,但其功能子集有限,包括本节中列出的所有限制。
  • 支持的 Python 版本仅包括 CPython 2.7 和 3.3+。
  • 若要将 Python 与 Visual Studio Shell 配合使用(例如,如果使用集成安装程序安装它),Visual Studio 无法打开C++项目。 因此,C++文件的编辑体验只是基本文本编辑器的编辑体验。 但是,Shell 完全支持 C/C++ 调试和混合模式调试,其中包括对源代码的支持、单步进入本机代码,以及在调试器窗口中进行 C++ 表达式求值。
  • “局部变量 ”和 “监视 ”调试器工具窗口中查看 Python 对象时,混合模式调试器仅显示对象的结构。 它不会自动评估属性或显示计算属性。 对于集合,它仅显示内置集合类型的元素(tuple、、listdictset)。 自定义集合类型不会可视化为集合,除非它们继承自某些内置集合类型。
  • 表达式计算的处理方式如以下部分所述。

使用表达式求值

标准 Python 调试器允许在代码的任何时间点暂停调试进程时,在 监视即时 窗口中评估任意 Python 表达式,只要它在 I/O作或其他类似的系统调用中不会被阻止。 在混合模式调试中,仅当在 Python 代码中停止、断点之后或单步执行代码时,才能计算任意表达式。 表达式只能在发生断点或单步操作的线程上计算。

当调试器在本机代码或 Python 代码中停止时,如果是所描述的条件不适用的情况,例如单步跳出操作后或在不同的线程上。 表达式计算仅限于访问当前所选帧范围内的局部变量和全局变量、访问其字段以及使用文本为内置集合类型编制索引。 例如,可以在任何上下文中计算以下表达式(前提是所有标识符都引用适当类型的现有变量和字段):

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

混合模式调试器也以不同的方式解析此类表达式。 所有成员访问操作只查找直接属于对象的字段(例如,对象中__dict____slots__的条目,或通过tp_members向Python公开的本机结构的字段),并忽略任何__getattr____getattribute__或描述符逻辑。 同样,所有索引作都会忽略 __getitem__,并直接访问集合的内部数据结构。

为了保持一致性,此名称解析方案用于匹配有限表达式计算约束的所有表达式。 无论当前停止点是否允许任意表达式,都会应用此方案。 若要在有功能齐全的评估器时强制执行 Python 语义的正确性,请将表达式括在括号中:

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