根据动态链接库和头文件生成 Python 库的方法

给工业控制器和各种仪器设备写业务代码的时候,我们经常将 python 用作一门胶水语言来连接前后端程序和嵌入式设备。许多经典的工业控制器都提供动态链接库 (.dll, .so) 文件和一些头文件 (.h) 来方便二次开发。在 python 中,调用这些库函数的经典办法是通过 ctypes 标准库加载这些库文件,并且手动指定其参数类型,从而将其转变为封装好的 python 函数。利用 ctypesgen 可以方便地将这一过程自动化,从而得到易用的 python 库。

例如,如果要在 python 中使用 libc 中的 printf 函数,在 Linux 下可以

from ctypes import *
libc = cdll.LoadLibrary("libc.so.6")
printf = libc.printf
printf(b"An int %d, a double %f\n", 1234, c_double(3.14))

在上面的代码片段中,我们将 libc 的库文件加载至内存中,然后 alias 到一个别名,最后传递 c 形式的参数给该函数,即可完成调用。然而我们注意到,虽然 bytes, int 可以直接传递给 alias 的 printf 函数,浮点数却需要将其转化为 c_double。实际上,ctypes 加载的库函数只允许直接传递 string, int, bytes 三种未经转化的 python 对象作为参数,其他的类型均需要先转化为对应的 c 类型。这对于调用非常繁琐,因此 ctypes 提供了简单的办法,即在 argtypes 属性中声明所需的参数类型,之后调用的时候就会自动转化。

printf.argtypes = [c_char_p, c_char_p, c_int, c_double]
printf(b"String '%s', Int %d, Double %f\n", b"Hi", 10, 2.2)

以上的办法是在 python 中常用的调用编译好的库的办法,用 python 进行计算的时候经常需要用这种办法绑定矩阵对角化函数之类高性能库中的函数。然而,对于工业控制器和各种仪器设备来说,其 SDK 通常都有几千个函数,手动逐个绑定非常痛苦。因此,可以使用 ctypesgen 这个工具进行批量生成。

ctypesgen 是一个 python 库,因此可以通过

pip install ctypesgen

来安装。在 linux 下其用法相对简单,直接安装 gcc 和 g++ 即可正常使用 (ctypesgen 是纯 python 代码,本身并不依赖 gcc 即可解析头文件,但如果头文件中有 macro,则需要靠预处理器先展开)。在 windows 下,虽然可以通过安装 vs studio 来获得 cl.exe,但是配置起来非常复杂并且很容易出 bug。因此我的建议是单独下载一个 portable LLVM,然后手动指定 gcc 的相对路径即可,比如生成某国产运动控制卡 sdk (官方给的库文件名为 MultiCard.dll 和 MultiCard.h) 的 python binding 的时候,可以先将文件如下放置:

genbinding/
    llvm-mingw-20190920-x86_64/
        ...
    MultiCard/
        MultiCard.h
        MultiCard.dll

切换到 MultiCard 目录,

ctypesgen.exe -o MultiCard.py -l MultiCard --cpp='..\llvm-mingw-20190920-x86_64\llvm-mingw\bin\gcc.exe -E' MultiCard.h

然后就生成了 MultiCard.py 文件,可以看到其中自动为我们生成了库中包含的函数,并且指定了参数和返回值类型,例如这是为控制板加载 config 的函数

from ctypes import *
...
if not _lib.has("MC_LoadConfig", "cdecl"):
    continue
MC_LoadConfig = _lib.get("MC_LoadConfig", "cdecl")
MC_LoadConfig.argtypes = [String]
MC_LoadConfig.restype = c_int
...

对于需要传递 struct 作为参数的函数,也自动生成了对应的 struct

...
class struct__AxisHomeParm(Structure):
    pass

struct__AxisHomeParm._pack_ = 2
struct__AxisHomeParm.__slots__ = [
    'nHomeMode',
    'nHomeDir',
    'lOffset',
    'dHomeRapidVel',
    'dHomeLocatVel',
    'dHomeIndexVel',
    'dHomeAcc',
]
struct__AxisHomeParm._fields_ = [
    ('nHomeMode', c_short),
    ('nHomeDir', c_short),
    ('lOffset', c_long),
    ('dHomeRapidVel', c_double),
    ('dHomeLocatVel', c_double),
    ('dHomeIndexVel', c_double),
    ('dHomeAcc', c_double),
]

TAxisHomePrm = struct__AxisHomeParm
...

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.