CPython 调用协议完全指南:tp_call、Vectorcall(PEP 590)与 Object Calling API 深度解析 CPython 调用协议完全指南tp_call、VectorcallPEP 590与 Object Calling API 深度解析【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonCPython 的 C API 为扩展模块提供了两套调用call协议经典的tp_call协议与 3.9 年引入的vectorcall协议PEP 590它们决定了 C 扩展如何触发 Python 对象、以及一次调用在底层会经过哪些参数转换。读完本文你将掌握两套协议的工作机制、PY_VECTORCALL_ARGUMENTS_OFFSET位标记的原理、完整的 Object Calling API 函数族PyObject_Call到PyObject_VectorcallMethod的选型依据并能结合 Objects/call.c 源码追踪一次 C 级调用的完整调用链。一、两套调用协议总览CPython 支持两种调用协议tp_call 协议通过PyTypeObject的tp_call槽位实现参数约定为位置参数 tuple 关键字参数 dictvectorcall 协议由 PEP 590 引入参数直接以 C 数组形式传递避免了 tuple/dict 的临时构建开销目标是让调用更高效。经验法则是对内部调用若被调对象支持 vectorcallCPython 会优先使用 vectorcall但这不是硬性规则例如某些第三方扩展会直接使用tp_call而不经过PyObject_Call因此支持 vectorcall 的类必须同时实现语义一致的tp_call。二、tp_call 协议设置了PyTypeObject.tp_call槽的类其实例是可调用对象。该槽的函数签名为PyObject *tp_call(PyObject *callable, PyObject *args, PyObject *kwargs);调用约定与 Python 代码中的callable(*args, **kwargs)一致args是位置参数组成的tuple不允许为 NULL无参数时需传空 tuplekwargs是关键字参数组成的dict无关键字参数时可以为 NULL这一约定不仅被tp_call使用PyTypeObject.tp_new和PyTypeObject.tp_init也以相同方式接收参数。调用对象时使用PyObject_Call或其他 Object Calling API 函数见第五节。tp_call 的底层执行路径在 Objects/call.c 中_PyObject_MakeTpCall()实现了 tp_call 的慢路径/* Slow path: build a temporary tuple for positional arguments and a * temporary dictionary for keyword arguments (if any) */ ternaryfunc call Py_TYPE(callable)-tp_call; if (call NULL) { object_is_not_callable(tstate, callable); return NULL; } PyObject *argstuple PyTuple_FromArray(args, nargs); ... if (_Py_EnterRecursiveCallTstate(tstate, while calling a Python object) 0) { result _PyCFunctionWithKeywords_TrampolineCall( (PyCFunctionWithKeywords)call, callable, argstuple, kwdict); _Py_LeaveRecursiveCallTstate(tstate); }从中可以确认文档的两点关键描述临时对象构建开销慢路径需要PyTuple_FromArray()把位置参数数组包装成临时 tuple若关键字参数是 kwnames 形式还会经_PyStack_AsDict()构建临时 dict——这正是 vectorcall 想要省掉的步骤递归保护自动完成CPython 在调用前后自动包裹_Py_EnterRecursiveCallTstate()/_Py_LeaveRecursiveCallTstate()因此走tp_call的被调方无需自己担心递归深度。另外Objects/call.c 中的object_is_not_callable()还会给出贴心的错误提示当误把模块当函数调用时如pprint(thing)会提示Did you mean: pprint.pprint(...)?。三、Vectorcall 协议PEP 590Vectorcall 协议由 PEP 590 引入3.9 年文档化PyObject_Vectorcall于 3.8 年以_PyObject_Vectorcall临时名出现核心思想是用 C 数组直接传参避免构建 tuple 与 dict。3.1 为什么必须同时实现 tp_call文档以醒目警告强调支持 vectorcall 的类必须同时实现语义一致的PyTypeObject.tp_call。原因是内部调用不保证总走 vectorcallCPython 只在被调对象支持 vectorcall时优先选择它同时仍有第三方扩展直接使用tp_call。推荐做法是把tp_call槽直接指向PyVectorcall_Call这样两条路径行为天然一致。3.12 起还有一个重要变化当类的__call__方法被重新赋值时Py_TPFLAGS_HAVE_VECTORCALL标志会被自动移除因为这种赋值只修改tp_call可能导致两条路径行为不一致。在 3.12 之前的版本中vectorcall 应只用于不可变Py_TPFLAGS_IMMUTABLETYPE或静态类型。另一个实用建议如果实现 vectorcall 反而更慢例如被调方无论如何都要把参数转回 args tuple 和 kwargs dict就不该实现它。3.2 启用方式与 vectorcallfunc 签名类通过两步启用 vectorcall打开Py_TPFLAGS_HAVE_VECTORCALL类型标志把PyTypeObject.tp_vectorcall_offset设置为对象结构体中vectorcallfunc指针字段的字节偏移。函数指针类型为PyObject *(*vectorcallfunc)(PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwnames);各参数含义参数说明callable被调用的对象本身argsC 数组先是位置参数再是关键字参数的值无参数时可以为NULLnargsf位置参数个数可能叠加PY_VECTORCALL_ARGUMENTS_OFFSET标志位用PyVectorcall_NARGS()提取真实个数kwnames关键字参数名组成的 tuple即 kwargs dict 的键元素必须是str或其子类且互不重复无关键字参数时可为NULL注意一个关键细节nargsf只计算位置参数个数不包含关键字参数。从 Include/internal/pycore_call.h 的内联函数_PyObject_VectorcallTstate()注释可见关键字参数的值就存放在args数组中位置参数之后的位置但不计入nargsf。3.3 PY_VECTORCALL_ARGUMENTS_OFFSET 标志在 Include/abstract.h 中该标志被定义为size_t的最高位#define PY_VECTORCALL_ARGUMENTS_OFFSET \ (_Py_STATIC_CAST(size_t, 1) (8 * sizeof(size_t) - 1))选择最高位的用意正是为了不与任何参数个数冲突参数个数不可能触及符号位。其语义分两种场景普通 vectorcall 调用若nargsf带此标志被调方被允许临时修改args[-1]——即args实际指向参数 1而非参数 0调用方在数组前多预留了一个槽位。被调方返回前必须恢复args[-1]的原值。PyObject_VectorcallMethod调用此标志含义变为允许临时修改args[0]因为args[0]是方法所在的对象见第五节。文档还给出了明确的使用建议调用方在能廉价做到不需要额外堆分配时就应使用PY_VECTORCALL_ARGUMENTS_OFFSET。这样做的直接收益是像绑定方法这类需要在后续调用中前置插入self参数的可调用对象可以零拷贝地完成续传。这一点在 Objects/call.c 的PyObject_VectorcallMethod()实现中体现得非常清楚——它对三种情形分别处理if (self_obj NULL) { /* Skip self. We can keep PY_VECTORCALL_ARGUMENTS_OFFSET since * args[-1] in the onward call is args[0] here. */ result _PyObject_VectorcallTstate(tstate, callable, args 1, nargsf - 1, kwnames); } else if (self_obj args[0]) { /* 去掉 OFFSET 标志因为 args[-1] 现在不可被修改 */ result _PyObject_VectorcallTstate(tstate, callable, args, nargsf ~PY_VECTORCALL_ARGUMENTS_OFFSET, kwnames); } else { /* classmethodself_obj 是类型而非 args[0]需要 prepend 后调用 */ result _PyObject_VectorcallPrepend(tstate, callable, self_obj, args 1, nargsf - 1, kwnames); }第三个分支用到的_PyObject_VectorcallPrepend()Objects/call.c同样展示了标志位的双刃剑当PY_VECTORCALL_ARGUMENTS_OFFSET已设置时它可以直接把args - 1处的槽位改写为arg再恢复完全避免拷贝否则需要借助栈上小数组或PyMem_Malloc复制整个参数向量。3.4 调用与快慢路径选择调用支持 vectorcall 的对象与普通可调用对象一样使用 Object Calling API 即可PyObject_Vectorcall通常是效率最高的选择。其内部实现_PyObject_VectorcallTstate()Include/internal/pycore_call.h清晰地展示了优先 vectorcall、回退 tp_call的选择逻辑static inline PyObject * _ObjectO_VectorcallTstate(PyThreadState *tstate, PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwnames) { vectorcallfunc func _PyVectorcall_FunctionInline(callable); if (func NULL) { Py_ssize_t nargs PyVectorcall_NARGS(nargsf); return _PyObject_MakeTpCall(tstate, callable, args, nargs, kwnames); } res func(callable, args, nargsf, kwnames); return _Py_CheckFunctionResult(tstate, callable, res, NULL); }而_PyVectorcall_FunctionInline()Include/internal/pycore_call.h的查找方式是先检查类型是否带Py_TPFLAGS_HAVE_VECTORCALL标志再用memcpy按tp_vectorcall_offset从对象内存中取出函数指针——这解释了为何 offset 是对象结构体中的字节偏移而非函数表索引。四、递归控制两套协议的关键差异这是编写 vectorcall 实现时最容易踩的坑tp_call 调用被调方不需要关心递归CPython 已通过Py_EnterRecursiveCall/Py_LeaveRecursiveCall包裹参见上文_PyObject_MakeTpCall()源码vectorcall 调用出于性能考虑CPython不会替你检查递归深度被调方如有需要必须自己调用Py_EnterRecursiveCall/Py_LeaveRecursiveCall。五、Vectorcall 支持 API5.1 PyVectorcall_NARGSPy_ssize_t PyVectorcall_NARGS(size_t nargsf);从nargsf中提取真实位置参数个数。当前实现等价于(Py_ssize_t)(nargsf ~PY_VECTORCALL_ARGUMENTS_OFFSET)在 Include/cpython/abstract.h 中它以静态内联函数_PyVectorcall_NARGS()提供而在 Objects/call.c 末尾它又被#undef后重新导出为函数——因为 stable ABI 需要它作为导出符号。文档提醒应始终使用PyVectorcall_NARGS而非手写掩码运算以便未来扩展。5.2 PyVectorcall_Functionvectorcallfunc PyVectorcall_Function(PyObject *op);若op不支持 vectorcall类型不支持或该实例不支持返回NULL否则返回存放在op中的 vectorcall 函数指针该函数永不抛异常。最常用的用途是能力探测if (PyVectorcall_Function(op) ! NULL) { /* op 支持 vectorcall */ }其内部实现PyVectorcall_Function()Objects/call.c只是一行转发到_PyVectorcall_FunctionInline()。5.3 PyVectorcall_CallPyObject *PyVectorcall_Call(PyObject *callable, PyObject *tuple, PyObject *dict);用 tuple dict 形式的参数直接调用callable的vectorcallfunc。这是一个专用函数设计目的是放进tp_call槽位或供tp_call的实现使用。两个重要限制Objects/call.c它不检查Py_TPFLAGS_HAVE_VECTORCALL标志它不会回退到tp_call对象若不支持 vectorcalloffset ≤ 0 或槽位函数指针为 NULL直接抛TypeError: ... object does not support vectorcall。其快慢路径逻辑在_PyVectorcall_Call()Objects/call.c中无关键字参数时直接复用 tuple 的内部指针数组func(callable, _PyTuple_ITEMS(tuple), nargs, NULL)零分配有关键字参数时经_PyStack_UnpackDict()展开并带上PY_VECTORCALL_ARGUMENTS_OFFSET调用后释放。六、Object Calling API 函数族CPython 提供了一系列调用函数每个函数都把参数转换成被调对象支持的约定tp_call 或 vectorcall。选型原则文档说得很直白用哪种取决于你手头数据的形态尽量做最少的转换。6.1 函数速查表函数callableargskwargsPyObject_CallPyObject *tupledict/NULLPyObject_CallNoArgsPyObject *——PyObject_CallOneArgPyObject *1 个对象—PyObject_CallObjectPyObject *tuple/NULL—PyObject_CallFunctionPyObject *格式串—PyObject_CallMethod对象 char *格式串—PyObject_CallFunctionObjArgsPyObject *可变参PyObject *—PyObject_CallMethodObjArgs对象 名称对象可变参PyObject *—PyObject_CallMethodNoArgs对象 名称对象——PyObject_CallMethodOneArg对象 名称对象1 个对象—PyObject_VectorcallPyObject *vectorcall 约定vectorcall 约定PyObject_VectorcallDictPyObject *vectorcall 约定dict/NULLPyObject_VectorcallMethod名称 args[0]vectorcall 约定vectorcall 约定6.2 基础调用函数PyObject_Call(PyObject *callable, PyObject *args, PyObject *kwargs)—— 等价于callable(*args, **kwargs)。args必须是 tuple 且不得为 NULL无参数传空 tuplekwargs无时可为 NULL。实现_PyObject_Call()Objects/call.c先尝试 vectorcall 快路径失败才回退 tp_call。PyObject_CallNoArgs(PyObject *callable)3.9—— 无参调用是无参数调用可调用对象的最高效方式。实现极简Objects/call.cPyObject * PyObject_CallNoArgs(PyObject *func) { EVAL_CALL_STAT_INC_IF_FUNCTION(EVAL_CALL_API, func); PyThreadState *tstate _PyThreadState_GET(); return _PyObject_VectorcallTstate(tstate, func, NULL, 0, NULL); }PyObject_CallOneArg(PyObject *callable, PyObject *arg)3.9—— 恰好 1 个位置参数。其实现展示了PY_VECTORCALL_ARGUMENTS_OFFSET的教科书式用法Objects/call.cPyObject *_args[2]; PyObject **args _args 1; // For PY_VECTORCALL_ARGUMENTS_OFFSET args[0] arg; size_t nargsf 1 | PY_VECTORCALL_ARGUMENTS_OFFSET; return _PyObject_VectorcallTstate(tstate, func, args, nargsf, NULL);只分配了栈上 2 个指针的数组第一个槽留给被调方可能的改写不触碰堆。PyObject_CallObject(PyObject *callable, PyObject *args)—— 等价于callable(*args)无参数时args可为 NULL此时内部直接走无参快路径Objects/call.c。PyObject_CallFunction(PyObject *callable, const char *format, ...)—— 用Py_BuildValue风格的格式串描述变参 C 参数format可为 NULL 表示无参数。等价于callable(*args)。注意文档提示若参数都是现成的PyObject *用PyObject_CallFunctionObjArgs更快。实现_PyObject_CallFunctionVa()Objects/call.c中还有个历史兼容细节PyObject_CallFunction(func, O, tuple)会退化为func(*tuple)。PyObject_CallMethod(PyObject *obj, const char *name, const char *format, ...)—— 调用obj上名为name的方法格式串同样需产出 tuple等价于obj.name(arg1, arg2, ...)。从源码看Objects/call.c其做法是PyObject_GetAttrString()取属性后复用_PyObject_CallFunctionVa()并先经PyCallable_Check()校验可调用性。PyObject_CallFunctionObjArgs(PyObject *callable, ...)—— 传变长PyObject *参数、以NULL结尾等价于callable(arg1, arg2, ...)。PyObject_CallMethodObjArgs(PyObject *obj, PyObject *name, ...)—— 方法名以 Python 字符串对象给出参数同样以NULL结尾的PyObject *变参列表给出。实现走_PyObject_GetMethodStackRef()object_vacall()Objects/call.c。PyObject_CallMethodNoArgs(PyObject *obj, PyObject *name)/PyObject_CallMethodOneArg(PyObject *obj, PyObject *name, PyObject *arg)均 3.9—— 分别是无参方法与单参方法调用。这些函数在成功时返回结果失败时置异常并返回NULL调用方必须检查。6.3 Vectorcall 系列PyObject_Vectorcall(PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwnames)—— 参数约定与vectorcallfunc完全相同若被调对象支持 vectorcall 则直接调用其 vectorcall 函数否则回退 tp_call。3.8 年以临时名_PyObject_Vectorcall出现3.9 年更名旧名已被软弃用。PyObject_VectorcallDict(PyObject *callable, PyObject *const *args, size_t nargsf, PyObject *kwdict)3.9—— 位置参数用 vectorcall 约定args数组只含位置参数关键字参数则以dict传入。由于无论内部走哪条协议都要做一次参数转换文档给出明确使用场景当你手头已有一个现成的 kwargs dict、却没有位置参数 tuple 时才用它。实现_PyObject_VectorcallDictTstate()Objects/call.c中若被调方支持 vectorcall会经_PyStack_UnpackDict()把 dict 展开成 kwnames 形式后再调用否则直接交给_PyObject_MakeTpCall()。PyObject_VectorcallMethod(PyObject *name, PyObject *const *args, size_t nargsf, PyObject *kwnames)3.9—— 用 vectorcall 约定调用方法name是方法名的 Python 字符串被调对象是args[0]args[1]起才是真正的调用参数至少要有 1 个位置参数nargsf包含args[0]若允许临时改写args[0]则叠加PY_VECTORCALL_ARGUMENTS_OFFSET。若对象具备Py_TPFLAGS_METHOD_DESCRIPTOR特性则以完整args向量调用未绑定方法对象。6.4 类型系统对 vectorcall 的继承规则除了文档正文Objects/typeobject.c 还揭示了子类型创建时的继承规则可帮助理解标志的生命周期/* Always inherit tp_vectorcall_offset to support PyVectorcall_Call(). * If Py_TPFLAGS_HAVE_VECTORCALL is not inherited, then vectorcall * wont be used automatically. */ COPYSLOT(tp_vectorcall_offset); /* Inherit Py_TPFLAGS_HAVE_VECTORCALL if tp_call is not overridden */ if (!type-tp_call _PyType_HasFeature(base, Py_TPFLAGS_HAVE_VECTORCALL)) { type_add_flags(type, Py_TPFLAGS_HAVE_VECTORCALL); } COPYSLOT(tp_call);即tp_vectorcall_offset总是从基类继承保证PyVectorcall_Call()始终可用但Py_TPFLAGS_HAVE_VECTORCALL只在子类未覆写tp_call时才继承——这与 3.12 版本变化覆写__call__即摘除标志在语义上完全呼应。七、Call Support APIPyCallable_Checkint PyCallable_Check(PyObject *o);判断对象是否可调用可调用返回1否则返回0永远成功。从 Objects/object.c 看它的判据只有一条int PyCallable_Check(PyObject *x) { if (x NULL) return 0; return Py_TYPE(x)-tp_call ! NULL; }即检查类型是否有tp_call槽——这也从侧面印证了文档开头那句要求支持 vectorcall 的类必须实现tp_call否则连PyCallable_Check都不会认它是可调用对象。八、实践要点小结扩展作者实现可调用类型tp_call必填PyCallable_Check依赖它tp_call指向PyVectorcall_Call、tp_vectorcall_offset指向实例中的vectorcallfunc字段、类型开启Py_TPFLAGS_HAVE_VECTORCALL是推荐的最小一致组合vectorcall 被调方需自行管理Py_EnterRecursiveCall/Py_LeaveRecursiveCall且返回前必须恢复被改写的前置槽位args[-1]或args[0]调用方选型无参数用PyObject_CallNoArgs单参数用PyObject_CallOneArg现成PyObject *列表用...ObjArgs系列格式串参数用PyObject_CallFunction/CallMethod已持有 vectorcall 形态数据时用PyObject_Vectorcall系列且尽量在零分配可达时带上PY_VECTORCALL_ARGUMENTS_OFFSET版本前提vectorcall 相关公共 API 需要 3.9PY_VECTORCALL_ARGUMENTS_OFFSET自 3.8 引入PyObject_Vectorcall3.8 时为_PyObject_Vectorcall临时名PyObject_Vectorcall/PyObject_VectorcallMethod在 limited API 下需要Py_LIMITED_API 0x030C0000见 Include/abstract.h 的条件编译。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考