基于SWIG实现Python模块:C函数eval的列表输入输出适配
用SWIG封装C eval函数:搞定Python输入输出适配
我之前做数值计算模块的时候刚好遇到过几乎一模一样的需求,给你一步步拆解怎么把C的void eval(double *x, int nx, int mx, double *f, int func_id)封装成Python里好用的value_list = module.eval(point_matrix, func_id)接口:
先明确参数对应关系
不用记那些C里的指针参数,先把Python和C的参数对应搞明白:
- Python的
point_matrix:就是你要计算的mx个nx维点,对应C里的x数组,同时我们可以从它里面自动算出nx(每个点的维度)和mx(点的数量),不用用户手动传这俩参数 - Python的
func_id:直接传给C的同名参数,用来指定要调用的基准函数 - C里的
f数组:存储每个点的计算结果,我们要把它转换成Python列表value_list返回给用户
编写SWIG接口文件(核心是typemap)
SWIG的typemap是做这种参数适配的神器,不用手动写胶水代码,直接在接口文件里定义转换规则就行。比如新建一个func_eval.i文件:
%module func_eval %{ #include "your_c_funcs.h" // 替换成你的C函数头文件路径 #include <stdlib.h> %} // -------------------------- // 处理输入:把Python的点矩阵转成C的double数组 // -------------------------- %typemap(in) (double *x, int nx, int mx) { // 先检查输入是不是合法的二维序列(列表/元组都行) if (!PySequence_Check($input)) { PyErr_SetString(PyExc_TypeError, "参数point_matrix必须是二维序列,比如[[1,2],[3,4]]"); return NULL; } // 拿到点的数量mx $3 = PySequence_Length($input); if ($3 <= 0) { PyErr_SetString(PyExc_ValueError, "point_matrix里至少要有一个点"); return NULL; } // 拿到第一个点的维度nx,顺便检查第一个点是不是序列 PyObject *first_point = PySequence_GetItem($input, 0); if (!PySequence_Check(first_point)) { PyErr_SetString(PyExc_TypeError, "point_matrix里的每个元素必须是一维序列(单个点)"); Py_DECREF(first_point); return NULL; } $2 = PySequence_Length(first_point); Py_DECREF(first_point); if ($2 <= 0) { PyErr_SetString(PyExc_ValueError, "每个点至少要有一个维度"); return NULL; } // 给C的x数组分配内存 $1 = (double*)malloc($2 * $3 * sizeof(double)); if (!$1) { PyErr_SetString(PyExc_MemoryError, "内存分配失败,无法存储点数据"); return NULL; } // 把Python里的点数据复制到C数组里 for (int i = 0; i < $3; i++) { PyObject *point = PySequence_GetItem($input, i); // 检查每个点的维度是不是和第一个点一致 if (!PySequence_Check(point) || PySequence_Length(point) != $2) { PyErr_SetString(PyExc_ValueError, "所有点的维度必须相同"); free($1); Py_DECREF(point); return NULL; } // 复制每个维度的值 for (int j = 0; j < $2; j++) { PyObject *val = PySequence_GetItem(point, j); $1[i * $2 + j] = PyFloat_AsDouble(val); Py_DECREF(val); } Py_DECREF(point); } } // -------------------------- // 处理输出:给f数组分配内存,调用完C函数后转成Python列表 // -------------------------- // 先给f分配内存(numinputs=0表示这个参数不需要用户传,我们自动处理) %typemap(in, numinputs=0) double *f (double *temp_f) { $1 = temp_f = (double*)malloc($3 * sizeof(double)); // $3是之前拿到的mx,点的数量 if (!$1) { PyErr_SetString(PyExc_MemoryError, "内存分配失败,无法存储计算结果"); return NULL; } } // 调用完C函数后,把f数组转成Python列表返回 %typemap(argout) double *f { PyObject *result_list = PyList_New($3); for (int i = 0; i < $3; i++) { PyList_SetItem(result_list, i, PyFloat_FromDouble($1[i])); } // 把结果加到SWIG的返回值里 $result = SWIG_Python_AppendOutput($result, result_list); } // 最后释放f的内存,避免泄漏 %typemap(freearg) double *f { free($1); } // -------------------------- // 声明要封装的C函数,SWIG会自动应用上面的typemap // -------------------------- void eval(double *x, int nx, int mx, double *f, int func_id);
编译和测试步骤
假设你的C代码文件是func_eval.c,按下面的步骤编译成Python模块:
- 用SWIG生成胶水代码:
swig -python func_eval.i - 编译C代码和胶水代码:
# 替换成你的Python头文件路径,比如/usr/include/python3.8 gcc -fPIC -c func_eval.c func_eval_wrap.c -I/usr/include/python3.x - 生成共享库:
gcc -shared func_eval.o func_eval_wrap.o -o _func_eval.so
然后就可以在Python里调用了:
import func_eval # 测试:2个3维点,调用func_id=1的函数 points = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]] results = func_eval.eval(points, 1) print(results) # 输出每个点的计算结果列表
可选优化:支持numpy数组
如果你的项目里常用numpy,可以添加SWIG的numpy支持,不用复制数据,直接用numpy数组的内存指针,效率更高:
- 在接口文件开头加入
%include "numpy.i"和%init %{import_array();%} - 用numpy的typemap替换原来的输入处理typemap,具体可以查SWIG的numpy文档,这里就不展开了
这样封装出来的Python接口完全符合你的需求,用户不用关心底层的C参数细节,用起来和普通Python函数一样顺手。
内容的提问来源于stack exchange,提问作者逸峰李
相关产品推荐
相关产品推荐

