python通过ctypes混合调用c/c++封装开源音频引擎libsoundio

其实python和c混合调用的方法很多,如swig、cpython等等,但这些都不是标准库,需要额外安装的,本文讲的是标准库的ctypes来调用c,实现强大的功能,没办法霸道C\C++就是那么强大,不服不行,有那种语言是无法调用C的,没有吧。

本文既不是扫盲也不是hello,world之类的,期初我百度了python通过ctypes封装调用c,全是千万一律的,一段基础代码拷贝了无数次,所以这次搞全面的,直接封装,当然本人也刚接触python语言半月,诸多的东西还不太会用,写的不好还请见谅。

python通过ctypes混合调用c/c++封装开源音频引擎libsoundio_第1张图片

在了解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}

 

10.python调用动态链接库dll的约定

ctypes中支持cdll,windll,oledll,一般来说cdll主要用来载入C语言调用方式(cdecl)。windll主要用来载入WIN32调用方式(stdcall),而oledll使用WIN32调用方式(stdcall)且返回值是Windows里返回的HRESULT值。如果你使用错了,调用时肯定会报错的,这涉及到参数入栈顺序和清理工作

python通过ctypes混合调用c/c++封装开源音频引擎libsoundio_第2张图片

 

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 (代码篇)


 

你可能感兴趣的:(python,python,ctypes调用,libsoundio)