py 调用 c(0)

众所周知

python 作为强大的胶水语言，调用各种其他流行的编程语言都非常方便。

我的记忆力差的离谱，学过的东西不稍微记录一下过两天就忘掉。

c 语言是最接近机器语言的编程语言 (别杠汇编、FPGA 脚本，那不是人写的)

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

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

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

编译动态库

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

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

Copy
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 插件里指定 (也是编译语句传参)。

Copy
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); dllexport 或 default 取决于平台 ——win/linux。

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

Copy
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 中使用这样的代码就可以引入。

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

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

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

结合 ctypesgen

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

Copy
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 语言中的结构体类型。

Copy
# 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

Copy
/**
* @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 语言的宏定义

Copy
# ./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

Copy
#define MBX_PORT_RETURN_DEFAULT 0x00 // 默认的无错误
#define MBX_PORT_RETURNT_ERR_INDEFINITE 0x01 // 默认的未定义错误(供临时使用或极简函数返回错误)

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

Copy
# 串口函数
@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,
)

Copy
/* 串口函数 */
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

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