python通過ctypes混合調用c/c++封裝開源音頻引擎libsoundio

其實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=50

Process finished with exit code 0

篇幅有限,詳細封裝python混合調用c請異步 python通過ctypes調用c封裝開源音頻引擎libsoundio (代碼篇)


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章