python與C語言調用模塊 ctypes的詳解


ctypes

ctypes是python的一個函數庫,提供和C語言兼容的數據類型,可以直接調用動態鏈接庫中的導出函數。
為了使用ctypes,必須依次完成以下步驟:

  • 加載動態鏈接庫
  • 將python對象轉換成ctypes所能識別的參數
  • 使用ctypes所能識別的參數調用動態鏈接庫中的函數

動態鏈接庫加載方式有三種:

  • cdll
  • windll
  • oledll

它們的不同之處在於:動態鏈接庫中的函數所遵守的函數調用方式(calling convention)以及返回方式有所不同。
cdll用於加載遵循cdecl調用約定的動態鏈接庫,windll用於加載遵循stdcall調用約定的動態鏈接庫,oledllwindll完全相同,只是會默認其載入的函數統一返回一個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_pc_wchar_pc_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?


參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM