ctypesとは
- PythonからC/C++の共有ライブラリ(いわゆる.dllとか.soとか)を呼び出すための方法の一つ
- よく使われているSWIGなどの方法より手軽に扱える
- C/C++のコードを追加する必要がない
- JavaのJNA (Java Native Access) の感覚に近い(個人的な意見)
15.17. ctypes — Pythonのための外部関数ライブラリ - Python 2.7.14 ドキュメント
JavaのJNAについてはこちらに書きました。
JNA (Java Native Access) パターン集
新しい題材を考えるのも面倒なので サンプルの内容は全く同じです。
準備
何もいりません。ctypesはPython 2.7/3で標準モジュールです。すごい。
import ctypes
パターン別の解法
Windows API (Win32API) を題材にして、様々な場合のパターンをまとめます。
実は、Windows APIを使う場合、ctypesにあらかじめ構造体の情報が入っているので、非常に簡単に呼び出せます。
ですが、あくまでもパターン集ですので、そういうのに頼らずに自分で書いて使う方法を紹介します。
(おそらく読者の皆様は、サードパーティ製の外部ライブラリを呼んだり、自分たちで開発しているライブラリを呼んだりするのでしょうから)
基本形
1秒間寝るだけの単純なプログラムです。まずはここから。
# -*- coding: utf-8 -*-
import sys
import ctypes
kernel32 = ctypes.WinDLL("kernel32")
kernel32.Sleep.restype = None # void
kernel32.Sleep.argtypes = (ctypes.c_uint32,) # (unsigned int)
print("started")
sys.stdout.flush()
kernel32.Sleep(1000)
print("finished")
WindowsのDLLの場合、ctypes.WinDLL
に名前を指定してアクセスします。呼び出し規約がstdcallの場合はWinDLL
を使います。使用するライブラリによっては、ctypes.CDLL
を使う場合もあると思います。
DLLに含まれる関数は属性としてアクセスできるようになっていて、与えられた引数に対してよろしく処理してくれます。
ただし、引数や戻り値の型をちゃんと指定しておかないと、想定しない動作をすることがあるかもしれません。
また、引数を宣言しておかないとPython側では引数チェックがなされず、エラーも出してくれません。
そこで、restype
やargtype
を指定して、型を宣言しておくのがお勧めです。
型は ctypes.c_uint32
のような表現で指定することができます。
文字列を渡す
MessageBox関数を使ってメッセージボックスを表示してみます。
ソースコードをUTF-8で書く前提だと、メッセージボックスの内容が文字化けしてしまいます。
そこでUnicode版のAPIを使い、文字列引数をctypes.c_wchar_p
型にします。
また、そこで宣言した引数にはUnicode文字列を与えます。
また後で出てきますが、ウィンドウハンドルは64bit OSならば64bitの値を持つので、ctypes.c_void_p
型の引数としています。
# -*- coding: utf-8 -*-
import ctypes
user32 = ctypes.WinDLL("user32")
# A/Wの区別がある場合はWを使う
user32.MessageBoxW.restype = ctypes.c_int32
user32.MessageBoxW.argtypes = (
ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32
)
user32.MessageBoxW(None, u"テスト", u"タイトル", 0);
参照渡しでデータを受け取る
- 参照渡しによりバッファの必要サイズを取得
- バッファを確保
- 文字列として結果を取得
- 結果を表示
の流れです。以下のあたりがポイントでしょうか。
- Unicode版の関数を使う(関数名の末尾にW)
-
uint
型を参照渡ししたいときは、ctypes.c_uint32
型のオブジェクトを作成して指定する- 中身の値は
value
属性で取得できる - 引数の型は
ctypes.POINTER(ctypes.c_uint32)
でポインタ型にする
- 中身の値は
- Unicode文字列バッファの引数は
c_wchar_p
型にする-
None
を渡すとNULLポインタ(void *)0
を渡したことになる
-
- Unicode文字列バッファは
ctypes.create_unicode_buffer()
で作成できる- 中身の文字列は、
c_uint32
などと同じくvalue
属性で取得できる
- 中身の文字列は、
# -*- coding: utf-8 -*-
import sys
import ctypes
kernel32 = ctypes.WinDLL("kernel32")
kernel32.GetComputerNameW.restype = ctypes.c_bool
kernel32.GetComputerNameW.argtypes = (ctypes.c_wchar_p, ctypes.POINTER(ctypes.c_uint32))
lenComputerName = ctypes.c_uint32()
kernel32.GetComputerNameW(None, lenComputerName)
computerName = ctypes.create_unicode_buffer(lenComputerName.value)
kernel32.GetComputerNameW(computerName, lenComputerName)
print(computerName.value)
taro-pc
構造体
例1
ctypes.Structure
を継承したクラスで構造体を定義します。
- メンバ変数を
_fields_
メンバで列挙する- 「メンバ変数名とデータ型のタプル」のリスト
- 構造体を参照渡しすべき引数はポインタ型にする
- 作成したクラスのインスタンスを指定すると、その先頭アドレスが渡される(参照渡し)
# -*- coding: utf-8 -*-
import ctypes
class POINT(ctypes.Structure):
_fields_ = [("X", ctypes.c_int32),
("Y", ctypes.c_int32)]
user32 = ctypes.WinDLL("user32")
user32.GetCursorPos.restype = ctypes.c_bool
user32.GetCursorPos.argtypes = (ctypes.POINTER(POINT),)
pt = POINT()
user32.GetCursorPos(pt)
print(u"x = {0}, y = {1}".format(pt.X, pt.Y))
x = 340, y = 1061
例2
次は構造体の中に固定長のchar
配列とか別の構造体が入ってきた時の話です。
FindFirstFile 関数 - MSDN
FindNextFile関数 - MSDN
FindCLose 関数 - MSDN
-
ctypes.c_wchar * 14
のようにデータ型に自然数を掛けると配列型を定義できる- 掛けた数が要素数になる
- 32bit OSで32bit、64bit OSで64bitのサイズを持つメンバは
ctypes.c_void_p
型で定義する- ハンドルやポインタ型(
UINT_PTR
なども含む) -
WPARAM
/LPARAM
型
- ハンドルやポインタ型(
JNAと比べると、引っ掛かりポイントは少ないと思います。
事前準備が長いだけで、実際の処理はそうでもありません。
# -*- coding: utf-8 -*-
import ctypes
MAX_PATH = 260
INVALID_HANDLE_VALUE = -1
class FILETIME(ctypes.Structure):
_fields_ = [("dwLowDateTime", ctypes.c_uint32), ("dwHighDateTime", ctypes.c_uint32)]
class WIN32_FIND_DATAW(ctypes.Structure):
_fields_ = [
("dwFileAttributes", ctypes.c_uint32), ("ftCreationTime", FILETIME),
("ftLastAccessTime", FILETIME), ("ftLastWriteTime", FILETIME),
("nFileSizeHigh", ctypes.c_uint32), ("nFileSizeLow", ctypes.c_uint32),
("dwReserved0", ctypes.c_uint32), ("dwReserved1", ctypes.c_uint32),
("cFileName", ctypes.c_wchar * MAX_PATH), ("cAlternateFileName", ctypes.c_wchar * 14),
("dwFileType", ctypes.c_uint32), ("dwCreatorType", ctypes.c_uint32),
("wFinderFlags", ctypes.c_uint16)
]
kernel32 = ctypes.WinDLL("kernel32")
kernel32.FindFirstFileW.restype = ctypes.c_void_p
kernel32.FindFirstFileW.argtypes = (
ctypes.c_wchar_p, ctypes.POINTER(WIN32_FIND_DATAW)
)
kernel32.FindNextFileW.restype = ctypes.c_bool
kernel32.FindNextFileW.argtypes = (
ctypes.c_void_p, ctypes.POINTER(WIN32_FIND_DATAW)
)
kernel32.FindClose.restype = ctypes.c_bool
kernel32.FindClose.argtypes = (ctypes.c_void_p,)
pattern = ur"C:\Windows\*.exe"
findData = WIN32_FIND_DATAW()
hfind = kernel32.FindFirstFileW(pattern, findData)
if hfind != INVALID_HANDLE_VALUE:
while True:
print(findData.cFileName)
if not kernel32.FindNextFileW(hfind, findData):
break
kernel32.FindClose(hfind)
bfsvc.exe
explorer.exe
HelpPane.exe
hh.exe
notepad.exe
regedit.exe
RtCRU64.exe
splwow64.exe
winhlp32.exe
write.exe
例3
-
ctypes.sizeof()
関数がsizeof
相当の機能 - 変更されるメモリ領域のアドレスを指定するときはポインタ型で定義する
- メモリ領域は
ctypes.create_unicode_buffer()
で確保する - メモリ領域(実態は配列)の先頭アドレスを取得するには、
ctypes.cast()
でキャストする
- メモリ領域は
# -*- coding: utf-8 -*-
import ctypes
OFN_FILEMUSTEXIST = 0x00001000
class OPENFILENAME(ctypes.Structure):
_fields_ = [
("lStructSize", ctypes.c_uint32), ("hwndOwner", ctypes.c_void_p),
("hInstance", ctypes.c_void_p), ("lpstrFilter", ctypes.c_wchar_p),
("lpstrCustomFilter", ctypes.c_wchar_p), ("nMaxCustFilter", ctypes.c_uint32),
("nFilterIndex", ctypes.c_uint32), ("lpstrFile", ctypes.c_wchar_p),
("nMaxFile", ctypes.c_uint32), ("lpstrFileTitle", ctypes.c_wchar_p),
("nMaxFileTitle", ctypes.c_uint32), ("lpstrInitialDir", ctypes.c_wchar_p),
("lpstrTitle", ctypes.c_wchar_p), ("Flags", ctypes.c_uint32),
("nFileOffset", ctypes.c_uint16), ("nFileExtension", ctypes.c_uint16),
("lpstrDefExt", ctypes.c_wchar_p), ("lCustData", ctypes.c_void_p),
("lpfnHook", ctypes.c_void_p), ("lpTemplateName", ctypes.c_wchar_p),
("pvReserved", ctypes.c_void_p), ("dwReserved", ctypes.c_uint32),
("FlagsEx", ctypes.c_uint32)
]
comdlg32 = ctypes.WinDLL("comdlg32")
comdlg32.GetOpenFileNameW.restype = ctypes.c_bool
comdlg32.GetOpenFileNameW.argtypes = (ctypes.POINTER(OPENFILENAME),)
ofn = OPENFILENAME()
lenFilenameBufferInChars = 1024
buf = ctypes.create_unicode_buffer(lenFilenameBufferInChars)
ofn.lStructSize = ctypes.sizeof(OPENFILENAME)
ofn.lpstrFilter = u"テキストファイル\0*.txt\0\0"
ofn.lpstrFile = ctypes.cast(buf, ctypes.c_wchar_p)
ofn.nMaxFile = lenFilenameBufferInChars
ofn.lpstrTitle = u"ファイルを選択してください"
ofn.Flags = OFN_FILEMUSTEXIST
ret = comdlg32.GetOpenFileNameW(ofn)
if ret:
print(buf.value)
# Python 2.7の場合は以下
#print(buf.value.encode("UTF-8"))
else:
print("キャンセルされました")
C:\Users\taro\test.txt
コールバック関数
Windows APIの関数の中には、イベントの発生に応じて指定したコールバック関数を呼び出すものがあります。
例えば、存在するウィンドウを列挙するEnumWindows
関数は、見つかったウィンドウをコールバック関数により通知します。
以下のような流れになります。
- Windows APIの場合(呼び出し規約がstdcallの場合)、
ctypes.WINFUNCTYPE()
関数を使ってコールバック関数クラスを定義する- 戻り値型、引数1の型, 引数2の型, ... を引数に指定する
- 呼び出し規約がcdeclの場合は
ctypes.CFUNCTYPE()
になる
- 作成したコールバック関数クラスのコンストラクタにPythonの関数を指定すると、APIに渡せるオブジェクトができる
ウィンドウハンドルだけを列挙してもわかりにくいので、ウィンドウのタイトルを合わせて出力する例を示します。
# -*- coding: utf-8 -*-
import ctypes
user32 = ctypes.WinDLL("user32")
EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
user32.EnumWindows.restype = ctypes.c_bool
user32.EnumWindows.argtypes = (EnumWindowsProc, ctypes.c_void_p)
user32.GetWindowTextW.restype = ctypes.c_int32
user32.GetWindowTextW.argtypes = (ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_int32)
def callback(hWnd, lParam):
windowText = ctypes.create_unicode_buffer(1024)
user32.GetWindowTextW(hWnd, windowText, len(windowText))
print("{0:x}: {1}".format(hWnd, windowText.value))
# Python 2.7の場合は以下
#print(u"{0:x}: {1}".format(hWnd, windowText.value).encode("UTF-8"))
return True
user32.EnumWindows(EnumWindowsProc(callback), None)
20730: クイック・アクセス
10226: バッテリ メーター
9d09aa: eclipse
f05b4: 電卓
ポインタのポインタ
Windows APIではあまり使うことがないので何をサンプルにしようか悩みましたが、ここでは文字列フォーマット関数であるwvsprintf
関数を使ってみます。
Pythonでは文字列のformat()
メソッドを使えば同様のことができるので、わざわざPythonから実行する必然性に乏しいですが。
この関数は引数の渡し方が特殊なのですが、文字列を1個だけ渡すときに限れば、文字列のポインタ(つまり、C言語的にはポインタのポインタ)を渡すのと同じです。(2個以上の場合の話はここではしません)
というわけで、wvsprintfW
の最後の引数は ctypes.POINTER(ctypes.c_wchar_p))
としてポインタのポインタ型で宣言します。
このとき、直感的には文字列の参照渡しと考えられるので、Unicode文字列をそのまま渡せば参照渡しになるのではと思うのですが、残念ながらエラーになってしまいます。
ctypes.c_wchar_p
型にキャストしておけば、参照渡しができます。
# -*- coding: utf-8 -*-
import ctypes
user32 = ctypes.WinDLL("user32")
user32.wvsprintfW.restype = ctypes.c_int32
user32.wvsprintfW.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.POINTER(ctypes.c_wchar_p))
buf = ctypes.create_unicode_buffer(1024)
name = u"Michael"
user32.wvsprintfW(buf, u"My name is %s", ctypes.cast(name, ctypes.c_wchar_p))
print(buf.value)
My name is Michael
なお、wvsprintfW
を呼び出す部分は以下のように書いても同じです。
つまり、キャスト先のデータ型のコンストラクタに、キャストしたいデータを指定するのです。
こちらのほうがわかりやすいかもしれません。
user32.wvsprintfW(buf, u"My name is %s", ctypes.c_wchar_p(name))
まとめ
おそらくもっと色々なことができるのでしょうが、取っ掛かりとしてはこれぐらいのパターンがあれば十分かなと。
他のパターンは、やりたくなった時(やらないといけなくなった時)に調べればいいでしょう。