python調用C語言接口

python調用C語言接口


注:本文全部示例介紹基於linux平臺html


在底層開發中,通常是使用C或者C++,可是有時候爲了開發效率或者在寫測試腳本的時候,會常用到python,因此這就涉及到一個問題,用C/C++寫的底層庫,怎麼樣直接被python來調用?python

python做爲一門膠水語言,固然有辦法來處理這個問題,python提供的方案就是ctypes庫。linux

ctypes

ctypes是python的外部函數庫,它提供了C語言的兼容類型,並且能夠直接調用用C語言封裝的動態庫。
若是各位有較好的英語水平,能夠參考ctypes官方文檔,可是我會給出更詳細的示例,以便各位更好地理解。程序員

庫的封裝

C代碼若是要可以被python調用,首先咱們先得把被調用C接口封裝成庫,通常是封裝成動態庫。編譯動態庫的指令是這樣的:編程

gcc --shared -fPIC -o target.c libtarget.so

在這裏,windows

--shared -fPIC 是編譯動態庫的選項。數組

-o 是指定生成動態庫的名稱框架

在linux下,通常的命名規則是:靜態庫爲lib.a,動態庫爲lib.so模塊化

target.c爲目標文件,在編譯時常有更復雜的調用關係和依賴,這裏就不詳說,有興趣的朋友能夠去了解了解gcc編譯規則。函數

在python中導入庫

既然庫已經封裝好了,那確定是就想把它用起來。咱們能夠在python中導入這個庫,以導入libtarget.so爲例:

import ctypes
target = cdll.LoadLibrary("./libtarget.so")

順帶提一下,若是在windows環境下,動態庫文件是.dll文件,例如導入libtarget.dll:

import ctypes
target = windll.LoadLibrary("./libtarget.dll")

在這裏,能夠將target當作是動態庫的示例,直接能夠以變量target來訪問動態庫中的內容。

LoadLibrary("./libtarget.so")表示導入同目錄下的libtarget.so文件。

細心的朋友已經發現了,在導入時,linux環境下使用的是cdll,而windows環境下使用的是windll。

這裏涉及到C語言的調用約定,gcc使用的調用約定是cdecl,windows動態庫通常使用stdcall調用約定,既然是調用約定,就確定是關於調用時的規則,他們之間的主要區別就是cdecl調用時由調用者清除被調用函數棧,而stdcall規定由被調用者清除被調用函數棧。

關於這個就不在這裏贅述了,有興趣的朋友能夠看看我另一篇博客:棧幀結構以及函數調用約定

hello world!

學會了封裝動態庫,學會了導入庫,接下來咱們就要動手寫一個hello_world,畢竟學會了hello_world就算是入門了。

代碼以下:

target.c:

#include <stdio.h>
void hello_world(void)
{
    printf("hello downey!!\r\n");
}

編譯動態庫:

gcc -fPIC --shared target.c -o libtarget.so

test.py:

from ctypes import *
test = cdll.LoadLibrary("./libtarget.so")
test.hello_world()

執行python腳本:

python test.py

輸出結果:

hello downey!!

雖然這些代碼都是很是簡單,可是我仍是準備梳理一下流程:

  • 在target.c中咱們定義了函數hello_world(),而後將其封裝成動態庫。
  • 在test.py中導入libtarget.so動態庫,而後調用動態庫中的hello_world()函數,結果顯而易見,執行了hello_world().

是否是很是簡單,是的,python調用C程序就是這麼簡單,可是可別忘了,入門簡單可並不表明真正使用起來簡單!
咱們能夠想想,上面的hello_world()函數沒有參數和返回值,若是是一個帶參數或者帶返回值的C函數呢,python該怎麼調用?

python的內建類型中可沒有C語言那麼多花裏胡哨的類型,在python中怎麼去區分int,short,char這些類型呢?

類型轉換

針對上面的問題,python定義了一系列兼容C語言的類型

如圖所示,這個圖算是很清晰地將python與C類型對應關係展示了出來。咱們將要使用的就是最左邊一列的ctypes type,以替代C庫中的各類類型。

函數帶參示例

對於程序員而言,看圖片看文檔永遠沒有看代碼來得直接,因此在這裏先上一段演示代碼,看看在C庫中的類型是怎麼被替換的,可是凡事講究個按部就班,咱們先來一個簡單的,普通變量版的,代碼以下:

較爲簡單的示例

target.c:

#include <stdio.h>
char hello_world(int num)
{
    printf("hello %d!!\r\n",num);
    return (char)num+1;
}

test.py:

1 from ctypes import *
2 test = cdll.LoadLibrary("./libtarget.so")
3 test.hello_world.restype = c_char
4 c = test.hello_world(48)
5 print(type(c))
6 print(c)

輸出:

hello 48!!
<type 'str'>
1

C語言代碼我就很少解釋,咱們主要來關注python部分:

  • 第一、2行不用解釋了吧
  • 第3行:這條指令的做用是指定函數的返回值,python解釋器並不能自動識別C函數的返回值,因此咱們須要人爲地指定,若是不指定,將會是默認的int型。
  • 第4行調用函數,並傳入參數48,第五行打印返回值的類型,第六行打印返回值。

咱們再來看輸出部分:

  • 第一行是hello_world()函數的輸出。
  • 第二行打印出來的返回值類型明顯是不對的,明明指定了返回值類型爲c_char,爲何在這裏變成了str(字符串)類型,並且在第三行的輸出中輸出了1,而不是49。緣由有如下幾點:
    1. 在python中,內置的類型有int, float,list, tuple等等,但並不包含char類型,既然程序中c是python中的變量,必然將會被轉換,並且與C不同的是,全部變量都是對象。
    2. 若是是須要轉換,那會遵循什麼規則呢?咱們只好從官方文檔中找答案,原文是這樣的:

      Represents the C char datatype, and interprets the value as a single character. The constructor accepts an optional string initializer, the length of the string must be exactly one character.
      翻譯就是,c_char表明C中的char,在python中被視爲單個字符,構造函數接受可選的字符串初始值設定項,字符串的長度必須剛好是一個字符。通俗地說,就是一個字符的字符串。
    3. 爲何輸出1而不是49,這個就很簡單了,十進制的49就是字符1,既然是被視爲字符,固然以字符顯示

其實在這裏,博主選取了一個比較特殊的例子,就是char在python中轉換的特殊性,各位朋友能夠思考下面兩個問題:

  • 若是在hello_world函數中,將返回值從char改爲short,輸出是什麼?(固然test.py中的第三行也要將c_char改成c_short)
  • 接上題,若是將返回值從char改成float,輸出將是什麼?
  • 本身動手試試,若是在test.py中不指定函數返回值類型,輸出將會是什麼?

進階版

若是你看完了上面那個簡單版的函數參數轉換,咱們進入進階版的。在這個進階版的示例中,將引入數組,指針,結構體。不說了,直接上碼:
target.c:

#include <stdio.h>
#include <string.h>
typedef struct{
    char   *ptr;
    float f;
    char array[10];
}target_struct;

target_struct* hello_world(target_struct* target)
{
    // printf("hello %s.%d!!\r\n",name,num[0]);
    static char temp = 0x30;
    target->ptr = &temp;
    target->f = 3.1;
    memset(target->array,1,sizeof(target->array));
    return target;
}

test.py:

1 from ctypes import *
2 test = cdll.LoadLibrary("./libtarget.so")

3 class test_struct(Structure):
4 _fields_ = [('ptr',c_char_p),
5              ('c',c_float),
6             ('array',c_char*10)]
7 struct = test_struct(c = 0.5)
8 test.hello_world.restype =POINTER(test_struct)

9 ret_struct = test.hello_world(pointer(struct))
10 print ret_struct.contents.ptr
11 print ret_struct.contents.c

輸出:

0
3.09999990463

對於target.c很少說,你們確定看得懂,咱們仍是主要來對照分析一下test.py的內容:

  • 第一、2行不用解釋,你們都懂
  • 第3-6行纔是重頭戲,這就是python中對結構體的支持,新建一個類,繼承Structure,將C中結構體內容一一對應地在類中進行聲明,你能夠將這個類當作是對應C庫中的結構體,_fields_是字典類型,key要與C庫中結構體相對應,value則是指定相應類型,在示例中你們應該能看懂了。
  • 第7行,構造一個對應C中結構體的類,能夠傳入對應參數進行構造。
  • 第8行,指定返回值類型爲test_struct指針類型,這裏的類型由POINTER()修飾,表示是指針類型。
  • 第9行,調用hello_world()函數,傳入struct類,pointer(struct)就是將struct轉爲指針類型實例。由於在C中的接口就是傳入target_struct類型,返回target_struct類型,因此ret_struct也是target_struct*類型
  • 第十、11行,打印函數返回值,查看執行結果。對於一個指針類型的變量,若是咱們要獲取它的值,可使用.contents方法,例如ret_struct.contents返回結構體類示例,而後再訪問結構體類中的元素。
  • 輸出結果,由於在hello_world中元素ptr指向變量的值爲0x30,因此輸出1,而float類型c被賦值爲3.1,可是輸出3.09999990463,這其實並非bug,只能算是python中對浮點數的取值精度問題,這裏就不展開討論了。

小結

通過這兩個示例,我相信你們對ctypes的使用有了一個大概的認識,可是我建議你們看過以後本身多嘗試嘗試,這樣纔有更深的體會,這裏再作一個總結:

  1. python中ctypes模塊支持python類型到C類型的轉換,具體對應關係參考上文的圖表。
  2. 通常狀況下,若是導入的目標動態庫爲linux下的.so類型庫,使用cdll.LoadLibrary()導入,若是是windows下的dll動態庫,使用windll.LoadLibrary()導入,兩種庫的區別在於函數調用約定
  3. python中須要$LIB.$FUNCTION.restype指定函數返回類型,若是不指定,返回值類型默認爲int,同時也可使用$LIB.$FUNCTION.argtypes指定傳入參數類型,$LIB.$FUNCTION.argtypes的類型爲列表,你們能夠自行試試
  4. 在python中c_char類型會被轉換成str類型,被視爲只有一個字符的字符串
  5. 對於指針,不能直接訪問,若是直接使用print(ptr),將會打印出一個地址,須要使用ptr.contents來訪問其實例
  6. 對於C中的結構體的支持,python中須要新定義一個結構體類,繼承Structure類,而後在_fields_字段中一一對應地定義結構體中的元素,在使用時,可視爲結構體類等於結構體
  7. POINTER()和pointer(),這兩個方法,一個大寫一個小寫,你們在上面的例子中有看到,博主剛接觸的時候也是一臉懵逼,後來查了一下官方文檔,而後本身嘗試了一遍,終於理解了它們之間的區別,這裏貼上官方說明:

    POINTER():This factory function creates and returns a new ctypes pointer type. Pointer types are cached and reused internally, so calling this function repeatedly is cheap. type must be a ctypes type.
    pointer():This function creates a new pointer instance, pointing to obj. The returned object is of the type POINTER(type(obj)).
    Note: If you just want to pass a pointer to an object to a foreign function call, you should use byref(obj) which is much faster.

    簡單翻譯一下就是:POINTER()建立並返回一個新的指針類型,pointer()建立一個新的指針實例,一個是針對類型,一個是針對實際對象,這裏還提到了byref(),上面有說到,若是你僅僅是想講一個外部對象做爲參數傳遞到函數,byref()能夠替代pointer()。若是你尚未明白這一部分,你能夠參考參考上面個人例子,而且本身試一試,這個並不難。
  8. 對於數組,其實也挺簡單,你們能夠參考上面示例,我相信你們能看懂。

擴展內容 —— 回調函數

在參數類型中,還有一種很是特殊的存在——函數指針,在C語言中,咱們常常將函數指針做爲參數來實現回調函數,這種作法在各類標準化框架中常常見到,在模塊化編程中也是很是實用。

那麼問題來了,C庫中的函數實現了回調函數,python調用時該怎麼作?
按照咱們對C語言的理解,其實函數指針也是指針的一種,咱們能夠將一個指針強制轉換成函數指針類型而後執行,而後博主就在python中嘗試了一下,結果不論是我試圖將什麼類型的指針轉換成函數執行,結果都是這樣的:

TypeError: XXXX object is not callable

好吧,我仍是老老實實地使用官方提供的接口,仍是直接上碼:
target.c:

#include <stdio.h>
typedef void (*callback)(int);
void func(callback c1,callback c2,int p1,int p2)
{
    c1(p1);
    c2(p2);
}

test.py:

1 from ctypes import *
2 test = cdll.LoadLibrary("./libtarget.so")

3 def test_callback1(val):
4     print "I'm callback1"
5     print val

6 def test_callback2(val):
7     print "I'm callback1"
8     print val

9 CMPFUNC = CFUNCTYPE(None, c_int)
10 cbk1 = CMPFUNC(test_callback1)
11 cbk2 = CMPFUNC(test_callback2)
12 test.func(cbk1,cbk2,1,2)

輸出:

I'm callback1
1
I'm callback1
2

能夠看到,在target.c中func函數傳入了兩個函數指針參數,而後在函數中調用這兩個函數。
咱們再來仔細分析python中的調用:

  • 第一、2行,請參考上面兩個示例。
  • 3-6行,6-8行,定義兩個回調函數,類型。
  • 第9行,站在C語言角度來講,至關於建立一個函數類型,指定函數的返回值和參數,第一個元素爲返回值,後面依次放參數,由於返回值爲void,因此這裏是None
  • 10-11行,用上面建立的函數類型修飾兩個函數,返回一個函數實例,這個函數實例就能夠做爲函數參數,以函數指針(再次聲明,python中沒有函數指針這回事,這裏爲了理解方便將這個概念引入)的形式傳遞到函數中。
  • 12行,調用func()函數,而func()函數的內容就是直接執行傳進來的兩個函數,傳入的參數是test_callback1和test_callback2,因此執行了test_callback1和test_callback2,打印了相應內容。

好了,關於python ctypes調用C代碼的問題就到此爲止了,若是朋友們對於這個有什麼疑問或者發現有文章中有什麼錯誤,歡迎留言

我的郵箱:linux_downey@sina.com
原創博客,轉載請註明出處!

祝各位早日實現項目叢中過,bug不沾身. (完)

相關文章
相關標籤/搜索