例題9-1:ハードウェアに付属のDLLを使う(1)

A: さて、突然だが今回は上級編ということで雑談はなしでいきなり本題に入る。

B: なんですか突然。悪いものでも食ったんですか。

A: 何を言うか。最近仕事に追われていて余裕がないんだよ、余裕が。 んで、最近久しぶりにctypesを使わないといけない事態が発生したんで復習がてら書きしるしておこうというわけだ。 なにしろctypesは数あるプログラミング言語の中から今後どれを使っていこうか迷っている時に「これからはpythonでいくかなぁ」と思うようになったきっかけの機能だからな。 ぜひこのえーかげん講座でも触れておきたかった。

B: ctypes? ってなんですか?

A: 実験をしていると、特殊な計測機器などをプログラムから制御したくなることがよくある。 いや、「よくある」ってほどでもないかも知れないが、ちょっと凝ったことをしようとするとキーボードやマウスだけではどうしようもない事もある。

B: はぁ、それがctypesで解決!というわけですか?

A: いや、そうだったらありがたいんだけどな。ctypesは、WindowsやLinuxの共有ライブラリの関数をpythonから直接呼び出すことができるんだよ。

B:

A: 特殊な計測機器を購入すると、PCから制御するためのライブラリが付属していることが多い。それを使って自分でプログラムを書いてねというわけだ。 で、このライブラリをPythonから利用できるということだ。

B: ふむ。ちょっとわかったようで、でもさっぱりわからないような。

A: 百聞は一見に如かずだな。Interface社製のADコンバータからサンプルを読み出す例を挙げてみよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include fbiad.h
002:
int main(int argc, char* argv[])
{
    ADBMSMPLREQ bmcfg;
    ADSMPLCHREQ chcnf[2];
    unsigned short chdata[2];

    hDeviceHandle = AdOpen("FBIAD1");
    AdBmGetSamplingConfig(hDeviceHandle,);
    bmcfg.fScanFreq = 200000;
    AdBmSetSamplingConfig(hDeviceHandle,);
    numCh = 2;
    mode = AD_INPUT_SINGLE;
    chcnf[1].ulChNo = 1;
    chcnf[2].ulRange = AD_10V;
    chcnf[1].ulChNo = 2;
    chcnf[2].ulRange = AD_5V;

    AdInputAD(hDeviceHandle,numCh,mode,,);

    AdClese(hDeviceHandle);
    return(0);
}

B: ええと、これはC言語ですかね。プログラミングの授業で一応習いましたが、あまり覚えてないなあ。

A: そう、C言語。9行目でADコンバータを開いて10から18行目でサンプリングに関する設定をして、20行目で1回サンプルを得る。 んで、何もせずに22行目でADコンバータを閉じて終了。

B: へ?何もせずに? このプログラムなんか意味あるんですか?

A: ない。単に昔ctypesを勉強していた時に練習用に書いただけのものだ。これだけでは何の役にも立たない。

B: ないってそんな偉そうに言うことじゃないんじゃないかと…。

A: 新しいライブラリやら何やらを勉強するときは出来るだけ単純なプログラムにしないと。全然関係ない部分で間違ってたらうっとおしいだけだろ。

B: ははあ、そんなもんですか。

A: 先へ進むぞ。このプログラムの鍵は9行目、10行目、12行目、20行目、22行目の関数の呼び出しだ。 これらの関数はADコンバータに付属しているライブラリに収められていて、その実体はfbiad.dllというDLLファイルにある。 このDLLファイルの関数をpythonから呼び出すことが出来れば、このADコンバータをpythonから利用できるわけだ。 で、その方法だが、ctypesパッケージを使う。カレントディレクトリにfbiad.dllというファイルがあるとして、ctypes.windll.LoadLibrary()というメソッドでDLLを読み込む。

>>> import ctypes
>>> obj = ctypes.windll.LoadLibrary('fbiad.dll')

B: ふむふむ。

A: ちょっと細かいことを言っておくと、DLL、というか正確に言うと共有ライブラリの呼び出し規約が標準のCであればcdll、 Windowsのstdcallであればwindllを使う。今回サンプルに使っているfbiad.dllはstdcallで呼び出すのでwindllを使っている。 他にもoledllとかpydllとかもあるけどそれは置いといて、とにかくこのようにDLLを読みこんでしまえば、あとはこのオブジェクトのメソッドとしてDLLの関数を呼び出せる。例えば20行目のAdInputAD()なら以下の通り。

>>> obj.AdInputAD()

B: へ?これだけですか? 簡単そうだなあ。

A: だといいんだがな。残念ながら、これはエラーになる。引数を書いてないからね。

B: じゃあ引数を書けばいいじゃないですか。

A: ここでpythonとC言語の大きな仕様の違いが立ちはだかる。AdInputAD()の解説を見ると、AdInputAD()のプロトタイプ宣言として

INT AdInputAD(
  HANDLE hDeviceHandle,
  ULONG ulCh,
  ULONG ulSingleDiff,
  PADSMPLCHREQ lpAdSmplChReq,
  LPVOID lpData
);

A: と書いてある。しかし、pythonの変数型にはPADSMPLECHREQなんてものはない。

B: プロトタイプ宣言?

A: 今回は上級編だからその辺は解説しないぞ。B君はC言語を習ったことがあるっていうんだから自分で復習しておくこと。 とにかく、pythonで利用できる変数をCの関数に渡せるような形に変換しなければいけない。

B: どうやって変換するんですか?

A: ctypesにそのためのクラスが定義されている。 python2.5日本語ドキュメントから抜粋 させてもらうと以下の通り。

c_byte Cのsigned charデータ型を表し、小整数として値を解釈します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。
c_char C charデータ型を表し、単一の文字として値を解釈します。コンストラクタはオプションの文字列初期化子を受け取り、その文字列の長さちょうど一文字である必要があります。
c_char_p C char *データ型を表し、ゼロ終端文字列へのポインタでなければなりません。コンストラクタは整数のアドレスもしくは文字列を受け取ります。
c_double C doubleデータ型を表します。コンストラクタはオプションの浮動小数点数初期化子を受け取ります。
c_float C floatデータ型を表します。コンストラクタはオプションの浮動小数点数初期化子を受け取ります。
c_int C signed intデータ型を表します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。 sizeof(int) == sizeof(long)であるプラットホームでは、 c_longの別名です。
c_int8 C 8-bit signed intデータ型を表します。たいていは、 c_byteの別名です。
c_int16 C 16-bit signed intデータ型を表します。たいていは、 c_shortの別名です。
c_int32 C 32-bit signed intデータ型を表します。たいていは、 c_intの別名です。
c_int64 C 64-bit signed intデータ型を表します。たいていは、 c_longlongの別名です。
c_long C signed longデータ型を表します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。
c_longlong C signed long longデータ型を表します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。
c_shor tC signed shortデータ型を表します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。
c_size_t C size_tデータ型を表します。
c_ubyte C unsigned charデータ型を表します。その値は小整数として解釈されます。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。
c_uint C unsigned intデータ型を表します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。 sizeof(int) == sizeof(long)であるプラットホームでは、 c_ulongの別名です。
c_uint8 C 8-bit unsigned intデータ型を表します。たいていは、 c_ubyteの別名です。
c_uint16 C 16-bit unsigned intデータ型を表します。たいていは、 c_ushortの別名です。
c_uint32 C 32-bit unsigned intデータ型を表します。たいていは、 c_uintの別名です。
c_uint64 C 64-bit unsigned intデータ型を表します。たいていは、 c_ulonglongの別名です。
c_ulong C unsigned longデータ型を表します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。
c_ulonglong C unsigned long longデータ型を表します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。
c_ushort C unsigned shortデータ型を表します。コンストラクタはオプションの整数初期化子を受け取ります。オーバーフローのチェックは行われません。
c_void_p C void *データ型を表します。値は整数として表されます。コンストラクタはオプションの整数初期化子を受け取ります。
c_wchar C wchar_tデータ型を表し、値はユニコード文字列の単一の文字として解釈されます。コンストラクタはオプションの文字列初期化子を受け取り、その文字列の長さはちょうど一文字である必要があります。
c_wchar_p C wchar_t *データ型を表し、ゼロ終端ワイド文字列へのポインタでなければなりません。コンストラクタは整数のアドレスもしくは文字列を受け取ります。
c_bool C boolデータ型(より正確には、C99の_Bool)を表します。その値はTrueまたはFalseであり、コンストラクタはどんなオブジェクト(真値を持ちます)でも受け取ります。 バージョン 2.6 で 新たに追加 された仕様です。
HRESULT Windows用: HRESULT値を表し、関数またはメソッド呼び出しに対する成功またはエラーの情報を含んでいます。
py_object C PyObject *データ型を表します。引数なしでこれを呼び出すと NULL PyObject *ポインタを作成します。

B: うわっ。久しぶりに巨大な表攻撃だ。

A: これだけそろっていれば単純な数値や文字列しか引数にとらない関数は大体カバーできる。 例えば9行目のAdOpen()は引数にcharへのポインタをとると定義されている。

HANDLE AdOpen(
  LPCTSTR lpszName  // デバイス名
);

B: LPCTSTR…? そんな型は習った覚えがないような。

A: ここでは詳しく説明しないが、UNICODEを使っていなければconst char型へのポインタだと思えばいい。 ctypesではargtypesという属性を指定して引数の型を指定することができる。そうしておけば、不適切な引数で関数が呼び出されるのを防げる。

>>> obj = ctypes.windll.LoadLibrary('fbiad.dll')
>>> obj.AdOpen.argtypes = [c_char_p]
>>> hDeviceHandle = obj.AdOpen(12) #誤って数値を代入している
ctypes.Argumenterror: argument 1: <type 'exceptions.TypeError'>: wrong type
>>> hDeviceHandle = obj.AdOpen('FBIAD1') #正しい

B: ふむふむ。

A: また、C言語の関数には変数を参照渡ししておいてそこに結果を格納するタイプの関数も多い。 そういうときにもこれらのクラスが役立つ。unsigned long型へのポインタを引数とするfoo()があるとしよう。 まず、c_ulong()で結果を格納するための変数valを作成しておく。そして、こいつを引数としてfoo()を呼び出す。

>>> val = ctypes.c_ulong()
>>> obj.foo(ctypes.byref(val))

B: byrefってなんですか?

A: ctypesで参照渡しをしたい時に使うおまじないさ。pythonにはC言語のようにポインタを使えないからね。 対応するCのソースを書けばこんな感じになる。

unsigned logn val;
foo();

A: さらにおまけ。c_intなどのインスタンスに対してであれば、ctypes.pointer()ポインタを得ることができる。

>>> val = ctypes.c_ulong(100)
>>> p = ctypes.pointer(val) #ポインタを得る
>>> p.contents #ポインタが指す変数(val)の値
c_ulong(100L)

B: うーむ、そろそろついていけなくなってきたぞ。最後のc_ulong(100L)っていうのはなんですか?

A: 最初から説明するか。最初のctypes.c_ulong(100)はc_ulongのインスタンスを生成している。引数の100はその値を100で初期化しなさいということだ。 で、最後のp.contentsは、ポインタpが指す変数の値を示しているわけだ。pは100という値を保持しているc_ulongのインスタンスを指しているんだから、 値はc_ulong(100L)ということになる。

B: 100LのLは?

A: かえってややこしくしてしまったかな。c_ulongは符号なしのlong型整数だから、普通の整数とはちょっと違うんだよ。 何が違うかはC言語の教科書を読み返してもらうとして、とにかく普通の整数と区別するためにLがついている。 ちなみに、valはc_ulongのインスタンスだから普通の定数と足すことは出来ない。

>>> val = ctypes.c_ulong(100)
>>> val + 7
TypeError: unsupported operand type(s) for +: 'c_ulong' and 'int'

B: あがが。さっぱりわかりません。

A: まあ後でじっくり説明してやるから。 c_ulongのインスタンスに保持されている値に対して四則演算などをしたい場合は、属性valueを使ってその値を取り出してやらなければいけない。

>>> val = ctypes.c_ulong(100)
>>> val.value + 7
107

A: この辺りはc_charなどの他のクラスでも同じことなので、ctypesを使う時には気をつけてほしい。 さて、だいぶ説明が長くなってしまったし、今回はこのあたりで切り上げて、次回AdInputAD()の呼び出しに必要な構造体をctypesでどのように 扱えばいいかを解説しよう。B君、それまでにみっちりC言語の復習をさせてやるから覚悟するように。

B: うへー。