cffi

【Python】CFFIで外部DLLを使用する方法

【Python】CFFIで外部DLLを使用する方法
naoki-hn

Pythonで CFFI モジュールを用いて外部 DLL を使用する方法を解説します。

CFFI で外部の DLL を使用する

Python は非常に豊富なライブラリが用意されているプログラミング言語であるため、多くのことは Python の標準ライブラリ、またはサードパーティ製ライブラリで解決策を見つけることができます。

しかし、ソフトウェア開発会社から提供されているソフトウェアや Windows などで提供されている動的リンクライブラリ (DLL: Dynamic Link Libraries) を Python プログラムから読み込み使用したい場合があります。

Python プログラムから外部の DLL を使用するためのサードパーティ製ライブラリとして CFFI モジュールがあります。Python の標準ライブラリとしては ctypes もありますが、より大規模なライブラリ連携では CFFI を採用するケースもあります。

この記事では、CFFIモジュールを用いて外部DLLを使用する方法を紹介します。

ctypes の使用方法は「ctypesで外部DLLを使用する方法」を参考にしてください。

CFFIとは

CFFI (Foreign Function Interface for Python) は、C 言語互換の共有ライブラリの関数を Python から呼び出すためのライブラリです。

CFFI を用いることで、C の関数や変数、データ構造に Python からアクセスできます。CFFI は API モードと ABI モードの 2 つの動作モードがあり、DLL を使用する場合は主に ABI モードを使用します。

ctypes との主な違い

外部 DLL を使用する方法としては、標準モジュールの ctypes があります。ここでは、ctypes と CFFI の主な違いを整理します。

API モードと ABI モード

CFFI は、API (Application Programming Interface) モードと ABI (Application Binary Interface) モードの両方を提供します。

  • API モード:C のソースコードをコンパイルし、Python 拡張モジュールとして使用します。
  • ABI モード:既存のバイナリ(.dll.so.dylib など)に直接アクセスして使用します。

ctypes は、ABI モードに相当する方式のみを提供します。この記事では、外部の DLL を呼び出すための ABI モードを中心に紹介します。

ローダーの違い

ctypes では、DLL の呼び出し規約に応じて、動的リンクライブラリローダーのクラス (ctypes.CDLLctypes.WinDLLctypes.OleDLLctypes.PyDLL) を使い分ける必要があります。

一方、CFFI では、その区別を意識する必要はありません。ただし、関数のプロトタイプ(引数型や戻り値型)を正確に宣言する必要があります。

データ型の宣言

CFFI では、C 言語のデータ型を文字列で記述します。C 言語のヘッダファイルをそのまま利用できるため、型の不一致を防ぎやすいことが特徴です。

エラーハンドリング

CFFI では、宣言した C の型に変換できない引数などは Python 例外として検出します。ただし、宣言自体を誤ると誤動作につながる可能性があります。Windows API の失敗は戻り値によるエラー判定が別途必要になります。

ctypes と CFFIの使い分けの基準

ctypes と CFFI のどちらを使用するかは、プロジェクトの要件や開発者の好みに依存します。しかし、一般的な使い分けの目安は以下の通りです。

項目概要
プロジェクト規模ctypes は、学習コストが低く小規模で簡単な開発には最適です。一方で、CFFI はより複雑なインタフェースやパフォーマンスが重要な大規模開発に適しています。
型安全性CFFI は型宣言を明示するため、型の不一致に気付きやすく、型安全を保ちやすい傾向があります。
依存関係ctypes は標準ライブラリのため、外部依存を避けたい場合に適しています。一方で、CFFI はサードパーティ製ライブラリのため別途インストールが必要です。
柔軟性大規模・高機能な統合でより柔軟な開発を求める場合には、CFFI の方が有利です。

上記の通り、簡単な試作や小規模用途には ctypes が適しており、複雑な C ライブラリとの本格的な統合には CFFI が適しています。

CFFI を使用した外部 DLL の使用方法

ctypes の紹介記事である「ctypesで外部DLLを使用する方法」でも紹介した DLL 呼び出し例を使用して CFFI を使用した外部 DLL の使用方法を紹介します。

user32.dll を用いた MessageBox の表示

ここでは CFFI の ABI モードで Windows のライブラリである user32.dll を使用して MessageBox を表示する例を紹介します。

user32.dll は Windows に標準で含まれている Windows API の一部で、Windows のユーザーインターフェースに関する関数を提供しています。

user32.dll を用いて MessageBox を表示するには以下のようにします。

from cffi import FFI

# FFIインスタンスの生成
ffi = FFI()

# 関数の宣言
ffi.cdef(
    """
    int MessageBoxW(void *hwnd, const wchar_t *text, const wchar_t *caption, unsigned int type);
    """
)

# DLLのロード (拡張子は省略可能)
user32 = ffi.dlopen("user32")

try:
    # 変数の生成
    text = ffi.new("wchar_t[]", "Hello, World!")
    caption = ffi.new("wchar_t[]", "From Python")

    # メッセージボックスの表示 (ffi.NULL は ffi.cast("void*", 0) と同等)
    result = user32.MessageBoxW(ffi.NULL, text, caption, 0)

    # --- 以下はエラーが発生する例 ---
    # 以下のようにすると型エラーとなる
    # result = user32.MessageBoxW(ffi.NULL, 123, 456, 0)

    # 以下のように無効な HWND を指定するとWindows APIエラーとなる
    # result = user32.MessageBoxW(
    #     ffi.cast("void*", 0x12345678), text, caption, 0
    # )
    # ------------------------------------

    # 返却値が 0 の場合はエラー
    if result == 0:
        # 直前の GetLastError() の値と対応するエラーメッセージを取得
        errcode, errmsg = ffi.getwinerror()
        raise OSError(errcode, errmsg)

    print(f"MessageBox return: {result}")

except OSError as ex:
    # Windows API 呼び出しでエラーが発生した場合
    print(f"Windows API エラー: {ex}")

except TypeError as ex:
    # 引数の型が不正な場合
    print(f"引数の型エラー: {ex}")

【実行結果】

CFFI 使用例

OK を押すと標準出力には以下のように表示されます。

MessageBox return: 1

以降で部分ごとにポイントを解説します。

【インポートとFFIインスタンスの準備】

from cffi import FFI

# FFIインスタンスの生成
ffi = FFI()

CFFI を使用するには、まずインポートする必要があります。cffi モジュールから FFI クラスをインポートして、インスタンスを生成します。

【関数のプロトタイプ定義】

# 関数の宣言
ffi.cdef(
    """
    int MessageBoxW(void *hwnd, const wchar_t *text, const wchar_t *caption, unsigned int type);
    """
)

CFFI を使用するうえで非常に重要になってくるのが、この関数のプロトタイプ定義です。プロトタイプ定義と言っているのは関数定義や引数の型指定のことをです。

関数の宣言では、FFI クラスの cdef メソッドを使用します。cdef に関数の宣言となる文字列を指定します。これにより Python の型と適切に変換を行うことができます。ctypes を使った外部 DLL 利用の際でも同様の型宣言 (argtypesrestype) がありますが CFFI では、文字列を使用して宣言ができることが特徴的です。

cdef メソッドに指定する文字列内は、C 言語のヘッダ宣言を参考にしつつ、必要な型・関数宣言を文字列で記述できます。C 言語のヘッダファイルから宣言を簡単にコピーできるため、不必要なミスを減らすことができる可能性があります。また、読み込み先の DLL が複数であっても、1 つの文字列でまとめて宣言することが可能です。

【DLLのロード】

# DLLのロード (拡張子は省略可能)
user32 = ffi.dlopen("user32")

DLL のロードでは FFI クラスの dlopen メソッドを使用して、DLL のファイル名を指定します。今回は user32.dll を読み込んでいますが、拡張子は省略可能です。

【実行とエラーハンドリング】

try:
    # 変数の生成
    text = ffi.new("wchar_t[]", "Hello, World!")
    caption = ffi.new("wchar_t[]", "From Python")

    # メッセージボックスの表示 (ffi.NULL は ffi.cast("void*", 0) と同等)
    result = user32.MessageBoxW(ffi.NULL, text, caption, 0)

    # 返却値が 0 の場合はエラー
    if result == 0:
        # 直前の GetLastError() の値と対応するエラーメッセージを取得
        errcode, errmsg = ffi.getwinerror()
        raise OSError(errcode, errmsg)

    print(f"MessageBox return: {result}")

except OSError as ex:
    # Windows API 呼び出しでエラーが発生した場合
    print(f"Windows API エラー: {ex}")

except TypeError as ex:
    # 引数の型が不正な場合
    print(f"引数の型エラー: {ex}")

実行する場合には、MessageBoxW に引数を渡して実行します。

第 1 引数に ffi.NULL を渡すと親ウィンドウを持たないことを意味します。なお、ffi.NULLffi.cast("void*", 0) と同等です。また、第 4 引数の 0 は、OK ボタンのみの標準メッセージボックスを指定しています。第 2, 3 引数の文字列については、ffi.new により型を適切に指定して生成して使用することが安全です。

実行すると以下のような画面が表示できます。

CFFI 使用例

CFFI では、型チェックなどについては、Python 側の例外でエラーチェックを行うことができますので try ... except ...で例外処理ができます。以下の部分のコメントアウトしてある部分に変更して実行すると TypeError の発生が確認できます。

 # 以下のようにすると型エラーとなる
result = user32.MessageBoxW(ffi.NULL, 123, 456, 0)
【実行結果】
引数の型エラー: initializer for ctype 'wchar_t *' must be a cdata pointer, not int

この例では、文字列(wchar_t *)の部分に int を指定しているためのエラーです。

一方で、MessageBoxW 側のエラーについては返却値を受け取って処理をする必要があります。例では、ffi.getwinerror で直前のエラーを取得し、OSErrorraise するようにしています。

    # 返却値が 0 の場合はエラー
    if result == 0:
        # 直前の GetLastError() の値と対応するエラーメッセージを取得
        errcode, errmsg = ffi.getwinerror()
        raise OSError(errcode, errmsg)

以下のコメントアウトの部分に変更して実行してみると例外の発生の処理に入っていることが確認できます。

# 以下のように無効な HWND を指定するとWindows APIエラーとなる
result = user32.MessageBoxW(
    ffi.cast("void*", 0x12345678), text, caption, 0
)
【実行結果】
Windows API エラー: [Errno 1400] ウィンドウ ハンドルが無効です。

エラーコード 1400 は、Windows API 関数の呼び出しで発生する可能性があるエラーコードの一つで ERROR_INVALID_WINDOW_HANDLE を表します。このエラーは「無効なウィンドウ ハンドルが指定されました」という意味です。

上記で紹介したように CFFI を使用することで簡単に外部の Windows API を Python から使用することができました。他にも様々な DLL を扱う際に CFFI は使用できます。使用の際には関数のプロトタイプ定義が重要になってきますので注意して使用するようにしましょう。

まとめ

Python で CFFI モジュールを用いて外部 DLL を使用する方法を解説しました。

この記事では CFFI の概要を紹介するとともに Windows API の DLL (user32.dll)を使用してメッセージボックスを表示する簡単な例を紹介しました。また、同様に外部 DLL を使用することができる Python 標準ライブラリの ctypes との違いについても触れながら紹介をしています。

CFFI は、C 言語の関数宣言をそのままうまく使うことができ、非常に使いやすいライブラリです。小規模な開発や簡単な試作では ctypes の方が簡単に使用できるかもしれませんが、大規模開発や複雑な C 言語ライブラリとの統合は ctypes よりも CFFI の方がより適している可能性があります。

CFFI の使用方法を理解していただき、必要に応じて調べながら使用してみていただければと思います。

CFFI の公式ドキュメントはこちらを参照してください。

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
【Python Tech】プログラミングガイド
【Python Tech】プログラミングガイド
ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。 私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

プログラミング言語はこれまでC、C++、JAVA等を扱ってきましたが、最近では特に機械学習等の分析でも注目されているPythonについてとても興味をもって取り組んでいます。これまでの経験をもとに、Pythonに興味を持つ方のお役に立てるような情報を発信していきたいと思います。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました