Python C扩展模块实例:spam

本文介绍了一个完整的Python C扩展模块实现,包含可执行的源码和详细解释。这个模块名为spam,提供了一个system()函数来执行系统命令,并自定义了异常处理。

这个示例展示了Python C扩展的基本结构和工作原理,包括参数解析、异常处理、模块初始化和系统调用集成等关键概念。文件spammodule.c,如下所示:

#define PY_SSIZE_T_CLEAN
#include<Python.h>
#include<stdlib.h>    /* system()、exit() */
#include<stdio.h>     /* printf(), fprintf() */

/* 全局异常对象指针 */
static PyObject *SpamError = NULL;


/*
 * 模块级函数:spam.system(command)
 * 接受一个C字符串参数,调用system(command),如果system()出错则抛出SpamError,否则返回其退出状态码(整数)
 */

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
constchar *command;
int sts;

// 1.从Python参数中解析出一个字符串
if (!PyArg_ParseTuple(args, "s", &command))
returnNULL;

// 2.调用C标准库的system()
    sts = system(command);
if (sts < 0) {
// 如果system()返回<0,认为执行失败,抛出自定义异常
        PyErr_SetString(SpamError, "System command failed");
returnNULL;
    }

// 3.将整型sts转换为Python整数(PyLong)并返回
return PyLong_FromLong(sts);
}


/*
 * 模块执行时的初始化函数(Module Exec Slot)
 * 在这里创建自定义异常,并把它添加到模块对象中(名字为SpamError)
 */

staticint
spam_module_exec(PyObject *m)
{
// 创建一个新的异常类spam.error,继承自Exception
    SpamError = PyErr_NewException("spam.error"NULLNULL);
if (SpamError == NULL)
return-1;

// 将异常对象添加到模块m中,名称为SpamError
if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
        Py_DECREF(SpamError);  // 失败时减少引用计数
        SpamError = NULL;      // 防止悬空指针
return-1;
    }

return0;
}

/* 模块清理函数,当模块被卸载时调用 */
staticvoidspam_module_free(void *m){
    Py_CLEAR(SpamError);  // 安全地减少引用计数并置为NULL
}

/* 模块方法表:将Python名称映射到C函数 */
static PyMethodDef spam_methods[] = {
        {
"system",                      // Python端调用名
                (PyCFunction) spam_system,     // 对应的C函数指针
                METH_VARARGS,                  // 接受位置参数 (tuple)
"Execute a shell command and return its exit status."// docstring
            },
        {NULLNULL0NULL}            // 末尾哨兵
};

/* 定义Module Execution Slot:导入时执行spam_module_exec() */
static PyModuleDef_Slot spam_module_slots[] = {
        {Py_mod_exec, spam_module_exec},
        {0NULL}
};

/* 定义模块对象结构 */
staticstructPyModuleDefspam_module = {
        PyModuleDef_HEAD_INIT,
        .m_name    = "spam",                 /* 模块名,必须与下面 PyInit_spam 保持一致 */
        .m_doc     = "Example module that wraps system().",
        .m_size    = 0,                      /* 0 表示不为 sub-interpreters 分配独立状态 */
        .m_methods = spam_methods,           /* 方法表 */
        .m_slots   = spam_module_slots,      /* 执行槽,用于初始化自定义异常 */
        .m_free    = spam_module_free        // 添加清理函数
};

/* 模块初始化函数:Python调用此函数来创建模块对象 */
PyMODINIT_FUNC PyInit_spam(void){
return PyModuleDef_Init(&spam_module);
}

一.源码解释

1.头文件和全局变量

#define PY_SSIZE_T_CLEAN
#include<Python.h>
#include<stdlib.h>    /* system()、exit() */
#include<stdio.h>     /* printf(), fprintf() */

/* 全局异常对象指针 */
static PyObject *SpamError = NULL;
  • PY_SSIZE_T_CLEAN:确保在API中使用Py_ssize_t类型代替int

  • Python.h:Python C API的核心头文件

  • stdlib.h:提供system()函数声明

  • stdio.h:可以使用如printf()fprintf()这样的标准输入输出函数

  • SpamError:自定义异常类的全局指针

2.核心函数实现

/*
 * 模块级函数:spam.system(command)
 * 接受一个C字符串参数,调用system(command),如果system()出错则抛出SpamError,否则返回其退出状态码(整数)
 */

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
constchar *command;
int sts;

// 1.从Python参数中解析出一个字符串
if (!PyArg_ParseTuple(args, "s", &command))
returnNULL;

// 2.调用C标准库的system()
    sts = system(command);
if (sts < 0) {
// 如果system()返回<0,认为执行失败,抛出自定义异常
        PyErr_SetString(SpamError, "System command failed");
returnNULL;
    }

// 3.将整型sts转换为Python整数(PyLong)并返回
return PyLong_FromLong(sts);
}
  • 参数解析:PyArg_ParseTuple(args, "s", &command)将Python元组参数转换为C字符串

  • 系统调用:system(command)执行传入的命令

  • 错误处理:命令执行失败时设置自定义异常

  • 返回值转换:PyLong_FromLong()将C整型转换为Python对象

3.模块方法表

/* 模块方法表:将Python名称映射到C函数 */
static PyMethodDef spam_methods[] = {
        {
"system",                      // Python端调用名
                (PyCFunction) spam_system,     // 对应的C函数指针
                METH_VARARGS,                  // 接受位置参数 (tuple)
"Execute a shell command and return its exit status."// docstring
        },
        {NULLNULL0NULL}            // 末尾哨兵
};

spam_methods 是一个 PyMethodDef 结构体数组,用于定义 Python 模块中的方法。每个元素描述一个可被 Python 调用的 C 函数,包括:

  • "system":Python 端调用的函数名(如 spam.system)。

  • (PyCFunction) spam_system:对应的 C 实现函数指针。

  • METH_VARARGS:表示该函数接受参数元组(即位置参数)。

  • "Execute a shell command and return its exit status.":该方法的文档字符串。

数组最后一项 {NULL, NULL, 0, NULL} 是哨兵,用于标记方法表结束。这个表会在模块初始化时注册到 Python 解释器,使 Python 代码可以调用这些 C 函数。

4.模块初始化

该代码定义了模块初始化时的执行函数 spam_module_exec,用于在 Python C 扩展模块加载时创建自定义异常类型,并将其添加到模块对象中,供 Python 端使用。

/*
 * 模块执行时的初始化函数(Module Exec Slot)
 * 在这里创建自定义异常,并把它添加到模块对象中(名字为SpamError)
 */

staticint
spam_module_exec(PyObject *m)
{
// 创建一个新的异常类spam.error,继承自Exception
    SpamError = PyErr_NewException("spam.error"NULLNULL);
if (SpamError == NULL)
return-1;

// 将异常对象添加到模块m中,名称为SpamError
if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
        Py_DECREF(SpamError);  // 失败时减少引用计数
        SpamError = NULL;      // 防止悬空指针
return-1;
    }

return0;
}

主要流程,如下所示:

  • 通过 PyErr_NewException("spam.error", NULL, NULL) 创建一个新的异常类型 spam.error,并赋值给全局变量 SpamError。检查异常对象是否创建成功,失败则返回 -1。

  • 使用 PyModule_AddObjectRef(m, "SpamError", SpamError) 将异常对象添加到模块中,名称为 SpamError,这样 Python 端可以通过 spam.SpamError 访问。

  • 如果添加失败,减少引用计数并清空指针,返回 -1。一切正常则返回 0,表示初始化成功。

这样做的目的是让模块有一个自定义异常类型,便于在 C 代码中抛出特定异常,Python 端可以捕获和处理。

5.模块定义结构

当 Python 导入 spam 模块时,会根据这些结构完成模块的注册、初始化自定义异常、方法绑定等工作。

static PyModuleDef_Slot spam_module_slots[] = {
    {Py_mod_exec, spam_module_exec},
    {0NULL}
};

staticstructPyModuleDefspam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_size = 0,
    .m_methods = spam_methods,
    .m_slots = spam_module_slots,
};

PyMODINIT_FUNC PyInit_spam(void){
return PyModuleDef_Init(&spam_module);
}

这段代码定义了 Python C 扩展模块的核心结构和初始化流程:

  • spam_module_slots:模块执行槽数组,指定模块导入时要执行的函数(这里是 spam_module_exec,用于初始化自定义异常)。

  • spam_module:模块定义结构体,包含模块名、文档字符串、方法表、执行槽、清理函数等信息。

  • PyInit_spam:模块初始化函数,Python 导入模块时会调用,返回模块对象。

二.编译和使用

1.编译步骤

创建setup.py文件:

from distutils.core import setup, Extension

module = Extension(
'spam',
    sources=['spammodule.c']
)

setup(
    name='spam',
    version='1.0',
    description='Python C Extension Example',
    ext_modules=[module]
)

编译模块命令,如下所示:

python setup.py build

安装模块命令,如下所示:

python setup.py install

2.使用示例

import spam

# 成功执行命令
status = spam.system("ls -l")
print(f"Command exited with status: {status}")

# 测试自定义异常
try:
# 尝试执行不存在的命令
    spam.system("nonexistent_command")
except spam.error as e:
    print(f"Caught custom exception: {e}")

预期输出,如下所示:

Command exited with status: 0
Caught custom exception: System command failed

三.main()方法解释

1.main()方法

该段代码实现了一个既能作为 Python 扩展模块、又能作为可执行程序(嵌入 Python 解释器)的 C 程序。

/*
 * 以下代码使得spammodule.c既能编译为Python扩展,也能编译成一个可执行程序(嵌入Python)
 * int main(...) 演示了如何:
 *   1) 在启动Python解释器之前,把spam模块加入内置模块表(PyImport_AppendInittab)
 *   2) 初始化嵌入式Python解释器
 *   3) 导入spammodule,然后调用spam.system()做示例演示
 */

intmain(int argc, char *argv[]){
    PyStatus status;
    PyConfig config;
int exit_code = 0;  // 添加返回码变量

// 初始化Python配置(Python3.8及以上)
    PyConfig_InitPythonConfig(&config);

/*
     * 在初始化解释器之前,将spam模块注册到Python的内置模块表中
     * 这样PyInit_spam()就会被Python运行时调用,创建并注册spam模块
     */

if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
fprintf(stderr"Error: could not extend in-built modules table\n");
exit(1);
    }

// 将argv[0]作为Python解释器的program_name
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
if (PyStatus_Exception(status)) {
        PyConfig_Clear(&config);
return1;
    }

// 初始化Python解释器
    status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
        PyConfig_Clear(&config);
return1;
    }
// 清除config(防止后面重复使用)
    PyConfig_Clear(&config);

/*
     * 到此为止,Python解释器已启动,并且spam模块以内置方式可用。可以:
     *   import spam
     *   spam.system("…")
     */

    PyObject *pmodule = PyImport_ImportModule("spam");
if (!pmodule) {
        PyErr_Print();
fprintf(stderr"Error: could not import module 'spam'\n");
        exit_code = 1;  // 设置错误退出码
    } else {
// 调用spam.system("echo Hello from embedded spam!")
        PyObject *result = PyObject_CallMethod(pmodule,
"system",
"s",
"echo Hello from embedded spam!");
if (result) {
long sts = PyLong_AsLong(result);
printf(">>> spam.system returned: %ld\n", sts);
            Py_DECREF(result);
        } else {
            PyErr_Print();
            exit_code = 1;  // 设置错误退出码
        }
        Py_DECREF(pmodule);
    }

/* 关闭并清理 Python 解释器 */
    Py_Finalize();
return exit_code;  // 返回适当的退出码
}

其主要执行流程如下:

  • 嵌入 Python 解释器:通过 main 函数,程序在 C 进程中初始化 Python 解释器。

  • 注册自定义模块:在解释器初始化前,调用 PyImport_AppendInittab 把自定义的 spam 模块注册到内置模块表,这样 Python 代码可以直接 import spam

  • 配置解释器参数:用 PyConfig 结构体设置解释器参数(如程序名)。

  • 初始化解释器:调用 Py_InitializeFromConfig 启动 Python 解释器。

  • 导入并调用模块方法:用 PyImport_ImportModule 导入 spam 模块,然后用 PyObject_CallMethod 调用 spam.system("echo Hello from embedded spam!"),并打印返回值。

  • 错误处理:如果模块导入或方法调用失败,会打印 Python 错误信息并设置退出码。

  • 清理资源:最后调用 Py_Finalize 关闭 Python 解释器,返回退出码。

2.CMakeLists.txt文件

CMakeLists.txt文件,如下所示:

cmake_minimum_required(VERSION 3.25)
project(CPythonExample C)

set(CMAKE_C_STANDARD 11)

## include python headers files
#include_directories(
#        /usr/local/opt/python-3.14.0b1/include/python3.14
#        /usr/local/opt/python-3.14.0b1/include/python3.14/internal
#        /usr/local/opt/python-3.14.0b1/include/python3.14/cpython
#)
## Link python library
#link_directories(/usr/local/opt/python-3.14.0b1/lib)


# include python headers files
include_directories(
        /mnt/l/20200707_Python/PythonSource/cpython
        /mnt/l/20200707_Python/PythonSource/cpython/Include
        /mnt/l/20200707_Python/PythonSource/cpython/Include/internal
        /mnt/l/20200707_Python/PythonSource/cpython/Include/cpython
)
# Link python library
link_directories(/mnt/l/20200707_Python/PythonSource/cpython)


add_executable(CPythonExample 0002-spam/spammodule.c
        Python/pythonrun.c
        Python/pylifecycle.c
        Python/ceval.c
        Python/pyarena.c
        0002-spam/spammodule.c)

# Define Py_BUILD_CORE
target_compile_definitions(CPythonExample PRIVATE Py_BUILD_CORE)

# link python library
target_link_libraries(CPythonExample python3.14 m)

3.输出结果

四.相关技术点

1.PyConfig_InitPythonConfig()

(1)PyConfig_InitPythonConfig(&config)

PyConfig config;
PyConfig_InitPythonConfig(&config);

这行代码用于初始化 PyConfig 结构体,为后续用 Py_InitializeFromConfig 以”配置驱动”方式启动 Python 解释器做准备。它会将 config 填充为默认值,确保后续对配置项的修改是安全的。

(2)和Py_Initialize()区别

  • Py_Initialize() 是传统的初始化方式,使用全局默认配置,不能灵活定制启动参数。

  • Py_InitializeFromConfig()(配合 PyConfig_InitPythonConfig)允许详细配置解释器启动参数(如模块路径、环境变量、I/O重定向等),适合嵌入式和高级场景。

总结:

  • Py_Initialize() 简单直接,适合大多数普通用例;

  • PyConfig_InitPythonConfig + Py_InitializeFromConfig() 提供更细粒度的控制,推荐用于嵌入式或需要自定义启动流程的场景。

2.PyConfig_SetBytesString()

status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
  • 作用是将 C 程序 argv[0](即程序自身的路径或名称)设置为 Python 解释器的 program_name 配置项。这样做可以让嵌入式 Python 解释器知道当前程序的名称,有助于日志、错误信息和某些模块的行为一致性。

  • PyConfig_SetBytesString 是 Python C API(3.8+)用于安全设置配置字符串的函数,status 用于接收可能的错误状态。

3.PyConfig_Clear()

// 将argv[0]作为Python解释器的program_name
status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
if (PyStatus_Exception(status)) {
    PyConfig_Clear(&config);
return1;
}

在使用完 PyConfig 结构体后,调用 PyConfig_Clear 来释放其内部动态分配的资源,防止内存泄漏。

在本例中,如果 PyConfig_SetBytesString 失败(即 PyStatus_Exception(status) 为真),就会清理 config,然后返回 1 退出。这样可以保证即使初始化失败也不会遗留内存资源。

4.Py_InitializeFromConfig()

status = Py_InitializeFromConfig(&config);

用于根据 config 结构体中的配置参数初始化嵌入式 Python 解释器。这样可以灵活地控制解释器的启动方式(如程序名、环境变量、模块路径等),适用于 Python 3.8 及以上版本。初始化后,C 程序就能运行 Python 代码和模块。返回值 status 用于判断初始化是否成功。

5.PyErr_Print()

当 Python C API 调用(如 PyObject_CallMethod)失败时,当前的 Python 异常会被打印到标准错误输出(stderr)。这有助于调试,能显示出错的异常类型、信息和 traceback。

在本例中,如果 spam.system 调用失败,PyErr_Print(); 会输出详细的 Python 错误信息,方便定位问题。

参考文献

[0] Python C扩展模块实例:spam:https://z0yrmerhgi8.feishu.cn/wiki/OfmZw6g0LiGKmDkeBumcAZycn8e

[1] 使用C或C++扩展Python:https://docs.python.org/zh-cn/3.13/extending/extending.html

[2] Python初始化配置:https://docs.python.org/zh-cn/3.14/c-api/init_config.html#


知识星球服务内容:Dify源码剖析及答疑,Dify对话系统源码,NLP电子书籍报告下载,公众号所有付费资料。加微信buxingtianxia21进NLP工程化资料群

(文:NLP工程化)

发表评论