文档:https://docs.python.org/zh-cn/3.12/extending/index.html

常用的python底层是C语言实现,更准确的说是python解释器是用C实现的,在py程序的执行中py解释器充当了一个翻译的角色,翻译py,调用C。

python的优点是写的快,但跑的慢。C写的慢,但跑的快。因此将一些常用的基础能力用C实现,python调用,可以兼顾二者的优点。

以下通过在C文件中自定义+ - * /运算,并基于该文件构建.so链接库文件,在py脚本中对其进行调用,模拟一次完整的实现流程。

一、C文件测试实现

新建一个C文件,文件名随意,测试是calculater.c

#define PY_SSIZE_T_CLEAN
#include <Python.h>

// 加法
static PyObject* calc_add(PyObject* self, PyObject* args) {
    double a, b;
    if (!PyArg_ParseTuple(args, "dd", &a, &b))
        return NULL;
    return Py_BuildValue("d", a + b);
}

// 减法
static PyObject* calc_sub(PyObject* self, PyObject* args) {
    double a, b;
    if (!PyArg_ParseTuple(args, "dd", &a, &b))
        return NULL;
    return Py_BuildValue("d", a - b);
}

// 乘法
static PyObject* calc_mul(PyObject* self, PyObject* args) {
    double a, b;
    if (!PyArg_ParseTuple(args, "dd", &a, &b))
        return NULL;
    return Py_BuildValue("d", a * b);
}

// 除法
static PyObject* calc_div(PyObject* self, PyObject* args) {
    double a, b;
    if (!PyArg_ParseTuple(args, "dd", &a, &b))
        return NULL;
    if (b == 0) {
        PyErr_SetString(PyExc_ZeroDivisionError, "division by zero");
        return NULL;
    }
    return Py_BuildValue("d", a / b);
}

// 方法定义表
static PyMethodDef CalcMethods[] = {
    {"add", calc_add, METH_VARARGS, "Add two numbers"},
    {"sub", calc_sub, METH_VARARGS, "Subtract two numbers"},
    {"mul", calc_mul, METH_VARARGS, "Multiply two numbers"},
    {"div", calc_div, METH_VARARGS, "Divide two numbers"},
    {NULL, NULL, 0, NULL}	// 哨兵 方法表的结束标识
};

// 模块定义
static struct PyModuleDef calcmodule = {
    PyModuleDef_HEAD_INIT,
    "mycalc",       // 模块名
    "Simple calculator module for demo",        // 模块说明文档
    -1,             // 单例模块 只加载一次 全局状态共享
    CalcMethods     // 方法表
};

// 初始化函数 py中第一次 "import module" 时调用
PyMODINIT_FUNC PyInit_mycalc(void) {
    return PyModule_Create(&calcmodule);
}

文件中主要包含:1)具体函数的定义。2)方法表的定义。3)模块定义。4)初始化函数。4个部分。

1.函数定义

要求返回值必须是PyObject*类型。参数PyObject* self固定写法,PyObject* args是用来接收py传递进来的位置参数,所有位置参数都会被打包成一个元组。

传递进来的位置参数用PyArg_ParseTuple函数解析,PyArg_ParseTuple(args, "dd", &a, &b)中,dd是一个格式化字符,表示传递进来的有两个位置参数,都按照C中的double类型解析,并分别赋值给a b。如果可以成功解析返回一个非0值(true),解析失败返回0(false)。

Py_BuildValue("d", a + b)中,表示用Py_BuildValue函数对传递进来的值a+b按照格式化符d(double)类型创建一个PyFloatObject类型的对象,该类型可以看作是通用类型PyObject的一个“子类”,所以最终返回的是一个PyObject*类型的指针。

2.方法表定义

定义所有暴露给python中的方法的数组。例如{"add", calc_add, METH_VARARGS, "Add two numbers"}

第一个值字符串,表示python中可调用的方法名。
第二个值对应C中实现的方法名。
第三个表示参数传递的类型,最常用METH_VARARGS,含义是所有参数都按照位置参数传递,C内部按照元组的形式解析,也可以指定其他参数类型格式。
第四个是方法说明。

最后一行{NULL, NULL, 0, NULL}充当哨兵的作用,是方法表的结束标识。

3.模块定义

一个结构体。每个位置值的含义如下:

第一个值PyModuleDef_HEAD_INIT,一般固定写法。
第二个值是模块名,通过模块的__name__属性可以访问。一般和构建时指定的模块名保持一致。虽然此处名字可以和 import 的模块名不一致,不影响导入使用,但可能会带来开发中的一些问题,强烈不建议!
第三个值一般日常开发中都使用-1,表示这个模块是单例模式,只导出一次且全局共享。
第四个值是模块对应的方法表。

开发中一般只关注上面4个参数值,还有一些其它参数,具体含义用到时再了解。

4.初始化函数

在py中导入模块时解释器会调用这个函数创建模块对象。函数名必须定义为PyInit_模块名的形式,否则导入时会找不到模块。模块名大小写敏感。

二、构建链接库文件

通过setuptools工具构建,在C文件同目录下创建一下py脚本,测试脚本名setup.py

from setuptools import setup, Extension

module = Extension('mycalc', sources=['./Cbuild/calculater.c'])

setup(
    name='mycalc',
    version='1.0',
    description='C extension testing in Python',
    ext_modules=[module]
)

脚本Extension中的第一个参数名就是后面在py中导入的模块名,也是.so文件名。这个名字必须和C文件中的初始化函数PyInit_模块名保持一致。也就是这里如果定义为mycalc,C文件中的初始化函数必须命名为PyInit_mycalc,否则导入模块时会找不到。

setup中的信息是模块的描述说明,在pip show 模块名的时候显示。具体内容不影响模块本身的构建执行,规范性描述说明就好。

当前的目录结构:
在这里插入图片描述

WorkSpace路径下执行命令.venv/bin/python ./Cbuild/setup.py build,会在build目录下生成一个.so文件:
在这里插入图片描述这个文件就是构建好的链接库文件,可直接被python导入调用。build目录下的temp文件夹是构建的中间过程文件,构建成功后可以直接删除。

三、调用

将这个文件拉到Cbuild目录下,并在该目录下创建一个测试文件test.py,导入链接库并调用。

拉到同目录下主要是为了在模块搜索路径中能找到这个链接库,也可以手动将链接库所在路径添加到模块搜索路径。

在这里插入图片描述

在py中调用时是通过import模块中的模块名mycalc,去模块的搜索路径中查找名称对应为mycalc.xxx.so的文件,再通过这个文件中的PyInit_模块名找到并初始化这个模块。

PS

1.链接库文件混用问题

1)构建的链接库文件和构建时的解释器版本强相关! 不同大版本py解释器构建的链接库不支持混用。在linux上用3.9版本的解释器构建了一个链接文件mycalc.cpython-39-x86_64-linux-gnu.so,用3.6版本测试执行时报错找不到模块。小版本一般不影响。

2)不同系统构建的链接库也不支持互相混用,即使是同一个版本的解释器构建。

2.Linux系统构建中可能出现的问题

报错ImportError: libffi.so.5: cannot open shared object file: No such file or directory,找不到这个链接库文件。原来也出现过这种问题(https://blog.csdn.net/atwdy/article/details/141127486),当时是通过重新安装个低版本的python环境解决。本次通过创建软链接的方式解决。

find /usr -name 'libffi.so*'找到这个链接库的文件,当前找到的两个路径:

/usr/lib64/libffi.so.6.0.1
/usr/lib64/libffi.so.6

在构建路径下ln -s /usr/lib64/libffi.so.6 $(pwd)/libffi.so.5创建一个软链接,让libffi.so.5指向libffi.so.6

执行export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH将当前路径临时加到环境变量。

此时再次构建就可以正常,在当前目录下生成build文件夹。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐