Help us understand the problem. What is going on with this article?

Python: ctypesパターン集

More than 1 year has passed since last update.

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で標準モジュールです。すごい。

terminal
import ctypes

パターン別の解法

Windows API (Win32API) を題材にして、様々な場合のパターンをまとめます。
実は、Windows APIを使う場合、ctypesにあらかじめ構造体の情報が入っているので、非常に簡単に呼び出せます。
ですが、あくまでもパターン集ですので、そういうのに頼らずに自分で書いて使う方法を紹介します。
(おそらく読者の皆様は、サードパーティ製の外部ライブラリを呼んだり、自分たちで開発しているライブラリを呼んだりするのでしょうから)

基本形

Sleep 関数 - MSDN

1秒間寝るだけの単純なプログラムです。まずはここから。

sample.py
# -*- 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側では引数チェックがなされず、エラーも出してくれません。

そこで、restypeargtypeを指定して、型を宣言しておくのがお勧めです。
型は ctypes.c_uint32 のような表現で指定することができます。

文字列を渡す

MessageBox関数を使ってメッセージボックスを表示してみます。

MessageBox 関数 - MSDN

ソースコードをUTF-8で書く前提だと、メッセージボックスの内容が文字化けしてしまいます。
そこでUnicode版のAPIを使い、文字列引数をctypes.c_wchar_p型にします。
また、そこで宣言した引数にはUnicode文字列を与えます。

また後で出てきますが、ウィンドウハンドルは64bit OSならば64bitの値を持つので、ctypes.c_void_p型の引数としています。

sample.py
# -*- 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);

参照渡しでデータを受け取る

GetComputerName 関数 - MSDN

  1. 参照渡しによりバッファの必要サイズを取得
  2. バッファを確保
  3. 文字列として結果を取得
  4. 結果を表示

の流れです。以下のあたりがポイントでしょうか。

  • 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属性で取得できる
sample.py
# -*- 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

GetCursorPos 関数 - MSDN

ctypes.Structure を継承したクラスで構造体を定義します。

  • メンバ変数を_fields_メンバで列挙する
    • 「メンバ変数名とデータ型のタプル」のリスト
  • 構造体を参照渡しすべき引数はポインタ型にする
    • 作成したクラスのインスタンスを指定すると、その先頭アドレスが渡される(参照渡し)
sample.py
# -*- 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と比べると、引っ掛かりポイントは少ないと思います。
事前準備が長いだけで、実際の処理はそうでもありません。

sample.py
# -*- 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)
実行結果:Windowsフォルダ直下にある.exeファイル一覧
bfsvc.exe
explorer.exe
HelpPane.exe
hh.exe
notepad.exe
regedit.exe
RtCRU64.exe
splwow64.exe
winhlp32.exe
write.exe

例3

GetOpenFileName 関数 - MSDN

  • ctypes.sizeof()関数がsizeof相当の機能
  • 変更されるメモリ領域のアドレスを指定するときはポインタ型で定義する
    • メモリ領域は ctypes.create_unicode_buffer() で確保する
    • メモリ領域(実態は配列)の先頭アドレスを取得するには、ctypes.cast() でキャストする
sample.py
# -*- 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関数は、見つかったウィンドウをコールバック関数により通知します。

EnumWindows 関数 - MSDN

以下のような流れになります。

  • Windows APIの場合(呼び出し規約がstdcallの場合)、ctypes.WINFUNCTYPE()関数を使ってコールバック関数クラスを定義する
    • 戻り値型、引数1の型, 引数2の型, ... を引数に指定する
    • 呼び出し規約がcdeclの場合はctypes.CFUNCTYPE()になる
  • 作成したコールバック関数クラスのコンストラクタにPythonの関数を指定すると、APIに渡せるオブジェクトができる

ウィンドウハンドルだけを列挙してもわかりにくいので、ウィンドウのタイトルを合わせて出力する例を示します。

sample.py
# -*- 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から実行する必然性に乏しいですが。

wvsprintf 関数 - MSDN

この関数は引数の渡し方が特殊なのですが、文字列を1個だけ渡すときに限れば、文字列のポインタ(つまり、C言語的にはポインタのポインタ)を渡すのと同じです。(2個以上の場合の話はここではしません)
というわけで、wvsprintfWの最後の引数は ctypes.POINTER(ctypes.c_wchar_p)) としてポインタのポインタ型で宣言します。

このとき、直感的には文字列の参照渡しと考えられるので、Unicode文字列をそのまま渡せば参照渡しになるのではと思うのですが、残念ながらエラーになってしまいます。
ctypes.c_wchar_p型にキャストしておけば、参照渡しができます。

sample.py
# -*- 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))

まとめ

おそらくもっと色々なことができるのでしょうが、取っ掛かりとしてはこれぐらいのパターンがあれば十分かなと。
他のパターンは、やりたくなった時(やらないといけなくなった時)に調べればいいでしょう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした