項(xiàng)目中可能會(huì)經(jīng)常用到第三方庫(kù),主要是出于程序效率考慮和節(jié)約開發(fā)時(shí)間避免重復(fù)造輪子。無論第三方庫(kù)開源與否,編程語言是否與當(dāng)前項(xiàng)目一致,我們最終的目的是在當(dāng)前編程環(huán)境中調(diào)用庫(kù)中的方法并得到結(jié)果或者借助庫(kù)中的模塊實(shí)現(xiàn)某種功能。這個(gè)過程會(huì)牽涉到很多東西,本篇文章將簡(jiǎn)要的介紹一下該過程的一些問題。
1.背景
多語言混合編程可以彌補(bǔ)某一種編程語言在性能表現(xiàn)或者是功能等方面的不足。雖然所有的高級(jí)語言都會(huì)最終轉(zhuǎn)換成匯編指令或者最底層的機(jī)器指令,但是語言本身之間的千差萬別很難一言以蔽之,這對(duì)不同語言之間相互通信造成很大的障礙。
工作中需要用python完成一項(xiàng)功能,但是所有現(xiàn)有的python庫(kù)都不滿足需求。最終找到了一個(gè)開源的C++庫(kù),編譯得到動(dòng)態(tài)庫(kù)被python調(diào)用才完成工作需求。雖然整個(gè)過程耗時(shí)不多,但是期間碰到很多的問題,而且這些問題都很有思考價(jià)值。
除了這篇博文外,后續(xù)還將有一到兩篇文章通過具體的實(shí)例講解一下跨語言調(diào)用。
2.問題思考
在進(jìn)行具體的介紹之前,先來思考一下調(diào)用外部庫(kù)或者自己實(shí)現(xiàn)庫(kù)所牽涉的一些一般性的問題。這樣或許實(shí)際中操作使用時(shí)會(huì)理解的更加深刻,遇到問題也能夠逐項(xiàng)的排查。
如果用C語言寫的庫(kù)調(diào)用了Linux的system call,縱使C本身是跨平臺(tái)的,那么該庫(kù)也不可能在Window上被使用,即便我們能拿到源碼。這里有兩個(gè)核心問題:
- 是否開源
- 是否跨平臺(tái)
如果庫(kù)的實(shí)現(xiàn)不依賴平臺(tái),且開源,那就意味著很大可能能在當(dāng)前項(xiàng)目中使用。為什么是可能,因?yàn)榧词箮?kù)的實(shí)現(xiàn)語言和當(dāng)前項(xiàng)目語言一致,也可能因?yàn)檎Z言版本差異或者標(biāo)準(zhǔn)迭代導(dǎo)致不兼容。
最差的情況就是只能拿到編譯后的庫(kù)文件,且需在特定的平臺(tái)運(yùn)行。
作為庫(kù)的開發(fā)者,最好是能夠開源且?guī)斓膶?shí)現(xiàn)不依賴于特定的平臺(tái),這樣才能最大限度的被使用。
作為庫(kù)的使用者,最不理想的情況是庫(kù)可以在當(dāng)前平臺(tái)使用,但是只能拿到靜態(tài)庫(kù)或者動(dòng)態(tài)庫(kù),且?guī)斓膶?shí)現(xiàn)語言和當(dāng)前項(xiàng)目語言不一致。
多數(shù)情況是第三方庫(kù)是跨平臺(tái)的且能夠拿到源代碼。這樣的話如果兩者的實(shí)現(xiàn)語言一致,我們可以直接將第三方庫(kù)的代碼移植到當(dāng)前的項(xiàng)目中;如果實(shí)現(xiàn)語言不一致,需要在當(dāng)前平臺(tái)上將庫(kù)的源碼編譯出當(dāng)前平臺(tái)上可用的庫(kù)文件,然后在當(dāng)前項(xiàng)目中引用編譯生成的庫(kù)文件。
本文將先簡(jiǎn)單的介紹在window平臺(tái)上,使用python 2.7 自帶的ctypes庫(kù)引用標(biāo)準(zhǔn)的C動(dòng)態(tài)庫(kù)msvcrt.dll。這里可以先思考以下幾個(gè)問題:
- python可不可以引用靜態(tài)庫(kù)?
- python中怎么拿到DLL導(dǎo)出的函數(shù)?
- python和C/C++之間的變量的類型怎樣轉(zhuǎn)換,如果是自定義的類型呢?
- 怎么處理函數(shù)調(diào)用約定(calling convention,eg:__cdecl,__stdcall,__thiscall,__fastcall)可能不同的問題?
- 如果調(diào)用DLL庫(kù)的過程中出現(xiàn)問題,是我們調(diào)用的問題還是庫(kù)本身的問題?應(yīng)該怎樣快速排查和定位問題?
- 有沒有什么現(xiàn)有的框架能夠幫我們處理python中引用第三方庫(kù)的問題呢?
- 對(duì)于自定義的類型(class 和 struct)是否能在python中被引用。
關(guān)于函數(shù)調(diào)用約定,有必要簡(jiǎn)單的提一下:
Calling Convention和具體的編程語言無關(guān),是由編譯器、連接器和操作系統(tǒng)平臺(tái)這些因素共同決定的。
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的官方解釋。注意最后一句話,表示對(duì)于函數(shù)調(diào)用,在平臺(tái)不支持的情況下,語言中指定關(guān)鍵字或者編譯器轉(zhuǎn)換均可能無效。
接下的介紹中來我們將一一回答上面的問題。
3.導(dǎo)入C標(biāo)準(zhǔn)動(dòng)態(tài)庫(kù)
先來簡(jiǎn)單看一下python中如何引用C的標(biāo)準(zhǔn)動(dòng)態(tài)庫(kù)。
[
](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> 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</pre>

](javascript:void(0); "復(fù)制代碼")
接下來我們一一分析上面的這段代碼。
3.1 加載庫(kù)的方式
根據(jù)當(dāng)前平臺(tái)分別加載Windows和Linux上的C的標(biāo)準(zhǔn)動(dòng)態(tài)庫(kù)msvcrt.dll和libc.so.6。
注意這里我們使用的ctypes.cdll來load動(dòng)態(tài)庫(kù),實(shí)際上ctypes中總共有以下四種方式加載動(dòng)態(tài)庫(kù):
-
class
ctypes.CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False) -
class
ctypes.OleDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False) -
class
ctypes.WinDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False) -
class
ctypes.PyDLL(name, mode=DEFAULT_MODE, handle=None)
關(guān)于這幾個(gè)加載動(dòng)態(tài)庫(kù)的方式區(qū)別細(xì)節(jié)可以參考一下官網(wǎng)的說明,這里僅簡(jiǎn)要說明一下。
除了PyDll用于直接調(diào)用Python C api函數(shù)之外,其他的三個(gè)主要區(qū)別在于
- 使用的平臺(tái);
- 被加載動(dòng)態(tài)庫(kù)中函數(shù)的調(diào)用約定(calling convention);
- 庫(kù)中函數(shù)假定的默認(rèn)返回值。
也就是平臺(tái)和被加載動(dòng)態(tài)庫(kù)中函數(shù)的調(diào)用約定決定了我們應(yīng)該使用哪種方式加載動(dòng)態(tài)庫(kù)。
本例中我們?cè)趙indows平臺(tái)上使用的是CDLL而不是WinDll,原因是msvcrt.dll中函數(shù)調(diào)用約定是C/C++默認(rèn)的調(diào)用約定__cdecl。
而WinDll雖然是可以應(yīng)用于windows平臺(tái)上,但是其只能加載標(biāo)準(zhǔn)函數(shù)調(diào)用約定為__stdcall的動(dòng)態(tài)庫(kù)。因此這里只能使用CDLL方式。
可以將上面的CDLL換成WinDll看一下會(huì)不會(huì)有問題。這里應(yīng)該能夠?qū)瘮?shù)調(diào)用理解的更加深刻一些了,同時(shí)也回答了上面第一小節(jié)中我們提問的問題4。
3.2 跨語言類型轉(zhuǎn)換
這里主要針對(duì)第一節(jié)提出的問題3。
我們是在python中調(diào)用C的函數(shù),函數(shù)實(shí)參是python類型的變量,函數(shù)形參則是C類型的變量,顯然我們將python類型的變量直接賦值給C類型的變量肯定會(huì)有問題的。
因此這里需要兩種語言變量類型之間有一一轉(zhuǎn)換的必要。這里僅僅列出部分對(duì)應(yīng)關(guān)系(由于博客園的表格顯示會(huì)有問題,因此這樣列出,請(qǐng)見諒):
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類型的一種轉(zhuǎn)換關(guān)系。
在看一下上面的例子Example 1。在調(diào)用C的函數(shù)時(shí),我們傳給C函數(shù)的實(shí)參需要經(jīng)過Ctypes轉(zhuǎn)換成C類型之后才能正確的調(diào)用C的函數(shù)。
3.3 設(shè)定C函數(shù)的返回類型
看一下上面的例子Example 2.
libc.sin.restype = ctypes.c_double
我們通過restype的方式指定了C(math 模塊)函數(shù)sin的返回類型為double,對(duì)應(yīng)到python即為float。顯然函數(shù)的返回類型在DLL中是無法獲取的。
開發(fā)人員也只能從庫(kù)的說明文檔或者頭文件中獲取到函數(shù)的聲明,進(jìn)而指定函數(shù)返回值的類型。
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">double sin (double x); float sin (float x); long double sin (long double x); double sin (T x); // additional overloads for integral types</pre>
上面是C++11中cmath中sin函數(shù)的聲明。這里幾個(gè)sin函數(shù)是C++中的函數(shù)重載。
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; color: rgb(0, 0, 0); font-size: 14px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: left; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">libc.sin(ctypes.c_double(30 * 3.14 / 180))</pre>
由于調(diào)用之前指定了sin函數(shù)的返回類型ctypes.c_double,因此sin的調(diào)用結(jié)果在python中最終會(huì)轉(zhuǎn)換為float類型。
3.4 假定的函數(shù)返回類型
由于我們?cè)趧?dòng)態(tài)庫(kù)中獲取的函數(shù)并不知道其返回類型,因?yàn)槲覀冎坏玫搅撕瘮?shù)的實(shí)現(xiàn),并沒有函數(shù)的聲明。
在沒有指定庫(kù)函數(shù)返回類型的情況下,ctypes.``CDLL``和ctyps.WinDll均假定函數(shù)返回類型是int,而ctypes.oleDll則假定函數(shù)返回值是Windows HRESULT。
那如果函數(shù)實(shí)際的返回值不是int,便會(huì)按照int返回值處理。如果返回類型能轉(zhuǎn)為int類型是可以的,如果不支持那函數(shù)調(diào)用的結(jié)果會(huì)是一個(gè)莫名其妙的數(shù)字。
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">time_t time (time_t* timer);</pre>
上面的例子Example 4則默認(rèn)將C類型time_t轉(zhuǎn)為了python 的int類型,結(jié)果是正確的。
對(duì)于Example 3中我們不僅要指定函數(shù)pow的返回類型,還要轉(zhuǎn)換函數(shù)的實(shí)參(這里很容易疏忽)。
因此在調(diào)用動(dòng)態(tài)庫(kù)之前一定要看下函數(shù)聲明,指定函數(shù)返回類型。
到這里很容易想到可以指定函數(shù)的返回值類型,那能不能指定函數(shù)形參的類型呢?答案是肯定的,argtypes 。
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">printf.argtypes = [c_char_p, c_char_p, c_int, c_double]</pre>
3.5 可變string buffer
上面的例子Exapmle 5中我們調(diào)用了C中的一個(gè)字符串拷貝函數(shù)strcpy,這里函數(shù)的返回值和被拷貝的對(duì)象均為正確的。
但是這里是故意這樣寫的,因?yàn)檫@里會(huì)有一個(gè)問題。
如果res = 'Hello'改為res = 'He'和res = 'HelloWorld',那么實(shí)際上res的結(jié)果會(huì)是‘Wo’和'World\x00orld'。
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">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'</pre>
這里我們可以通過ctypes.create_string_buffer來指定一個(gè)字符串緩存區(qū)。
使用string buffer改寫Example 5:
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">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'</pre>
注意上面的res的類型是c_char_Array_xxx。這里只是為了介紹string buffer,實(shí)際上不會(huì)這么用。
3.6 小節(jié)
這里簡(jiǎn)單的介紹了一下ctypes如何和動(dòng)態(tài)庫(kù)打交道。限于篇幅還有指針,引用類型和數(shù)組等的傳遞,以及自定義類型等沒有介紹。但是這一小結(jié)應(yīng)該能對(duì)python引用動(dòng)態(tài)庫(kù)過程有一個(gè)大致的認(rèn)識(shí)。
更加詳細(xì)信息可以參考官網(wǎng):ctypes
4. 自定義DLL文件導(dǎo)入
為了更好的理解python調(diào)用DLL的過程,有必要了解一下DLL的定義文件。
4.1 C/C++引用DLL
首先,作為對(duì)比我們看一下C/C++如何引用DLL文件的。下面的文件是* .*/Project2/Source2.cpp
工程配置為:Conguration Properties>General>Configuration Types: Dynamic Library (.dll)
輸出路徑:./Debug/Project2.dll
[
](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> 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 }</pre>

](javascript:void(0); "復(fù)制代碼")
下面的文件是* .*/Project1/Source1.cpp
工程配置為:Conguration Properties>General>Configuration Types:** Application (.exe)**
輸出路徑:./Debug/Project1.exe
[
](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> 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 }</pre>

](javascript:void(0); "復(fù)制代碼")
上面的這個(gè)例子已經(jīng)清楚的展示了C/C++如何導(dǎo)出和引用DLL文件。有以下幾點(diǎn)需要注意:
- 上面#pragma comment(lib, "../Debug/Project2.lib")中引用的是生成Project2.dll過程中產(chǎn)生的導(dǎo)出庫(kù),并非靜態(tài)庫(kù)。
- __declspec聲明只在Windows平臺(tái)用,若是引用靜態(tài)庫(kù),則不需要__declspec聲明。
- 不管動(dòng)態(tài)庫(kù)還是靜態(tài)庫(kù),除了用#pragma comment引用lib文件外,還可以在Conguration Properties>Linker>Input>Additional Dependencies中添加lib文件。
- 上面例子中我們導(dǎo)出和引用均聲明了extern "C",表示讓編譯器以C的方式編譯和鏈接文件。意味著導(dǎo)出的函數(shù)不支持重載,且函數(shù)調(diào)用約定為C和C++的默認(rèn)調(diào)用約定__cdecl。
- DLL_EXPORT void __stdcall hello_world(void)指定了函數(shù)使用__stdcall的Calling Convention,該方式聲明優(yōu)先于編譯器默認(rèn)的__cdecl方式。
- 不同的調(diào)用約定不僅會(huì)影響實(shí)際的函數(shù)調(diào)用過程,還會(huì)影響編譯輸出函數(shù)的命名。比如函數(shù)hello_world以__cdecl方式和__stdcall方式輸出到DLL中的函數(shù)分別為hello_world和_hello_world@0。
4.2 python引用DLL
先使用VS自帶的dumpbin工具看一下Project2.dll文件部分內(nèi)容:
dumpbin -exports "./Debug/project2.dll"
[
](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">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</pre>
[
](javascript:void(0); "復(fù)制代碼")
話不多說,先上代碼:
[
](javascript:void(0); "復(fù)制代碼")
<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;"> 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</pre>

](javascript:void(0); "復(fù)制代碼")
上面的代碼加上注釋和結(jié)果已經(jīng)很詳細(xì)的說明了python引用DLL的過程,限于篇幅,這里就不在贅述。
有一點(diǎn)需要強(qiáng)調(diào),我們使用__stdcall方式聲明函數(shù)hello_world方式,并且用CDLL方式引入。導(dǎo)致無法直接用lib.func_name的方式訪問函數(shù)hello_world。
如果想要使用my_lib.hello_world的方式調(diào)用該函數(shù),只需要使用windll的方式引入DLL,或者使用默認(rèn)的__cdecl方式聲明hello_world。
5 總結(jié)
先來看一下開始提問的問題,部分問題已經(jīng)在文中說明。
1.python可不可以引用靜態(tài)庫(kù)?
首先,靜態(tài)庫(kù)是會(huì)在鏈接的過程組裝到可執(zhí)行文件中的,靜態(tài)庫(kù)是C/C++代碼。
其次,python是一種解釋性語言,非靜態(tài)語言,不需要編譯鏈接。
最后,官網(wǎng)好像沒有提供對(duì)應(yīng)的對(duì)接模塊。
5.如果調(diào)用DLL庫(kù)的過程中出現(xiàn)問題,是我們調(diào)用的問題還是庫(kù)本身的問題?應(yīng)該怎樣快速排查和定位問題?
python中怎么定位問題這個(gè)不多說。
DLL中的問題可以使用VS的attach to process功能,將VS Attach 到當(dāng)前運(yùn)行的python程序,然后調(diào)用到DLL,加斷點(diǎn)。
6.有沒有什么現(xiàn)有的框架能夠幫我們處理python中引用第三方庫(kù)的問題呢?
常用的有ctypes,swig, cython, boost.python等
7.對(duì)于自定義的類型(class 和 struct)是否能在python中被引用。
至少ctypes中沒有相關(guān)的操作。
其實(shí)也沒必要,因?yàn)椴粌Hpython中沒有對(duì)應(yīng)的類型,而且完全可以通過將自定義的類或者結(jié)構(gòu)體封裝在DLL輸出的函數(shù)接口中進(jìn)行訪問等操作。
總結(jié):
本文使用python自帶的庫(kù)ctypes介紹了如果引用動(dòng)態(tài)庫(kù)DLL文件,相對(duì)于其他的第三方庫(kù),這是一個(gè)相對(duì)比較低級(jí)的DLL包裝庫(kù)。但正是因?yàn)檫@樣我們才能看清楚調(diào)用DLL過程的一些細(xì)節(jié)。使用ctypes過程遇到的每一個(gè)錯(cuò)誤都可能是一個(gè)我們未知的知識(shí)點(diǎn),因此建議先熟悉該庫(kù),盡可能深入的了解一下python調(diào)用動(dòng)態(tài)庫(kù)的過程。其他的庫(kù)原理是一樣的,只不過進(jìn)行了更高級(jí)的封裝而已。