其实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定义与否。
文字和代码都比较多,贴上要点吧
//C 默认0开始
enum Sex {
Boy,
Girl,
};
python可以多种方式定义,如全局变量,class等,还是用Enum吧
from enum import Enum, unique
@unique
class Sex(Enum):
Boy=0
Girl=1
是不是简单,带上@unique是python修饰符,让值唯一的功能
**int,别懵,既然*int 在ctypes中为 POINTER(c_int),那么想当然**int就是 POINTER(POINTER(c_int)),括号可别打错了,有几级就嵌套几个POINTER()
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字)
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
千万别这么干,孩子。你会发现c返回的结构体指针(对应我们的类)并没有创建实例,只是通过类变量复制内容而已,压根不会创建对象,所以你的实例方法都没有用,访问会出错;唯一例外的就是你自己去创建对象,那么就按python访问吧
int[5]=c_int*5 数组一片连续存储的内存单元,此操作仅限ctypes类型
BigEndianStructure LittleEndianStructure大小端字节排序的结构体
请自行了解大小端知识,一般情况网络通用大字节,其他小字节
CMPFUNC = CFUNCTYPE(返回值, 参数1, 参数2, 参数3...)
CMPFUNC(pyhton函数名称)
CFUNCTYPE()只要从C代码中使用对象,请确保保留对对象的引用。ctypes没有,如果你不这样做,它们可能被垃圾收集,在回调时崩溃你的程序。
如果想对一个接收*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}
ctypes中支持cdll,windll,oledll,一般来说cdll主要用来载入C语言调用方式(cdecl)。windll主要用来载入WIN32调用方式(stdcall),而oledll使用WIN32调用方式(stdcall)且返回值是Windows里返回的HRESULT值。如果你使用错了,调用时肯定会报错的,这涉及到参数入栈顺序和清理工作
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()) 直接返回指针
在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 (代码篇)