最近我在研究 Python 的执行模型。我对 Python 内部的东西挺好奇,比如:类似 YIELDVALUE 和 YIELDFROM 此类操作码的实现;列表表达式、生成器表达式以及一些有趣的Python 特性是怎么编译的;异常触发之时,字节码层面发生了什么。
阅读 CPython 代码是相当有益的,但是我觉得要完全理解字节码的执行和堆栈的变化,光读源码是远远不够的。GDB 是个好选择,但我很懒,只想写一些高级的接口和 Python 代码。
因此我想做一个字节码级别的追踪 API,就像 sys.settrace 所提供的那样,但颗粒度更出色。这种练习完美地锻炼了我将 C 转化为 Python 的能力。我们所需的有以下几点:
注:在这篇文章中,Python版本是3.5
这个新的操作码DEBUG_OP是我第一次尝试用C代码来实现CPython。我会尽量使之保持简洁。
我想要达到的目的是,无论我的操作码何时执行,都有一种方式调用一些Python代码,与此同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。我能辨识出的有用信息如下:
因此我们 DEBUG_OP 所需做的所有事情是:
听起来挺简单啊,让我们开始吧!
声明:以下的解释和代码都是经过大量段错误得到的。首先要做的事情,就是给我们的操作码命名并赋值,因此我们需要在Include/opcode.h中添加
| 123456789101112 | /** My own comments begin by \’**\’ **//** From: Includes/opcode.h **/ /* Instruction opcodes for compiled code */ /** We just have to define our opcode with a free value 0 was the first one I found **/#define DEBUG_OP 0 #define POP_TOP 1#define ROT_TWO 2#define ROT_THREE 3 |
这简单的部分是完成了,现在我们必须真正去编写我们的操作码。
在考虑实现DEBUG_OP之前,我们需要问我们自己的第一个问题是:“我的接口应该是什么样的?”
拥有一个可以调用其他代码的新操作码是很酷的,但是它实际上会调用哪些代码呢?这个操作码怎么找到回调函数呢?我选择了一种看起来最简单的解决方案,在帧的全局区域写死函数名。
现在问题就变成了:“我怎么从一个字典中找到一个不变的C字符串?”
为了回答这个问题,我们可以寻找一些用在Python的main循环中的用到的和上下文管理相关的标识符**enter**和**exit**。
我们可以看到标识符被用在 SETUP_WITH 操作码中。
| 1234567 | /** From: Python/ceval.c **/TARGET(SETUP_WITH) {_Py_IDENTIFIER(__exit__);_Py_IDENTIFIER(__enter__);PyObject *mgr = TOP();PyObject *exit = special_lookup(mgr, &PyId___exit__), *enter;PyObject *res; |
现在,看一下_Py_IDENTIFIER 的宏定义:
| 12345678910111213141516171819202122232425262728293031 | /** From: Include/object.h **/ /********************* String Literals ****************************************//* This structure helps managing static strings. The basic usage goes like this: Instead of doing r = PyObject_CallMethod(o, \”foo\”, \”args\”, ...); do _Py_IDENTIFIER(foo); ... r = _PyObject_CallMethodId(o, &PyId_foo, \”args\”, ...); PyId_foo is a static variable, either on block level or file level. On first usage, the string \”foo\” is interned, and the structures are linked. On interpreter shutdown, all strings are released (through _PyUnicode_ClearStaticStrings). Alternatively, _Py_static_string allows to choose the variable name. _PyUnicode_FromId returns a borrowed reference to the interned string. _PyObject_{Get,Set,Has}AttrId are __getattr__ versions using _Py_Identifier*.*/typedef struct _Py_Identifier { struct _Py_Identifier *next; const char* string;>。
最近我在研究 Python 的执行模型。我对 Python 内部的东西挺好奇,比如:类似 YIELDVALUE 和 YIELDFROM 此类操作码的实现;列表表达式、生成器表达式以及一些有趣的Python 特性是怎么编译的;异常触发之时,字节码层面发生了什么。 阅读 CPython 代码是相当有益的,但是我觉得要完全理解字节码的执行和堆栈的变化,光读源码是远远不够的。GDB 是个好选择,但我很懒,只想写一些高级的接口和 Python 代码。 因此我想做一个字节码级别的追踪 API,就像 sys.settrace 所提供的那样,但颗粒度更出色。这种练习完美地锻炼了我将 C 转化为 Python 的能力。我们所需的有以下几点:
注:在这篇文章中,Python版本是3.5 一种新的CPython操作码我们的新操作码:DEBUG_OP这个新的操作码DEBUG_OP是我第一次尝试用C代码来实现CPython。我会尽量使之保持简洁。 我想要达到的目的是,无论我的操作码何时执行,都有一种方式调用一些Python代码,与此同时,我们也想能够追踪一些与执行上下文有关的数据。我们的操作码会把这些信息当作参数传递给我们的回调函数。我能辨识出的有用信息如下:
因此我们 DEBUG_OP 所需做的所有事情是:
听起来挺简单啊,让我们开始吧! 声明:以下的解释和代码都是经过大量段错误得到的。首先要做的事情,就是给我们的操作码命名并赋值,因此我们需要在Include/opcode.h中添加
这简单的部分是完成了,现在我们必须真正去编写我们的操作码。 实现 DEBUG_OP在考虑实现DEBUG_OP之前,我们需要问我们自己的第一个问题是:“我的接口应该是什么样的?” 拥有一个可以调用其他代码的新操作码是很酷的,但是它实际上会调用哪些代码呢?这个操作码怎么找到回调函数呢?我选择了一种看起来最简单的解决方案,在帧的全局区域写死函数名。 现在问题就变成了:“我怎么从一个字典中找到一个不变的C字符串?” 为了回答这个问题,我们可以寻找一些用在Python的main循环中的用到的和上下文管理相关的标识符**enter**和**exit**。 我们可以看到标识符被用在 SETUP_WITH 操作码中。
现在,看一下
|