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. 本身不被編譯,也不會產生任何行為,僅僅搜集文件,以便於其他項目直接調用。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。