cffi

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

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

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

外部のDLLを使用する

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

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

Pythonプログラムから外部のDLLを使用するためのサードパーティ製ライブラリとしてCFFIモジュールがあります。この記事では、CFFIモジュールを用いて外部DLLを使用する方法を紹介します。

外部DLLを使用する方法としては、標準モジュールのctypesを使用する方法もあります。ctypesの使用方法は「ctypesで外部DLLを使用する方法」でまとめているので参考にしてください。

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

CFFIとは

CFFI (Foreign Function Interface for Python)は、C言語の規約に従って開発されたDLLの関数を呼び出すためのPythonライブラリです。

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

ctypesとの主な違い

外部DLLを使用する方法としては、標準モジュールのctypesを使用する方法もあります。ctypesの使用方法は「ctypesで外部DLLを使用する方法」を参照してください。ここでは、ctypesとCFFIの主な違いを紹介します。

APIモードとABIモード

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

  • APIモード:C言語のソースコードからPython拡張モジュールをコンパイルして使用します。
  • ABIモード:コンパイルされたバイナリ(例えば、.dll.so.dylib等)に直接アクセスして使用します。

ctypesは、ABIモードのような方式のみを提供するため、コンパイルされたバイナリに直接アクセスすることのみ可能です。CFFIは、2つのモードを使用できますが、この記事では外部のDLLを呼び出すためのABIモードの利用方法を中心に紹介します。

クラスに関する考慮

ctypesを使用する際には、リンクするDLLがどのような規約に従っているかによって、動的リンクライブラリローダーのクラス (ctypes.CDLLctypes.WinDLLctypes.OleDLLctypes.PyDLL)のうち、どれを使用するかを考量する必要がありました。

CFFIでは、このような区別を気にすることなくDLLへアクセスすることができます。しかし、関数のプロトタイプ(関数定義や引数の型など)を正確に指定する必要があるなどCFFI独自の注意点があることにも理解が必要です。

データ型の宣言

CFFIでは、C言語のデータ型を文字列で直接宣言します。これにより、C言語のヘッダファイルから型宣言を簡単にコピペできるため、直感的でエラーが発生しにくくなります。

エラーハンドリング

CFFIは、Pythonの例外を使用してエラーを処理します。ctypesに比べてよりPythonicなアプローチをとります。

ctypesとCFFIの使い分けの基準

ctypesとCFFIのどちらを使用するかは、プロジェクトの要件により異なりますし、開発者の好みにも依存します。しかし、一般的に考えることができる使い分けの基準について紹介します。

項目概要
プロジェクト規模と複雑さctypesは、学習コストが低く小規模で簡単な開発には最適です。一方で、CFFIはより複雑なインタフェースやパフォーマンスが重要な大規模開発に適しています。
型安全性とエラー処理CFFIは型チェックが行われるので型安全性を保ちやすくなります。また、例外でのエラーハンドリングで適切な例外処理が可能です。
依存関係と配布ctypesは標準ライブラリのため、外部依存を避けたい場合に適しています。一方で、CFFIはサードパーティ製ライブラリのため別途インストールが必要です。
開発速度と柔軟性小規模な開発や試作にはctypesが便利ですが、CFFIの方がより柔軟な開発や深い統合を求める場合に強力です。

上記のように、簡単なプロジェクトの開発やスピードが求められる試作ではctypesが適しているかもしれませんが、大規模開発や複雑なC言語ライブラリとの統合はCFFIの方がより適していると考えることができます。

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

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

user32.dllを用いたMessageBoxの表示

ここでは、ctypes.WinDLLを用いて、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.dll")

try:
    # MessageBoxの表示
    result = user32.MessageBoxW(ffi.NULL, "Hello, World!", "From Python", 0)
    print(f"MessageBox return: {result}")

except TypeError as ex:
    print(f"An error occurred: {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が複数であっても、一つの文字列でまとめて宣言することが可能です。

【DLLのロード】

# DLLのロード
user32 = ffi.dlopen("user32.dll")

DLLのロードでは、FFLクラスのdlopenメソッドを使用して、DLLのファイル名を指定します。今回は、user32.dllkernel32.dllを読み込んでいます。

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

try:
    # MessageBoxの表示
    result = user32.MessageBoxW(ffi.NULL, "Hello, World!", "From Python", 0)
    print(f"MessageBox return: {result}")
except TypeError as ex:
    print(f"An error occurred: {ex}")

MessageBoxの表示では、ロードしたDLLに対して関数を呼び出すだけで使用できます。

CFFIでは、例外でエラーチェックを行うことができますので、try..except..で例外の処理をしています。例えば、以下のようにHWND (ウィンドウハンドルを示す識別子)に該当する第1引数に-1を指定して実行すると例外が発生します。

result = user32.MessageBoxW(-1, "Hello, World!", "From Python", 0)
【実行結果】
An error occurred: initializer for ctype 'void *' must be a cdata pointer, not int

結果を見ると引数の型に対するチェックをして例外を出してくれていることが分かります。

なお、ctypesの紹介記事である「ctypesで外部DLLを使用する方法」でも、同じように-1を設定するような例を紹介しましたが、ctypesの例では戻り値でエラーをチェックし、GetLastErrorを使ってエラーを識別していました。

もちろんCFFIでも、戻り値をベースにGetLastErrorを使用することもできますし、戻り値でのエラー処理が問題があるということでもありません。ここでお伝えしたいのは、ctypesとCFFIで例外に対する処理が少し異なるという点です。

上記で紹介したように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の使用方法を理解していただき、必要に応じて調べながら使用してみていただければと思います。