十任带我嘎嘎飞 发表于 2024-5-3 09:56

申请会员ID:picklemorty【未报到,已注销】

1. 申请会员ID:picklemorty
2. 个人邮箱:2371905571@qq.com
3. 原创技术文章

# 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的流程

1. 准备DLL文件
2. 了解DLL函数签名,厂商一般提供的有SDK手册
3. 定义函数原型,这一步最为繁杂,不过有ctypesgen这个神器。
4. ctypes.WinDLL来加载DLL文件,同样可以由ctypesgen负责加载。
5. 开始使用

然后看一下我们本次要处理的重量级头文件内容吧
我顺便把海康的sdk也给统计了一下

| 类别 | 大华网络SDK头文件 | 海康网络SDK头文件 |
| --- | --- | ---|
|结构体数量| 7398 | 2661 |
|函数数量| 1357 | 627 |
|枚举数量| 1737 | 269 |
|全局变量| 3178 | 8131 |


## 手动编写ctypes包装器都有哪些问题?
如果我们没有工具批量生成ctypes包装器的话我们就需要下面这样手动操作

随便拿一个结构体和函数举例
```C

// 网络访问规则 结构体
typedef struct tagNET_NETACCESS_RULE_INFO
{
    EM_NET_ACCESS_TYPE            emNetAccessType;      // 访问模式
    UINT                            nAllowAddrNum;          // 允许名单控制地址个数
    NET_ALLOW_ADDR_INFO             stuAllowAddrInfo;// 允许名单控制地址列表
    BYTE                            bReserverd1;         // 字节 对齐
    UINT                            nBlockAddrNum;          // 禁止名单控制地址个数
    NET_BLOCK_ADDR_INFO             stuBlockAddrInfo;//禁止名单控制地址列表   
    BYTE                            bReserverd;       // 保留字节
} NET_NETACCESS_RULE_INFO;

// 获取设备配置 函数
CLIENT_NET_API BOOLCALL_METHOD CLIENT_GetDevConfig(LLONG lLoginID, DWORD dwCommand, LONG lChannel, LPVOID lpOutBuffer, DWORD dwOutBufferSize, LPDWORD lpBytesReturned,int waittime=500);
```

需要编写的ctypes包装为

``` python
# 网络访问规则 结构体
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 =
CLIENT_GetDevConfig.restype = c_bool
```

Q: 大眼一看,貌似写个脚本就可以了呀?
A: 嗯,定睛一看再仔细一想,脚本需要考虑的情况包括但不限于
1. 删除注释
    - 单行注释
    - 用了\换行符的单行注释
    - 多行注释
2. 结构体
    - 枚举成员类型
    - 结构体成员类型
    - 成员变量类型转换为对应ctypes模块中的变量类型。比如win头文件的BYTE需要先转c的unsigned char然后再转换为ctypes的c_ubyte
    - 成员变量数组的大小
3. 枚举同上
4. 函数则是定义返回值类型和入参类型


上面这些都还好,唯独不可能简单解决的问题就是代码格式和各种纰漏。首先可以确定的是头文件本身可以编译并成功运行,我们可以用代码格式化工具整理一下,但依旧含有部分代码格式无法被统一处理。还有代码本身的错误,格式化工具是无法完美处理的。这 **116259** 行,占用空间 **6.25MB** 的头文件总是会不断的给你的小脚本一些你考虑不到的问题,需要特殊处理的情况太多太多了。

Q: 那为什么ctypesgen可以呢?
A: 因为ctypesgen使用LEX生成了一个词法分析器,可以从C语法上正确识别代码,考虑到的情况肯定比小脚本要多得多。

## ctypesgen使用流程


### 删除注释
关于代码格式这一步依旧需要我们手动处理一下,不然ctypesgen容易报错。
第一个要处理的就是注释,虽然ctypesgen会自动忽略,不过注释会影响我们处理头文件格式时的繁杂度,容易眼花缭乱
如上所说,C的注释有三种,我写了三段正则用于匹配这些代码

``` python
    # 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 = # 我这个只用到了-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


### 包装器使用

最后只需要像导入模块一样导入整个文件就好了

``` python
    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,这段代码已经非常清晰了对吧。

Hmily 发表于 2024-5-6 12:26

I D:picklemorty
邮箱:2371905571@qq.com

申请通过,欢迎光临吾爱破解论坛,期待吾爱破解有你更加精彩,ID和密码自己通过邮件密码找回功能修改,请即时登陆并修改密码!
登陆后请在一周内在此帖报道,否则将删除ID信息。

十任带我嘎嘎飞 发表于 2024-5-19 10:41

领导,我收不到密码重置的邮件呀:'(weeqw。再给个机会吧

十任带我嘎嘎飞 发表于 2024-5-19 12:50

不好意思啊,我间隔两天尝试获取密码重置邮件,却一直没收到,这俩天挺忙的,把这事给忘了。麻烦大哥再给次机会吧。顺便问下,为啥没收到密码重置的邮件呀?

Hmily 发表于 2024-5-21 10:59

十任带我嘎嘎飞 发表于 2024-5-19 12:50
不好意思啊,我间隔两天尝试获取密码重置邮件,却一直没收到,这俩天挺忙的,把这事给忘了。麻烦大哥再给次 ...

两次邮件都发成功了,你是不是搞错邮箱了,可以自行按论坛教程排查:https://www.52pojie.cn/thread-98585-1-1.html

Hmily 发表于 2024-6-6 15:40

未报到,已注销。
页: [1]
查看完整版本: 申请会员ID:picklemorty【未报到,已注销】