众所周知#
- 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);
dllexport 或 default 取決於平台 ——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#
-
本身不被編譯,也不會產生任何行為,僅僅搜集文件,以便於其他項目直接調用。 ↩