4. 阅读CPython C代码
4. 阅读 CPython C 代码
阅读 CPython C 代码需要同时使用两种思维模型。
第一个模型是普通的 C:结构、指针、宏、函数指针、引用所有权、分配、错误返回和条件编译。
第二个模型是Python的运行时模型:对象、类型、框架、异常、引用计数、描述符、迭代器、模块和字节码。
大多数 CPython 源文件都结合了两者。一行 C 代码可能看起来像普通的指针操作,但它通常编码 Python 语言规则。
4.1 从运行时不变量开始
中心不变量很简单:text id="vm1twr" Every Python value is represented as a PyObject pointer or a pointer to a struct whose first field is compatible with PyObject. 大多数 CPython 函数通过以下方式处理值PyObject *。```c id="x34zg4"
PyObject *obj;
实际行为来自对象的类型:```c id="y1r4zx"
Py_TYPE(obj)
```类型决定哪些操作是有效的以及哪些 C 函数实现它们。
## 4.2 阅读`PyObject`第一
简化的对象头如下所示:```c id="ogd09v"
typedef struct {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;
```可变大小的对象扩展了这个想法:```c id="s6ik3g"
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size;
} PyVarObject;
```列表、元组、字符串、字节对象、字典、集合和许多其他类型都以此公共对象头开头。这使得 CPython 在特定的对象结构之间进行转换`PyObject *`。
形状示例:```c id="05z3tr"
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
```宏观`PyObject_VAR_HEAD`扩展为公共对象头加上大小字段。
重要的一点是布局兼容性。 CPython 可以通过`PyListObject *`通过将其强制转换为通用对象 API`PyObject *`。
## 4.3 学习核心宏
CPython 大量使用宏。不要跳过它们。许多看似简单的操作都会扩展为重要的行为。
常用对象宏:```c id="0tneoh"
Py_TYPE(obj) /* get object type */
Py_SIZE(obj) /* get variable object size */
Py_REFCNT(obj) /* get reference count */
Py_INCREF(obj) /* increment reference count */
Py_DECREF(obj) /* decrement reference count */
Py_XINCREF(obj) /* increment if not NULL */
Py_XDECREF(obj) /* decrement if not NULL */
```类型检查宏:```c id="rkdbnt"
PyLong_Check(obj)
PyUnicode_Check(obj)
PyList_Check(obj)
PyTuple_Check(obj)
PyDict_Check(obj)
```快速精确类型检查通常使用以下变体:```c id="houo83"
PyLong_CheckExact(obj)
PyUnicode_CheckExact(obj)
PyList_CheckExact(obj)
```差异很重要。`PyList_Check(obj)`接受列表子类。`PyList_CheckExact(obj)`仅接受精确的内置列表。
## 4.4 了解引用所有权
引用所有权是 CPython C 代码中的第一个主要困难。
一个函数返回`PyObject *`可能会返回:
|参考实物|意义|
| ------------------ | ---------------------------------------------------------- |
|新参考|调用者拥有它并且最终必须`Py_DECREF`|
|借用参考|调用者不拥有它 |
|被盗的参考资料|被调用者从调用者那里获取所有权 |
这在C型中是看不到的。新的和借用的参考文献都只是`PyObject *`。
您必须了解 API 合约。
新参考示例:```c id="4ox0zo"
PyObject *x = PyLong_FromLong(42);
/* use x */
Py_DECREF(x);
PyLong_FromLong返回一个新的引用。
借用参考示例:```c id="6j9ceg" PyObject item = PyList_GetItem(list, 0); / do not Py_DECREF(item) */
`PyList_GetItem`返回借用的引用。
来自较新 API 的强引用示例:```c id="kqi42o"
PyObject *item = PySequence_GetItem(seq, 0);
/* must Py_DECREF(item) */
Py_DECREF(item);
PySequence_GetItem返回一个新的引用。
一个正确的读者会要求每一个PyObject *:
Who owns this reference?
Who must release it?
Can this pointer be NULL?
Can this call execute Python code?
Can this call mutate the container?
```## 4.5 识别错误返回
CPython C API 通常通过返回标记值并设置异常来发出错误信号。
常见模式:
|返回类型 |错误值|
| ----------------- | -------------------------------- |
|`PyObject *` | `NULL` |
| `int` | `-1` |
| `Py_ssize_t` | `-1`,经常进行异常检查 |
|指针|`NULL`|
|对比结果|`-1`可能意味着错误|
例子:```c id="3udr6f"
PyObject *value = PyObject_GetAttrString(obj, "name");
if (value == NULL) {
return NULL;
}
```例外已经设置。调用者通常通过返回来传播它`NULL`。
对于类似整数的 API,`-1`可能会含糊不清。有些API需要检查是否发生异常:```c id="29oa6e"
Py_ssize_t n = PyLong_AsSsize_t(obj);
if (n == -1 && PyErr_Occurred()) {
return NULL;
}
```这种模式很常见,因为`-1`也可能是有效的 Python 值。
## 4.6 跟踪异常状态
CPython 异常存储在运行时线程状态中,通常不作为 C 值返回。
当此 Python 代码引发时:```python id="0j7gku"
raise ValueError("bad value")
```CPython 记录一个活动异常。然后,C 函数通过返回错误标记来传播失败。
典型的 C 模式:```c id="y9ubjz"
if (bad_condition) {
PyErr_SetString(PyExc_ValueError, "bad value");
return NULL;
}
```然后调用者执行以下操作:```c id="356gdj"
result = some_function();
if (result == NULL) {
return NULL;
}
```大多数情况下,没有显式异常对象通过 C 调用堆栈传递。异常存储在解释器状态中,而`NULL`或者`-1`承载控制流。
这解释了为什么未能检查返回值会破坏以后的执行。
## 4.7 仔细阅读清理路径
大多数重要的 CPython C 函数都有多个失败出口。
一个常见的模式:```c id="u4ryic"
PyObject *a = NULL;
PyObject *b = NULL;
PyObject *result = NULL;
a = make_a();
if (a == NULL) {
goto error;
}
b = make_b();
if (b == NULL) {
goto error;
}
result = combine(a, b);
error:
Py_XDECREF(a);
Py_XDECREF(b);
return result;
```这种代码并不是偶然的。它对引用所有权进行编码。
阅读清理代码时,请验证:```text id="73pxzg"
Every owned reference is released exactly once.
Borrowed references are not decref'd.
Objects are still valid when used.
Error paths preserve the active exception.
Success paths return the correct ownership.
```许多 CPython 错误是不常见故障路径中的引用计数错误。
## 4.8 了解 C 代码何时可以运行 Python 代码
C 函数调用可以执行任意 Python 代码。
示例包括:```text id="s31qgj"
attribute access
method calls
comparisons
hashing
iteration
descriptor invocation
numeric operations
imports
finalizers
weakref callbacks
```这很重要,因为任意 Python 代码都可以改变对象、释放引用、重新进入解释器、触发垃圾回收或引发异常。
例如:```c id="u11u69"
int equal = PyObject_RichCompareBool(a, b, Py_EQ);
```这可能会调用用户定义的`__eq__`。
同样地:```c id="kfptiw"
Py_hash_t h = PyObject_Hash(obj);
```这可能会调用用户定义的`__hash__`。
阅读 CPython C 代码时,切勿假设对象在可执行 Python 代码的调用中保持不变,除非代码拥有正确的引用并保护其不变量。
## 4.9 将类型对象读取为调度表
一个`PyTypeObject`描述类型的行为方式。
简化思路:```c id="48r26q"
PyTypeObject PyList_Type = {
.tp_name = "list",
.tp_basicsize = sizeof(PyListObject),
.tp_dealloc = list_dealloc,
.tp_repr = list_repr,
.tp_as_sequence = &list_as_sequence,
.tp_methods = list_methods,
.tp_new = list_new,
};
```类型对象包含以下插槽:```text id="jf802y"
allocation
deallocation
attribute access
call behavior
numeric operations
sequence operations
mapping operations
iteration
methods
members
getters and setters
subclass behavior
```Python 语法通常映射到这些槽。
| Python操作 |内部路线|
| ---------------- | ---------------------------------- |
|`len(x)`|序列或映射长度槽 |
|`x[y]`|映射或序列下标槽 |
|`x + y`|数字添加槽 |
|`x()`|呼叫槽|
|`iter(x)`|迭代器槽 |
|`x.y`|属性访问槽 |
|`repr(x)`|代表槽 |
所以在读取内置类型时,首先找到它的`PyTypeObject`。它充当实施的目录。
## 4.10 区分通用 API 和特定类型 API
CPython 通常同时具有通用对象 API 和精确类型 API。
通用API:```c id="e77tm2"
PyObject_GetItem(obj, key)
PyObject_SetAttr(obj, name, value)
PyObject_Call(func, args, kwargs)
PyObject_RichCompare(a, b, Py_EQ)
```这些尊重 Python 级别的定制。他们可能会调用用户代码。
特定于类型的 API:```c id="3m2zu8"
PyList_GET_ITEM(list, i)
PyTuple_GET_ITEM(tuple, i)
PyDict_GetItemWithError(dict, key)
```这些通常采用精确的类型,并且可能绕过 Python 级别的调度。
快速宏,例如`PyList_GET_ITEM`如果使用错误的类型或无效的索引,可能会不安全。它们速度很快,因为它们跳过检查。
阅读代码时,询问该函数是否需要Python语义或内部速度。
## 4.11 了解借用到容器中的指针
一些 API 公开对存储在容器内的对象的借用引用。
例子:```c id="9tw6vq"
PyObject *item = PyList_GetItem(list, i);
```返回的`item`仅当列表保持该引用处于活动状态时才有效。
如果稍后的代码允许 Python 执行,则列表可能会发生变化并释放该项目。安全代码通常会在此类调用之前增加引用:```c id="0wl8te"
PyObject *item = PyList_GetItem(list, i); /* borrowed */
if (item == NULL) {
return NULL;
}
Py_INCREF(item);
/* safe across calls that may mutate list */
...
Py_DECREF(item);
```This pattern is fundamental.借用的引用是有效的,但它们需要严格的生命周期推理。
## 4.12 Read Argument Parsing Code
暴露给 Python 的 C 函数通常使用辅助 API 或 Argument Clinic 生成的代码来解析参数。
手动样式:```c id="0jk1n5"
static PyObject *
mod_func(PyObject *self, PyObject *args)
{
int n;
if (!PyArg_ParseTuple(args, "i", &n)) {
return NULL;
}
return PyLong_FromLong(n + 1);
}
```关键词风格:```c id="6r9q8i"
static PyObject *
mod_func(PyObject *self, PyObject *args, PyObject *kwargs)
{
static char *kwlist[] = {"name", NULL};
const char *name;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", kwlist, &name)) {
return NULL;
}
Py_RETURN_NONE;
}
```Argument Clinic 风格生成了大部分这种包装器代码。当您看到生成的块时,识别手写逻辑并将其与生成的解析样板分开。
## 4.13 了解常见的返回助手
CPython 使用辅助宏来获取公共返回值。```c id="83y546"
Py_RETURN_NONE;
Py_RETURN_TRUE;
Py_RETURN_FALSE;
```这些增加单例引用并返回它。
等效的想法:```c id="0iy1et"
Py_INCREF(Py_None);
return Py_None;
```较新的代码可能会使用强引用助手和内部便利 API。规则保持不变:返回的对象通常需要由调用者拥有。
## 4.14 慢慢阅读释放函数
每个对象类型都有一个释放路径。
形状示例:```c id="tr2vxz"
static void
type_dealloc(MyObject *self)
{
Py_XDECREF(self->field);
Py_TYPE(self)->tp_free((PyObject *)self);
}
```释放必须释放拥有的引用和可用内存。但解除分配可能会很微妙,因为`Py_DECREF`可以执行更多的释放,这可以触发终结器或弱引用回调。
对于容器对象,释放通常会仔细清除包含的引用。
重要问题:```text id="va1g1h"
Does this object participate in cyclic GC?
Does it need to untrack itself before clearing fields?
Can clearing a field run Python code?
Does it support weakrefs?
Does it have a finalizer?
Which allocator frees the memory?
```释放错误通常表现为泄漏、崩溃、复活对象或无效内存访问。
## 4.15 识别垃圾收集器协议代码
可以参与循环的容器类型实现 GC 支持。
您可能会看到如下功能:```c id="wm6ch5"
tp_traverse
tp_clear
PyObject_GC_Track
PyObject_GC_UnTrack
PyObject_GC_Del
```遍历函数访问包含的引用:```c id="zo14wa"
static int
my_traverse(MyObject *self, visitproc visit, void *arg)
{
Py_VISIT(self->field);
return 0;
}
```clear 函数释放可能形成循环的引用:```c id="l0u4gi"
static int
my_clear(MyObject *self)
{
Py_CLEAR(self->field);
return 0;
}
Py_CLEAR将字段设置为NULL在减少参考值之前。这可以防止可重入代码看到悬空指针。
GC 支持代码看起来很机械,但它对于正确性至关重要。
4.16 打开测试文件进行读取
不要单独阅读实施文件。
为了Objects/listobject.c, 保持Lib/test/test_list.py附近。
为了Objects/dictobject.c, 保持Lib/test/test_dict.py附近。
对于描述符和类,请使用Lib/test/test_descr.py。
对于编译器行为,请使用Lib/test/test_compile.py, Lib/test/test_ast.py, 和Lib/test/test_dis.py。
测试显示预期行为、边缘情况和历史回归情况。
高效的阅读循环:```text id="2ghmn1" find Python feature find test file run targeted test read implementation modify small behavior or add print rebuild run test again
有用的搜索模式:```bash id="7h5kd5"
grep -R "PyList_Type" Objects Include Python Modules
grep -R "list_append" Objects
grep -R "PyArg_ParseTuple" Modules Objects Python
grep -R "tp_as_mapping" Objects
grep -R "PyErr_SetString" Objects Python Modules
```使用`git grep`在存储库内:```bash id="4als46"
git grep "PyDict_GetItem"
git grep "tp_dealloc"
git grep "PyObject_RichCompareBool"
git grep "Argument Clinic"
```首先搜索类型对象,然后按照槽查找函数。
## 4.18 一个实际阅读示例:`list.append`从Python开始:```python id="6wptq9"
xs = []
xs.append(1)
```找到方法表`Objects/listobject.c`。
它将包含一个方法条目`append`。
从概念上讲:```c id="rbqcp6"
{"append", list_append, METH_O, "..."}
```然后阅读实现。
简化的形状:```c id="06amht"
static PyObject *
list_append(PyListObject *self, PyObject *object)
{
if (_PyList_AppendTakeRef(self, Py_NewRef(object)) < 0) {
return NULL;
}
Py_RETURN_NONE;
}
```重点是:```text id="ud5hhf"
self is the list object
object is the item passed from Python
the list stores a new reference to object
failure returns NULL with an exception set
success returns None
```然后按照需要调整列表大小的助手进行操作。
该路径教导:```text id="bc2gxm"
method tables
argument calling convention
list over-allocation
reference ownership
error handling
return helpers
```一个小方法可以公开多个 CPython 习惯用法。
## 4.19 一个实际阅读示例:`dict[key]`从Python开始:```python id="ghl7wv"
value = d[key]
```此操作映射到字典下标行为。
阅读路径:```text id="agveol"
Objects/dictobject.c
↓
dict type object
↓
mapping methods
↓
subscript function
↓
hash lookup path
```重要问题:```text id="iws5gu"
Is the key hashable?
Does hashing call Python code?
How are missing keys handled?
How are exceptions distinguished from absence?
Does this path return a borrowed or new reference?
```字典代码对性能至关重要并且高度优化。分层阅读:首先是公共行为,其次是查找助手,第三是表格布局。
## 4.20 CPython C 风格
CPython C 代码倾向于支持显式控制流而不是抽象。
共同特征:```text id="k252pu"
manual reference counting
explicit error checks
goto-based cleanup
macros for hot paths
function pointers through type slots
separate fast paths and generic paths
conditional compilation for platforms
generated wrappers for Python-callable functions
```这种风格很实用。 CPython 是古老的、可移植的、性能敏感的 C 代码,具有严格的兼容性要求。
不要指望一个小型的、纯粹的现代 C 架构。期待分层进化。
## 4.21 阅读 CPython 时的常见错误
|错误|更正|
| ---------------------------------------------------------- | -------------------------------------------------------- |
|治疗`PyObject *`作为具体类型 |它是指向任何 Python 对象的通用指针 |
|忽略引用所有权 |每个对象指针都有所有权规则 |
|假设`NULL`意味着没有价值|它通常意味着异常 |
|假设 C 调用无法运行 Python |许多对象 API 可以运行 Python 代码 |
|编辑生成的代码 |编辑源输入并重新生成 |
|读取快速宏作为安全 API |许多跳过检查|
|假设字节码是稳定的 |版本之间的字节码变化 |
|假设 CPython 行为是语言行为 |某些行为是特定于实现的 |
## 4.22 任何功能的最小清单
在阅读 CPython C 函数时,请回答以下问题:```text id="ztg0lo"
What Python behavior does this implement?
What are the input reference ownership rules?
What does the function return on success?
What does it return on failure?
Does it set or propagate an exception?
Which references does it own?
Which references are borrowed?
Can any call execute Python code?
Can any object be mutated during the function?
Are there cleanup paths?
Is this public API, private API, or internal helper?
Is any part generated?
Which tests cover it?
```这份清单可以防止大多数误读。
## 4.23 章节总结
一旦您一致地跟踪三件事:对象布局、引用所有权和错误传播,CPython C 代码就可读。大多数运行时值被处理为`PyObject *`。类型对象通过槽定义行为。函数通过返回标记和解释器异常状态来发出错误信号。引用计数使所有权在每一行代码中都可见。
阅读打开测试的 CPython 代码,跟踪类型对象到槽,将宏视为真实代码,并假设许多通用对象操作可以执行任意 Python 代码。