一个简单的 C 程序,函数参数是指针结构体,请问如何封装给 Python 调用

2017-01-12 14:00:56 +08:00
 lyricorpse

square.c程序如下,想把其中的calc_area()函数封装了给 Python 调用

#include <stdio.h>
#include <stdlib.h>

struct Square {
    float length;
    float width;
};

typedef struct Square *sq;

float calc_area(sq a) {
    float s;
    s = a->length * a->width;
    return s;
}

int main() {
    sq a;
    a = malloc(sizeof(struct Square));
    a->length = 10.0;
    a->width = 3.0;

    printf("%f\n", calc_area(a));
}

我写了个头文件square.h如下,我很少写 C ,所以也不确定写的对不对

#include <stdio.h>
#include <stdlib.h>

struct Square {
    float length;
    float width;
};

typedef struct Square *sq;

float calc_area(sq a);

然后试着用 Cython ,写了square_wrapper.pyx如下,主要涉及到指针和自定义结构体,真不知道怎么写了,反正报错

cdef extern from 'square.h':

    struct Square:
        float length
        float width

    ctypedef struct Square *sq

    float calc_area(sq a)

def c_area(a):
    return calc_area(sq a)

请教熟悉 Cython 或者其他如 SWIG 的 V 友,这个简单的 C 程序该如何封装? 已经困扰很久,万分感谢!

3218 次点击
所在节点    Python
20 条回复
dant
2017-01-12 14:06:01 +08:00
ctypes/cffi
lyricorpse
2017-01-12 14:23:21 +08:00
@dant 谢谢,我去学习一下
enenaaa
2017-01-12 16:24:24 +08:00
Square 结构 在 python 里面要用吗。 如果只是引用, 不实际使用里面的字段。 用 PyObject* 相互传递即可。
你也可以直接将指针当作一个长整型数值, 需要的时候再强制转换为指针。
justou
2017-01-12 16:50:34 +08:00
ctypedef struct Square *sq 不符合 cython 语法, 把 struct 去掉

要封装 Square 给 py 的话, 可以在 pyx 中定一个 cdef 的 class, 维护一个指向 struct Square 的指针. 把 cal_area 作为这个 cdef class 的方法

https://gist.github.com/justou/ac94501d664f32872b2ae546099d874c

这是封装 C 库的流程,如果是自定义的扩展模块 ,c/c++ -> cython -> py 这样做其实略繁琐 .
完全可以直接 cython -> py, 简化给 py 写 C 扩展的是 cython 的目的之一.
spice630
2017-01-12 21:31:41 +08:00
如果你用 golang ,我可以教你 cgo ,很简单。

/*
// go preamble
#include "foo.h"
*/
import "C"

func bar(){
C.foo()
}
herozem
2017-01-12 23:54:17 +08:00
我研究了一下,贴下代码吧。
```
root@arch area: # 首先是 area.h 和 area.c
root@arch area: cat area.h
#ifndef _AREA_H
#define _AREA_H

struct Square {
float length;
float width;
};

typedef struct Square *pSquare;

float calc_area(pSquare);

#endif /* area.h */
root@arch area: cat area.c
#include <stdio.h>
#include <stdlib.h>

#include "area.h"


float calc_area(pSquare s) {
return s->length * s->width;
}

int main(void) {
pSquare s = (pSquare)malloc(sizeof(struct Square));
if (s == NULL) {
printf("memory not enough");
exit(1);
}
s->length = 10.0;
s->width = 3.0;
printf("area of the square: %f\n", calc_area(s));
free(s);
}
root@arch area: # 运行一下
root@arch area: cc area.c && ./a.out && rm a.out
area of the square: 30.000000
root@arch area: # 为 area.c 定义包裹的 Cython 头文件
root@arch area: cat carea.pxd
cdef extern from "area.h":
cdef struct Square:
float length
float width

ctypedef Square *pSquare;

cdef float calc_area(pSquare);
root@arch area: # 为 python 版本 Square 定义 Cython 头文件
root@arch area: cat py_area.pxd
cimport carea

cdef class Square:
cdef carea.pSquare _square

cpdef float calc_area(Square)
root@arch area: # 为 python 版本 Square 包裹一下
root@arch area: cat py_area.pyx
from cpython.mem cimport PyMem_Malloc, PyMem_Free

cimport carea

cdef class Square:
def __cinit__(self, length, width):
self._square = <carea.pSquare>PyMem_Malloc(sizeof(carea.Square))
if not self._square:
raise MemoryError("Memory not enough")

self._square.length = length
self._square.width = width

def __dealloc__(self):
PyMem_Free(self._square)

cpdef float calc_area(self):
return carea.calc_area(self._square)
root@arch area: # 调用一下
root@arch area: # 哦不,先写好 setup.py ,然后编译
root@arch area: cat setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize

ext_modules = cythonize([
Extension("py_area", ["py_area.pyx", "area.c"])
])

setup(ext_modules=ext_modules)
root@arch area: python3 setup.py build_ext --inplace
running build_ext
building 'py_area' extension
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -march=x86-64 -mtune=generic -O2 -pipe -fstack-protector-strong -march=x86-64 -mtune=generic -O2 -pipe -fstack-protector-strong -march=x86-64 -mtune=generic -O2 -pipe -fstack-protector-strong -fPIC -I/usr/include/python3.6m -c py_area.c -o build/temp.linux-x86_64-3.6/py_area.o
gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -march=x86-64 -mtune=generic -O2 -pipe -fstack-protector-strong -march=x86-64 -mtune=generic -O2 -pipe -fstack-protector-strong -march=x86-64 -mtune=generic -O2 -pipe -fstack-protector-strong -fPIC -I/usr/include/python3.6m -c area.c -o build/temp.linux-x86_64-3.6/area.o
gcc -pthread -shared -Wl,-O1,--sort-common,--as-needed,-z,relro -Wl,-O1,--sort-common,--as-needed,-z,relro build/temp.linux-x86_64-3.6/py_area.o build/temp.linux-x86_64-3.6/area.o -L/usr/lib -lpython3.6m -o /root/tests/area/py_area.cpython-36m-x86_64-linux-gnu.so
root@arch area: # 调用
root@arch area: ipython
Python 3.6.0 (default, Dec 24 2016, 08:03:08)
Type "copyright", "credits" or "license" for more information.

IPython 5.1.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.

In [1]: import py_area

In [2]: a = py_area.Square(10, 20)

In [3]: a.calc_area()
Out[3]: 200.0

In [4]:
Do you really want to exit ([y]/n)?
root@arch area:
```
herozem
2017-01-12 23:56:06 +08:00
lyricorpse
2017-01-13 06:51:27 +08:00
@enenaaa 多谢!

@justou 感谢!已 star 。我只读了一点 Kurt M. Smith 的 Cython 那本书,本身不是程序员,好多年没写过 C ,感觉看 Cython 文档也很不友好,请问你是如何学习 Cython 的(比如流程方面)?你 notebook 里生成文件的用法之前没见过,很赞!

@spice630 谢了哈!目前只有用 Python 的需求

@herozem 感谢!已 star 。之前不知道一定要写 pxd ,还得写两个。另外,请问你 Cython 的学习路径是怎样的?感觉对非程序员初学者来说学习曲线好陡峭
lyricorpse
2017-01-13 07:36:37 +08:00
@justou 另外再请教下,中间这步制成静态库有什么作用?跳过这步似乎不影响给 Python 调用。
!gcc -c square.c -o square.o
!ar rcs libsquare.a square.o
herozem
2017-01-13 10:09:41 +08:00
py_area.pxd 不是必须的,可以把声明_square 类型那一行移到 py_area.pyx 里。我是看了一遍官方文档,然后看了一下 cython : a guide to python programmers 前几章,又跳回去把官方文档看了一遍。我也是这几天才开始看 cython 的😅如果有 python 和 c 基础的话,仔细读读这两个文档,然后一边看一边自己验证应该不用太久的
lyricorpse
2017-01-13 12:26:33 +08:00
@herozem 感谢!我现在发现我真正要 wrap 的复杂 C 程序的函数接口写得很糟糕,在函数中调用了一些全局变量,这些全局变量通过 IO 读取数值,我在 Cython 中该如何处理这种情况?需要把那些全局变量也全部 wrap 成 python 的变量么?
lyricorpse
2017-01-13 12:56:13 +08:00
@justou @herozem 我把关于公共变量的问题相关代码放到这个 gist 中去了 https://gist.github.com/lyricorpse/6f9a7123ce64adf8f778d98fb65ec442

square.c 文件中添加了公共变量 glb_f ,请问如何 wrap 能在 Python 中访问修改这个变量?
justou
2017-01-13 14:15:37 +08:00
@lyricorpse 我也不是程序员出生, 有做科学计算的需要才开始写程序的

根据个人经验, 要用好 cython 的话需要一定的 c 基础, 要比较熟悉 c 的各种玩法. 我感觉 cython 最大的好处就是把权衡交给程序员, 也可以完全避开 c, 就用 cython 跟 python 的东西, 这几乎可以解决大多数 python 执行效率的问题了, 极少时候, 如果还想继续提升性能, 一般是需要更复杂的并行, 或者无法完全避开 gil 的问题, 就在 cython 中用更多的 c/cpp, 还不行就把这部分完全剥离到 c/cpp 库中完成计算, python 负责预处理后处理, cython 作为桥梁.

反复地看 cython 官方文档, 有空就去逛逛 stackoverflow, 看看相关的问题, 多逛逛相关博客, 不明觉厉的东西要保存下来, 然后逐步的处理, 整理到自己的知识系统, 使用中遇到的问题以及解决方法要详细的记下来, 因为很可能再次遇到, 这时候想起以前遇到过但是忘记解决办法了是很恼火的. 多看看 cython 的相关项目, 因为投身于科学计算, 所以比较关注这方面的库, 比如 cython_gsl, 包装的科学计算库 GNU Scientific Library(GSL); cy_armadillo, 包装 cpp 的 armadillo 库, 都是开源项目, 看这些源码的时候 c/cpp/cython 的熟悉程度是同步提升的, 还有 cython 自带的那些 pxd 是包装 c/cpp 标准库最好的例子. 其实学任何技术都是这个过程吧. cython 的书籍的话我也只有你上面提到的那本.如果你从事科学计算方面, 推荐下张老师 @ruoyu0088, 我是跟着他一路小跑过来的 233, 可以去搜下他的博客, 论坛, 书籍
-------------------------------------------------------------------------------------------------

如果不是已经无法修改的 C 库的话, 建议还是直接放到 cython 中写, 不必写个.c 来包装.

包装那个全局变量的话:
1.c 的头文件声明 float glb_f;
2.pxd 里面也做相应声明 cdef extern from "square.h": float glb_f
3.pyx 里面的修改
cimport square
...
def __init__(self, width, length, global_f=1.0):
square.glb_f = global_f # 从这儿设置这个全局变量
self.s_ptr.width = width
self.s_ptr.length = length
herozem
2017-01-13 14:22:37 +08:00
@lyricorpse 试了一下直接声明全局变量没成功,编译不过。但是我想可以用曲折的办法就是给全局变量写上 getter/setter 然后把这几个函数包装一下,在 python 层面再做成 property 。
lyricorpse
2017-01-13 14:34:22 +08:00
@justou 你好强大!我应该算不上做科学计算的,但是我是做科学数据分析的,所以会和各种数据分析工具打交道。最近就需要把一个科学家写的很糟糕的 C 程序 wrap 了给 python 调用。。。才开始看 Cython ,本身对 C 语言只知道点皮毛而已。

@herozem 多谢!我试试看
justou
2017-01-13 14:44:01 +08:00
@lyricorpse 那差不多, 我也是做些计算, 数据分析之类的, 物理生→_→
ruoyu0088
2017-01-13 19:28:15 +08:00
下面是用 cffi 包装的例子:

https://gist.github.com/ruoyu0088/47135d7726abfdc03a61f63d4696cb74

调用 build.py 编译扩展模块,然后运行 test.py 测试。

在 square.h 中声明 extern 的全局变量 glb_f 。在 Python 中可以直接通过 lib.glb_f 设置该全局变量。使用 ffi.new("sq", ...)创建结构体,可以使用字典初始化结构体,也可以通过属性访问结构体的字段。

build.py 中的 ffibuilder.cdef("...")是声明需要包装的类型,变量以及函数的地方。这里可以直接使用 C 语言的头文件 square.h 中的内容。不过头文件中不能有预处理命令。如果有预处理命令的话,则需要手动调整声明。
lyricorpse
2017-01-14 03:21:10 +08:00
@ruoyu0088 谢谢张老师! cffi 看着好简洁,我去刷一遍文档
herozem
2017-01-15 11:26:07 +08:00
lyricorpse
2017-01-15 13:03:15 +08:00
@herozem 厉害!

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/334111

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX