Python使用Ctypes與C/C++ DLL文件通訊過程介紹及實例分析

項目中可能會常常用到第三方庫,主要是出於程序效率考慮和節約開發時間避免重複造輪子。不管第三方庫開源與否,編程語言是否與當前項目一致,咱們最終的目的是在當前編程環境中調用庫中的方法並獲得結果或者藉助庫中的模塊實現某種功能。這個過程會牽涉到不少東西,本篇文章將簡要的介紹一下該過程的一些問題。html

1.背景

多語言混合編程能夠彌補某一種編程語言在性能表現或者是功能等方面的不足。雖然全部的高級語言都會最終轉換成彙編指令或者最底層的機器指令,可是語言自己之間的千差萬別很難一言以蔽之,這對不一樣語言之間相互通訊形成很大的障礙。python

工做中須要用python完成一項功能,可是全部現有的python庫都不知足需求。最終找到了一個開源的C++庫,編譯獲得動態庫被python調用才完成工做需求。雖然整個過程耗時很少,可是期間碰到不少的問題,並且這些問題都頗有思考價值。編程

除了這篇博文外,後續還將有一到兩篇文章經過具體的實例講解一下跨語言調用。windows

2.問題思考

在進行具體的介紹以前,先來思考一下調用外部庫或者本身實現庫所牽涉的一些通常性的問題。這樣或許實際中操做使用時會理解的更加深入,遇到問題也可以逐項的排查。api

若是用C語言寫的庫調用了Linux的system call,縱使C自己是跨平臺的,那麼該庫也不可能在Window上被使用,即使咱們能拿到源碼。這裏有兩個核心問題:數組

  • 是否開源
  • 是否跨平臺

若是庫的實現不依賴平臺,且開源,那就意味着很大可能能在當前項目中使用。爲何是可能,由於即便庫的實現語言和當前項目語言一致,也可能由於語言版本差別或者標準迭代致使不兼容。緩存

 最差的狀況就是隻能拿到編譯後的庫文件,且需在特定的平臺運行。框架

做爲庫的開發者,最好是可以開源且庫的實現不依賴於特定的平臺,這樣才能最大限度的被使用。編程語言

做爲庫的使用者,最不理想的狀況是庫能夠在當前平臺使用,可是隻能拿到靜態庫或者動態庫,且庫的實現語言和當前項目語言不一致。函數

多數狀況是第三方庫是跨平臺的且可以拿到源代碼。這樣的話若是二者的實現語言一致,咱們能夠直接將第三方庫的代碼移植到當前的項目中;若是實現語言不一致,須要在當前平臺上將庫的源碼編譯出當前平臺上可用的庫文件,而後在當前項目中引用編譯生成的庫文件。

本文將先簡單的介紹在window平臺上,使用python 2.7 自帶的ctypes庫引用標準的C動態庫msvcrt.dll。這裏能夠先思考如下幾個問題:

  1. python可不能夠引用靜態庫?
  2. python中怎麼拿到DLL導出的函數?
  3. python和C/C++之間的變量的類型怎樣轉換,若是是自定義的類型呢?
  4. 怎麼處理函數調用約定(calling convention,eg:__cdecl,__stdcall,__thiscall,__fastcall)可能不一樣的問題?
  5. 若是調用DLL庫的過程當中出現問題,是咱們調用的問題仍是庫自己的問題?應該怎樣快速排查和定位問題?
  6. 有沒有什麼現有的框架可以幫咱們處理python中引用第三方庫的問題呢?
  7. 對於自定義的類型(class 和 struct)是否能在python中被引用。

關於函數調用約定,有必要簡單的提一下:

Calling Convention和具體的編程語言無關,是由編譯器、鏈接器和操做系統平臺這些因素共同決定的。

The Visual C++ compilers allow you to specify conventions for passing arguments and return values between functions and callers. Not all conventions are available on all supported platforms, and some conventions use platform-specific implementations. In most cases, keywords or compiler switches that specify an unsupported convention on a particular platform are ignored, and the platform default convention is used.

這是MS的官方解釋。注意最後一句話,表示對於函數調用,在平臺不支持的狀況下,語言中指定關鍵字或者編譯器轉換都可能無效。

接下的介紹中來咱們將一一回答上面的問題。

3.導入C標準動態庫

先來簡單看一下python中如何引用C的標準動態庫。

 1 import ctypes, platform, time
 2 if platform.system() == 'Windows':
 3     libc = ctypes.cdll.LoadLibrary('msvcrt.dll')
 4 elif platform.system() == 'Linux':
 5     libc = ctypes.cdll.LoadLibrary('libc.so.6')
 6 print libc
 7 # Example 1
 8 libc.printf('%s\n', 'lib c printf function')
 9 libc.printf('%s\n', ctypes.c_char_p('lib c printf function with c_char_p'))
10 libc.printf('%ls\n', ctypes.c_wchar_p(u'lib c printf function with c_wchar_p'))
11 libc.printf('%d\n', 12)
12 libc.printf('%f\n', ctypes.c_double(1.2))
13 # Example 2
14 libc.sin.restype = ctypes.c_double
15 print libc.sin(ctypes.c_double(30 * 3.14 / 180))
16 # Example 3
17 libc.pow.restype = ctypes.c_double
18 print libc.pow(ctypes.c_double(2), ctypes.c_double(10))
19 # Example 4
20 print libc.time(), time.time()
21 # Example 5
22 libc.strcpy.restype = ctypes.c_char_p
23 res = 'Hello'
24 print libc.strcpy(ctypes.c_char_p(res), ctypes.c_char_p('World'))
25 print res

接下來咱們一一分析上面的這段代碼。

3.1 加載庫的方式

根據當前平臺分別加載Windows和Linux上的C的標準動態庫msvcrt.dll和libc.so.6。

 注意這裏咱們使用的ctypes.cdll來load動態庫,實際上ctypes中總共有如下四種方式加載動態庫:

  1. class ctypes.CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
  2. class ctypes.OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
  3. class ctypes.WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False)
  4. class ctypes.PyDLL(name, mode=DEFAULT_MODE, handle=None)

關於這幾個加載動態庫的方式區別細節能夠參考一下官網的說明,這裏僅簡要說明一下。

 除了PyDll用於直接調用Python C api函數以外,其餘的三個主要區別在於

  • 使用的平臺;
  • 被加載動態庫中函數的調用約定(calling convention);
  • 庫中函數假定的默認返回值。

 也就是平臺和被加載動態庫中函數的調用約定決定了咱們應該使用哪一種方式加載動態庫。

本例中咱們在windows平臺上使用的是CDLL而不是WinDll,緣由是msvcrt.dll中函數調用約定是C/C++默認的調用約定__cdecl。

而WinDll雖然是能夠應用於windows平臺上,可是其只能加載標準函數調用約定爲__stdcall的動態庫。所以這裏只能使用CDLL方式。

能夠將上面的CDLL換成WinDll看一下會不會有問題。這裏應該可以對函數調用理解的更加深入一些了,同時也回答了上面第一小節中咱們提問的問題4。

3.2 跨語言類型轉換

 這裏主要針對第一節提出的問題3。

咱們是在python中調用C的函數,函數實參是python類型的變量,函數形參則是C類型的變量,顯然咱們將python類型的變量直接賦值給C類型的變量確定會有問題的。

所以這裏須要兩種語言變量類型之間有一一轉換的必要。這裏僅僅列出部分對應關係(因爲博客園的表格顯示會有問題,所以這樣列出,請見諒):

Python type        Ctypes type          C type

int/long             c_int             int

float             c_double           double

string or None        c_char_p           char * (NUL terminated)

unicode or None       c_wchar_p          wchar_t * (NUL terminated)

 經過Ctypes type中提供類型,咱們創建了一種python類型到c類型的一種轉換關係。

在看一下上面的例子Example 1。在調用C的函數時,咱們傳給C函數的實參須要通過Ctypes轉換成C類型以後才能正確的調用C的函數。

3.3 設定C函數的返回類型

看一下上面的例子Example 2.

libc.sin.restype = ctypes.c_double

咱們經過restype的方式指定了C(math 模塊)函數sin的返回類型爲double,對應到python即爲float。顯然函數的返回類型在DLL中是沒法獲取的。

開發人員也只能從庫的說明文檔或者頭文件中獲取到函數的聲明,進而指定函數返回值的類型。

double sin (double x);
float sin (float x);
long double sin (long double x);
double sin (T x);           // additional overloads for integral types

上面是C++11中cmath中sin函數的聲明。這裏幾個sin函數是C++中的函數重載。

libc.sin(ctypes.c_double(30 * 3.14 / 180))

因爲調用以前指定了sin函數的返回類型ctypes.c_double,所以sin的調用結果在python中最終會轉換爲float類型。

3.4 假定的函數返回類型

因爲咱們在動態庫中獲取的函數並不知道其返回類型,由於咱們只獲得了函數的實現,並無函數的聲明。

在沒有指定庫函數返回類型的狀況下,ctypes.CDLL和ctyps.WinDll均假定函數返回類型是int,而ctypes.oleDll則假定函數返回值是Windows HRESULT。

那若是函數實際的返回值不是int,便會按照int返回值處理。若是返回類型能轉爲int類型是能夠的,若是不支持那函數調用的結果會是一個莫名其妙的數字。

time_t time (time_t* timer);

  上面的例子Example 4則默認將C類型time_t轉爲了python 的int類型,結果是正確的。

對於Example 3中咱們不只要指定函數pow的返回類型,還要轉換函數的實參(這裏很容易疏忽)。

所以在調用動態庫以前必定要看下函數聲明,指定函數返回類型。

到這裏很容易想到能夠指定函數的返回值類型,那能不能指定函數形參的類型呢?答案是確定的,argtypes 。

printf.argtypes = [c_char_p, c_char_p, c_int, c_double]

3.5 可變string buffer

 上面的例子Exapmle 5中咱們調用了C中的一個字符串拷貝函數strcpy,這裏函數的返回值和被拷貝的對象均爲正確的。

可是這裏是故意這樣寫的,由於這裏會有一個問題。

若是res = 'Hello'改成res = 'He'和res = 'HelloWorld',那麼實際上res的結果會是‘Wo’和'World\x00orld'。

str_buf = ctypes.create_string_buffer(10)
print ctypes.sizeof(str_buf)                       # 10
print repr(str_buf.raw)                            # '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
str_buf.raw = 'Cnblogs'
print repr(str_buf.raw)                            # 'Cnblogs\x00\x00\x00'
print repr(str_buf.value)                          # 'Cnblogs'

這裏咱們能夠經過ctypes.create_string_buffer來指定一個字符串緩存區。

使用string buffer改寫Example 5:

libc.strcpy.restype = ctypes.c_char_p
res = ctypes.create_string_buffer(len('World') + 1)
print libc.strcpy(res, ctypes.c_char_p('World'))
print repr(res.raw), res.value                     # 'World\x00' 'World'

注意上面的res的類型是c_char_Array_xxx。這裏只是爲了介紹string buffer,實際上不會這麼用。

3.6 小節

這裏簡單的介紹了一下ctypes如何和動態庫打交道。限於篇幅還有指針,引用類型和數組等的傳遞,以及自定義類型等沒有介紹。可是這一小結應該能對python引用動態庫過程有一個大體的認識。

更加詳細信息能夠參考官網:ctypes

4. 自定義DLL文件導入

爲了更好的理解python調用DLL的過程,有必要了解一下DLL的定義文件。

4.1 C/C++引用DLL

首先,做爲對比咱們看一下C/C++如何引用DLL文件的。下面的文件是 ./Project2/Source2.cpp

工程配置爲:Conguration Properties>General>Configuration Types: Dynamic Library (.dll)

輸出路徑:./Debug/Project2.dll

 1 #include <stdio.h>
 2 #include <math.h>
 3 #include <string.h>
 4 
 5 #ifdef _MSC_VER
 6 #define DLL_EXPORT extern "C" __declspec( dllexport )
 7 #else
 8 #define DLL_EXPORT
 9 #endif
10 
11 __declspec(dllexport) char* gl = "gl_str";
12 
13 DLL_EXPORT void __stdcall hello_world(void) {
14     printf("%s Hello world!\n", gl);
15 }
16 
17 DLL_EXPORT int my_add(int a, int b) {
18     printf("calling my_add@int func\n");
19     return a + b;
20 }
21 
22 //DLL_EXPORT double my_add(double a, double b) {
23 //    printf("calling my_add@double func\n");
24 //    return a + b;
25 //}
26 
27 DLL_EXPORT int my_mod(int m, int n) {
28     return m % n;
29 }
30 
31 DLL_EXPORT bool is_equal(double a, double b) {
32     return fabs(a - b) < 1e-3;
33 }
34 
35 DLL_EXPORT void my_swap(int *p, int *q) {
36     int tmp = *p;
37     *p = *q;
38     *q = tmp;
39 }
40 
41 inline void swap_char(char *p, char *q) {
42     char tmp = *p;
43     *p = *q;
44     *q = tmp;
45 }
46 
47 DLL_EXPORT void reverse_string(char *const p) {
48     if (p != nullptr) {
49         for (int i = 0, j = strlen(p) - 1; i < j; ++i, --j)
50             swap_char(p + i, p + j);
51             //swap_char(&p[i], &p[j]);
52     }
53 }

 下面的文件是 ./Project1/Source1.cpp

工程配置爲:Conguration Properties>General>Configuration Types: Application (.exe)

輸出路徑:./Debug/Project1.exe

 1 #include "stdio.h"
 2 #include "cstdlib"
 3 #pragma comment(lib, "../Debug/Project2.lib")
 4 
 5 #ifdef _MSC_VER
 6 #define DLL_IMPORT extern "C" __declspec( dllimport )
 7 #else
 8 #define DLL_IMPORT
 9 #endif
10 
11 DLL_IMPORT void __stdcall hello_world(void);
12 DLL_IMPORT int my_add(int, int);
13 DLL_IMPORT int my_mod(int, int);
14 DLL_IMPORT bool is_equal(double, double);
15 DLL_IMPORT void my_swap(int*, int*); 
16 DLL_IMPORT void reverse_string(char* const);
17 
18 __declspec(dllimport) char* gl;
19 
20 int main() {
21     int a = 0, b = 1;
22     char s[] = "123456";
23     hello_world();
24     my_swap(&a, &b);
25     reverse_string(s);
26     printf("DLL str gl: %s \n", gl);
27     printf("DLL func my_add: %d\n", my_add(1,2));
28     printf("DLL func my_mod: %d\n", my_mod(9, 8));
29     printf("DLL func my_comp: %s\n", is_equal(1, 1.0001) ? "true":"false");
30     printf("DLL func my_swap: (%d, %d)\n", a, b);
31     printf("DLL func reverse_string: %s\n", s);
32     system("pause");
33 }

 上面的這個例子已經清楚的展現了C/C++如何導出和引用DLL文件。有如下幾點須要注意:

  1. 上面#pragma comment(lib, "../Debug/Project2.lib")中引用的是生成Project2.dll過程當中產生的導出庫,並不是靜態庫。
  2. __declspec聲明只在Windows平臺用,如果引用靜態庫,則不須要__declspec聲明。
  3. 無論動態庫仍是靜態庫,除了用#pragma comment引用lib文件外,還能夠在Conguration Properties>Linker>Input>Additional Dependencies中添加lib文件。
  4. 上面例子中咱們導出和引用均聲明瞭extern "C",表示讓編譯器以C的方式編譯和連接文件。意味着導出的函數不支持重載,且函數調用約定爲C和C++的默認調用約定__cdecl。
  5. DLL_EXPORT void __stdcall hello_world(void)指定了函數使用__stdcall的Calling Convention,該方式聲明優先於編譯器默認的__cdecl方式。
  6. 不一樣的調用約定不只會影響實際的函數調用過程,還會影響編譯輸出函數的命名。好比函數hello_world以__cdecl方式和__stdcall方式輸出到DLL中的函數分別爲hello_world和_hello_world@0。

4.2 python引用DLL

先使用VS自帶的dumpbin工具看一下Project2.dll文件部份內容:

dumpbin -exports "./Debug/project2.dll"

ordinal hint RVA      name

1    0 00018000 ?gl@@3PADA
2    1 00011217 _hello_world@0
3    2 00011046 is_equal
4    3 0001109B my_add
5    4 000112D0 my_mod
6    5 00011005 my_swap
7    6 0001118B reverse_string

 話很少說,先上代碼:

 1 import ctypes, platform, time
 2 if platform.system() == 'Windows':
 3     my_lib = ctypes.cdll.LoadLibrary(r'.\Debug\Project2.dll')
 4     # my_lib = ctypes.CDLL(r'.\Debug\Project2.dll')
 5 elif platform.system() == 'Linux':
 6     my_lib = ctypes.cdll.LoadLibrary('libc.so.6')
 7 
 8 # [C++] __declspec(dllexport) char* gl = "gl_str";
 9 print ctypes.c_char_p.in_dll(my_lib, '?gl@@3PADA').value    # result: gl_str 10 
11 # [C++] DLL_IMPORT void __stdcall hello_world(void);
12 getattr(my_lib, '_hello_world@0')()    # result: gl_str Hello world! 13 
14 # [C++] DLL_IMPORT int my_add(int, int);
15 print my_lib.my_add(1, 2)         # result: 3                 
16 
17 # [C++] DLL_IMPORT int my_mod(int, int);
18 print my_lib.my_mod(123, 200)    # result: 123 19 
20 # [C++] DLL_IMPORT void my_swap(int*, int*); 
21 a, b = 111, 222
22 pa, pb = ctypes.pointer(ctypes.c_int(a)), ctypes.pointer(ctypes.c_int(b))
23 my_lib.my_swap(pa, pb)
24 print pa.contents.value, pb.contents.value  # result: 222, 111 25 print a, b    # result: 111, 222 26 
27 # [C++] DLL_IMPORT bool is_equal(double, double);
28 my_lib.is_equal.restype = ctypes.c_bool
29 my_lib.is_equal.argtypes = [ctypes.c_double, ctypes.c_double]
30 # print my_lib.is_equal(ctypes.c_double(1.0), ctypes.c_double(1.0001))
31 print my_lib.is_equal(1.0, 1.0001)    # result: True 32 print my_lib.is_equal(1.0, 1.0100)    # result: False 33 
34 # [C++] DLL_IMPORT void reverse_string(char *const);
35 s = "123456"
36 ps = ctypes.pointer(ctypes.c_char_p(s))
37 print ps.contents    # result: c_char_p('123456') 38 my_lib.reverse_string(ctypes.c_char_p(s))
39 print ps.contents, s  # result: c_char_p('654321') 654321

 上面的代碼加上註釋和結果已經很詳細的說明了python引用DLL的過程,限於篇幅,這裏就不在贅述。

有一點須要強調,咱們使用__stdcall方式聲明函數hello_world方式,而且用CDLL方式引入。致使沒法直接用lib.func_name的方式訪問函數hello_world。

若是想要使用my_lib.hello_world的方式調用該函數,只須要使用windll的方式引入DLL,或者使用默認的__cdecl方式聲明hello_world。

5 總結

先來看一下開始提問的問題,部分問題已經在文中說明。

1.python可不能夠引用靜態庫?

首先,靜態庫是會在連接的過程組裝到可執行文件中的,靜態庫是C/C++代碼。

其次,python是一種解釋性語言,非靜態語言,不須要編譯連接。

最後,官網好像沒有提供對應的對接模塊。

5.若是調用DLL庫的過程當中出現問題,是咱們調用的問題仍是庫自己的問題?應該怎樣快速排查和定位問題?

python中怎麼定位問題這個很少說。

DLL中的問題可使用VS的attach to process功能,將VS Attach 到當前運行的python程序,而後調用到DLL,加斷點。

6.有沒有什麼現有的框架可以幫咱們處理python中引用第三方庫的問題呢?

經常使用的有ctypes,swig, cython, boost.python等

7.對於自定義的類型(class 和 struct)是否能在python中被引用。

至少ctypes中沒有相關的操做。

其實也不必,由於不只python中沒有對應的類型,並且徹底能夠經過將自定義的類或者結構體封裝在DLL輸出的函數接口中進行訪問等操做。

總結:

本文使用python自帶的庫ctypes介紹了若是引用動態庫DLL文件,相對於其餘的第三方庫,這是一個相對比較低級的DLL包裝庫。但正是由於這樣咱們才能看清楚調用DLL過程的一些細節。使用ctypes過程遇到的每個錯誤均可能是一個咱們未知的知識點,所以建議先熟悉該庫,儘量深刻的瞭解一下python調用動態庫的過程。其餘的庫原理是同樣的,只不過進行了更高級的封裝而已。

相關文章
相關標籤/搜索