よく知られていること#
- python は強力なグルー言語として、さまざまな他の人気プログラミング言語を呼び出すのが非常に便利です。
- 私の記憶力はひどく悪く、学んだことを少しでも記録しないと数日後には忘れてしまいます。
- c 言語は機械語に最も近いプログラミング言語です(アセンブリや FPGA スクリプトは人が書いたものではありません)。
python が c をどのようにグルーするかを徹底的に理解し、快適なゾーンで c 言語を書くためには、これは比較的面倒なことなので、学習プロセスを記録するための一連の記事が必要です。
python は ソースコード または 動的ライブラリ 形式で c 言語コードを呼び出すことができ、私の初期の学習では動的ライブラリ形式を使用しました。他人が用意した so/dll を python で呼び出すのは難しくないと信じています。ほとんどのチュートリアルや他人が用意した文書は、この簡単なプロセスを説明しています。
難しいのは、既知の 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 "生成されたコードはC11以上が必要です")
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 .
はインクルードパスを追加することを意味し、ディレクトリのルートです。-o mbx_bindings.py
は出力ファイル名を mbx_bindings.py に設定します。MBx_api.h MBx_port.h
は処理するヘッダーファイルを追加します。これらの 2 つのファイルには、ライブラリに必要なすべての定義が含まれています。
生成された 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"送信: {buffer.hex()} ({bytes_written} バイト)")
return MBX_PORT_RETURN_DEFAULT
else:
print(f"送信不完全: 期待される {length} バイト、送信された {bytes_written} バイト")
return MBX_PORT_RETURNT_ERR_INDEFINITE
except serial.SerialException as e:
print(f"シリアルエラー: {e}")
return MBX_PORT_RETURNT_ERR_INDEFINITE
except Exception as e:
print(f"予期しないエラー: {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"受信: {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, // ライブラリ内受信バッファの割り当て
84, // 受信バッファの最大長
STxBuffer, // ライブラリ内送信バッファの割り当て
84) // 送信バッファの最大長
[!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#
-
本体はコンパイルされず、何の動作も生じず、単にファイルを収集し、他のプロジェクトが直接呼び出せるようにします。 ↩