V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
lyricorpse
V2EX  ›  Python

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

  •  
  •   lyricorpse · 2017-01-12 14:00:56 +08:00 · 3210 次点击
    这是一个创建于 2653 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 程序该如何封装? 已经困扰很久,万分感谢!

    20 条回复    2017-01-15 13:03:15 +08:00
    dant
        1
    dant  
       2017-01-12 14:06:01 +08:00   ❤️ 1
    ctypes/cffi
    lyricorpse
        2
    lyricorpse  
    OP
       2017-01-12 14:23:21 +08:00
    @dant 谢谢,我去学习一下
    enenaaa
        3
    enenaaa  
       2017-01-12 16:24:24 +08:00   ❤️ 1
    Square 结构 在 python 里面要用吗。 如果只是引用, 不实际使用里面的字段。 用 PyObject* 相互传递即可。
    你也可以直接将指针当作一个长整型数值, 需要的时候再强制转换为指针。
    justou
        4
    justou  
       2017-01-12 16:50:34 +08:00   ❤️ 1
    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
        5
    spice630  
       2017-01-12 21:31:41 +08:00   ❤️ 1
    如果你用 golang ,我可以教你 cgo ,很简单。

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

    func bar(){
    C.foo()
    }
    herozem
        6
    herozem  
       2017-01-12 23:54:17 +08:00   ❤️ 1
    我研究了一下,贴下代码吧。
    ```
    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
        7
    herozem  
       2017-01-12 23:56:06 +08:00
    lyricorpse
        8
    lyricorpse  
    OP
       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
        9
    lyricorpse  
    OP
       2017-01-13 07:36:37 +08:00
    @justou 另外再请教下,中间这步制成静态库有什么作用?跳过这步似乎不影响给 Python 调用。
    !gcc -c square.c -o square.o
    !ar rcs libsquare.a square.o
    herozem
        10
    herozem  
       2017-01-13 10:09:41 +08:00 via iPhone   ❤️ 1
    py_area.pxd 不是必须的,可以把声明_square 类型那一行移到 py_area.pyx 里。我是看了一遍官方文档,然后看了一下 cython : a guide to python programmers 前几章,又跳回去把官方文档看了一遍。我也是这几天才开始看 cython 的😅如果有 python 和 c 基础的话,仔细读读这两个文档,然后一边看一边自己验证应该不用太久的
    lyricorpse
        11
    lyricorpse  
    OP
       2017-01-13 12:26:33 +08:00
    @herozem 感谢!我现在发现我真正要 wrap 的复杂 C 程序的函数接口写得很糟糕,在函数中调用了一些全局变量,这些全局变量通过 IO 读取数值,我在 Cython 中该如何处理这种情况?需要把那些全局变量也全部 wrap 成 python 的变量么?
    lyricorpse
        12
    lyricorpse  
    OP
       2017-01-13 12:56:13 +08:00
    @justou @herozem 我把关于公共变量的问题相关代码放到这个 gist 中去了 https://gist.github.com/lyricorpse/6f9a7123ce64adf8f778d98fb65ec442

    square.c 文件中添加了公共变量 glb_f ,请问如何 wrap 能在 Python 中访问修改这个变量?
    justou
        13
    justou  
       2017-01-13 14:15:37 +08:00   ❤️ 1
    @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
        14
    herozem  
       2017-01-13 14:22:37 +08:00
    @lyricorpse 试了一下直接声明全局变量没成功,编译不过。但是我想可以用曲折的办法就是给全局变量写上 getter/setter 然后把这几个函数包装一下,在 python 层面再做成 property 。
    lyricorpse
        15
    lyricorpse  
    OP
       2017-01-13 14:34:22 +08:00
    @justou 你好强大!我应该算不上做科学计算的,但是我是做科学数据分析的,所以会和各种数据分析工具打交道。最近就需要把一个科学家写的很糟糕的 C 程序 wrap 了给 python 调用。。。才开始看 Cython ,本身对 C 语言只知道点皮毛而已。

    @herozem 多谢!我试试看
    justou
        16
    justou  
       2017-01-13 14:44:01 +08:00
    @lyricorpse 那差不多, 我也是做些计算, 数据分析之类的, 物理生→_→
    ruoyu0088
        17
    ruoyu0088  
       2017-01-13 19:28:15 +08:00   ❤️ 1
    下面是用 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
        18
    lyricorpse  
    OP
       2017-01-14 03:21:10 +08:00
    @ruoyu0088 谢谢张老师! cffi 看着好简洁,我去刷一遍文档
    herozem
        19
    herozem  
       2017-01-15 11:26:07 +08:00   ❤️ 1
    lyricorpse
        20
    lyricorpse  
    OP
       2017-01-15 13:03:15 +08:00
    @herozem 厉害!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   946 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 21:58 · PVG 05:58 · LAX 14:58 · JFK 17:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.