python與C語言呼叫模組 ctypes的詳解
ctypes
ctypes是python的一個函式庫,提供和C語言相容的資料型別,可以直接呼叫動態連結庫中的匯出函式。
為了使用ctypes,必須依次完成以下步驟:
- 載入動態連結庫
- 將python物件轉換成ctypes所能識別的引數
- 使用ctypes所能識別的引數呼叫動態連結庫中的函式
動態連結庫載入方式有三種:
- cdll
- windll
- oledll
它們的不同之處在於:動態連結庫中的函式所遵守的函式呼叫方式(calling convention
)以及返回方式有所不同。cdll
用於載入遵循cdecl
呼叫約定的動態連結庫,windll
用於載入遵循stdcall
呼叫約定的動態連結庫,oledll
windll
完全相同,只是會預設其載入的函式統一返回一個Windows HRESULT錯誤編碼。
函式呼叫約定:函式呼叫約定指的是函式引數入棧的順序、哪些引數入棧、哪些通過暫存器傳值、函式返回時棧幀的回收方式(是由呼叫者負責清理,還是被呼叫者清理)、函式名稱的修飾方法等等。常見的呼叫約定有cdecl和stdcall兩種。在《程式設計師的自我修養--連結、裝載與庫》一書的第10章有對函式呼叫約定的更詳細介紹。 cdecl規定函式引數列表以從右到左的方式入棧,且由函式的呼叫者負責清除棧幀上的引數。stdcall的引數入棧方式與cdecl一致,但函式返回時是由被呼叫者自己負責清理棧幀。而且stdcall是Win32 API函式所使用的呼叫約定。
例子:
Linux下:
或者:
其他例子:
一個完整的例子:
1,編寫動態連結庫
// filename: foo.c #include "stdio.h" char* myprint(char *str) { puts(str); return str; } float add(float a, float b) { return a + b; }
將foo.c編譯為動態連結庫:gcc -fPIC -shared foo.c -o foo.so
2.使用ctypes呼叫foo.so
#coding:utf8 #FILENAME:foo.py from ctypes import * foo = CDLL('./foo.so') myprint = foo.myprint myprint.argtypes = [POINTER(c_char)] # 引數型別為char指標 myprint.restype = c_char_p # 返回型別為char指標 res = myprint('hello ctypes') print(res) add = foo.add add.argtypes = [c_float, c_float] # 引數型別為兩個float add.restype = c_float # 返回型別為float print(add(1.3, 1.2))
執行:
[jingjiang@iZ255w0dc5eZ test]$ python2.6 foo.py hello ctypes hello ctypes 2.5
ctypes資料型別和C資料型別對照表
查詢動態連結庫
>>> from ctypes.util import find_library >>> find_library("m") 'libm.so.6' >>> find_library("c") 'libc.so.6' >>> find_library("bz2") 'libbz2.so.1.0'
函式返回型別
函式預設返回 C int 型別,如果需要返回其他型別,需要設定函式的 restype 屬性。
>>> from ctypes import * >>> from ctypes.util import find_library >>> libc = cdll.LoadLibrary(find_library("c")) >>> strchr = libc.strchr >>> strchr("abcdef", ord("d")) -808023673 >>> strchr.restype = c_char_p >>> strchr("abcdef", ord("d")) 'def' >>> strchr("abcdef", ord("x"))
回撥函式
- 定義回撥函式型別,類似於c中的函式指標,比如:void (*callback)(void* arg1, void* arg2),定義為:callack = CFUNCTYPE(None, cvoidp, cvoidp)
None表示返回值是void,也可以是其他型別。剩餘的兩個引數與c中的回撥引數一致。 - 定義python回撥函式:
def _callback(arg1, arg2): #do sth # ... #return sth
- 註冊回撥函式:
cb = callback(_callback)
另外,使用ctypes可以避免GIL的問題。
一個例子:
//callback.c #include "stdio.h" void showNumber(int n, void (*print)()) { (*print)(n); }
編譯成動態連結庫:gcc -fPIC -shared -o callback.so callback.c
編寫測試程式碼:
#FILENAME:callback.py from ctypes import * _cb = CFUNCTYPE(None, c_int) def pr(n): print 'this is : %d' % n cb = _cb(pr) callback = CDLL("./callback.so") showNumber = callback.showNumber showNumber.argtypes = [c_int, c_void_p] showNumber.restype = c_void_p for i in range(10): showNumber(i, cb)
執行:
$ python2.7 callback.py this is : 0 this is : 1 this is : 2 this is : 3 this is : 4 this is : 5 this is : 6 this is : 7 this is : 8 this is : 9
結構體和聯合
union(聯合體 共用體) 1、union中可以定義多個成員,union的大小由最大的成員的大小決定。 2、union成員共享同一塊大小的記憶體,一次只能使用其中的一個成員。 3、對某一個成員賦值,會覆蓋其他成員的值(也不奇怪,因為他們共享一塊記憶體。但前提是成員所佔位元組數相同,當成員所佔位元組數不同時只會覆蓋相應位元組上的值,>比如對char成員賦值就不會把整個int成員覆蓋掉,因為char只佔一個位元組,而int佔四個位元組) 4、聯合體union的存放順序是所有成員都從低地址開始存放的。
結構體和聯合必須從Structure和Union繼承,子類必須定義__fields__
屬性,__fields__
屬性必須是一個二元組的列表,包含field的名稱和field的型別,field型別必須是一個ctypes的型別,例如:c_int, 或者其他繼承自ctypes的型別,例如:結構體,聯合,陣列,指標。
from ctypes import * class Point(Structure): __fields__ = [ ("x", c_int), ("y", c_int), ] def __str__(self): return "x={0.x}, y={0.y}".format(self) point1 = Point(x=10, y=20) print "point1:", point1 class Rect(Structure): __fields__ = [ ("upperleft", Point), ("lowerright", Point), ] def __str__(self): return "upperleft:[{0.upperleft}], lowerright:[{0.lowerright}]".format(self) rect1 = Rect(upperleft=Point(x=1, y=2), lowerright=Point(x=3, y=4)) print "rect1:", rect1
執行:
python test.py point1: x=10, y=20 rect1: upperleft:[x=1, y=2], lowerright:[x=3, y=4]
陣列
陣列定義很簡單,比如:定義一個有10個Point元素的陣列,TenPointsArrayType = Point * 10
。
初始化和使用陣列:
from ctypes import * TenIntegersArrayType = c_int * 10 array1 = TenIntegersArrayType(*range(1, 11))print array1 for i in array1: print i
執行:
$ python2.7 array.py <__main__.c_int_Array_10 object at 0x7fad0d7394d0> 1 2 3 4 5 6 7 8 9 10
指標
pointer()可以建立一個指標,Pointer例項有一個contents屬性,返回指標指向的內容。
>>> from ctypes import * >>> i = c_int(42) >>> p = pointer(i) >>> p<__main__.LP_c_int object at 0x7f413081d560> >>> p.contents c_int(42) >>>
可以改變指標指向的內容
>>> i = c_int(99) >>> p.contents = i >>> p.contents c_int(99)
可以按陣列的方式訪問,並改變值
>>> p[0] 99 >>> p[0] = 22 >>> i c_int(22)
傳遞指標或引用
很多情況下,c函式需要傳遞指標或引用,ctypes也完美支援這一點。
byref()用來傳遞引用引數,pointer()也可以完成同樣的工作,但是pointer會建立一個實際的指標物件,如果你不需要一個指標物件,用byref()會快很多。
>>> from ctypes import * >>> i = c_int() >>> f = c_float() >>> s = create_string_buffer('\000' * 32) >>> print i.value, f.value, repr(s.value) 0 0.0 '' >>> libc = CDLL("libc.so.6") >>> libc.sscanf("1 3.14 Hello", "%d %f %s", byref(i), byref(f), s) 3 >>> print i.value, f.value, repr(s.value) 1 3.1400001049 'Hello'
可改變內容的字串
如果需要可改變內容的字串,需要使用 createstringbuffer()
>>> from ctypes import * >>> p = create_string_buffer(3) # create a 3 byte buffer, initialized to NUL bytes >>> print sizeof(p), repr(p.raw) 3 '/x00/x00/x00'>>> p = create_string_buffer("Hello") # create a buffer containing a NUL terminated string >>> print sizeof(p), repr(p.raw) 6 'Hello/x00' >>> print repr(p.value) 'Hello' >>> p = create_string_buffer("Hello", 10) # create a 10 byte buffer >>> print sizeof(p), repr(p.raw) 10 'Hello/x00/x00/x00/x00/x00' >>> p.value = "Hi" >>> print sizeof(p), repr(p.raw) 10 'Hi/x00lo/x00/x00/x00/x00/x00' >>>
賦值給c_char_p
,c_wchar_p
,c_void_p
只改變他們指向的記憶體地址,而不是改變記憶體的內容
>>> s = "Hello, World" >>> c_s = c_char_p(s) >>> print c_s c_char_p('Hello, World')>>> c_s.value = "Hi, there" >>> print c_s c_char_p('Hi, there') >>> print s # first string is unchanged Hello, World >>>
資料都可以改變
>>> i = c_int(42) >>> print i c_long(42) >>> print i.value42 >>> i.value = -99 >>> print i.value -99 >>>
使用中遇到的一些問題
1:當動態庫的匯出函式返回char *
的時候,如何釋放記憶體
如果把restype
設定為c_char_p
,ctypes會返回一個常規的Python字串物件。一種簡單的方式就是使用void *
和強制轉換結果。
string.c:
#include #include #include char *get(void) { char *buf = "Hello World"; char *new_buf = strdup(buf); printf("allocated address: %p\n", new_buf); return new_buf; } void freeme(char *ptr) { printf("freeing address: %p\n", ptr); free(ptr); }
Python使用:
from ctypes import * lib = cdll.LoadLibrary('./string.so') lib.freeme.argtypes = c_void_p, lib.freeme.restype = None lib.get.argtypes = [] lib.get.restype = c_void_p >>> ptr = lib.get() allocated address: 0x9facad8 >>> hex(ptr) '0x9facad8' >>> cast(ptr, c_char_p).value 'Hello World' >>> lib.freeme(ptr) freeing address: 0x9facad8
也可以使用c_char_p
的子類,因為ctypes
不會對簡單型別的子類呼叫getfunc
:
class c_char_p_sub(c_char_p): pass lib.get.restype = c_char_p_sub
value
屬性會返回字串。在這個例子中,可以把freeme
的引數改為更通用的c_void_p
,它接受任何指標型別或整型地址。
2:如何把含有‘\0’的char*轉換成python字串
How do you convert a char * with 0-value bytes into a python string?
參考資料
- python ctypes庫中動態連結庫載入方式
- 用python ctypes呼叫動態連結庫
- ctypes 使用方法與說明
- C語言union(聯合體 共用體)
- Python ctypes: how to free memory? Getting invalid pointer error
ctypes是python的一個函式庫,提供和C語言相容的資料型別,可以直接呼叫動態連結庫中的匯出函式。
為了使用ctypes,必須依次完成以下步驟:
- 載入動態連結庫
- 將python物件轉換成ctypes所能識別的引數
- 使用ctypes所能識別的引數呼叫動態連結庫中的函式
動態連結庫載入方式有三種:
- cdll
- windll
- oledll
它們的不同之處在於:動態連結庫中的函式所遵守的函式呼叫方式(calling convention
)以及返回方式有所不同。cdll
用於載入遵循cdecl
呼叫約定的動態連結庫,windll
用於載入遵循stdcall
呼叫約定的動態連結庫,oledll
與windll
完全相同,只是會預設其載入的函式統一返回一個Windows HRESULT錯誤編碼。
函式呼叫約定:函式呼叫約定指的是函式引數入棧的順序、哪些引數入棧、哪些通過暫存器傳值、函式返回時棧幀的回收方式(是由呼叫者負責清理,還是被呼叫者清理)、函式名稱的修飾方法等等。常見的呼叫約定有cdecl和stdcall兩種。在《程式設計師的自我修養--連結、裝載與庫》一書的第10章有對函式呼叫約定的更詳細介紹。 cdecl規定函式引數列表以從右到左的方式入棧,且由函式的呼叫者負責清除棧幀上的引數。stdcall的引數入棧方式與cdecl一致,但函式返回時是由被呼叫者自己負責清理棧幀。而且stdcall是Win32 API函式所使用的呼叫約定。
例子:
Linux下:
或者:
其他例子:
一個完整的例子:
1,編寫動態連結庫
// filename: foo.c #include "stdio.h" char* myprint(char *str) { puts(str); return str; } float add(float a, float b) { return a + b; }
將foo.c編譯為動態連結庫:gcc -fPIC -shared foo.c -o foo.so
2.使用ctypes呼叫foo.so
#coding:utf8 #FILENAME:foo.py from ctypes import * foo = CDLL('./foo.so') myprint = foo.myprint myprint.argtypes = [POINTER(c_char)] # 引數型別為char指標 myprint.restype = c_char_p # 返回型別為char指標 res = myprint('hello ctypes') print(res) add = foo.add add.argtypes = [c_float, c_float] # 引數型別為兩個float add.restype = c_float # 返回型別為float print(add(1.3, 1.2))
執行:
[jingjiang@iZ255w0dc5eZ test]$ python2.6 foo.py hello ctypes hello ctypes 2.5
ctypes資料型別和C資料型別對照表
查詢動態連結庫
>>> from ctypes.util import find_library >>> find_library("m") 'libm.so.6' >>> find_library("c") 'libc.so.6' >>> find_library("bz2") 'libbz2.so.1.0'
函式返回型別
函式預設返回 C int 型別,如果需要返回其他型別,需要設定函式的 restype 屬性。
>>> from ctypes import * >>> from ctypes.util import find_library >>> libc = cdll.LoadLibrary(find_library("c")) >>> strchr = libc.strchr >>> strchr("abcdef", ord("d")) -808023673 >>> strchr.restype = c_char_p >>> strchr("abcdef", ord("d")) 'def' >>> strchr("abcdef", ord("x"))
回撥函式
- 定義回撥函式型別,類似於c中的函式指標,比如:void (*callback)(void* arg1, void* arg2),定義為:callack = CFUNCTYPE(None, cvoidp, cvoidp)
None表示返回值是void,也可以是其他型別。剩餘的兩個引數與c中的回撥引數一致。 - 定義python回撥函式:
def _callback(arg1, arg2): #do sth # ... #return sth
- 註冊回撥函式:
cb = callback(_callback)
另外,使用ctypes可以避免GIL的問題。
一個例子:
//callback.c #include "stdio.h" void showNumber(int n, void (*print)()) { (*print)(n); }
編譯成動態連結庫:gcc -fPIC -shared -o callback.so callback.c
編寫測試程式碼:
#FILENAME:callback.py from ctypes import * _cb = CFUNCTYPE(None, c_int) def pr(n): print 'this is : %d' % n cb = _cb(pr) callback = CDLL("./callback.so") showNumber = callback.showNumber showNumber.argtypes = [c_int, c_void_p] showNumber.restype = c_void_p for i in range(10): showNumber(i, cb)
執行:
$ python2.7 callback.py this is : 0 this is : 1 this is : 2 this is : 3 this is : 4 this is : 5 this is : 6 this is : 7 this is : 8 this is : 9
結構體和聯合
union(聯合體 共用體) 1、union中可以定義多個成員,union的大小由最大的成員的大小決定。 2、union成員共享同一塊大小的記憶體,一次只能使用其中的一個成員。 3、對某一個成員賦值,會覆蓋其他成員的值(也不奇怪,因為他們共享一塊記憶體。但前提是成員所佔位元組數相同,當成員所佔位元組數不同時只會覆蓋相應位元組上的值,>比如對char成員賦值就不會把整個int成員覆蓋掉,因為char只佔一個位元組,而int佔四個位元組) 4、聯合體union的存放順序是所有成員都從低地址開始存放的。
結構體和聯合必須從Structure和Union繼承,子類必須定義__fields__
屬性,__fields__
屬性必須是一個二元組的列表,包含field的名稱和field的型別,field型別必須是一個ctypes的型別,例如:c_int, 或者其他繼承自ctypes的型別,例如:結構體,聯合,陣列,指標。
from ctypes import * class Point(Structure): __fields__ = [ ("x", c_int), ("y", c_int), ] def __str__(self): return "x={0.x}, y={0.y}".format(self) point1 = Point(x=10, y=20) print "point1:", point1 class Rect(Structure): __fields__ = [ ("upperleft", Point), ("lowerright", Point), ] def __str__(self): return "upperleft:[{0.upperleft}], lowerright:[{0.lowerright}]".format(self) rect1 = Rect(upperleft=Point(x=1, y=2), lowerright=Point(x=3, y=4)) print "rect1:", rect1
執行:
python test.py point1: x=10, y=20 rect1: upperleft:[x=1, y=2], lowerright:[x=3, y=4]
陣列
陣列定義很簡單,比如:定義一個有10個Point元素的陣列,TenPointsArrayType = Point * 10
。
初始化和使用陣列:
from ctypes import * TenIntegersArrayType = c_int * 10 array1 = TenIntegersArrayType(*range(1, 11))print array1 for i in array1: print i
執行:
$ python2.7 array.py <__main__.c_int_Array_10 object at 0x7fad0d7394d0> 1 2 3 4 5 6 7 8 9 10
指標
pointer()可以建立一個指標,Pointer例項有一個contents屬性,返回指標指向的內容。
>>> from ctypes import * >>> i = c_int(42) >>> p = pointer(i) >>> p<__main__.LP_c_int object at 0x7f413081d560> >>> p.contents c_int(42) >>>
可以改變指標指向的內容
>>> i = c_int(99) >>> p.contents = i >>> p.contents c_int(99)
可以按陣列的方式訪問,並改變值
>>> p[0] 99 >>> p[0] = 22 >>> i c_int(22)
傳遞指標或引用
很多情況下,c函式需要傳遞指標或引用,ctypes也完美支援這一點。
byref()用來傳遞引用引數,pointer()也可以完成同樣的工作,但是pointer會建立一個實際的指標物件,如果你不需要一個指標物件,用byref()會快很多。
>>> from ctypes import * >>> i = c_int() >>> f = c_float() >>> s = create_string_buffer('\000' * 32) >>> print i.value, f.value, repr(s.value) 0 0.0 '' >>> libc = CDLL("libc.so.6") >>> libc.sscanf("1 3.14 Hello", "%d %f %s", byref(i), byref(f), s) 3 >>> print i.value, f.value, repr(s.value) 1 3.1400001049 'Hello'
可改變內容的字串
如果需要可改變內容的字串,需要使用 createstringbuffer()
>>> from ctypes import * >>> p = create_string_buffer(3) # create a 3 byte buffer, initialized to NUL bytes >>> print sizeof(p), repr(p.raw) 3 '/x00/x00/x00'>>> p = create_string_buffer("Hello") # create a buffer containing a NUL terminated string >>> print sizeof(p), repr(p.raw) 6 'Hello/x00' >>> print repr(p.value) 'Hello' >>> p = create_string_buffer("Hello", 10) # create a 10 byte buffer >>> print sizeof(p), repr(p.raw) 10 'Hello/x00/x00/x00/x00/x00' >>> p.value = "Hi" >>> print sizeof(p), repr(p.raw) 10 'Hi/x00lo/x00/x00/x00/x00/x00' >>>
賦值給c_char_p
,c_wchar_p
,c_void_p
只改變他們指向的記憶體地址,而不是改變記憶體的內容
>>> s = "Hello, World" >>> c_s = c_char_p(s) >>> print c_s c_char_p('Hello, World')>>> c_s.value = "Hi, there" >>> print c_s c_char_p('Hi, there') >>> print s # first string is unchanged Hello, World >>>
資料都可以改變
>>> i = c_int(42) >>> print i c_long(42) >>> print i.value42 >>> i.value = -99 >>> print i.value -99 >>>
使用中遇到的一些問題
1:當動態庫的匯出函式返回char *
的時候,如何釋放記憶體
如果把restype
設定為c_char_p
,ctypes會返回一個常規的Python字串物件。一種簡單的方式就是使用void *
和強制轉換結果。
string.c:
#include #include #include char *get(void) { char *buf = "Hello World"; char *new_buf = strdup(buf); printf("allocated address: %p\n", new_buf); return new_buf; } void freeme(char *ptr) { printf("freeing address: %p\n", ptr); free(ptr); }
Python使用:
from ctypes import * lib = cdll.LoadLibrary('./string.so') lib.freeme.argtypes = c_void_p, lib.freeme.restype = None lib.get.argtypes = [] lib.get.restype = c_void_p >>> ptr = lib.get() allocated address: 0x9facad8 >>> hex(ptr) '0x9facad8' >>> cast(ptr, c_char_p).value 'Hello World' >>> lib.freeme(ptr) freeing address: 0x9facad8
也可以使用c_char_p
的子類,因為ctypes
不會對簡單型別的子類呼叫getfunc
:
class c_char_p_sub(c_char_p): pass lib.get.restype = c_char_p_sub
value
屬性會返回字串。在這個例子中,可以把freeme
的引數改為更通用的c_void_p
,它接受任何指標型別或整型地址。
2:如何把含有‘\0’的char*轉換成python字串
How do you convert a char * with 0-value bytes into a python string?