4. Đọc mã CPython C
#4. Đọc mã CPython C
Đọc mã CPython C yêu cầu hai mô hình tinh thần cùng một lúc.
Mô hình đầu tiên là C thông thường: cấu trúc, con trỏ, macro, con trỏ hàm, quyền sở hữu tham chiếu, phân bổ, trả về lỗi và biên dịch có điều kiện.
Mô hình thứ hai là mô hình thời gian chạy của Python: đối tượng, loại, khung, ngoại lệ, số lượng tham chiếu, bộ mô tả, bộ lặp, mô-đun và mã byte.
Hầu hết các tệp nguồn CPython đều kết hợp cả hai. Một dòng mã C có thể trông giống như thao tác con trỏ thông thường, nhưng nó thường mã hóa quy tắc ngôn ngữ Python.
4.1 Bắt đầu từ bất biến thời gian chạy
Bất biến trung tâm rất đơn giản: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. Hầu hết các hàm CPython đều hoạt động với các giá trị thông quaPyObject *.
PyObject *obj;
```Con trỏ này có thể tham chiếu đến một số nguyên, danh sách, hàm, lớp, mô-đun, ngoại lệ, chuỗi hoặc bất kỳ đối tượng nào do người dùng định nghĩa.
Hành vi thực tế xuất phát từ loại đối tượng:```c id="y1r4zx"
Py_TYPE(obj)
```Kiểu xác định thao tác nào hợp lệ và hàm C nào thực hiện chúng.
##4.2 Đọc`PyObject`đầu tiên
Tiêu đề đối tượng được đơn giản hóa trông như thế này:```c id="ogd09v"
typedef struct {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;
```Các đối tượng có kích thước thay đổi mở rộng ý tưởng này:```c id="s6ik3g"
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size;
} PyVarObject;
```Một danh sách, bộ dữ liệu, chuỗi, đối tượng byte, dict, set và nhiều loại khác bắt đầu bằng tiêu đề đối tượng chung này. Điều đó cho phép CPython truyền giữa các cấu trúc đối tượng cụ thể và`PyObject *`.
Hình dạng ví dụ:```c id="05z3tr"
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
```vĩ mô`PyObject_VAR_HEAD`mở rộng đến tiêu đề đối tượng chung cộng với trường kích thước.
Điểm quan trọng là khả năng tương thích bố cục. CPython có thể vượt qua`PyListObject *`tới các API đối tượng chung bằng cách truyền nó tới`PyObject *`.
## 4.3 Tìm hiểu các Macro cốt lõi
CPython sử dụng macro rất nhiều. Đừng bỏ qua chúng. Nhiều hoạt động có vẻ đơn giản sẽ mở rộng thành hành vi quan trọng.
Macro đối tượng phổ biến:```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 */
```Macro kiểm tra loại:```c id="rkdbnt"
PyLong_Check(obj)
PyUnicode_Check(obj)
PyList_Check(obj)
PyTuple_Check(obj)
PyDict_Check(obj)
```Kiểm tra loại chính xác nhanh thường sử dụng các biến thể như:```c id="houo83"
PyLong_CheckExact(obj)
PyUnicode_CheckExact(obj)
PyList_CheckExact(obj)
```Sự khác biệt quan trọng.`PyList_Check(obj)`chấp nhận các lớp con danh sách.`PyList_CheckExact(obj)`chỉ chấp nhận danh sách tích hợp chính xác.
##4.4 Hiểu quyền sở hữu tài liệu tham khảo
Quyền sở hữu tham chiếu là khó khăn lớn đầu tiên trong mã CPython C.
Một hàm trả về`PyObject *`có thể trở lại:
| Loại tham khảo | Ý nghĩa |
| ------------------ | ---------------------------------------------- |
| Tài liệu tham khảo mới | Người gọi sở hữu nó và cuối cùng phải`Py_DECREF`|
| Tài liệu tham khảo mượn | Người gọi không sở hữu nó |
| Tài liệu tham khảo bị đánh cắp | Callee nhận quyền sở hữu từ người gọi |
Điều này không thể nhìn thấy được từ loại C. Cả tài liệu tham khảo mới và mượn đều chỉ là`PyObject *`.
Bạn phải biết hợp đồng API.
Ví dụ tài liệu tham khảo mới:```c id="4ox0zo"
PyObject *x = PyLong_FromLong(42);
/* use x */
Py_DECREF(x);
PyLong_FromLongtrả về một tham chiếu mới.
Ví dụ mượn tài liệu tham khảo:```c id="6j9ceg" PyObject item = PyList_GetItem(list, 0); / do not Py_DECREF(item) */
`PyList_GetItem`trả về một tài liệu tham khảo mượn.
Ví dụ tham chiếu mạnh mẽ từ các API mới hơn:```c id="kqi42o"
PyObject *item = PySequence_GetItem(seq, 0);
/* must Py_DECREF(item) */
Py_DECREF(item);
PySequence_GetItemtrả về một tham chiếu mới.
Một người đọc đúng sẽ yêu cầu mọiPyObject *:
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 Nhận biết lỗi trả về
API CPython C thường báo hiệu lỗi bằng cách trả về các giá trị trọng điểm và đặt ngoại lệ.
Các mẫu phổ biến:
| Kiểu trả về | Giá trị lỗi |
| ----------------- | -------------------------------- |
|`PyObject *` | `NULL` |
| `int` | `-1` |
| `Py_ssize_t` | `-1`, thường có kiểm tra ngoại lệ |
| con trỏ |`NULL`|
| kết quả so sánh |`-1`có thể có nghĩa là lỗi |
Ví dụ:```c id="3udr6f"
PyObject *value = PyObject_GetAttrString(obj, "name");
if (value == NULL) {
return NULL;
}
```Ngoại lệ đã được thiết lập. Người gọi thường truyền bá nó bằng cách quay lại`NULL`.
Đối với các API giống số nguyên,`-1`có thể mơ hồ. Một số API yêu cầu kiểm tra xem có ngoại lệ xảy ra hay không:```c id="29oa6e"
Py_ssize_t n = PyLong_AsSsize_t(obj);
if (n == -1 && PyErr_Occurred()) {
return NULL;
}
```Mô hình này phổ biến vì`-1`cũng có thể là giá trị Python hợp lệ.
## 4.6 Theo dõi trạng thái ngoại lệ
Các ngoại lệ CPython được lưu trữ ở trạng thái luồng thời gian chạy, thường không được trả về dưới dạng giá trị C.
Khi mã Python này tăng lên:```python id="0j7gku"
raise ValueError("bad value")
```CPython ghi lại một ngoại lệ đang hoạt động. Sau đó, các hàm C sẽ truyền lỗi bằng cách trả về các thông báo lỗi.
Mẫu C điển hình:```c id="y9ubjz"
if (bad_condition) {
PyErr_SetString(PyExc_ValueError, "bad value");
return NULL;
}
```Sau đó, người gọi sẽ thực hiện:```c id="356gdj"
result = some_function();
if (result == NULL) {
return NULL;
}
```Không có đối tượng ngoại lệ rõ ràng nào được chuyển qua ngăn xếp lệnh gọi C trong hầu hết các trường hợp. Ngoại lệ được lưu trữ ở trạng thái thông dịch viên, trong khi`NULL`hoặc`-1`mang dòng điều khiển.
Điều này giải thích tại sao việc không kiểm tra giá trị trả về có thể làm hỏng quá trình thực thi sau này.
## 4.7 Đọc kỹ đường dẫn dọn dẹp
Hầu hết các hàm CPython C không cần thiết đều có nhiều lần thoát lỗi.
Một mô hình phổ biến:```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;
```Loại mã này không phải là ngẫu nhiên. Nó mã hóa quyền sở hữu tham chiếu.
Khi đọc mã dọn dẹp, hãy xác minh:```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.
```Nhiều lỗi CPython là lỗi đếm tham chiếu trong các đường dẫn lỗi không phổ biến.
## 4.8 Biết khi nào mã C có thể chạy mã Python
Lệnh gọi hàm C có thể thực thi mã Python tùy ý.
Ví dụ bao gồm:```text id="s31qgj"
attribute access
method calls
comparisons
hashing
iteration
descriptor invocation
numeric operations
imports
finalizers
weakref callbacks
```Điều này quan trọng vì mã Python tùy ý có thể biến đổi các đối tượng, giải phóng các tham chiếu, nhập lại trình thông dịch, kích hoạt thu thập rác hoặc đưa ra các ngoại lệ.
Ví dụ:```c id="u11u69"
int equal = PyObject_RichCompareBool(a, b, Py_EQ);
```Điều này có thể gọi do người dùng xác định`__eq__`.
Tương tự:```c id="kfptiw"
Py_hash_t h = PyObject_Hash(obj);
```Điều này có thể gọi do người dùng xác định`__hash__`.
Khi đọc mã CPython C, đừng bao giờ cho rằng một đối tượng không thay đổi trong suốt cuộc gọi có thể thực thi mã Python trừ khi mã sở hữu các tham chiếu phù hợp và đã bảo vệ các bất biến của nó.
## 4.9 Đọc các đối tượng loại dưới dạng bảng điều phối
A`PyTypeObject`mô tả cách một loại hoạt động.
Ý tưởng đơn giản hóa:```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,
};
```Một đối tượng kiểu chứa các vị trí cho:```text id="jf802y"
allocation
deallocation
attribute access
call behavior
numeric operations
sequence operations
mapping operations
iteration
methods
members
getters and setters
subclass behavior
```Cú pháp Python thường ánh xạ tới các vị trí này.
| Hoạt động Python | Tuyến nội bộ |
| ---------------- | ---------------------------------- |
|`len(x)`| khe độ dài trình tự hoặc ánh xạ |
|`x[y]`| vị trí đăng ký ánh xạ hoặc trình tự |
|`x + y`| khe thêm số |
|`x()`| khe gọi |
|`iter(x)`| khe lặp |
|`x.y`| khe truy cập thuộc tính |
|`repr(x)`| khe đại diện |
Vì vậy, khi đọc một loại có sẵn, trước tiên hãy tìm nó`PyTypeObject`. Nó đóng vai trò như mục lục để thực hiện.
## 4.10 Phân biệt API chung và API cụ thể theo loại
CPython thường có cả API đối tượng chung và API loại chính xác.
API chung:```c id="e77tm2"
PyObject_GetItem(obj, key)
PyObject_SetAttr(obj, name, value)
PyObject_Call(func, args, kwargs)
PyObject_RichCompare(a, b, Py_EQ)
```Những điều này tôn trọng khả năng tùy chỉnh ở cấp độ Python. Họ có thể gọi mã người dùng.
API dành riêng cho loại:```c id="3m2zu8"
PyList_GET_ITEM(list, i)
PyTuple_GET_ITEM(tuple, i)
PyDict_GetItemWithError(dict, key)
```Chúng thường giả định các loại chính xác và có thể bỏ qua công văn cấp Python.
Các macro nhanh như`PyList_GET_ITEM`có thể không an toàn nếu sử dụng sai loại hoặc chỉ mục không hợp lệ. Họ nhanh chóng vì họ bỏ qua việc kiểm tra.
Khi đọc mã, hãy hỏi xem hàm có cần ngữ nghĩa Python hay tốc độ bên trong hay không.
## 4.11 Hiểu con trỏ mượn vào vùng chứa
Một số API hiển thị các tham chiếu mượn đến các đối tượng được lưu trữ bên trong vùng chứa.
Ví dụ:```c id="9tw6vq"
PyObject *item = PyList_GetItem(list, i);
```Sự trở lại`item`chỉ hợp lệ khi danh sách vẫn giữ tham chiếu đó.
Nếu sau này mã cho phép thực thi Python thì danh sách có thể thay đổi và giải phóng mục đó. Mã an toàn thường tăng tham chiếu trước các lệnh gọi như vậy:```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);
```Mô hình này là cơ bản. Các tài liệu tham khảo mượn có hiệu quả nhưng chúng đòi hỏi phải có lý luận chặt chẽ suốt đời.
## 4.12 Đọc mã phân tích đối số
Các hàm C tiếp xúc với Python thường phân tích cú pháp các đối số bằng API trợ giúp hoặc mã được tạo bởi Argument Clinic.
Phong cách thủ công:```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);
}
```Kiểu từ khóa:```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;
}
```Kiểu Phòng khám đối số tạo ra phần lớn mã bao bọc này. Khi bạn thấy các khối được tạo, hãy xác định logic viết tay và tách nó khỏi bản mẫu phân tích cú pháp được tạo.
## 4.13 Biết Người Trợ Giúp Hoàn Trả Chung
CPython sử dụng macro trợ giúp cho các giá trị trả về chung.```c id="83y546"
Py_RETURN_NONE;
Py_RETURN_TRUE;
Py_RETURN_FALSE;
```Chúng tăng tham chiếu singleton và trả về nó.
Ý tưởng tương đương:```c id="0iy1et"
Py_INCREF(Py_None);
return Py_None;
```Mã mới hơn có thể sử dụng trình trợ giúp tham chiếu mạnh và API tiện lợi nội bộ. Quy tắc vẫn giữ nguyên: các đối tượng được trả về thường cần phải thuộc sở hữu của người gọi.
## 4.14 Đọc hàm Deallocation chậm
Mỗi loại đối tượng đều có một đường dẫn phân bổ.
Hình dạng ví dụ:```c id="tr2vxz"
static void
type_dealloc(MyObject *self)
{
Py_XDECREF(self->field);
Py_TYPE(self)->tp_free((PyObject *)self);
}
```Deallocation phải giải phóng các tài liệu tham khảo thuộc sở hữu và bộ nhớ trống. Nhưng việc phân bổ có thể tinh tế bởi vì`Py_DECREF`có thể thực hiện nhiều sự phân bổ hơn, điều này có thể kích hoạt các lệnh hoàn thiện hoặc các lệnh gọi lại yếu.
Đối với các đối tượng vùng chứa, việc phân bổ thường xóa các tham chiếu được chứa một cách cẩn thận.
Các câu hỏi quan trọng:```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?
```Lỗi phân bổ thường xuất hiện dưới dạng rò rỉ, treo máy, đối tượng được phục hồi hoặc truy cập bộ nhớ không hợp lệ.
## 4.15 Nhận dạng mã giao thức thu gom rác
Các loại vùng chứa có thể tham gia vào chu trình sẽ triển khai hỗ trợ GC.
Bạn có thể thấy các chức năng như:```c id="wm6ch5"
tp_traverse
tp_clear
PyObject_GC_Track
PyObject_GC_UnTrack
PyObject_GC_Del
```Các lượt truy cập hàm duyệt có chứa các tham chiếu:```c id="zo14wa"
static int
my_traverse(MyObject *self, visitproc visit, void *arg)
{
Py_VISIT(self->field);
return 0;
}
```Hàm clear giải phóng các tham chiếu có thể tạo thành chu trình:```c id="l0u4gi"
static int
my_clear(MyObject *self)
{
Py_CLEAR(self->field);
return 0;
}
Py_CLEARđặt trường thànhNULLtrước khi giảm tham chiếu. Điều này ngăn không cho mã đăng ký lại nhìn thấy con trỏ lơ lửng.
Mã hỗ trợ GC trông có vẻ máy móc nhưng lại cần thiết để đảm bảo tính chính xác.
4.16 Đọc khi mở tệp thử nghiệm
Đừng đọc các tập tin thực hiện một mình.
VìObjects/listobject.c, giữLib/test/test_list.pygần đó.
VìObjects/dictobject.c, giữLib/test/test_dict.pygần đó.
Đối với mô tả và các lớp, sử dụngLib/test/test_descr.py.
Đối với hành vi của trình biên dịch, hãy sử dụngLib/test/test_compile.py, Lib/test/test_ast.py, VàLib/test/test_dis.py.
Các thử nghiệm cho thấy hành vi dự kiến, các trường hợp đặc biệt và các trường hợp hồi quy lịch sử.
Một vòng lặp đọc hiệu quả:```text id="2ghmn1" find Python feature find test file run targeted test read implementation modify small behavior or add print rebuild run test again
Các mẫu tìm kiếm hữu ích:```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
```Sử dụng`git grep`bên trong kho lưu trữ:```bash id="4als46"
git grep "PyDict_GetItem"
git grep "tp_dealloc"
git grep "PyObject_RichCompareBool"
git grep "Argument Clinic"
```Trước tiên hãy tìm kiếm đối tượng loại, sau đó đi theo các vị trí đến hàm.
## 4.18 Ví dụ đọc thực tế:`list.append`Bắt đầu từ Python:```python id="6wptq9"
xs = []
xs.append(1)
```Tìm bảng phương pháp trong`Objects/listobject.c`.
Nó sẽ chứa một mục phương thức cho`append`.
Về mặt khái niệm:```c id="rbqcp6"
{"append", list_append, METH_O, "..."}
```Sau đó đọc phần thực hiện.
Hình dạng đơn giản:```c id="06amht"
static PyObject *
list_append(PyListObject *self, PyObject *object)
{
if (_PyList_AppendTakeRef(self, Py_NewRef(object)) < 0) {
return NULL;
}
Py_RETURN_NONE;
}
```Những điểm quan trọng là:```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
```Sau đó làm theo người trợ giúp để thay đổi kích thước danh sách nếu cần.
Con đường đó dạy:```text id="bc2gxm"
method tables
argument calling convention
list over-allocation
reference ownership
error handling
return helpers
```Một phương pháp nhỏ có thể hiển thị một số thành ngữ CPython.
## 4.19 Ví dụ đọc thực tế:`dict[key]`Bắt đầu từ Python:```python id="ghl7wv"
value = d[key]
```Thao tác này ánh xạ tới hành vi chỉ số từ điển.
Đường đọc:```text id="agveol"
Objects/dictobject.c
↓
dict type object
↓
mapping methods
↓
subscript function
↓
hash lookup path
```Các câu hỏi quan trọng:```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?
```Mã từ điển rất quan trọng về hiệu suất và được tối ưu hóa cao. Đọc nó theo từng lớp: hành vi công khai trước tiên, thứ hai là tra cứu người trợ giúp, thứ ba là bố cục bảng.
## 4.20 Phong cách của CPython C
Mã CPython C có xu hướng ưu tiên luồng điều khiển rõ ràng hơn là trừu tượng hóa.
Đặc điểm chung:```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
```Phong cách này là thực tế. CPython là mã C cũ, di động, nhạy cảm với hiệu suất với các yêu cầu tương thích nghiêm ngặt.
Đừng mong đợi một kiến trúc C nhỏ gọn, thuần túy hiện đại. Mong đợi sự tiến hóa theo lớp.
##4.21 Những lỗi thường gặp khi đọc CPython
| Sai lầm | Sửa chữa |
| ---------------------------------------------- | -------------------------------------------- |
| điều trị`PyObject *`như một loại bê tông | Nó là một con trỏ chung tới bất kỳ đối tượng Python nào |
| Bỏ qua quyền sở hữu tài liệu tham khảo | Mọi con trỏ đối tượng đều có quy tắc sở hữu |
| Giả sử`NULL`nghĩa là không có giá trị | Nó thường có nghĩa là ngoại lệ |
| Giả sử cuộc gọi C không thể chạy Python | Nhiều API đối tượng có thể chạy mã Python |
| Chỉnh sửa mã được tạo | Chỉnh sửa đầu vào nguồn và tạo lại |
| Đọc macro nhanh dưới dạng API an toàn | Nhiều người bỏ qua kiểm tra |
| Giả sử mã byte ổn định | Thay đổi mã byte giữa các phiên bản |
| Giả sử hành vi CPython là hành vi ngôn ngữ | Một số hành vi được triển khai cụ thể |
## 4.22 Danh sách kiểm tra tối thiểu cho mọi chức năng
Khi đọc hàm CPython C, hãy trả lời những câu hỏi sau:```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?
```Danh sách kiểm tra này ngăn ngừa hầu hết các trường hợp đọc sai.
## 4.23 Tóm tắt chương
Mã CPython C có thể đọc được khi bạn theo dõi ba điều một cách nhất quán: bố cục đối tượng, quyền sở hữu tham chiếu và lan truyền lỗi. Hầu hết các giá trị thời gian chạy được xử lý như`PyObject *`. Đối tượng loại xác định hành vi thông qua các vị trí. Các hàm báo hiệu lỗi thông qua các trọng điểm trả về và trạng thái ngoại lệ của trình thông dịch. Việc đếm tham chiếu giúp hiển thị quyền sở hữu trong mỗi dòng mã.
Đọc mã CPython khi mở các bài kiểm tra, theo dõi các đối tượng loại đến các vị trí, coi macro là mã thực và giả định rằng nhiều thao tác đối tượng chung có thể thực thi mã Python tùy ý.