其實python和c混合調用的方法很多,如swig、cpython等等,但這些都不是標準庫,需要額外安裝的,本文講的是標準庫的ctypes來調用c,實現強大的功能,沒辦法霸道C\C++就是那麼強大,不服不行,有那種語言是無法調用C的,沒有吧。
本文既不是掃盲也不是hello,world之類的,期初我百度了python通過ctypes封裝調用c,全是千萬一律的,一段基礎代碼拷貝了無數次,所以這次搞全面的,直接封裝,當然本人也剛接觸python語言半月,諸多的東西還不太會用,寫的不好還請見諒。
在瞭解ctypes之前,請一定記住上圖,ctypes基礎類型(其實就是C基礎類型)和python基礎類型的映射,基礎類型太重要了,再複雜的代碼也是由基礎類型構成的,另外補充一點,你有沒有發現上面似乎少了枚舉Enum,對的,就是少了枚舉,其實枚舉就是整形int,有些語言當然也可以指定其他類型,枚舉就是一個基礎類型(如int)可取值的集合,每次用的就裏面一個值而已,所以本文的代碼枚舉都用int,枚舉在c中還是用的挺多的
ctypes調用c或c++只能以動態庫的形式調用,而且必須是動態庫導出的函數纔可以。ctypes導出了 cdll,在windows上還有 windll 和 oledll 對象用於載入動態鏈接庫。
載入動態鏈接庫可以直接存取其屬性。 cdll 載入導出函數符合cdecl調用規範的庫,而 windll 載入導出函數符合 stdcall 調用規範的庫, oledll 也使用 stdcall 調用規範,並假設函數返回Windows的HRESULT錯誤碼。錯誤碼用於在出錯時自動拋出WindowsError這個Python異常。
注意win32系統動態鏈接庫,如kernel32和user32經常同時導出ANSI和UNICODE版本的函數。UNICODE版本的會在名字末尾加"W",而ANSI版本的加上"A"。Win32版本的 GetModuleHandle 函數,返回給定模塊名的句柄,有如下C原型,還有一個宏用於暴露其中一個作爲 GetModuleHandle ,依賴於UNICODE定義與否。
文字和代碼都比較多,貼上要點吧
1.ctypes中的枚舉Enum類型其實就是int
//C 默認0開始
enum Sex {
Boy,
Girl,
};
python可以多種方式定義,如全局變量,class等,還是用Enum吧
from enum import Enum, unique
@unique
class Sex(Enum):
Boy=0
Girl=1
是不是簡單,帶上@unique是python修飾符,讓值唯一的功能
2.ctypes多級指針(指向指針的指針)
**int,別懵,既然*int 在ctypes中爲 POINTER(c_int),那麼想當然**int就是 POINTER(POINTER(c_int)),括號可別打錯了,有幾級就嵌套幾個POINTER()
3.結構體內嵌或者在結構內含有自己的引用(如函數指針傳遞的參數就是結構體自身指針)
struct SoundIo {
void *userdata;
void (*on_devices_change)(struct SoundIo *);
void (*on_backend_disconnect)(struct SoundIo *, int err);
void (*on_events_signal)(struct SoundIo *);
enum SoundIoBackend current_backend;
const char *app_name;
void (*emit_rtprio_warning)(void);
void (*jack_info_callback)(const char *msg);
void (*jack_error_callback)(const char *msg);
};
碰到上面的結構體咋辦,你在想百度一下,百度到例子demo哪有什麼實際用途呀,全是簡單的幾個變量賦值,你要清楚函數指針也是一種指針,要佔用4個字節的,所以千萬別跳過函數指針只寫變量,那樣會粗大事的,按套路來寫,先寫個錯誤的,然後再換個寫個正確的
#這個代碼是錯誤的,千萬別...
class SoundIoStructure(Structure):
_fields_ = [
("userdata", c_void_p),
("on_devices_change", CFUNCTYPE(None, POINTER(SoundIoStructure))),
("on_backend_disconnect", CFUNCTYPE(None, c_void_p, c_int)),
("on_events_signal", CFUNCTYPE(None, c_void_p)),
("current_backend", c_int),
("app_name", c_char_p),
("emit_rtprio_warning", CFUNCTYPE(None)),
("jack_info_callback", CFUNCTYPE(None, c_char_p)),
("jack_error_callback", CFUNCTYPE(None, c_char_p))
]
上面的代碼是錯誤的,千萬別...,後果自負呀,上面的代碼是錯誤的,千萬別...,後果自負呀,重要的事3遍
說說爲什麼吧,python是解釋性語言,默認從上往下解釋的,所以在寫 CFUNCTYPE(None, POINTER(SoundIoStructure)) 時其實這個結構體類時還沒加載完呢,類沒加載完那不就是沒有了,當然報錯了,100%報錯的,信我那該怎麼辦?當然有解決辦法了,換個寫法
class SoundIoStructure(Structure):
# 結構體無法傳入自己,所以採用後面的形式,全局來初始化類變量
pass
SoundIoStructure._fields_ = [
("userdata", c_void_p),
("on_devices_change", CFUNCTYPE(None, POINTER(SoundIoStructure))),
("on_backend_disconnect", CFUNCTYPE(None, c_void_p, c_int)),
("on_events_signal", CFUNCTYPE(None, c_void_p)),
("current_backend", c_int),
("app_name", c_char_p),
("emit_rtprio_warning", CFUNCTYPE(None)),
("jack_info_callback", CFUNCTYPE(None, c_char_p)),
("jack_error_callback", CFUNCTYPE(None, c_char_p))
現在OK了,python屬於動態語言,_fields_是類變量,當然可以創建完後再補充了,現在不就是引用自己的,如果嵌套也是一樣的,另外_fields_是元組,其實就是數組,搞這麼多名字幹啥呀,元祖是不可變對象,不能改變值的,所以元素只能賦值一次,所以不要把_fields_寫在裏面,然後外面填充元素進去,會報錯的,不可能變元素只能改變指向,不能改變元素內容,python中,數字 字符串 元組都是不可變對象,不信你可以試試;
補充1句_fields_是支持位域的,如("current_backend", c_int,1) 佔用1位而已,不再是32位(4字)
4.ctypes函數
POINTER(c_int)=*int 獲取ctypes類型指針表現形式,常用來傳遞參數和返回值,僅限於ctypes類型,就是上圖中的基礎類型,另外還有繼承Structure Union等,python類型不試用的
pointer(obj) 獲取ctypes類型中的地址請使用此方法,千萬不要&,將返回 POINTER(type(obj))
.
byref(obj) 獲取引用,引用和指針的區別就不說了
sizeof(obj) 獲取字節數,僅限ctypes類型
https://docs.python.org/2/library/ctypes.html#utility-functions
5.在結構體中創建實例方法或添加變量,ctypes返回的結構體並不創建實例__init__
千萬別這麼幹,孩子。你會發現c返回的結構體指針(對應我們的類)並沒有創建實例,只是通過類變量複製內容而已,壓根不會創建對象,所以你的實例方法都沒有用,訪問會出錯;唯一例外的就是你自己去創建對象,那麼就按python訪問吧
6.數組
int[5]=c_int*5 數組一片連續存儲的內存單元,此操作僅限ctypes類型
7.BigEndianStructure LittleEndianStructure大小端字節排序的結構體
請自行了解大小端知識,一般情況網絡通用大字節,其他小字節
8.回調函數
CMPFUNC = CFUNCTYPE(返回值, 參數1, 參數2, 參數3...)
CMPFUNC(pyhton函數名稱)
CFUNCTYPE()只要從C代碼中使用對象,請確保保留對對象的引用。ctypes沒有,如果你不這樣做,它們可能被垃圾收集,在回調時崩潰你的程序。
9.*args **kwargs拆包,二次傳遞
如果想對一個接收*args **kwargs的函數進行包裝,就需要進行拆包了,在調用時候傳遞的N個參數會自動封包,所以傳遞時候得拆包,絕對不能傳遞不拆包就直接傳 args kwargs,需要傳遞元參形式:*args **kwargs 實際就是拆包,在golang中叫打散
如ctypes中的CFUNCTYPE函數,原本就接收一個*args **kwargs,這是我們封裝後,傳遞進去的就是*args **kwargs了,比如
比如參數爲爲(1,2,3,4,5,6) 傳給args,這時args實際已經封裝了,拆包後會還原成1,2,3,4,5,6,而不能傳遞未拆包的對象args,在對一個類進行裝飾器函數時也是一樣,需要拆包再調用
def callback_ptr(self, restype, *argtypes, **kw):
"""構建回調函數,需要對 *argtypes, **kw 拆包"""
return CFUNCTYPE(restype, *argtypes, **kw)
def b(*args, **kwargs):
print(args)
print(kwargs)
pass
def a(*args, **kwargs):
return b(*args, **kwargs)
a(1, 3, step=2,sex=2)
(1, 3)
{'step': 2, 'sex': 2}<class 'soundio.LP_SoundIoStructure'>
10.python調用動態鏈接庫dll的約定
ctypes中支持cdll,windll,oledll,一般來說cdll主要用來載入C語言調用方式(cdecl)。windll主要用來載入WIN32調用方式(stdcall),而oledll使用WIN32調用方式(stdcall)且返回值是Windows裏返回的HRESULT值。如果你使用錯了,調用時肯定會報錯的,這涉及到參數入棧順序和清理工作
11.python傳遞結構體指針給c
c系風格很多函數調用都是傳遞一個指針給函數,函數內部爲指針對象賦值,然後返回狀態或不返回數據void。這個和你理解的返回結構體指針是不是不一樣,win32 sdk很多操作都這麼做的,那麼該怎麼做了?很簡單,給我們的結構體創建一個實例,然後用pointer(obj)就能拿到指針了,直接傳遞即可,請務必注意dll調用約定,不然會報錯的
// Device info structure
typedef struct {
#if defined(_WIN32_WCE) || (WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP)
const wchar_t *name; // description
const wchar_t *driver; // driver
#else
const char *name; // description
const char *driver; // driver
#endif
DWORD flags;
} BASS_DEVICEINFO;
//第二個參數需要傳遞一個結構體指針
BOOL BASS_GetDeviceInfo(DWORD device, BASS_DEVICEINFO *info);
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__version__ = "1.0.3"
__author__ = "[email protected]"
import os
from ctypes import *
def cdll_method_find(alisa=""):
def _wrap(func):
method = alisa
if method == "":
method = func.__name__
def _param(*args, **kwargs):
_ins = args[0]
if hasattr(_ins, "_" + method) is False:
setattr(_ins, "_" + method, getattr(_ins.lib, method))
return func(*args, **kwargs)
return _param
return _wrap
class BASS_DEVICEINFO(Structure):
if os.name == "nt":
_fields_ = [
("name", c_wchar_p),
("driver", c_wchar_p),
("flags", c_int32)
]
else:
_fields_ = [
("name", c_char_p),
("driver", c_char_p),
("flags", c_int32)
]
class Bass(object):
__lib = ""
__soundio = None
def __init__(self, lib=""):
"""
:param lib: 動態庫文件名稱,windows爲dll,linux爲so,需要正確填入路徑
"""
if lib == "":
os_name = os.name
if os_name == "nt":
lib = "bass.dll"
elif os_name == "posix":
lib = "/usr/local/lib/bass.so"
assert lib != "", "lib can't null"
if os.name == "nt":
# __stdcall
self.__lib = windll.LoadLibrary(lib)
else:
# __cdecl
self.__lib = cdll.LoadLibrary(lib)
@property
def lib(self):
return self.__lib
@cdll_method_find(alisa="BASS_GetDeviceInfo")
def GetDeviceInfo(self, device):
self._BASS_GetDeviceInfo = [c_int32, POINTER(BASS_DEVICEINFO)]
self._BASS_GetDeviceInfo.restype = c_bool
code, bd = 0, pointer(BASS_DEVICEINFO("", "", 0))
code = self._BASS_GetDeviceInfo(device, bd)
return code, bd
就這麼簡單 pointer(BASS_DEVICEINFO()) 直接返回指針
12.python中的指針id()及從id中獲取對象(指針強制轉換)
在c中回調常常會有傳遞一個自定義定義參數指針 void*,然後在回調中強制轉換爲對象即可,其實在python中也有指針,那就是內置函數id(),此函數返回int型數字,其實就是對象的內存地址,通過hex()轉成16進制也許會更明白,此id值可以通過ctypes.cast(obj,py_object).value 函數轉換 爲對象,但要注意的是作用域,別還沒接收前就被回收了,轉換後就是我們傳遞的對象了,不侷限於ctypes類型對象,舉個例子
from ctypes import *
class Point(object):
_x, _y = 0, 0
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
def callback(addr):
#convert
p = cast(addr, py_object).value
print("x=%d,y=%d" % (p.x, p.y))
if __name__ == "__main__":
p = Point(30, 50)
#pp is address of p
pp = id(p)
print(hex(pp))
callback(pp)
轉換後的結果就是我們傳遞的對象,實現了python從id(指針)中讀取對象,一定要保證回調前指定內存尚未被回收(類變量 全局變量 靜態變量都可以放大作用域)
/usr/bin/python3.4 /home/mengdj/work/python/4/point.py
0xb6fd5e4c
x=30,y=50Process finished with exit code 0
篇幅有限,詳細封裝python混合調用c請異步 python通過ctypes調用c封裝開源音頻引擎libsoundio (代碼篇)