Avance.Lab

技術紹介

Pythonから呼び出されるDLL(C++)を作成する

公開日:2023.07.28 更新日:2023.07.28

tag: Python

こんにちは、Yu-saです。

最近はPythonで記載することが多くなってきましたが、高速化する為に一部のコードをC++に変換することがありました。

Python側からは「ctypes」を使用してDLLを呼び出すことになり、その際の経験を基に「ctypes」の使い方を紹介させて頂きます。

「ctypes」とは?

「ctypes」は標準で使用できるライブラリです。
Cと互換性のあるデータ型を提供しており、動的リンク/共有ライブラリ内の関数の呼び出しが可能です。
基本のデータ型としては以下の表の型がサポートされています。

ctypesの型Cの型Pythonの型
c_bool_Boolbool (1)
c_charchar1文字のバイト列オブジェクト
c_wcharwchar_t1文字の文字列
c_bytecharint
c_ubyteunsigned charint
c_shortshortint
c_ushortunsigned shortint
c_intintint
c_uintunsigned intint
c_longlongint
c_ulongunsigned longint
c_longlong__int64 または long longint
c_ulonglongunsigned __int64 または unsigned long longint
c_size_tsize_tint
c_ssize_tssize_t または Py_ssize_tint
c_floatfloat浮動小数点数
c_doubledouble浮動小数点数
c_longdoublelong double浮動小数点数
c_char_pchar* (NUL 終端)バイト列オブジェクトまたは None
c_wchar_pwchar_t* (NUL 終端)文字列または None
c_void_pvoid*整数または None

また、構造体やコールバック関数もサポートされています。

※引用元※
ctypes(Python公式サイト)

Pythonからの使用例

引数/戻り値

Python側ではctypesで提供されている型を使用することでC++側とデータのやり取りが可能です。

#define DLL_EXPORT extern "C" __declspec(dllexport)

DLL_EXPORT int __stdcall Func_001(int value1 , int value2)
{
    int sum = value1 + value2;
    return sum;
}
import ctypes

def test_001():
    # DLLを指定
    dll = ctypes.WinDLL(r"DllTest.dll")

    # 関数を指定(DLLで公開している名称を使用する)
    func = dll.Func_001

    # 戻り値の型を指定
    func.restype = ctypes.c_int32

    # 引数の型を指定
    func.argtypes = (ctypes.c_int32 , ctypes.c_int32)

    # 関数を実行
    num = func( 3 ,2 )

    print(num)
5

文字列配列

文字列は「ctypes.c_char_p」を使用します。

#define DLL_EXPORT extern "C" __declspec(dllexport)

DLL_EXPORT void __stdcall Func_002(char* msg)
{
    printf("%s\r\n", msg);
}
import ctypes

def test_002():
    # DLLを指定
    dll = ctypes.WinDLL(r"DllTest.dll")

    # 関数を指定(DLLで公開している名称を使用する)
    func = dll.Func_002

    # 戻り値の型を指定
    func.restype = None

    # 引数の型を指定
    func.argtypes = (ctypes.c_char_p , )

    # 引数の準備
    msg_base  = "Hello World"
    msg_tmp = ctypes.create_string_buffer(msg_base.encode('UTF-8'))      # 新しく文字列を作成
    msg_data = ctypes.cast(msg_tmp, ctypes.c_char_p)                     # ポインタにキャスト


    # 関数を実行
    func( msg_data )

Hello World

配列

文字列以外の配列は「ctypes.POINTER」と「ctypes.pointer」を使用します。この時に使用できるのもctypesで提供されている型となります。
「ctypes.POINTER」で型を取得し、「ctypes.pointer」ではアドレスを取得します。
また、動的配列を使用する場合はC++側では要素数を取得できないので、ポインタとは別に要素数を渡す必要があります。

#define DLL_EXPORT extern "C" __declspec(dllexport)

DLL_EXPORT void __stdcall Func_003(int* values, int length)
{
    for (int idx = 0; idx < length; idx++)
    {
        printf("index[%d] = %d\r\n", idx , values[idx]);
    }
}
import ctypes

def test_003():
    # DLLを指定
    dll = ctypes.WinDLL(r"DllTest.dll")

    # 関数を指定(DLLで公開している名称を使用する)
    func = dll.Func_003

    # 戻り値の型を指定
    func.restype = None

    # 引数の型を指定
    func.argtypes = ( ctypes.POINTER(ctypes.c_int32) , ctypes.c_int32 )

    # 引数の準備
    num_base = [ 11 , 22 , 33 , 44 , 55 ]
    num_tmp  = (ctypes.c_int32 * len(num_base))(*num_base)  # c_int32の配列に変換
                                                            # 変換する際には「型 * 要素数」でキャストする
    num_data = ctypes.cast(ctypes.pointer(num_tmp) , ctypes.POINTER(ctypes.c_int32))
                                                            # ポインタにキャスト


    # 関数を実行
    func(num_data , len(num_base))      # 第1引数で配列のポインタ、第2引数で配列の要素数を渡す
index[0] = 11
index[1] = 22
index[2] = 33
index[3] = 44
index[4] = 55

構造体

「ctypes.Structure」を継承したクラスを定義することで構造体を使用できます。
また、構造体のポインタを使用したい場合は「ctypes.Structure」を継承したクラスであれば、「ctypes.POINTER(Structure_Test_004)」のようにすることでポインタとして使用できます。

#define DLL_EXPORT extern "C" __declspec(dllexport)

typedef struct
{
    char*   value_1;
    int     value_2;
    int*    value_list;
    size_t  value_list_length;
} ST_TEST_004;

DLL_EXPORT void __stdcall Func_004(ST_TEST_004 data)
{
    printf("value_1 = [%s]\r\n", data.value_1);
    printf("value_2 = [%d]\r\n", data.value_2);

    for (int idx = 0; idx < data.value_list_length; idx++)
    {
        printf("value_list[%d] = %d\r\n", idx, data.value_list[idx]);
    }
}
import ctypes

class Structure_Test_004(ctypes.Structure):
    # 構造体パッキングを指定。#pragma pack (push, 1)と同等
    _pack_ = 1

    # 構造体のメンバを指定。
    _fields_ = [
        ('value_1',ctypes.c_char_p),                        # Cでの「char*」
        ('value_2',ctypes.c_int32),                         # Cでの「int」
        ('value_list',ctypes.POINTER(ctypes.c_int32)),      # Cでの「int*」
        ('value_list_length',ctypes.c_size_t),              # 配列の要素数
    ]

def test_004():
    # DLLを指定
    dll = ctypes.WinDLL(r"DllTest.dll")

    # 関数を指定(DLLで公開している名称を使用する)
    func = dll.Func_004

    # 戻り値の型を指定
    func.restype = None

    # 引数の型を指定
    func.argtypes = ( Structure_Test_004 , )

    # 文字列を準備
    msg_base = "Hello World"
    msg_tmp  = ctypes.create_string_buffer(msg_base.encode('UTF-8'))    # 新しく文字列を作成
    msg_data = ctypes.cast(msg_tmp, ctypes.c_char_p)                    # キャスト


    # 配列を準備
    num_base = [ 11 , 22 , 33 , 44 , 55 ]
    num_tmp  = (ctypes.c_int32 * len(num_base))(*num_base)  # c_int32の配列に変換
                                                            # 変換する際には「型 * 要素数」でキャストする
    num_data = ctypes.cast(ctypes.pointer(num_tmp) , ctypes.POINTER(ctypes.c_int32))     
                                                            # ポインタにキャスト

    # 構造体を準備
    st_test_004 = Structure_Test_004()
    st_test_004.value_1 = msg_data
    st_test_004.value_2 = 12345678
    st_test_004.value_list = num_data
    st_test_004.value_list_length = len(num_base)

    # 関数を実行
    func(st_test_004)
value_1 = [Hello World]
value_2 = [12345678]
value_list[0] = 11
value_list[1] = 22
value_list[2] = 33
value_list[3] = 44
value_list[4] = 55

コールバック

関数ポインタを使用するには「ctypes.CFUNCTYPE」もしくは「ctypes.WINFUNCTYPE」を使用します。
なお、コールバック関数に指定する関数はctypesで提供されている型を使用している必要があります。

#define DLL_EXPORT extern "C" __declspec(dllexport)

typedef int (*CALLBACK_POINTER)(int*, size_t);

DLL_EXPORT void __stdcall Func_005(CALLBACK_POINTER func)
{
    // データの準備
    const size_t length = 10;
    int data[length] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // コールバック関数の実行
    int sum = func(data, length);

    // 戻り値の表示
    printf("sum=%d\r\n", sum);

}
import ctypes

# C++側から呼び出されるコールバック関数
def test_005_callback( data:ctypes.POINTER(ctypes.c_int32) , length:ctypes.c_size_t)->ctypes.c_int32:
    sum = 0
    for idx in range(length):
        sum += data[idx] 

    return sum

def test_005():
    # コールバック関数の型を定義
    CallbackProc = ctypes.WINFUNCTYPE( 
        ctypes.c_int32 ,                    # コールバック関数の戻り値の型
        ctypes.POINTER(ctypes.c_int32) ,    # コールバック関数の第1引数の型
        ctypes.c_size_t                     # コールバック関数の第2引数の型
        )

    # 関数ポインタへの変換
    callback = CallbackProc(test_005_callback)

    # DLLを指定
    dll = ctypes.WinDLL(r"DllTest.dll")

    # 関数を指定(DLLで公開している名称を使用する)
    func = dll.Func_005

    # 戻り値の型を指定
    func.restype = None

    # 引数の型を指定
    func.argtypes = ( CallbackProc , )

    # 関数を実行
    func(callback)
sum=55

おわり

いかがでしたでしょうか。
ここで紹介させて頂いたのは基本的な使い方となり、全て紹介できておりません。
機会があれば、続きを紹介してみたいと思います。

Yu-sa

Windowsアプリや組み込み系を担当しています。
最近はハードの知識も取り込みたいです。

関連記事