python 调用dll类作用域_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}

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__ = "mengdj@outlook.com"

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值