Python: ctypesパターン集

# ctypesとは

- PythonからC/C++の共有ライブラリ（いわゆる.dllとか.soとか）を呼び出すための方法の一つ
- よく使われているSWIGなどの方法より手軽に扱える
    - C/C++のコードを追加する必要がない
    - JavaのJNA (Java Native Access) の感覚に近い（個人的な意見）

[15.17. ctypes — Pythonのための外部関数ライブラリ - Python 2.7.14 ドキュメント](https://docs.python.jp/2.7/library/ctypes.html)

JavaのJNAについてはこちらに書きました。
[JNA (Java Native Access) パターン集](https://qiita.com/hira_physics/items/b888cbec643f14de5ea6)


新しい題材を考えるのも面倒なので
 サンプルの内容は全く同じです。

# 準備

何もいりません。ctypesはPython 2.7/3で標準モジュールです。すごい。

```Python:terminal
import ctypes
```

# パターン別の解法

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

## 基本形
[Sleep 関数 - MSDN](https://msdn.microsoft.com/ja-jp/library/cc429358.aspx?f=255&MSPPError=-2147217396)

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

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

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

## 文字列を渡す

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

[MessageBox 関数 - MSDN](https://msdn.microsoft.com/ja-jp/library/cc410914.aspx)

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

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

```Python: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](https://msdn.microsoft.com/ja-jp/library/cc429732.aspx)

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``属性で取得できる

```Python: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)
```

```text:実行結果：コンピュータ名
taro-pc
```

## 構造体

### 例1

[GetCursorPos 関数 - MSDN](https://msdn.microsoft.com/ja-jp/library/cc364640.aspx)

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

- メンバ変数を``_fields_``メンバで列挙する
    - 「メンバ変数名とデータ型のタプル」のリスト
- 構造体を参照渡しすべき引数はポインタ型にする
    - 作成したクラスのインスタンスを指定すると、その先頭アドレスが渡される（参照渡し）

```Python: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))
```

```text:実行結果：マウスカーソルの位置
x = 340, y = 1061
```

### 例2

次は構造体の中に固定長の``char``配列とか別の構造体が入ってきた時の話です。

[FindFirstFile 関数 - MSDN](https://msdn.microsoft.com/ja-jp/library/cc429233.aspx)
[FindNextFile関数 - MSDN](https://msdn.microsoft.com/ja-jp/library/cc429242.aspx)
[FindCLose 関数 - MSDN](https://msdn.microsoft.com/ja-jp/library/cc429224.aspx)

- ``ctypes.c_wchar * 14`` のようにデータ型に自然数を掛けると配列型を定義できる
    - 掛けた数が要素数になる
- 32bit OSで32bit、64bit OSで64bitのサイズを持つメンバは``ctypes.c_void_p``型で定義する
    - ハンドルやポインタ型（``UINT_PTR``なども含む）
    - ``WPARAM``/``LPARAM``型

JNAと比べると、引っ掛かりポイントは少ないと思います。
事前準備が長いだけで、実際の処理はそうでもありません。

```Python: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)
```

```text:実行結果：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](https://msdn.microsoft.com/ja-jp/library/cc364716.aspx)

- ``ctypes.sizeof()``関数が``sizeof``相当の機能
- 変更されるメモリ領域のアドレスを指定するときはポインタ型で定義する
    - メモリ領域は ``ctypes.create_unicode_buffer()`` で確保する
    - メモリ領域（実態は配列）の先頭アドレスを取得するには、``ctypes.cast()`` でキャストする

```Python: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("キャンセルされました")
```

```text:実行結果：選択されたファイル名
C:\Users\taro\test.txt
```

## コールバック関数

Windows APIの関数の中には、イベントの発生に応じて指定したコールバック関数を呼び出すものがあります。
例えば、存在するウィンドウを列挙する``EnumWindows``関数は、見つかったウィンドウをコールバック関数により通知します。

[EnumWindows 関数 - MSDN](https://msdn.microsoft.com/ja-jp/library/cc410851.aspx)

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

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

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

```Python: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)
```

```text:実行結果：開かれているウィンドウ一覧（抜粋）
20730: クイック・アクセス
10226: バッテリ メーター
9d09aa: eclipse
f05b4: 電卓
```

## ポインタのポインタ

Windows APIではあまり使うことがないので何をサンプルにしようか悩みましたが、ここでは文字列フォーマット関数である``wvsprintf``関数を使ってみます。
Pythonでは文字列の``format()``メソッドを使えば同様のことができるので、わざわざPythonから実行する必然性に乏しいですが。

[wvsprintf 関数 - MSDN](https://msdn.microsoft.com/ja-jp/library/cc364880.aspx)

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

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

```Python: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)
```

```text:実行結果：フォーマットされた文字列
My name is Michael
```

なお、``wvsprintfW``を呼び出す部分は以下のように書いても同じです。
つまり、キャスト先のデータ型のコンストラクタに、キャストしたいデータを指定するのです。
こちらのほうがわかりやすいかもしれません。

```Python
user32.wvsprintfW(buf, u"My name is %s", ctypes.c_wchar_p(name))
```

# まとめ

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