Python ctypes 调用C/C++动态库总结

2,794 阅读4分钟

开篇

最近两次遇到在Python中调用C/C++的动态链接库,实际调用的是关于RTSP视频流解码的SDK库,过程中踩了不少坑,在此做个记录。其中主要涉及Python中的ctypes模块的使用,怎样定义以及设置动态库中的回调函数。

动态库

C/C++程序可编译成动态链接库提供给用户调用,并且可在windows和linux平台下编译出对应平台使用的库。正好先后使用Python调用了windows和Linux平台的动态库,使用时要确定拿到的是对应平台的库文件。 动态库的加载示例:

# _*_coding:utf-8_*_
import os
import platform
from ctypes import *

linux_dll_name = 'xxx.so'
win_dll_name = 'xxx.dll'
sdk = None
sdk_dll_dir = os.getcwd()
sys_type = platform.system()
if sys_type == "Windows":
    sdk = cdll.LoadLibrary(sdk_dll_dir + win_dll_name)
else:
    sdk = cdll.LoadLibrary(sdk_dll_dir + linux_dll_name)

说明一下:若使用C++编写和编译动态库时,导出的方法名会发生变化,原因是C++中有方法重载,编译时会添加额外标识。此时需要做声明:extern "C"

Python中的ctypes模块

Python中有个专门用来调用c代码的模块ctypes,ctypes中定义了C语言中的基本变量类型,甚至还考虑了不同平台的差异,指定了所占的长度,如:

  • c_int8
  • c_int16
  • c_int32
  • c_int64

普通类变量类型定义

在调用动态库中的C函数时需要注意相应的变量类型都需要使用c_xxx这样的方式进行定义。

结构体的定义(这个很容易出错,需要额外注意)

C代码中的定义

typedef struct _FrameHeader {
	u16 Width;
	u16 Height;
}FrameHeader;

对应到Python中的定义

 class FrameHeader(Structure):
    _fields_ = [
        ('Width', c_uint16),
       ('Height', c_uint16)
    ]

关于值的比较

在Python中调用C代码返回值为ctypes类型,如常见的c_int类型,如果需要对该值进行判断,代码如下:

ret = init() #调用动态库中的C函数init(),返回值为c_int
if ret == 123 :  #变量值判断,不能用ret == c_int(123)   
   dosomething()

特殊情况,结构体里面包含自身,类似链表节点的定义

typedef struct _DataFrame {
	void* Head;
	void* Bmp;				
	void* H264;				
	struct _DataFrame* Prev;
}DataFrame;

对应到Python中的定义

class DataFrame(Structure):
    pass

DataFrame._fields_ = [
    ('Head', POINTER(FrameHeader)),
    ('Bmp', POINTER(BmpData)),
    ('H264', c_void_p),
    ('Prev', POINTER(DataFrame))
]

关于指针的表示

ctypes中用pointer和byref对应C语言中的指针,通常使用byref()的地方同样也可用指针函数pointer(),但pointer()作为参数通常会额外创建一个指针对象,如果并不需要再次使用该指针对象的话,使用 byref()会更快。一般情况可参考C代码中的标识,如果遇到&a这样的就用byref, 遇到int *p这样的就用pointer。

C代码内容

#define Handle void*
void function(Handle *handle)

Python代码在实际调用该函数时的写法

handle = c_void_p(0)
init(pointer(handle)) # 此处也可用byref(handle)

需要注意的是pointer和POINTER的区别,POINTER返回类型对象,用来指定函数的参数和返回值的类型用,比如声明一个回调函数:

CALLBACK_FUNC = CFUNCTYPE(c_int32, c_int32, POINTER(DataFrame), c_void_p) #其中DataFrame是一个结构体

怎样编写回调函数

关于回调函数的使用主要有两点:

  1. 回调函数的定义
  2. 回调函数的注册

可能遇到的问题有:

回调函数声明时,由于Python的函数不需要指定返回值类型,所以回调函数的定义中,第一个参数为返回值类型,如果返回值为void,则定义为None

Python中回调函数的声明

# 返回值int32
DATA_CALLBACK_FUNC1 = CFUNCTYPE(c_int32, c_int32, POINTER(DataFrame), c_void_p) 
# 返回值为void
DATA_CALLBACK_FUNC2 = CFUNCTYPE(None, c_int32, POINTER(DataFrame), c_void_p)    

注册回调函数时,需要将回调函赋值给一个变量,然后再进行注册, 否则在运行时会产生段错误,原因是若没有相关的赋值引用,回调函数相当与匿名,会被回收。

cb = CALLBACK_FUNC(frame_callback)
sdk.SetCallback(cb)

回调函数中返回一个指针指向一个buffer,此时需要在Python代码中定义一个buffer

# 下面的6220800是一个图像的buffer大小(1920*1080*3),实际使用时可定义的更大些以满足更高分辨率
# 如果把POINTER(c_char * 6220800) 改为c_char_p 则会报错
CALLBACK_FUNC = CFUNCTYPE(
   c_int, c_int, c_void_p, c_int, POINTER(c_char * 6220800),POINTER(FRAME_INFO))

总结

以上就是最近在使用Python调用摄像头的SDK时的一些心得体会,写的不是特别详细,主要 是记录下踩过的一些坑,可能还有些坑还没被踩到,后面遇到时再进一步补充。