我在探索 Python 3.11 源代码时遇到了
_PyCode_CODE
宏。我注意到在这个宏中,co_code_adaptive
的PyCodeObject
成员被转换为uint16_t*
指针。但是,co_code_adaptive
被定义为长度为 1 的 char
数组。
/*******************************************/
// code in code.h
/*******************************************/
#define _PyCode_CODE(co) ((const uint16_t *)(co)->co_code_adaptive)
#define _PyCode_DEF(SIZE) { \
// ... \
char co_code_adaptive[(SIZE)]; \
}
/* Bytecode object */
struct PyCodeObject _PyCode_DEF(1);
/*******************************************/
// code in specialize.c
/*******************************************/
void
_PyCode_Quicken(PyCodeObject *code)
{
_Py_QuickenedCount++;
int previous_opcode = -1;
_Py_CODEUNIT *instructions = _PyCode_CODE(code);
for (int i = 0; i < Py_SIZE(code); i++) {
int opcode = _Py_OPCODE(instructions[i]);
// ...
}
这种转换似乎可能会带来内存访问风险,因为 char 和 uint16_t 具有不同的类型大小和对齐要求。为什么Python开发者选择这样实现呢?在这种情况下如何保证内存安全?
具体来说,在
specialize.c
文件中,_PyCode_Quicken
函数内,该宏用于定义一个名为uint16_t*
的instructions
指针。然后代码使用 []
运算符访问内存。 Python 如何确保这些内存访问有效?
任何对底层设计决策或解释此方法的文档的见解将不胜感激。
对于 Python,每个位码指令都是一个两字节对象1,带有操作码和相应的参数:
typedef struct _CodeUnit {
uint8_t opcode;
uint8_t oparg;
} _CodeUnit;
实际的 PyCodeObject 结构是使用 C 中的“灵活数组成员”习惯用法来分配的,以减少分配(如果没有该习惯用法,我们将需要为 PyCodeObject 结构分配一次,为编译后的位码分配一次)。这种模式在实现中经常使用。
这意味着可以像这样分配一个 PyCodeObject 及其相应的代码:
PyCodeObject *code = malloc(sizeof(PyCodeObject) + 2*instruction_count);
指令数量编码在由
ob_size
给出的 PyVarObject
成员中。这就是 Py_SIZE
在指令循环中检索的内容。
因此,访问通过构造是有效的。
1。 出于本答案的目的,我忽略了用于某些指令的内联缓存。