- 申请会员ID:picklemorty
- 个人邮箱:2371905571@qq.com
- 原创技术文章
python如何快速高效的调用古老厂商SDK中的Dll文件
ctypesgen使用记录
前言
本文举例对象为大华提供的设备SDK(C语言端)
主要工具就是副标题的 ctypesgen 模块
快速高效是指ctypesgen可以批量处理C语言头文件并生成ctypes包装器
古老厂商是指厂商SDK中提供的demo里含有2004年的编写注释,且代码结构过于臃肿庞大。
Q: 厂商有提供python端的SDK吗?
A: 提供了。
Q: 那为什么还有自己 造轮子 呢?
A: 函数原型定义的太少了,不及C端的十分之一。简单说就是用户不能通过python端SDK进行 同步NTP服务器,获取设备时间,异步下载录像等重要功能。而dll本身是支持这些功能的。
先简单介绍下一般调用dll的流程
- 准备DLL文件
- 了解DLL函数签名,厂商一般提供的有SDK手册
- 定义函数原型,这一步最为繁杂,不过有ctypesgen这个神器。
- ctypes.WinDLL来加载DLL文件,同样可以由ctypesgen负责加载。
- 开始使用
然后看一下我们本次要处理的重量级头文件内容吧
我顺便把海康的sdk也给统计了一下
类别 |
大华网络SDK头文件 |
海康网络SDK头文件 |
结构体数量 |
7398 |
2661 |
函数数量 |
1357 |
627 |
枚举数量 |
1737 |
269 |
全局变量 |
3178 |
8131 |
手动编写ctypes包装器都有哪些问题?
如果我们没有工具批量生成ctypes包装器的话我们就需要下面这样手动操作
随便拿一个结构体和函数举例
// 网络访问规则 结构体
typedef struct tagNET_NETACCESS_RULE_INFO
{
EM_NET_ACCESS_TYPE emNetAccessType; // 访问模式
UINT nAllowAddrNum; // 允许名单控制地址个数
NET_ALLOW_ADDR_INFO stuAllowAddrInfo[128]; // 允许名单控制地址列表
BYTE bReserverd1[4]; // 字节 对齐
UINT nBlockAddrNum; // 禁止名单控制地址个数
NET_BLOCK_ADDR_INFO stuBlockAddrInfo[128]; // 禁止名单控制地址列表
BYTE bReserverd[1024]; // 保留字节
} NET_NETACCESS_RULE_INFO;
// 获取设备配置 函数
CLIENT_NET_API BOOL CALL_METHOD CLIENT_GetDevConfig(LLONG lLoginID, DWORD dwCommand, LONG lChannel, LPVOID lpOutBuffer, DWORD dwOutBufferSize, LPDWORD lpBytesReturned,int waittime=500);
需要编写的ctypes包装为
# 网络访问规则 结构体
class NET_NETACCESS_RULE_INFO():
__slot__ = [ 'emNetAccessType', 'nAllowAddrNum', 'stuAllowAddrInfo', 'bReserverd1', 'nBlockAddrNum', 'stuBlockAddrInfo', 'bReserverd', ]
_fields_ = [
('emNetAccessType', EM_NET_ACCESS_TYPE),
('nAllowAddrNum', c_uint),
('stuAllowAddrInfo', NET_ALLOW_ADDR_INFO * int(128)),
('bReserverd1', c_ubyte * int(4)),
('nBlockAddrNum', c_uint),
('stuBlockAddrInfo', NET_BLOCK_ADDR_INFO * int(128)),
('bReserverd', c_ubyte * int(1024)),
]
# 获取设备配置 函数
CLIENT_GetDevConfig.argtypes = [c_longlong, c_uint, c_int, POINTER(None), c_uint, POINTER(c_uint), c_int]
CLIENT_GetDevConfig.restype = c_bool
Q: 大眼一看,貌似写个脚本就可以了呀?
A: 嗯,定睛一看再仔细一想,脚本需要考虑的情况包括但不限于
- 删除注释
- 结构体
- 枚举成员类型
- 结构体成员类型
- 成员变量类型转换为对应ctypes模块中的变量类型。比如win头文件的BYTE需要先转c的unsigned char然后再转换为ctypes的c_ubyte
- 成员变量数组的大小
- 枚举同上
- 函数则是定义返回值类型和入参类型
上面这些都还好,唯独不可能简单解决的问题就是代码格式和各种纰漏。首先可以确定的是头文件本身可以编译并成功运行,我们可以用代码格式化工具整理一下,但依旧含有部分代码格式无法被统一处理。还有代码本身的错误,格式化工具是无法完美处理的。这 116259 行,占用空间 6.25MB 的头文件总是会不断的给你的小脚本一些你考虑不到的问题,需要特殊处理的情况太多太多了。
Q: 那为什么ctypesgen可以呢?
A: 因为ctypesgen使用LEX生成了一个词法分析器,可以从C语法上正确识别代码,考虑到的情况肯定比小脚本要多得多。
ctypesgen使用流程
删除注释
关于代码格式这一步依旧需要我们手动处理一下,不然ctypesgen容易报错。
第一个要处理的就是注释,虽然ctypesgen会自动忽略,不过注释会影响我们处理头文件格式时的繁杂度,容易眼花缭乱
如上所说,C的注释有三种,我写了三段正则用于匹配这些代码
# 1.多行注释 /* */ 2.单行注释 // 用了换行符 \ 3.单行注释 //
# (/*(.|\r\n|\n)*?\*/) | (//(.*\\\n)+.*) | (//.*)
# OrgText = open("headFile.h","r",encoding="utf8")
pattern = r"(/\*(.|\r\n|\n)*?\*/)|(\/\/(.*\\\n)+.*)|(\/\/.*)"
text1 = re.sub(pattern, "", OrgText, flags=re.M)
pattern = "^\\s*(?=\r?$)\n" # 删除所有空行。11万的头文件,删完注释而留下的空行也很多,需要处理。
text2 = re.sub(pattern, "", text1, flags=re.M)
# print(text2)
# return text2
格式化代码
因为VS IDE里自带的代码格式化速度太慢(要刷新UI,语法分析,代码高亮等,我的电脑配置低容易卡死),又为配置格式化格式和复现方便,而单独用的 clang-format,我的版本是 clang-format version 16.0.0。
LLVM的发行页面:https://github.com/llvm/llvm-project/releases
下载完成后不要进行安装,解压这个exe文件,会看到有一个clang-format.exe。把这个可执行文件放到需要的地方就可以了。
首先生成基于llvm的格式化配置文件
clang-format -style=llvm -dump-config > .clang-format
生成的文件名称尽量不要改动哈,不然还要加参指定格式化配置文件,别掉坑里了。
然后修改.clang-format文件中的数值
ColumnLimit:0 # 每行最大字符数
IndentWidth: 4 # 缩进宽度
PointerAlignment: Left # 指针对齐方式
BreakBeforeBraces: Custom # 大括号换行方式
AfterStruct: true # 结构体大括号统一格式
AfterEnum: true
AfterUnion: true # 就比如这个,clang-format无法控制处理嵌套数据类型中的 { 和 } 符号位置等
IndentPPDirectives: BeforeHash # 预处理指令对齐
ctypesgen生成包装器
ctypesgen可以作为模块安装,然后可以使用命令行窗口操作。不过我是推荐你最好把ctypesgen的源代码clone下来,这样查错跑调试方便点。
指令解析方面用的是argparse.ArgumentParser模块,所以传入 -h 参数就能看到详细指令了。
dllFile = "./Libs/win64/dhnetsdk.dll" # 大华NetSDK主DLL,其他的DLL放在同一路径就行。
inputPath = "readyToGenCtypesWrapperFile.h"
outputPath = "generatedCtypesWrapperFile.py"
arg_list = [inputPath, "-l", dllFile, "-o", outputPath] # 我这个只用到了-l指定LIBRARY文件
print("指令列表为", arg_list) # 检查路径的
if log2file is TrgeneratedCtypesWrapperFilee: # 希望将错误报告输出到文件中
error_file = open('ctypesgen.log', 'w')
sys.stderr = error_file
from sub_ctypesgen.run import main as ctypesgenscript
ctypesgenscript(arg_list)
附赠cl指令手册,预处理器节点https://learn.microsoft.com/zh-cn/cpp/build/reference/compiler-options-listed-by-category?view=msvc-170#preprocessor
不出意外的话你就能看到 244800 行的py文件了。这个就是ctypesgen生成的包装器。在上面使用了-l参数时,包装器会自动加载这个dll。部分代码如下
# Begin libraries
_libs["Libs/win64/dhnetsdk.dll"] = load_library("Libs/win64/dhnetsdk.dll")
# 1 libraries
# End libraries
包装器使用
最后只需要像导入模块一样导入整个文件就好了
import UnifyNetSDK.dahua.dh_netsdk_wrapper as DH
class DaHuaNetSDK(AbsNetSDK):
netDll = DH # netsdk.dll由ctypesgen包装器加载
...
@classmethod
def getNTP_CFG(cls, userID: int) -> UninfyNTPArg:
# 获取设备NTP配置
lChannel = 0 # 通道,部分指令下本参数无效
dwOutBufferSize = sizeof(DH.DHDEV_NTP_CFG) # 输出缓冲区大小
lpOutBuffer = DH.DHDEV_NTP_CFG() # 输出缓冲器
lpBytesReturned = c_ulong() # 实际返回的字节大小
waittime = 1000 # 等待时长,单位ms
result = cls.netDll.CLIENT_GetDevConfig(userID, DH.DH_DEV_NTP_CFG, lChannel, byref(lpOutBuffer), dwOutBufferSize, byref(lpBytesReturned), waittime)
最终效果就是上面这段代码了,相比于C,这段代码已经非常清晰了对吧。