寫python的c擴展簡介

轉自:http://www.isnowfy.com/introduction-to-python-c-extension/#ctypes

        python是一門非常方便的動態語言,很多你用c或者java要很多行的代碼,可能python幾行就搞定了,所以python社區一直有個口號“人生苦短,我用python”,但是方便至於,也帶來速度上的問題。python最被人詬病的就是程序的運行速度了,所以結合c的快速和python的方便,就誕生了很多解決方案。首先注意到python就是c寫成的,所以最根本的解決方案就是利用原生的python c api來寫c程序,然後編譯成鏈接庫文件(linux下就是so文件),然後在python中直接調用,而且其他的解決方案也基本是圍繞這個思路,只不過替你做了很多重複的工作。這次主要是簡要介紹下python c api,swig,sip,ctypes,cython,cffi的使用。

        python c api

         首先來看最原始的就是使用python c api了。

</pre><pre code_snippet_id="137979" snippet_file_name="blog_20140101_1_1985980" name="code" class="cpp">#include <Python.h>
 
static PyObject* add(PyObject* self, PyObject* args){
    int a = 0;
    int b = 0;
    if(!PyArg_ParseTuple(args, "i|i", &a, &b))
        return NULL;
    return Py_BuildValue("i", a+b);
}
 
static PyObject* sub(PyObject* self, PyObject* args){
    int a = 0;
    int b = 0;
    if(!PyArg_ParseTuple(args, "i|i", &a, &b))
        return NULL;
    return Py_BuildValue("i", a-b);
}
 
static PyMethodDef addMethods[]={
    {"add", add, METH_VARARGS},
    {"sub", sub, METH_VARARGS},
    {NULL, NULL, 0, NULL}
};
 
void initmytest(){
    Py_InitModule("mytest", addMethods);
}
         首先是引入Python.h這個頭文件,所以編譯的時候要注意引入python的庫,python中的對象在c中都是用PyObject來表示的,程序中定義了add和sub兩個方法,然後編寫init函數,名字注意是init加上你的module的名字,然後調用Py_InitModule函數來告訴python你定義的函數有哪些。然後就是把他編譯成so文件。

gcc mytest.c -shared -lpython2.7 -L /usr/lib/python2.7/ -I /usr/include/python2.7/ -o mytest.so
        這樣你就可以在python中import mytest這樣引用,用法就和用其他python的模塊一樣了。

swig
         首先要說明的是swig可以進行很多語言的調用轉換,不止是可以讓python調用c。swig和sip都被稱作wrapper,就是說他對你的原有函數進行了包裝。看到之前用python c api的方式裏,我們必須嚴格按照python c api的方式來寫代碼,破壞了原有c程序的可讀性,於是wrapper的思想就是把原生c程序包裝成python c api那種方式的代碼,再去生成so文件。因此我們要做的是首先寫c文件。

int add(int a, int b){
    return a+b;
}
int sub(int a, int b){
    return a-b;
}
        然後再去寫一個swig格式的接口文件。

%module mytest
%{
extern int add(int a, int b);
extern int sub(int a, int b);
%}
 
extern int add(int a, int b);
extern int sub(int a, int b);
        然後就可以運行swig,他會自動生成python c api寫的代碼,並且會自動編譯出so文件來調用。

sip

        來看sip,sip是swig發展而來是方便python調用c的,所以基本使用方式都是差不多,只不過接口文件略有差異。

%Module(name=mytest, language="C")
int add(int a, int b);
int sub(int a, int b);
ctypes

         ctypes提供了另外的思路來調用c程序。首先ctypes是python的標準庫,所以如果用ctypes你不需要額外的其他的東西。ctypes讓你可以在python直接寫代碼加載c的鏈接庫so文件來調用,就是說如果你用so文件而沒有源文件的話,你仍然可以用ctypes去調用。

from ctypes import *
 
f = 'mytest.so'
cdll.LoadLibrary(f)
api = CDLL(f)
api.add.argtypes = [c_int, c_int]
api.add.restype = c_int
api.sub.argtypes = [c_int, c_int]
api.sub.restype = c_int
 
print api.add(3, 2)
print api.sub(3, 2)
        有點像在python中去寫接口文件,由於是python的標準庫,所以這種方式用的還是蠻多的。

cython

        cython的方法呢是利用類似python的語法來寫調用c程序的接口,並且可以同時方便的地用c函數和python函數。看代碼理解。

int sub(int a, int b){
    return a-b;
}
我們可以有一些c寫成的代碼。
cdef extern from 'test.c':
    int sub(int a, int b)
 
def add(int a, int b):
    return a+b
 
def mysub(a, b):
    return sub(a, b)
         然後在cython中我們既可以引入c文件調用c文件中的函數,也可以去調用python中的函數,調用cython程序會把他變成純正的c文件,然後編譯成so文件就可以使用了。
cffi

        最後是cffi,cffi類似於ctypes直接在python程序中調用c程序,但是比ctypes更方便不要求編譯成so再調用,注意到上面的所有方式都是需要去編譯成so文件後再在python中調用,而cffi允許你直接調用c文件來使用裏面的函數了,爲什麼這麼神奇呢,其實是cffi在解釋過程中才幫你把c編譯爲so文件的。。。

from cffi import FFI
ffi = FFI()
ffi.cdef("""
int add(int a, int b);
int sub(int a, int b);
""")
lib = ffi.verify('#include "mytest.c"')
print lib.add(1,2)
         然後基本就是這樣了,最後給我的感覺就是:基本上原生的python c api的寫法最麻煩了,但是一些需要高級用法的話還是這個更容易控制;方便一點的就是用wrapper;ctypes好處是,他是python的標準模塊並且不需要另外寫其他的額外程序(接口程序之類的);cython好處就是可以方便的同時調用c函數和python函數,並且是類python語法,用起來很方便;CFFI好處是調用c更加方便,不用編譯 so。最後本文只是對各種用法簡單的介紹,並沒有深入的對各種用法的優缺點進行比較,因此如果想了解更多內容還是去看官方文檔吧。。。

自己的體會:

1、使用python c api寫C代碼看着確實挺彆扭的,使用起來相當繁複,但既然是原生支持,想必肯定支持的更徹底,可靠性更高些吧。

2、ctypes模塊是python的內置模塊,不需要另外安裝和寫專門的包裝文件,也挺方便的。ctypes也可以支持對C++代碼調用,需要對代碼進行一點封裝。根據官方文檔,ctypes支持的數據類型都是跟C對應的,不支持C++類對象。但是可以把所有的類處理過程寫進函數內,然後在python裏調用這個函數達到使用類的目的。如下:

//test.cpp
#include<iostream>                               
class A{   
      public:
           A(){
             std::cout<<"class A created"<<std::endl;
           }
          ~A(){}
           void printA(int arg){
                  std::cout<<"printA arg: "<<arg<<std::endl;
           }
  };
class B:public A{
      public:
          B(){
              std::cout<<"class B created"<<std::endl;
          }
          ~B(){} 
 };
extern "C"{
      void foo(){
          B* b = new B();
          b->printA(3);
          delete b;
      }
}   
g++ -o libtest.so -shared -fPIC test.cpp
# python 調用腳本 call.py
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import ctypes

lib = ctypes.CDLL("./libtest4.so")
lib.foo()
執行結果:
King@machine:~/code/python$ python call.py
class A created
class B created
printA arg: 3

        可以看到,在foo()函數內部引用的類對象,使用了類的一些特性,經過ctypes調用之後還是正確執行了。所以我推測,只要對python暴露的調用接口(如foo())符合C語言的標準,就可以被正常調用,至於接口內部使用了多少類、模板等C++特性,都不影響。當然,這只是我的一種推測,還沒有用複雜的代碼進行驗證。

另外,細節上需要注意的是,python開放的接口要用extern “C”修飾,否則python腳本里會找不到函數,比如test.cpp代碼裏的foo()函數。還有,一定要用g++編譯C++代碼,不然會報類似“OSError: ./libtest4.so: undefined symbol: _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_”這樣的錯誤,因爲gcc不會鏈接C++的庫文件。

ctypes的官方文檔鏈接:https://docs.python.org/2/library/ctypes.html#ctypes-tutorial

3、cffi模塊是第三方模塊,不是python的標準庫,目前才發展到0.8.6版,貌似有點小衆,用的人還比較少,不過有人說比ctypes更方便。官方地址:https://pypi.python.org/pypi/cffi

4、分享一個牛人寫的python調用C++代碼方案的分析文章,http://hgoldfish.com/blogs/article/87/。









發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章