banner
yono

yono

哈喽~欢迎光临
follow
github

py 调用 c(0)

众所周知#

  • python 作为强大的胶水语言,调用各种其他流行的编程语言都非常方便。
  • 我的记忆力差的离谱,学过的东西不稍微记录一下过两天就忘掉。
  • c 语言是最接近机器语言的编程语言 (别杠汇编、FPGA 脚本,那不是人写的)

打算彻底搞清楚 python 怎么胶 c,以便于我呆在舒适区编写 c 语言,这还是一件相对麻烦的事情,所以需要一个系列的文章来记录学习过程。

python 可以以源码动态库形式调用 c 语言代码,我前期的学习使用动态库形式。相信使用 python 调用别人准备好的 so/dll 是没什么难度的,大多数教程或者别人准备好的文档都描述了这个简单过程。

难点在于如何找到一个广泛通用的方法来调用任何一个已知的 c 语言库。

编译动态库#

首先需要将已有的 c 语言库编译成动态库的形式。

这个编译过程多种多样,下面介绍使用 cmake 如何编译动态库,大多数好的 c 语言库都会提供搜集了所有源码文件的 CMakeLists.txt ,例如 modbusX 的 common 文件夹下。定义一个接口库1,以供搜集所有的源码文件。

cmake_minimum_required(VERSION 3.22)

project(MODBUSX)
add_library(modbusx INTERFACE) # INTERFACE意味着这个库本身并不会被编译,而是作为依赖被其他目标使用,以便于MBx_user.h可以自己定义并且对库行为进行变更

# 递归查找所有源码文件
file(GLOB_RECURSE SRC ${CMAKE_CURRENT_LIST_DIR}/source/*.c)
# 非递归的案例
# file(GLOB SRC ${CMAKE_CURRENT_LIST_DIR}/source/*.c)

target_include_directories(modbusx INTERFACE
    ${CMAKE_CURRENT_LIST_DIR}/include
)

if(WIN32)
    target_include_directories(modbusx INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}/../port/win32/inc
    )
else()
    target_include_directories(modbusx INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}/../port/generic/inc
    )
endif()

target_sources(modbusx INTERFACE
     ${SRC}
)

if(CMAKE_C_STANDARD LESS 11)
    message(ERROR "Generated code requires C11 or higher")
endif()

那么在顶层项目中就可以调用这个接口库,并且增加编译属性,直接地生成动态库文件。这是顶层目录 CMakeLists.txt 的一个示例。这个 CMakeLists.txt 没有指定编译器和优化属性等参数,需要编译语句传参或者 cmake 插件里指定 (也是编译语句传参)。

cmake_minimum_required(VERSION 3.22)
project(MODBUSX_WRAPPER)

# 包含原库配置
add_subdirectory(common)   # 引入搜集所有文件的那个 cmakelists

# 创建新的共享库目标
add_library(modbusx_shared SHARED ${SRC}) # 名称为 modbusx_shared 的动态库目标

# 继承原库的INTERFACE属性
target_link_libraries(modbusx_shared PUBLIC modbusx) # 将接口库引入动态库

# 设置符号可见性(关键!)
if(WIN32)
    target_compile_definitions(modbusx_shared PRIVATE "MBX_API=__declspec(dllexport)")
else()
    target_compile_definitions(modbusx_shared PRIVATE "MBX_API=__attribute__((visibility("default")))")
endif()

# 强制C11标准
set_target_properties(modbusx_shared PROPERTIES
    C_STANDARD 11
    C_VISIBILITY_PRESET hidden
)

这里有一个关键问题,通常的头文件中函数声明类似 extern void func(uint8_t* data, uint16_t len); 修饰词 extern 并不会使得这个函数暴露在动态库中。需要增加修饰词使得声明类似于 dllexport void func(uint8_t* data, uint16_t len);default void func(uint8_t* data, uint16_t len); dllexportdefault 取决于平台 ——win/linux。

编译出 dll/so 后,需要使用 dump 工具检查一下生成的 dll/so,是否暴露出了期望的符号,例如我在 win 平台使用上面的 cmakelist 编译出 modbusx_x86.dll。然后在 vs 终端使用 dumpbin 工具检查。确认暴露出了我需要的一些函数,这个 dll 就可以使用了。

PS E:\PY64_PROJECT\CFFI_TEST> dumpbin /exports modbusx_x86.dll
Microsoft (R) COFF/PE Dumper Version 14.42.34435.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file modbusx_x86.dll

File Type: DLL

  Section contains the following exports for libmodbusx_shared.dll

    00000000 characteristics
    6808B5B8 time date stamp Wed Apr 23 17:41:12 2025
        0.00 version
           1 ordinal base
          15 number of functions
          15 number of names

    ordinal hint RVA      name

          1    0 00005510 MBx_Master_Error_Get
          2    1 00002200 MBx_Master_Member_Add
          3    2 00001F20 MBx_Master_RTU_Init
          4    3 000024C0 MBx_Master_Read_Coil_Request
          5    4 00002530 MBx_Master_Read_Disc_Input_Request
          6    5 00002610 MBx_Master_Read_Input_Reg_Request
          7    6 000025A0 MBx_Master_Read_Reg_Request
          8    7 00002090 MBx_Master_TCP_Init
          9    8 00002770 MBx_Master_Write_Coil_Mul_Request
         10    9 00002680 MBx_Master_Write_Coil_Request
         11    A 000027F0 MBx_Master_Write_Reg_Mul_Request
         12    B 00002700 MBx_Master_Write_Reg_Request
         13    C 00003C30 MBx_Slave_RTU_Init
         14    D 00003E10 MBx_Slave_TCP_Init
         15    E 00004770 MBx_Ticks

  Summary

        1000 .CRT
        1000 .bss
        1000 .data
        2000 .debug_abbrev
        1000 .debug_aranges
        1000 .debug_frame
        8000 .debug_info
        2000 .debug_line
        1000 .debug_line_str
        2000 .debug_loclists
        1000 .debug_rnglists
        1000 .debug_str
        1000 .edata
        1000 .idata
        1000 .pdata
        2000 .rdata
        1000 .reloc
        7000 .text
        1000 .tls
        1000 .xdata

python 的 ctypes 方式#

使用这个准备好的 dll 文件非常简单。在 python 中使用这样的代码就可以引入。

try:
    lib = ctypes.CDLL("./lib/modbusx_x86.dll")
except OSError as e:
    print(f"加载 DLL 失败: {e}")
    exit(1)

然后就可以随意 lib. 调用暴露的函数。

while True:
    lib.MBx_Ticks(1000)
    time.sleep(0.001)  # 约 1ms 周期

结合 ctypesgen#

我们的 c 语言程序中通常有大量的类型定义、结构体定义、宏定义等,这些定义在 c 语言的处理中非常重要,而 python 想要调用库内函数也要传递相同的类型变量 (对象),手动在 python 中复刻所有的类型、结构体不太现实。使用 ctypesgen 工具即可。类似以下的指令。

ctypesgen -I . -o mbx_bindings.py MBx_api.h  MBx_port.h

这些属性与 gcc 类似,或者说根本一样,ctypesgen 就是调用 gcc 完成这个过程。

  • -I . 表示增加一个 include path,是目录根。
  • -o mbx_bindings.py 表示输出文件名为 mbx_bindings.py 。
  • MBx_api.h MBx_port.h 是增加需要处理的头文件,这两个文件中包含了所有库需要的定义。

生成的 mbx_bindings.py 有类似如下的小段,复刻了 c 语言中的结构体类型。

# E:\\PY64_PROJECT\\CDLL_TEST\\h\\MBx_api.h: 292
class struct_anon_12(Structure):
    pass

struct_anon_12.__slots__ = [
    'SlaveID',
    'Func',
    'AddrStart',
    'RegNum',
    'Value',
]
struct_anon_12._fields_ = [
    ('SlaveID', uint8_t),
    ('Func', uint8_t),
    ('AddrStart', uint16_t),
    ('RegNum', uint16_t),
    ('Value', uint8_t * int((0x7F * 2))),
]

_MBX_MASTER_REQUEST_RING_NODE = struct_anon_12# E:\\PY64_PROJECT\\CDLL_TEST\\h\\MBx_api.h: 292

# E:\\PY64_PROJECT\\CDLL_TEST\\h\\MBx_api.h: 299
class struct_anon_13(Structure):
    pass

struct_anon_13.__slots__ = [
    'Queue',
    'Head',
    'Tail',
]
struct_anon_13._fields_ = [
    ('Queue', _MBX_MASTER_REQUEST_RING_NODE * int(40)),
    ('Head', uint8_t),
    ('Tail', uint8_t),
]

_MBX_MASTER_REQUEST_RING = struct_anon_13# E:\\PY64_PROJECT\\CDLL_TEST\\h\\MBx_api.h: 299
/**
 * @brief 主机对从指令待发送请求的环形栈
 */
typedef struct
{
    uint8_t  SlaveID;                             // 待发送的从机ID
    uint8_t  Func;                                // 待发送的功能码
    uint16_t AddrStart;                           // 待发送的地址起始
    uint16_t RegNum;                              // 待发送的寄存器数量
    uint8_t  Value[MBX_MASTER_MULTI_REG_MAX * 2]; // 待发送的设置值域
} _MBX_MASTER_REQUEST_RING_NODE;

typedef struct
{
    _MBX_MASTER_REQUEST_RING_NODE Queue[MBX_MASTER_REQUEST_QUEUE_MAX]; // 待发送请求队列
    uint8_t                       Head;                                // 环形队列的头指针(入)
    uint8_t                       Tail;                                // 环形队列的尾指针(出)
} _MBX_MASTER_REQUEST_RING;

又有类似如下的小段,复刻了 C 语言的宏定义

# ./MBx_port.h: 114
try:
    MBX_PORT_RETURN_DEFAULT = 0x00
except:
    pass

# ./MBx_port.h: 115
try:
    MBX_PORT_RETURNT_ERR_INDEFINITE = 0x01
except:
    pass
#define MBX_PORT_RETURN_DEFAULT         0x00 // 默认的无错误
#define MBX_PORT_RETURNT_ERR_INDEFINITE 0x01 // 默认的未定义错误(供临时使用或极简函数返回错误)

所以后续在 python 程序中就可以无感复刻 c 语言的操作,来调用 dll 的资源。例如最为复杂的回调函数和各种内存空间的绑定,可以有类似如下的代码。

# 串口函数
@MBX_SEND_PTR
def SerialSendPort(data: ctypes.POINTER(ctypes.c_uint8), length: ctypes.c_uint32) -> ctypes.c_uint32:
    try:
        buffer = ctypes.string_at(data, length)
        bytes_written = ser.write(buffer)
        if bytes_written == length:
            print(f"Sent: {buffer.hex()} ({bytes_written} bytes)")
            return MBX_PORT_RETURN_DEFAULT
        else:
            print(f"Send incomplete: expected {length} bytes, sent {bytes_written} bytes")
            return MBX_PORT_RETURNT_ERR_INDEFINITE
    except serial.SerialException as e:
        print(f"Serial error: {e}")
        return MBX_PORT_RETURNT_ERR_INDEFINITE
    except Exception as e:
        print(f"Unexpected error: {e}")
        return MBX_PORT_RETURNT_ERR_INDEFINITE


@MBX_GTEC_PTR
def SerialGetcPort(data: ctypes.POINTER(ctypes.c_uint8)) -> ctypes.c_uint32:
    if ser.in_waiting > 0:
        byte = ser.read(1)
        data[0] = byte[0]
        print(f"Received: {byte.hex()}")
        return MBX_PORT_RETURN_DEFAULT
    return MBX_PORT_RETURNT_ERR_INDEFINITE


# 初始化从机
slave = MBX_SLAVE()
SRxBuffer = (ctypes.c_uint8 * 84)()
STxBuffer = (ctypes.c_uint8 * 84)()

# 初始化
result = lib.MBx_Slave_RTU_Init(
    ctypes.byref(slave),
    1,  # 从机 ID
    MapList,
    SerialSendPort,
    SerialGetcPort,
    115200,  # 波特率
    SRxBuffer,
    84,
    STxBuffer,
    84,
)
/* 串口函数 */
uint32_t SerialSendPort(const void *Data, size_t Len)
{
    WINBOOL b     = FALSE; // 发送操作标识
    DWORD   wWLen = 0;     // 实际发送数据长度
    /* 尝试发送 */
    b = WriteFile(comHandle, Data, Len, &wWLen, NULL);
    if(b && wWLen == Len)
        return MBX_PORT_RETURN_DEFAULT;
    else
        return MBX_PORT_RETURNT_ERR_INDEFINITE;
}

uint32_t SerialGetcPort(uint8_t *Data)
{
    WINBOOL b     = FALSE; // 接收操作标识
    DWORD   wRLen = 0;     // 实际接收数据长度
    /* 尝试接收 */
    b = ReadFile(comHandle, Data, 1, &wRLen, NULL);
    if(b == TRUE && wRLen == 1)
    {
        return MBX_PORT_RETURN_DEFAULT;
    }
    else
    {
        return MBX_PORT_RETURNT_ERR_INDEFINITE;
    }
}

_MBX_SLAVE MBxSlave;
uint8_t *SRxBuffer = (uint8_t *)malloc(84 * sizeof(uint8_t));
uint8_t *STxBuffer = (uint8_t *)malloc(84 * sizeof(uint8_t));
state = MBx_Slave_RTU_Init(&MBxSlave,     // 从机对象
                          1,              // 从机ID
                          MapList,        // 地址映射表
                          SerialSendPort, // 发送函数
                          SerialGetcPort, // 接收函数
                          9600,           // 波特率
                          SRxBuffer,      // 库内接收buffer分配
                          84,             // 接收buffer最大长度
                          STxBuffer,      // 库内发送buffer分配
                          84)             // 发送buffer最大长度

[!NOTE]

需要重点注意的是,c 中的类型是 _MBX_SLAVE ,而在 python 中我却使用 MBX_SLAVE。

这里是我在 mbx_bindings.py 手动修改的,因为在 python 中,下杠开头的是匿名内容,不暴露在文件外,导致调用出错,所以有需要 C 中以下杠开头的类型或其他定义,可以自己手动修正一下 mbx_bindings.py 。

此文由 Mix Space 同步更新至 xLog
原始链接为 https://www.yono233.cn/posts/novel/25_5_6_py4c0


Footnotes#

  1. 本身不被编译,也不会产生任何行为,仅仅搜集文件,以便于其他项目直接调用。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。