CPython C API 扩展与嵌入指南:编写、构建并深入你的第一个原生扩展模块
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
本文基于 CPython 仓库 Extending and Embedding the Python Interpreter 文档编写,系统讲解如何用 C/C++ 扩展 Python 解释器:从零开始编写第一个 C API 扩展模块、配置构建工具(meson-python)、理解模块导出钩子(PyModExport_*)与槽表机制,并深入源码验证 CPython 动态加载扩展模块的底层流程。读完后,你将掌握完整的扩展模块开发流程,并能结合仓库源码理解错误处理约定、符号导出机制与嵌入(Embedding)场景。
1. 文档定位:扩展与嵌入的完整知识地图
CPython 的 C API(Application Programmers Interface)定义了一组函数、宏和变量,提供对 Python 运行时系统绝大部分方面的访问能力。文档开篇明确了三大主题:
- 用 C 或 C++ 编写模块扩展 Python 解释器。这些模块能做的事情和 Python 代码一样——定义函数、对象类型和方法——此外还能与原生库交互,或通过避免解释器开销获得更好的性能;
- 将 Python 解释器嵌入到另一个应用程序中,把 Python 当作扩展语言使用;
- 如何编译和链接扩展模块,使其能在运行时被解释器动态加载(前提是操作系统支持该特性)。
文档假设读者具备 C 与 Python 的基础知识:非正式的 Python 入门见 教程,语言的形式化定义见 语言参考,现有对象类型、函数和模块的完整文档见 库参考,而完整的 Python/C API 描述则单独整理在 C API 文档。
在 C 源码文件中引入 Python API 的方式很简单——包含头文件Python.h。但文档特别强调了一个重要的可移植性提示:
C 扩展接口是 CPython 特有的,扩展模块不能在其他 Python 实现(如 PyPy、Jython)上工作。在很多情况下,可以避免编写 C 扩展以保留可移植性。例如,如果用途只是调用 C 库函数或系统调用,应考虑使用
ctypes模块或 cffi 库,而不是编写自定义 C 代码。这些工具让你在 Python 中编写与 C 代码对接的代码,比编译 C 扩展模块更可移植。
CPython 本身并不附带构建扩展模块的工具,官方推荐使用第三方构建后端(见 C API 工具列表)。文档还指出,教程模块可以作为构建工具的简单测试用例,或作为代码生成器的预期输出样例——这也说明了该文档面向的读者既包括扩展作者,也包括扩展开发工具的作者。
知识地图:文档目录结构
Doc/extending/目录下的文档按学习路径组织:
| 章节文档 | 内容定位 |
|---|---|
| first-extension-module.rst | 入门教程:创建第一个 C API 扩展模块 |
| extending.rst | 中级主题:C API assorted topics(错误与异常等) |
| newtypes_tutorial.rst | 教程:定义新的对象类型 |
| newtypes.rst | 新类型相关进阶话题 |
| building.rst | 构建扩展模块的一般指南 |
| windows.rst | Windows 平台构建指南 |
| embedding.rst | 将 CPython 运行时嵌入更大的应用程序 |
其中,“中级主题”部分(错误与异常、新类型、构建)主要面向那些开发扩展工具本身的人,而非推荐普通用户用它来写扩展;“嵌入”部分则讨论相反的场景——不是创建运行在 Python 解释器内部的主应用程序的扩展,而是把 CPython 运行时嵌入到一个更大的应用里。
2. 前置条件与版本约束
进入教程之前,需要明确环境要求(源自 first-extension-module.rst):
- C 编译器与Python 开发头文件。在 Linux 上,头文件通常在
python3-dev(Debian/Ubuntu 系)或python3-devel(RHEL 系)包中; - 能安装 Python 包的能力。教程使用
pip(pip install),也可以替换为任何能构建并安装基于pyproject.toml项目的工具(如uv pip install);建议在虚拟环境中进行; - 目标系统:教程假设 Unix 类系统(包括 macOS 与 Linux)或 Windows,其他系统可能需要调整部分细节(例如系统命令名);
- 版本注意:本仓库当前版本为 3.16.0a0(见 patchlevel.h),教程使用了CPython 3.15 新增的 API(如模块导出钩子
PyModExport_*)以及C11/C++20 语法。如果要创建兼容更早版本 CPython 的扩展,应查阅对应版本文档——例如 CPython 3.14 及以下要求扩展模块定义PyInit_*初始化函数。
教程选择实现一个名为spam的模块,作为 C 标准库函数system的 Python 接口(spam是 Monty Python 粉丝的最爱食物,这是教程的命名彩蛋):
#include <stdlib.h> int system(const char *command);目标调用形式:
>>> import spam >>> status = spam.system("whoami") User Name >>> status 0文档同时提醒:system这样的 C 标准库函数在 Python 中已经有现成暴露,生产环境中请使用os.system或subprocess.run,而不是自己写的模块。选择whoami作为演示命令,是因为它在 Unix 和 Windows 上同名,方便跨平台演示。
3. 教程实战:从零构建spam模块
3.1 从头部文件开始
创建目录并切换进去,然后创建spammodule.c文件(文件名随意,但传统上扩展模块用*module.c后缀;Python 非主语言的项目可能用py_spam.c之类)。文件开头包含两个头文件:
#include <Python.h> #include <stdlib.h> // for system()关键规则:stdlib.h等标准库头文件必须放在Python.h之后。因为在某些系统上,Python 会定义一些影响标准头文件行为的预处理宏。虽然技术上包含stdlib.h并非必需——Python.h本身就会为自身使用或向后兼容而包含它和若干标准头——但显式包含自己需要的头文件是好习惯。
3.2 配置构建工具(meson-python)
虽然此时扩展还什么都没做,但先编译验证构建工具可用很有价值,方便后续增量开发。教程选用meson-python作为构建后端,它需要两个项目文件。
pyproject.toml:
[build-system] build-backend = 'mesonpy' requires = ['meson-python'] [project] # Placeholder project information # (change this before distributing the module) name = 'sampleproject' version = '0'meson.build:
project('sampleproject', 'c') py = import('python').find_installation(pure: false) py.extension_module( 'spam', # name of the importable Python module 'spammodule.c', # the C source file install: true, )构建并安装当前目录(.)中的项目:
python -m pip -v install .-v(--verbose)选项让 pip 显示编译器输出,开发期间经常有用。两个实用提示:
- 如果系统没有 pip,先运行
python -m ensurepip(最好在虚拟环境中); - 每次修改扩展后都要重新运行安装命令——不像 Python,C 有显式的编译步骤。
3.3 第一次导入:观察报错以验证加载机制
编译安装后启动 Python 尝试导入,此时应该失败并抛出:
>>> import spam Traceback (most recent call last): ... ImportError: dynamic module does not define module export function (PyModExport_spam or PyInit_spam)这个报错本身就是一条宝贵的诊断信息:它证明动态加载机制已经生效,CPython 正在 .so 文件中按符号名查找模块导出函数。这条错误消息正是 CPython 源码 Python/importdl.c 中_PyImport_GetModuleExportHooks()函数生成的:
if (!PyErr_Occurred()) { PyObject *msg; msg = PyUnicode_FromFormat( "dynamic module does not define " "module export function (%s_%s or %s_%s)", info->hook_prefixes->export_prefix, name_buf, info->hook_prefixes->init_prefix, name_buf); ... PyErr_SetImportError(msg, info->name, info->filename); }从源码结构看,CPython 在加载动态模块时优先查找新式导出钩子符号(PyModExport_<name>),找不到再回退到旧式初始化函数符号(PyInit_<name>)。两种前缀常量定义在 Python/importdl.c:
static const struct hook_prefixes ascii_only_prefixes = { "PyInit", "PyModExport"}; static const struct hook_prefixes nonascii_prefixes = { "PyInitU", "PyModExportU"};对非 ASCII 模块名,符号名按 PEP 489 规则使用 Punycode 编码,前缀相应变为PyInitU/PyModExportU。加载流程先尝试 export 前缀(找到则返回 2),再尝试 init 前缀(找到则返回 1),都找不到才抛出上述ImportError(见 Python/importdl.c)。
3.4 定义模块导出钩子
错误信息告诉我们 CPython 在寻找“模块导出函数”(module export function,亦称模块导出钩子)。定义方式分两步。
第一步:添加函数原型(放在#include行下方):
PyMODEXPORT_FUNC PyModExport_spam(void);原型并非严格必需,但某些现代编译器没有它会发出警告——通常添加原型比禁用警告更好。PyMODEXPORT_FUNC宏声明函数的返回类型,并添加使函数在 CPython 加载时可见、可用的特殊链接声明。该宏的定义见 Include/exports.h:
#ifndef PyMODEXPORT_FUNC #define PyMODEXPORT_FUNC _PyINIT_FUNC_DECLSPEC PySlot* #endif也就是说,导出钩子的返回类型是PySlot*(槽表数组)。而_PyINIT_FUNC_DECLSPEC根据平台展开为extern "C"(C++ 时)加上导出符号修饰:在 Windows/Cygwin 下是__declspec(dllexport)(见 Include/exports.h),在其他平台是__attribute__((visibility("default")))。这些宏还负责区分核心模块与扩展模块的符号可见性——扩展模块的导出钩子必须具有外部链接,CPython 才能通过动态符号查找(dlsym等)定位到它。
第二步:实现函数。先让它返回NULL:
PyMODEXPORT_FUNC PyModExport_spam(void) { return NULL; }重新编译并再次导入,会得到不同的错误:
>>> import spam SystemError: module export hook for module 'spam' failed without setting an exception仅返回NULL并不是导出钩子的正确行为,CPython 会抱怨。但这恰恰是好消息——它意味着 CPython 已经找到了你的函数!
3.5 槽表(Slot Table):模块身份的声明
导出钩子应该返回创建模块所需的信息。最基础的信息是模块名和 docstring,它们应定义在一个PySlot条目数组中——本质上是键值对。把数组定义在导出钩子之前:
PyABIInfo_VAR(abi_info); static PySlot spam_slots[] = { PySlot_STATIC_DATA(Py_mod_abi, &abi_info), PySlot_STATIC_DATA(Py_mod_name, "spam"), PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"), PySlot_END };逐条说明:
PySlot_STATIC_DATA宏用于槽值是“指向常量、静态分配数据的指针”的场景(这里分别是&abi_info、"spam"和 docstring)。Py_mod_name与Py_mod_doc的取值都是 C 字符串——NUL 结尾、UTF-8 编码的字节数组;PyABIInfo_VAR(abi_info)宏与Py_mod_abi槽是样板代码(boilerplate),用于防止为不同 Python 版本编译的扩展加载后导致解释器崩溃;PySlot_END是哨兵条目,标记数组结束。忘记它会导致未定义行为;- 数组声明为
static——即在此.c文件外不可见。这是常见主题:CPython 只需要访问导出钩子,所有全局变量和其他函数通常都应该是static的,以免与其他扩展冲突(对比PyMODEXPORT_FUNC展开出的导出可见性修饰——整个.c文件中只有导出钩子需要外部链接)。
让导出钩子返回该数组:
PyMODEXPORT_FUNC PyModExport_spam(void) { return spam_slots; }重新编译测试:
>>> import spam >>> print(spam) <module 'spam' from '/home/encukou/dev/cpython/spam.so'>你已经拥有了一个扩展模块!用help(spam)可以看到 docstring。
3.6 暴露函数:胶水代码与PyMethodDef
要把 C 函数system直接暴露给 Python,需要写一层胶水代码(glue code),把参数从 Python 对象转换成 C 值,再把 C 返回值转回 Python。最简单的方式之一是METH_O函数——接收两个 Python 对象、返回一个对象。所有 Python 对象无论类型,在 C 中都表示为指向PyObject结构的指针。
在槽数组上方添加这样的函数:
static PyObject * spam_system(PyObject *self, PyObject *arg) { Py_RETURN_NONE; }暂时忽略参数,用Py_RETURN_NONE宏返回 Python 的None对象(它展开为一个正确返回None的return语句)。重新编译后可能收到spam_system未使用的警告——这是正常的,因为它还没有被加到模块里。
方法定义表(Method Definitions):要把 C 函数暴露给 Python,需要提供PyMethodDef结构中的若干信息:
ml_name:Python 函数名;ml_doc:docstring;ml_meth:被调用的 C 函数;ml_flags:描述细节的标记集,例如 Python 参数如何传递给 C 函数。这里用METH_O——匹配spam_system函数签名的标记。
(PyMethodDef结构同时用于创建类的方法,因此不存在单独的“PyFunctionDef”。)
由于模块通常要创建多个函数,这些定义要收集在一个数组中,末尾放一个零填充的哨兵:
static PyMethodDef spam_methods[] = { { .ml_name="system", .ml_meth=spam_system, .ml_flags=METH_O, .ml_doc="Execute a shell command.", }, {NULL, NULL, 0, NULL} /* Sentinel */ };然后向槽表添加Py_mod_methods槽,指向该PyMethodDef数组:
static PySlot spam_slots[] = { PySlot_STATIC_DATA(Py_mod_abi, &abi_info), PySlot_STATIC_DATA(Py_mod_name, "spam"), PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"), PySlot_STATIC_DATA(Py_mod_methods, spam_methods), PySlot_END };重新编译、重启 Python 解释器(让import spam拿到新版本模块),测试:
>>> import spam >>> print(spam.system) <built-in function system> >>> print(spam.system('whoami')) None此时spam.system还没有真正执行whoami命令,只是返回None。再验证参数个数检查(由METH_O标记指定恰好一个参数):
>>> print(spam.system('too', 'many', 'arguments')) TypeError: spam.system() takes exactly one argument (3 given)3.7 返回整数:PyLong_FromLong
接下来处理返回值。要让spam.system返回一个数字——Python 的int对象。C API 提供了从 C 的int值创建 Pythonint对象的函数PyLong_FromLong。
(函数名可能不太直观:PyLong指 Python 的int类——它最初叫long;FromLong则指 C 的long(即long int)类型。)
替换Py_RETURN_NONE:
static PyObject * spam_system(PyObject *self, PyObject *arg) { int status = 3; PyObject *result = PyLong_FromLong(status); return result; }重新编译、重启解释器,确认函数现在返回 3:
>>> import spam >>> spam.system('whoami') 33.8 接受字符串参数:PyUnicode_AsUTF8AndSize与错误处理
最后处理函数参数。C 函数spam_system接收两个参数:第一个PyObject *self会被设为spam模块对象(本例无用,忽略);第二个PyObject *arg是用户从 Python 传入的对象,期望是 Python 字符串。
这里存在一个微妙的类型不匹配:Python 的str对象存储 Unicode 文本,而 C 字符串是字节数组。所以需要把数据编码,本例使用 UTF-8。(UTF-8 未必总是适合系统命令,但它是str.encode的默认编码,且 C API 对它有专门支持。)
把 Python 字符串编码为 UTF-8 缓冲区的函数是PyUnicode_AsUTF8AndSize:
static PyObject * spam_system(PyObject *self, PyObject *arg) { const char *command = PyUnicode_AsUTF8AndSize(arg, NULL); int status = 3; PyObject *result = PyLong_FromLong(status); return result; }(名字中PyUnicode指向str类的最初名称unicode;AndSize部分指该函数还能通过输出参数获取缓冲区大小——本例不需要,所以第二个参数传NULL。)
调用成功时,command指向结果 C 字符串——零结尾的字节数组。这个缓冲区由arg对象管理,无需释放,但必须遵守规则:
- 只应在
spam_system函数内部使用该缓冲区。函数返回后,arg及其管理的缓冲区可能已被垃圾回收; - 不得修改它,因此使用
const。
若调用不成功,PyUnicode_AsUTF8AndSize返回NULL。调用任何 Python C API 时都必须处理这类错误情况。本例中正确的处理方式是:spam_system直接返回NULL:
static PyObject * spam_system(PyObject *self, PyObject *arg) { const char *command = PyUnicode_AsUTF8AndSize(arg); if (command == NULL) { return NULL; } int status = 3; PyObject *result = PyLong_FromLong(status); return result; }这个“失败时返回NULL且不重复设置异常”的模式是整个 C API 错误传播约定的缩影(详见 4.1 节)。测试错误处理——传入非字符串值:
>>> import spam >>> spam.system(3) TypeError: bad argument type for built-in operation3.9 最终形态:调用system并返回真实结果
剩下就是把system库函数用char *缓冲区调用起来,并用其结果替换3:
static PyObject * spam_system(PyObject *self, PyObject *arg) { const char *command = PyUnicode_AsUTF8AndSize(arg); if (command == NULL) { return NULL; } int status = system(command); PyObject *result = PyLong_FromLong(status); return result; }编译模块、重启 Python、测试。这次会看到whoami命令的输出——你的用户名:
>>> import spam >>> result = spam.system('whoami') User Name >>> result 0也可以测试其他命令,如ls、dir,或一个不存在的命令:
>>> import spam >>> result = spam.system('nonexistent-command') sh: line 1: nonexistent-command: command not found >>> result 32512完整的spammodule.c源码收录在 Doc/includes/capi-extension/spammodule-01.c(该文件头部注释明确说明需与教程文档保持同步)。一个值得注意的脚注:我们忽略了 Python 字符串可以包含 NUL 字节(会截断 C 字符串)这一事实,即spam.system("foo\0bar")会被当作spam.system("foo")。这可能带来安全问题,所以真正的os.system会检查这种情况并报错。
4. 进阶主题:C API 的核心约定
教程刻意避开了错误处理与引用计数等“重要概念”,它们由 extending.rst(Using the C API: Assorted topics)覆盖。理解这些约定是写出健壮扩展的关键。
4.1 错误与异常(Errors and Exceptions)
Python 解释器中一个重要的约定是:函数失败时,应设置一个异常条件并返回错误值(通常是-1或NULL指针)。异常信息存储于解释器线程状态的三个成员中:异常类型、异常实例、traceback 对象——无异常时它们为NULL,否则等价于sys.exc_info()返回的元组的 C 版本。
常用的设置异常的 API:
PyErr_SetString:最常用的一个,参数是异常对象和一个 C 字符串。异常对象通常是预定义对象(如PyExc_ZeroDivisionError);C 字符串表示错误原因,会被转换成 Python 字符串对象存为异常的“关联值”;PyErr_SetFromErrno:只接收异常参数,通过检查全局变量errno构造关联值;PyErr_SetObject:最通用的,接收两个对象参数——异常与其关联值。传给这些函数的对象不需要Py_INCREF。
错误传播规则(与教程中command == NULL → return NULL的做法呼应):
- 调用另一个函数
g的函数f,在检测到g失败时,f应自己返回错误值(通常NULL或-1),而不应再调用PyErr_*函数——g已经调用了。f的调用者同样应向它的调用者返回错误指示而不调用PyErr_*,如此一路向上传播,直到解释器主循环,那里会中止当前执行的 Python 代码并尝试查找程序员指定的异常处理器; - 唯一需要调用
PyErr_Clear清除异常的场景:你不想把错误交给解释器,而是想完全自己处理(比如重试或假装什么都没发生)。模块可以在某些情况下用另一个PyErr_*函数给出更详细的错误消息,但一般规则是不要这样做,否则会丢失错误原因的信息; - 每个失败的
malloc调用都必须转换为异常——malloc(或realloc)的直接调用者必须调用PyErr_NoMemory并自行返回失败指示(对象创建函数如PyLong_FromLong已经这样做了,所以该注意事项只针对直接调用malloc的代码); - 注意:除
PyArg_ParseTuple及其伙伴外,返回整数状态的函数通常成功返回正值或零、失败返回-1,类似 Unix 系统调用; - 返回错误指示时,务必清理垃圾——对已创建的对象调用
Py_XDECREF或Py_DECREF。
异常类型选择完全由你决定,但应明智选择:所有内置 Python 异常都有对应的预声明 C 对象(如PyExc_ZeroDivisionError)可直接使用。不要用PyExc_TypeError表示“文件打不开”(那应该是PyExc_OSError);参数列表有问题时PyArg_ParseTuple通常抛PyExc_TypeError;参数值超出范围或必须满足其他条件时用PyExc_ValueError合适。也可以为模块定义独有的新异常,最简单的方式是在文件开头声明一个 static 全局对象变量,并在模块初始化时用PyErr_NewException初始化它。
4.2 模块导出机制的源码级印证
教程中的每个报错,都能在 CPython 加载器源码中找到对应逻辑,这一印证帮助我们把“文档行为”上升为“实现事实”:
- 符号查找顺序:
_PyImport_GetModuleExportHooks先用export_prefix(PyModExport)查符号,成功后返回 2;再退回init_prefix(PyInit),成功后返回 1;两者皆无则抛出含两个候选符号名的ImportError。因此旧式PyInit_*模块在新版本上仍可加载,而新式钩子返回的PySlot*槽表才是 3.15+ 的推荐路径; - 非 ASCII 模块名:模块短名(最后一个
.之后的部分)先尝试 ASCII 编码,失败则按 PEP 489 转 Punycode,符号前缀切换为PyInitU/PyModExportU(见 Python/importdl.c); - 钩子返回
NULL的处理:若导出钩子返回NULL且未设置异常,加载器判定为“failed without setting an exception”并抛SystemError——这正是教程中观察到的第二个报错; - 符号可见性:
PyMODEXPORT_FUNC经由 Include/exports.h 展开,保证钩子在所有目标平台上都以外部链接可见(Windows 用__declspec(dllexport),GCC/Clang 平台用visibility("default")),与教程中“其他全局变量和函数都应为static”的告诫形成对照。
5. 其他构建工具与直接编译
教程正文使用 meson-python,但附录提供了替代路径(源自 first-extension-module.rst 的 “Appendix: Other build tools”):
5.1 缺失PyInit函数的临时对策
如果你的构建工具输出抱怨缺少PyInit_spam,可以临时添加:
// A workaround void *PyInit_spam(void) { return NULL; }这是旧式初始化函数(initialization function,CPython 3.14 及以下的扩展模块要求定义)的垫片(shim)。当前 CPython 不需要它,但某些构建工具可能仍然假设所有扩展模块都要定义它。使用这个对策后,你会得到SystemError: initialization of spam failed without raising an exception而不是ImportError: dynamic module does not define module export function。
5.2 直接调用编译器(仅限特定系统自用场景)
使用第三方构建工具被强烈推荐,因为它会处理平台与 Python 安装的诸多细节、生成扩展的命名,以及日后的分发。但如果你只为特定系统或自己构建扩展,也可以直接运行编译器——方式是系统相关的,需自行解决可能出现的问题。
以 Linux 为例,Python 开发包可能附带python3-config命令,可打印所需的编译旗标。使用它时,确认它对应于你要用来加载模块的 CPython 解释器,然后:
gcc --shared $(python3-config --cflags --ldflags) spammodule.c -o spam.so这会生成spam.so文件,需要把它放到sys.path上的某个目录中。
6. 嵌入(Embedding)与后续学习路径
Doc/extending/的最后一个主题方向是反向场景:不是把 C 代码塞进 Python 解释器,而是把 CPython 运行时嵌入到一个更大的应用程序中,让 Python 作为该应用的脚本/扩展语言。embedding.rst 覆盖成功完成此事所需的一些细节。
综合学习路径建议如下:
- 先完成 first-extension-module.rst 教程,跑通
spam模块全流程; - 精读 extending.rst 的错误与异常等中级主题——它们决定扩展在生产环境下的健壮性;
- 需要自定义对象类型时,学习 newtypes_tutorial.rst 与 newtypes.rst;
- 跨平台分发前,参考 building.rst 的一般构建指南与 windows.rst 的 Windows 专项指南;
- 应用形态为宿主程序时,转向 embedding.rst;
- API 细节随时查 Doc/c-api/ 中的完整 C API 文档。
7. 关键 API 速查表
| API | 用途 | 失败行为/要点 |
|---|---|---|
PyMODEXPORT_FUNC | 声明模块导出钩子,返回PySlot* | 定义见 Include/exports.h |
PyModExport_<name>() | 3.15+ 新式模块导出钩子 | 返回NULL且不设异常会触发SystemError |
PyInit_<name>() | 旧式初始化函数(3.14 及以下要求) | 新式钩子存在时优先被查找的是PyModExport_* |
PySlot/PySlot_STATIC_DATA | 模块槽表键值对 | 数组必须以PySlot_END结尾 |
Py_mod_abi+PyABIInfo_VAR | ABI 版本校验样板代码 | 防止版本不匹配的扩展崩溃解释器 |
Py_mod_name/Py_mod_doc | 模块名与 docstring | 取值为 NUL 结尾 UTF-8 C 字符串 |
Py_mod_methods | 挂载PyMethodDef数组 | 数组以{NULL, NULL, 0, NULL}哨兵结尾 |
PyMethodDef | 描述一个绑定方法/函数 | ml_name/ml_doc/ml_meth/ml_flags四要素 |
METH_O | 函数标记 | 恰好接收一个参数,多余参数抛TypeError |
PyLong_FromLong | Clong→ Pythonint | 失败返回NULL并设PyErr_NoMemory等异常 |
PyUnicode_AsUTF8AndSize | str→ UTF-8 只读 C 缓冲区 | 失败返回NULL;缓冲区生命周期绑定源对象,不得修改 |
Py_RETURN_NONE | 返回None的宏 | 展开为正确的return语句 |
PyErr_SetString/PyErr_SetFromErrno/PyErr_SetObject | 设置异常 | 参数对象无需Py_INCREF |
PyErr_Clear | 清除异常 | 仅在模块要自行完全处理错误时调用 |
PyErr_NoMemory | malloc失败时设置内存错误 | malloc/realloc直接调用者必须处理 |
8. 小结
Doc/extending/文档给出了 CPython 扩展开发的完整坐标系:C API 经Python.h暴露运行时能力,但这是 CPython 专属接口、不跨实现可移植;构建交由第三方后端(教程选定 meson-python);3.15+ 起模块通过PyModExport_*导出钩子返回PySlot槽表来声明模块身份与方法(源码验证见Python/importdl.c的查找顺序与错误消息生成逻辑);错误处理遵循“设置一次、逐层返回错误值”的约定。教程中的spam模块(完整源码在 Doc/includes/capi-extension/spammodule-01.c)恰好覆盖了“头文件 → 构建配置 → 导出钩子 → 槽表 → 方法表 → 参数编码 → 返回值转换 → 错误传播”的全链路,是任何构建工具与代码生成器的天然基准测试用例。掌握这条主线后,新类型定义、平台化构建与解释器嵌入便都是在此骨架上的自然延伸。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考