在 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 及更高版本中,适用于 Visual Studio 1.x 的 Python 工具不提供混合模式调试功能。

  • 安装了 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,而后者又使用了本机扩展模块,且你想要调试所有三个代码项目,则请选中 Python本机托管复选框。 此方法可提供统一的调试体验,其中包括组合式调用堆栈以及在所有三个运行时之间进行单步执行。

使用虚拟环境

将此混合模式调试方法用于虚拟环境 (venv) 时,Python for Windows 会将 python.exe 存根文件用于 venv,而 Visual Studio 会查找该文件并将其作为子进程进行加载。

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

  • 对于 3.8 之前的 Python 版本,混合模式调试应可在 venv 中正常工作。

在任意版本的 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.org 中所述将 Python 嵌入其他应用程序中

以下步骤介绍如何为 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 中的合并式调用堆栈窗口与混合模式调试的屏幕截图。

  • 若要使转换显示为 [External Code] 而不指定转换方向,则请工具>选项>调试>常规>启用仅我的代码选项。

  • 若要使任一调用帧处于活动状态,请双击该帧。 此操作还会打开相应的源代码(如果可能)。 如果源代码不可用,该帧仍将处于活动状态并可检查局部变量。

在 Python 和本机代码之间进行单步执行

Visual Studio 提供单步执行 (F11) 或单步跳出 (Shift+F11) 命令来启用混合模式调试器,以便正确处理不同代码类型之间的更改。

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

  • 当本机代码调用导致调用 Python 代码的某一 Python API 函数时也会出现此情况。 对原先在 Python 中定义的函数值的 PyObject_CallObject 调用进行单步执行时会在 Python 函数的开头处停止。

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

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

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

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

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

  • 若要禁用此功能,单键单击局部变量窗口中的任何位置并切换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 创作扩展时,此缺憾通常不是问题。 任一对象最终均有一个其类型为所列某一 C 类型的 ob_base 字段,因而会显示 [Python 视图]

查看 Python 代码中的本机值

当 Python 帧处于活动状态时,可为局部变量窗口中的本机值启用 [C++ 视图]。 此功能默认未启用。

  • 若要启用此功能,请右键单击局部变量窗口并设置 Python>显示 C++ 视图节点菜单选项。

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

  • [C++ 视图] 节点将提供值的基础 C/C++ 结构的表示形式,而该形式与本机帧中看到的形式一致。 它会为 Python 长整型整数显示一个 _longobject 实例(而 PyLongObject 是其 typedef),且会尝试推断自行创作的本机类的类型。 此节点的子级可编辑。

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

如果对象的子字段的类型为 PyObject 或其他受支持的类型,则它具有 [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++ 视图] 节点会将该对象作为 PyObject 实例显示。

若要始终获得自定义创作类型的有用表示形式,注册类型时最好注册至少一个特殊函数,并使用强类型 self 参数。 大多数类型可自然满足该要求。 对于其他类型,tp_init 检查通常是用于此目的的最便捷入口。 针对单独显示的某一类型的 tp_init 虚拟实现,它可让调试类型推理只能立即返回零,如前面的示例所示。

查看与标准 Python 调试之间的区别

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

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

使用表达式计算

标准 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'])