例題21-1:あのctypesをもう一度

Note

  • 2014/09/12
    • この例題および例題21-3で取り上げたAPI-USBPライブラリのDIO、AIO部分をPythonから利用するためのパッケージpyAPIUSBPを公開しました( http://pyapiusbp.sourceforge.net/ )。 例題21-4 もご覧ください。

A: さてさて、今日は…っと、おわっ!

B: やあ。どうさかれましたかAさん。

A: や、やあB君。…なんで居るの?

B: ははは。いやだなあ、Aさん。毎日研究室に来るのは当然のことじゃないですか。

A: あ、あぁ…、そうだな。

B: さて、ちょうど来週のゼミで発表する論文のレジュメが出来たところです。何かお手伝いしましょうか?

A: (おかしい… B君が来週の発表準備をすでに済ませているなんて… そもそもB君は前回 あんな目そんな目 に遭ってとてもそんな元気はないはず…)

B: Aさん? どうされましたか?

A: あ、いや。すまん。実は先日仕入れたIOユニットをpythonからコントロールできるかどうか試しておこうと思ってな。ドライバはもうそのPCにインストールしてあるんだ。

B: なるほど。コントロールできるかどうか試すという事は公式にはpythonをサポートしていないのですね? ctypesの出番ですか?

A: お、おう…。ご明察。ctypesを使う。

B: ctypesは 例題9 で教えていただいたきりで自分ではまだ使ったことがないので楽しみですね。いつも復習は欠かさないようにしているのですが、ctypesはなかなか自分で考えてみる題材がなくて?

A: …あの、大丈夫? B君だよね?

B: 今日のAさんは変だなあ。ライブラリはどれですか?

A: えっと。これがAPIリファレンス。サンプルプログラムはこれ。

B: 拝見します。

A: っと。その間に読者の皆様に今回の機材を紹介します。 CONTEC 社のデジタル入出力(DIO)ユニット DIO-0808TY-USB です。非絶縁TTLレベル入力8点、非絶縁オープンコレクタ出力8点。USBバスパワー駆動で動くので別途ACアダプター等がいらない上に、端子台も不要です。なによりうれしいのがその価格、2013年10月現在でCONTEC社のオンラインショップで2万円ちょっと。昔この何倍ものお金をつぎ込んでISAバスのDIOボードと端子台を買ったのとは隔世の感があります。うう、あの今よりずっと貧しかった…。まぁ何倍もしたボードはチャネル数も倍以上なので直接比べるのアレなのですが。

../_images/21-1-01.jpg

B: Aさん。感慨に浸っているところに申し訳ありませんが、このサンプルはちょっと複雑で読むのに時間がかかりそうです。APIリファレンスを見て大体イメージがつかめましたし、pythonのスクリプトを書いてみませんか。

A: えぇ? もう目を通したの? きちんとAPIマニュアルとサンプルプログラムを読んで、実際にコンパイルしたりしてからpythonに進むべきだと思うが…。

B: ふふっ、建前ではそうですよね。でもAさんの性格ならさっさとpythonで書いてみたいのではないですか?

A: む。確かに。皆さんに手本を示すという意味では着実に行くべきだが、そこは、そら。時間もないしな。がばっといっちゃうか。

B: そうこなくては。さて、どこから始めましょうか。

A: じゃあ 「よい子のみなさんはきちんとマニュアルとサンプルを読みましょう」 と断っておいた上で始めますか。まず、C言語用のヘッダとライブラリを探す。

B: DIO-0808TY-USBの付属ソフトウェアのインストールディレクトリはC:\Program Files (x86)\CONTEC\API-USBP(WDM)ですね。

../_images/21-1-02.png

B: C言語のヘッダファイルは拡張子が .h ですからここにはありませんね。ライブラリの拡張子はWindowsでは .lib でしたっけ?

A: ん。間違いではないのだが、ctypesから利用する場合は一般に拡張子 .dll のファイルを探す必要がある。dllってのは…

B: ダイナミックリンクライブラリ(Dynamic Link Library:DLL)のことですね。「動的にリンクされる」、すなわちプログラムの実行ファイルの中に組み込んでしまうのではなく、実行するときに呼び出されるライブラリです。pythonのスクリプトを実行している最中にpythonから呼び出すのですから、ダイナミックリンクライブラリでなければならないということですね。なるほどなるほど。

A: (これがあのB君なのか… あんな目やそんな目に遭って何かのスイッチが入ってしまったのだろうか…)

../_images/21-1-03.png

B: DIOというディレクトリの下にSampleというディレクトリがありました。Incがインクルードファイル、Lib_amd64がライブラリのディレクトリですかね。ディレクトリ名から判断して…VcNet2005がC言語のサンプルでしょうか?

A: えっ、あ、ああ。そうだな。 ちゃんと付属のドキュメントを探せばどこかに書いてあるはずだが だいたいディレクトリ名の付け方にルールがあるので見当がつく。VbNetはVisual Basic、VcNetCLIはC++/CLI、VcsはC#だろう。C++/CLIやC#はC言語みたいな名前なんで混乱しがちなんだがあくまで別物なので注意が必要だ。

B: じゃあIncディレクトリの中を見ましょうか。

../_images/21-1-04.png

B: ありましたね。拡張子 .h のファイル。これがC言語用のヘッダファイルですね。

A: そのようだな。そうそう、今回はC言語のプログラミングをあまりご存知ない方向けに少し説明を。ヘッダ(header)ファイル、インクルード(include)、ライブラリ(library)という言葉が出てきましたが、乱暴な言い方をすればヘッダファイルはトッピングを自由に指定できるピザ屋のトッピング一覧、「お品書き」です。プログラムはピザを作るレシピです。プログラマはヘッダファイルに書かれているメニューの中から使いたいものを選んでプログラムの中に並べます。レシピの先頭に「このトッピング一覧表の中から選ぶよ!」と表を貼りつけておくことを「インクルードする」と言います。たとえ話をされると却ってわかりにくければ、ここでいうトッピングは関数やマクロ、ヘッダファイルは関数やマクロの宣言が書かれた一覧表。プログラムの中から関数やマクロを使いたい時にはその宣言が書かれた一覧表であるヘッダファイルをインクルードしなければいけないという事です。

../_images/21-1-05.png

B: これはAさんらしい例えですね。この流れならコンパイル(compile)はレシピに従って実際にピザを作ること、ライブラリはピザに載せるトッピングの…在庫かな。とにかく、ヘッダファイルはプログラムに組み込むことができるパーツの一覧などが書かれているだけで、パーツの実体はライブラリに格納されているというのがポイントですね。

../_images/21-1-06.png

A: ん。そうなるかな。ならダイナミックリンクライブラリは食べるときにかける調味料とかかな。まあ例え話はあくまで例え話であって、調味料とか言ってしまうとDLLの意義がわけわかんなくなる。興味を持った方は自分で調べていただくとして…。っと、最後にもう一つ脱線。この例え話を使うとプログラムをインタプリタで実行するのとコンパイルして実行するのとではなぜ後者が速いかがわかりやすいかもしれないな。pythonのように通常インタプリタで実行する言語は、食べたくなった時に毎回レシピを見て調理しているのに等しい。一方コンパイルしてから実行するのは、すでに調理済みの料理を保管してあるのに等しい。食べたくなったらすぐに食べることができる。後者の方が速いに決まっている。

B: んー。例え話とはわかっていますが、調理済み料理を保存していると言われると傷まないのかとか言いたくなりますね(笑)。それはともかく、ソフトウェアのディレクトリにはDLLファイルがありませんね。見落としたかと思って検索をかけましたが見つかりません。

../_images/21-1-07.png

A: あ、そこまで進めてくれてたのか。ありがとう。さて、こういう場合は(ドキュメントを読めという正論は置いといて)どこを探せばよいと思う?

B: ええっと…。プログラムを実行すると時に探せないといけないんですよね。ということは環境変数pathに登録されているディレクトリですかね? パスが通っていてDLLがたくさん登録されているディレクトリと言えばC:\Windows以下?

A: ご名答。いや、本当にキミ、B君?

B: なんですか今日は。Aさんこそどうかされましたか?

A: ううっ、なかなか受け入れれられなかったけどやはりB君なんだな。いや、もう、B君にいろいろ教え続けて早や○年、ついにここまで成長したか。ぐすっ。

B: やっぱり今日のAさんは変だなあ。それはそうとC:\Windowsを検索しましたよ。ヘッダファイルがCDio.hでしたから対応するDLLは多分cdio.dllで検索してみましたが当たりっぽいです。

../_images/21-1-08.png

A: みたいだな。じゃあこれで確認はOK。ここからは私流のctypesの使い方だが、私はいつもまず ヘッダファイルの移植 から始める。 ヘッダファイルには関数の引数や戻り値に使う定数が定義されていることが多く、これらの定数が使えないとプログラミンがとても不便だからだ

B: Aさん、もう少しわかりやすく説明しないと。

A: 例えば以下は今回のDIOユニットの開発キットのヘルプの一部で、関数を実行したときに得られるエラーコードの記載です。2つ目の項目に注目してみましょう。「値」に10001とあり、「意味」に「無効なIDが指定されました」とあります。要するに何かの処理をしようとして無効なIDが指定されていたら10001という値がエラーコードとして得られるということですが、このエラーコードをそのまま使ってプログラムを書くととても読みにくいプログラムになります。

../_images/21-1-09.png
if(errorcode == 10001)  //何のことかさっぱりわからない
    exit();

A: そこで出て来るのが表の「定義」と書かれている列です。ヘッダファイルには、DIO_ERR_DLL_INVALID_IDという名前が10001という値であると定義されています。この定義をプログラムに取り込んでおくと、上のさっぱりわからないif文が以下のように書けるわけです。慣れてない方にはこれでもやっぱりわかりにくいかもしれませんが、if(errorcode==10001)と書かれるよりはかなりマシだというのは同意いただけるのではないかと思います。

if(errorcode == DIO_ERR_DLL_INVALID_ID)  //IDが無効だったら、という判定をしているのがわかりやすい
    exit();

B: このDIO_ERR_DLL_INVALID_IDのような名前は、変数と異なりプログラムの実行中に値が変化しないので 定数 とも呼ばれます。この定数をpythonからも利用できるようにしてやろうというわけですね。

A: その通り。いろんなやり方があると思いますが、私の方法は モジュールにしてしまう というもの。まず、適当な名前で…、そうですね。ContecCDIOというモジュールにしますか。モジュール名に.pyを付けたファイルを作成します。ContecCDIO.pyですね。で、cdio.hの内容をコピぺします。

B: C言語のヘッダファイルをそのまま貼りつけてしまっていいんですか?

A: 貼りつけてから加工します。cdio.hの冒頭はこんな感じですね。最初にtypedef宣言、続いてprototype宣言。typedefは後で必要になるかも知れませんが、その場合はまたcdio.hからコピーするとしてとりあえず削除。prototype宣言は本格的にこのデバイスのpython用モジュールを作る気なら移植した方がいいかもしれませんが、 とにかく動けばいいや という場合はばっさり削除ですね。

../_images/21-1-10.png

B: typedefはデータ型に名前を付けるもので、プログラマが独自に定義した構造体に名前を付けたりする時に使います。pythonから利用したい関数がこの独自の構造体を使用していない場合は移植する必要がないというわけですね。prototype(プロトタイプ)宣言というのはライブラリの中から利用したい関数がどのような引数や戻り値を持つのかを宣言するものです。コンパイルする時にはこれらの情報が必要なので、利用したい関数のプロトタイプ宣言が書かれたヘッダファイルを取り込んでおく必要があるわけです。

A: お、B君。フォローありがとう。頼りになるなあ。

B: C言語やC++言語をよくご存じな方にはこんな説明は蛇足だと思いますし、ご存知ない方には何のことやらさっぱりだと思いますが、 ご存知ない方に是非読んでいただいて「へえ、よくわかんないけどこんなものがあるんだ」と思っていただけるように と思って付け加えています。よく御存じの方は 生暖かい目 で見守っていただければと思います。

A: typedefとプロトタイプ宣言をざっくり削除したところです。ここから先は定数の宣言なので、これをpythonの文法に適合するように編集します。まずはざっと見たところコメントがすべて // で書かれているようなので、//を # に置換します。pythonでは # が出現するとその行の以後の文字列はコメントと見なされますが、C言語では // が同じ意味を持っています。

B: 正確に言うと // は C++ から導入された文法で、大元のC言語では/*と*/で囲まれる文字列がコメントですね。しかし最近のCコンパイラのほとんどが // によるコメントを認識しますので問題ないようです。

../_images/21-1-11.png

A: 続いて定数の宣言そのものの置き換えに入りましょう。あ、ちなみにサンプル画像では サクラエディタ を使用しています。C言語の定数の宣言は以下のような書式です。

#define  定数名   値   //C言語における定数の宣言

B: pythonでは定数をどうやって定義するんですか?

A: 実は、私が知る限り pythonにC言語の定数に相当するものはありません。なので普通の変数で対応します 。今回の目的にはこれで十分です。変数にするということは、以下のような形に変形できればいいわけですね。

定数名 =       #変換目標

B: 変数で代用すると言われるとちょっと不安な気がしますが、要するに 生のエラーコードではなくて多少は読みやすい名前が付いていればそれでいい わけですから、変数でいいんですね。

A: いいんです。さて、普通の検索、置換でこの書き換えをしようとするとちょっと面倒です。例えば変換目標に近づけるにはまず行頭の #define を取り除けばいいというのはわかりますが、ただ #define を取り除いてしまうと、行頭にスペースなどの空白文字が残ってしまいます。 pythonでは字下げが重要な意味を持つ ので、この空白文字は取り除かなければなりません。ところが厄介なことに、このcdio.hでは #define とそれに続く定数名の間が何故かタブ文字だったりスペース文字だったりするんですね。下図の#defineの後ろに灰色で ^ が入っている行と入っていない行がありますが、^はタブ文字を表しています。行によってタブ文字が入ってたりいなかったりしますね。

../_images/21-1-12.png

B: 確かに。何故でしょうね。

A: まあ私自身もよくやらかすことなので特に理由もないんだと思うんですが、とにかく置換の時にはこれが面倒を引き起こします。こういう時に便利なのが 正規表現(regular expression) です。

B: 正規表現ですか。いままでの例題にも名前は出てきましたが解説されたことは一度もありませんね。

A: ここで正規表現の解説までする余裕はないので、 python公式ドキュメントの「正規表現操作」 などをご覧ください。いずれ正規表現はちゃんと解説したいですね。さて、正規表現を用いると、「スペースやらタブ文字やらをひっくるめた空白文字が最低1文字以上」といった柔軟な指定が出来ます。まあ敢えて例を挙げておくと、\s という正規表現は「スペースやタブ文字をひっくるめた空白文字」に当てはまります。 + という正規表現は、「+ の直前の文字が1文字以上繰り返されている」文字列にマッチします。

B: #define\s+と書くと、「#defineとその後に続く空白文字すべて」とマッチさせることが出来るわけですね。

../_images/21-1-13.png

A: その通り。サクラエディタで試してみたところが以下の図です。図の右側の「置換」ダイアログで「正規表現」にチェックが入っている点にご注目ください。サクラエディタの場合はここにチェックが入っていれば正規表現を用いて検索、置換を行います。エディタによって使い方が違いますので皆さん愛用のエディタのヘルプ等を確認してください。Windowsのメモ帳などには正規表現による置換機能はありませんので、念のため。

B: 黄色くなっている部分がマッチした文字列ですね。狙い通りにマッチしているようですね。

../_images/21-1-14.png

A: うまくマッチしているようなので、がばっとすべて削除してしまいました。問題は次。現在は

定数名   

という形まで変形しましたが、これを

定数名 =       #変換目標

にしないといけません。

B: 定数名と値の間の空白文字を = に置き換えないといけないんですね。

A: この置換は「こうすれば必ずOK!」という方法がありませんので、丁寧にソースを確認する必要があります。今回私が取った戦略は「タブ文字とその後ろに数値が一文字」を見つけたら、「タブ文字 = その数値」という形に置き換えるというもの。今回のcdio.hはこれでうまくいきそうです。マッチした文字の一部を置き換え後にも使用する場合は ( ) という正規表現を使います。

B: 正規表現の ( ) の中に含まれる文字列は、置換後の文字列で \1 と表現することが出来ます。元の正規表現に ( ) が2個以上出て来る場合は、順番に \1、\2、\3...と表現できます。便利ですね。

A: ( ) を使えば、私の戦略は「\t([0-9]) を \t= \1 に置き換える」と書くことが出来ます。いやあ、正規表現って便利ですよね。ホント。

../_images/21-1-15.png

B: Aさん、正規表現 [ ] を説明していませんよ。[ ]はその中に含まれる文字を指します。[0-9]は0から9ですね。ちなみに \d という正規表現でも0-9にマッチさせることが出来ます。あと \t はタブ文字を表しています。

A: おっと、こりゃ失礼。今回はちょっと目標としている部分以外にもマッチするかも知れないので、様子を見ながらワンステップずつ置き換えます。

../_images/21-1-16.png

B: うまくマッチしているようですね。図中央やや下の16進数表記(0x1300など)も上手く置き換えられているようですし、不必要な部分にマッチしてもいないようです。

A: そのようだね。じゃあガバッと置き換えて、これで完成。変換目標と同じ形になりました。 ContecCDIO.pyという名前で保存しておきます 。拡張子が.pyであれば別の名前でも大丈夫ですが、後でこのファイルを利用する時にファイル名が必要になりますので覚えておいてください。

../_images/21-1-17.png

B: じゃあ、変換ミスがないか、ゴミが残っていないか確かめて…

A: いや、どうせゴミがあったらpythonからインポートした時にエラーが出て発覚するからさっさと先に進もう。早く動かしたい。

B: …その油断が思わぬエラーにつながって余計な時間を費やす原因になるものですが。

A: まあ、 良い子の皆さんは(ry ってことで。ページ分量の問題もあるからね。さっさと進めなければ。

B: はあ、そうですか。それなら。

A: まずはデバイスの初期化から。ヘルプファイルによると、DioInit( )という関数を最初に呼び出してデバイスを初期化し、IDを得なければいけないということです。DioInit( )の使用例は以下の通り。

../_images/21-1-18.png

B: 最初の引数はデバイス名、次のIdはshort型へのポインタ。Idに初期化されたデバイスのIDが格納されるというパターンでしょうか。

A: そのようです。これをctypesでささっと書いてみます。 例題9-1 を参考にしてくださいね。

import ctypes

cdio = ctypes.windll.LoadLibrary('cdio.dll')

id = ctypes.c_short()
ret = cdio.DioInit('DIO000',ctypes.byref(id))

B: ええと、まずctypesをインポートして、ctypes.windll.LoadLibraryでDLLを読み込みます。g変数cdioに格納された戻り値はctypes.WinDLLクラスのインスタンスで、以後cdio.foo()と言う形でこのDLLに含まれるfooという関数を呼びだせます。

A: ctypes.windll.LoadLibraryの引数には読み込みたいDLLのファイル名を書きますが、 パスが通っている場所にDLLファイルがあるならばこのようにファイル名だけで呼び出せます

B: 先ほどcdio.dllの位置を確認しておいたことがここで活きるわけですね。パスが通っていない場所にある場合は絶対パスでファイル名を書かないといけないということですか?

A: その通り。さて、DoInit()の第一引数ですが、ここに指定する値はWindowsのデバイスマネージャーで確認できます。このPCの場合は”DIO000”という名前なので、これを第一引数に指定します。

../_images/21-1-19.png

B: 第二引数はshort型へのポインタですので、対応するctypesのオブジェクトを作成してctypes.byref()を使って渡します。ctypes.c_shortというのがC言語のshort型に対応するctypesのオブジェクトです。C言語の他のデータ型に対応するctypesのオブジェクトの表は 例題9-1 をご覧ください。

A: 具体的にはC言語で以下のような書かれ方をしている場合はctypes.byref()を使う必要があるでしょう。

  • int bar; と宣言されていて & bar と渡されている場合
  • int * bar; と宣言されていて bar と渡されている場合
  • int [ ] bar; と宣言されていて bar と渡されている場合 …などなど

A: 実行してみて何もエラーメッセージが出ずに終了すれば、ひとまずは成功です。ですが、これだけでは正常に動いているのか不安なので、わざとエラーを起こさせてみます。ここで先ほど作成したContecCDIO.pyの登場です。ContecCDIO.pyを以下のファイルと同じディレクトリに置いて、import ContecCDIO と書いてimportします。

#coding: utf-8
import ctypes
import ContecCDIO

cdio = ctypes.windll.LoadLibrary('cdio.dll')

id = ctypes.c_short()
ret = cdio.DioInit('DIO000',ctypes.byref(id))
if ret != ContecCDIO.DIO_ERR_SUCCESS:
    print u'初期化できませんでした。終了します。'
    quit()
else:
    print u'初期化に成功しました。'

cdio.DioExit(id)

B: ソースに日本語が入っているので 先頭行に文字コードの指定(#coding: utf-8)が入っていますね。

A: うん。で、ContecCDIO.pyをimportすることによって、そこで定義されている変数を利用することが出来ます。 if ret != ContecCDIO.DIO_ERR_SUCCESS: というやつですね。これ、これですよ。今回書いているくらいのプログラムなら大したことないかも知れませんが、後々使い回しの利くしっかりとしたプログラムを書こうとするなら、こういう気遣いの有無が後々ボディーブローのように効いてくるんですよ。

B: 私はボクシングって全然経験ないのでよくわからないのですが、ボディーブローってやっぱり後で効いてくるんですかね。

A: っっ! ここまで脱線なしにいきなり来たから意表を突かれてしまった。いや、私も知らないけどさ(笑)。

B: すみません、失礼しました。これで正常に動くことが確認できますね。

A: 実物をお持ちの方は、わざとデバイス名を違う名前にしてエラーが出ることを確認してみてください。では、ここまで順調に来たから一気に信号の出力までやってみましょうか。ここではDioOutByte()という関数を使ってみます。関数の引数は以下の通りです。DioInit()と異なりポインタを用いる引数がないのでDioInitより簡単です。ここではtime.sleep()を使って1秒ごとに全ビットO(0x00)、全ビット1(0xFF)に切り替えます。

ret = DioOutByte (short Id, short PortNo, BYTE Data)出力ポートに1バイト(8ビット)出力します。

  • Id: DioInit()で取得したデバイスIDを指定します。
  • PortNo: 出力論理ポート番号を指定します。
  • Data: 出力データを指定します。(0x00 - 0xFF)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#coding: utf-8
import ctypes
import ContecCDIO
import time

cdio = ctypes.windll.LoadLibrary('cdio.dll')

id = ctypes.c_short()
ret = cdio.DioInit('DIO000',ctypes.byref(id))
if ret != ContecCDIO.DIO_ERR_SUCCESS:
    print u'初期化できませんでした。終了します。'
    quit()
else:
    print u'初期化に成功しました。'

outdata = 0xff

while True:
    if outdata==0xff:
        print u'全ビット0にします'
        outdata = 0x00
    else:
        print u'全ビット1にします'
        outdata = 0xff
    ret = cdio.DioOutByte(id, 0, outdata)
    time.sleep(1)


cdio.DioExit(id)

B: Aさん。実行してみたところ上手く動いているようですが、これってどうやって終了するんですか?

A: あー。そこはもう面倒くさいんでCtrl+Cで終了してください。きちんと終了させるにはどうしたらいいかは練習問題ということで。

../_images/21-1-20.png

A: 同様に、今度は入力をしてみます。もう、そろそろ疲れてきましたのでぱぱっと。実行するとひたすら入力ポートを読み込んで結果を画面に表示します。例によって終了する時にはCtrl+Cで強制終了してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#coding: utf-8
import ctypes
import ContecCDIO
import time

cdio = ctypes.windll.LoadLibrary('cdio.dll')

id = ctypes.c_short()
ret = cdio.DioInit('DIO000',ctypes.byref(id))
if ret != ContecCDIO.DIO_ERR_SUCCESS:
    print u'初期化できませんでした。終了します。'
    quit()
else:
    print u'初期化に成功しました。'

indata = ctypes.c_ushort()
msg = (ctypes.c_char*256)()

while True:
    ret = cdio.DioInpByte(id, 0, ctypes.byref(indata))
    cdio.DioGetErrorString(ret,ctypes.byref(msg))
    print msg.value, indata.value

cdio.DioExit(id)
../_images/21-1-21.png

A: この例ではDioInpByte()という関数を使っています。

ret = DioInpByte (short Id, short PortNo, BYTE * Data)入力ポートから1バイト(8ビット)入力します。

  • Id: DioInit()で取得したデバイスIDを指定します。
  • PortNo: 入力論理ポート番号を指定します。
  • Data:入力データを格納する変数の アドレスを指定 します。

A: まあ大体出力と同じですが、DioInpByte()の第三引数がBYTE DataではなくBYTE * Dataとなっていること、下の説明文に アドレスを指定 とある点にご注意ください。こういう場合はデータを格納する変数をまず確保しておいて、ctypes.byref()を使って渡す必要があります。

B: 16行目の indata = ctypes.c_ubyte() がデータを格納する変数ですね。これを20行目のDioInpByteで ctypes.byref(indata)) という具合にbyrefで渡している、と。ところで17行目の msg = (ctypes.c_char*256)() も説明が必要じゃありませんか?

A: 17行目は21行目のDioGetErrorString()で使用するためのものです。これはエラーコードをよりわかりやすい文字列に変換してくれる関数で、こんな便利な関数がありますよという例として、そして配列へのポインタを渡す関数の例として取り上げてみました。

ret = DioGetErrorString (long ErrorCode, char * ErrorString)エラーコードからエラー文字列を取得します。

  • ErrorCode: 各機能関数の戻り値を指定します。
  • ErrorString: エラー文字列を格納するバッファの先頭アドレスを指定します。 文字列の長さは、最大256文字です

B: なるほど。これは便利な機能ですね。プログラムを書く人と使う人が違う時にはわかりやすいエラーメッセージを表示することが大切ですからね。

A: まあ、プログラムの内容を知らない人の参考になるほど詳しいメッセージではないんだけど、バグ報告をしてもらう時にこのメッセージを添えてもらえばわかりやすくなるかも知れないね。とにかく、ポイントは第二引数が char * 、すなわちchar型変数へのポインタであること。そして、256文字のメッセージを格納できるスペースがなければならないこと。C言語にある程度習熟している人はここでピーンとくると思いますが、こういう場合は 長さ256のchar型の配列 が必要になります。

B: …256文字を格納するのでしたら、NULL文字も合せて長さ257でなければいけないのでは?

A: んー。それは付属ドキュメントの曖昧なところですかね。個人的にはNULL文字も合わせて256文字と受け取りました。257なんて中途半端なサイズには普通しないと思うんですよ。本当に257確保しないといけないんだったらそう注意書きがありそうだと思いません?

B: プログラマは2の冪乗数が好きですよね(笑)。確かにおっしゃるとおりかも。

A: ま、エラーが出たらその時に考えるという事で。 ctypesのデータ型に* nとすると、そのデータ型をn個並べた配列を宣言したことになる ので、ctypesを使ってみようという方は覚えておいてください。char msg[256];はmsg = (ctypes.c_char*256)()と書くわけです。

B: 最後の( )はなんでしょうか?

A: ちょっと難しい話になりますが、ctypes.c_char*256という式は、ctypes.c_char型のデータが256個並んだデータのクラスを作成します。

>>> ctypes.c_char
<class 'ctypes.c_char'>   #ctypes.c_charというクラス

>>> ctypes.c_char*256
<class '__main__.c_char_Array_256'>
#__main__.c_char_Array_256という新しいクラスが出来た

B: なるほど。では最後の( )は__main__.c_char_Array_256クラスのコンストラクタを呼ぶための演算子と言うわけですね。

>>> (ctypes.c_char*256)() #最後の( )はクラスインスタンスを生成する関数を呼ぶ演算子
<__main__.c_char_Array_256 object at 0x02868DF0>
#アドレス0x02868DFに生成された__main__.c_char_Array_256クラスのインスタンス

A: ははは。さっきから「そんな説明じゃわかりませんよ」と何度も注意されたけど、B君が今使った「コンストラクタ」もわからないんじゃないかな。

B: ああ、すみません。コンストラクタは…そのデータを使えるようにする時に最初に呼び出される関数、ということでいかがでしょうか。 例題5-3 ですね。

A: OK。まだ難しいけどそこをかみ砕いていくとキリがないので。ところで、演算子の優先順の問題で、ctypes.c_char*256を( )で囲んでやらないとエラーになってしまいますのでご注意ください。

B: まあこれは( )を付けるものとして覚えておけば問題ないですよね。

>>> ctypes.c_char*256()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
#ctypes.c_char\*256を囲む( )を省略してしまうと演算子の優先順位から
#256()という関数を呼んでいると判断されてしまいエラーとなる

A: これで全部説明したかな、と思ったけどあと一つ。22行目も補足しておく必要がありますね。ctypesのc_ubyteやc_charなどで生成した変数にはそれらのクラスのオブジェクトが格納されているので、そのままではpythonの中では使えません。pythonから参照できる値はvalueというデータ属性に格納されていますので、22行目のようにmsg.valueとかindata.valueと書く必要があります。

B: ちなみにvalueには代入も出来ますので、python側で何か値を設定してC言語の関数に渡さないといけない時にもvalue属性が使えます。

A: しつこく補足ですが、今のB君の発言についてです。ctypesは賢いので、pythonの変数を直接ctypesでのC言語の関数呼び出しに書いても、pythonインタプリタが自動的に変換をしてくれます。ただし、いくらctypesが賢いと言っても限界がありますし、意図しない変換をされてしまうとなかなか取れないバグの原因にもなりかねません。そういう時にはargtypesという属性を指定することによってctypesに変換方法を教えてやることが出来るのですが、今回はさすがにもう分量オーバーですので 例題9-1 をご覧ください。

B: 先ほど言っておられたプロトタイプ宣言はこの変換方法の指定のときに便利なんですね。

A: その通り。C言語のヘッダファイルを上手くargtypesの形に変換することが出来れば、いちいちargtypesを手書きで設定しなくても一気に設定できます。個人的には、そこまでするのなら今回作ったContecCDIO.pyのようなシンプルなものではなく、きちんとしたクラスを設計してあげた方が方がいいと思います。

B: …入力と出力が出来るようになって、これで一応デジタルIOユニットとして最低限使えるようになったんじゃないですか? いや、お疲れさまでした。

A: ふーっ。さすがにちょっと疲れたよ。B君があまりにもしっかり相手してくれるもんだから口調までなんだかいつもと違っちゃって。いやはや。それにしてもB君、すばらしい成長だよ。

B: ふふ。Aさんに褒められると変な感じがしますね。ちょっとお腹がすきました。この「かもめの玉子」っていうお菓子をいただきますね。

A: おう。どうぞどうぞ。

?: (ガタン!と倒れ込むように扉を開いて)あの… どなたか… なにか… たべさせて…

A: やや、どうされました。さあ、「かもめの玉子」でも食べて… って、B君! B君じゃないか!

B: うう、Aさん、いただきます。むぐっ、ふぐっっ! げふげふっ!!

A: 落ち着け、水、水、B君水持ってきて…じゃなくて! あんた、いったい何者だ!!

B?: いやだなあAさん。ぼくはBですよ。その方に水を持ってきますね。

A: いや、最初からずーっと違和感を感じていたんだ。B君がこんなに優秀なはずもないし、脱線もツッコミもせずにきちんと話せるはずがない! なによりそのお菓子の食べ方! B君はもっとこう品がないというかはしたないというか、例えばこう、こんな風にがっついて喉を詰まらせて吹き出すような食べ方をするはずだ!

B?: ふっ、ばれてしまっては仕方がないな。私なりにツッコミを入れてみようと努力したつもりだったのだが。

A: くっ、何者だ!

B?: さらばだ! きっと私をこのまま助手にしておいた方が良かったと後悔するであろうぞ! とうっ!(窓から飛び降りる)

A: ちょ、ここ8階…!

B: Aさぁぁあん、ちょっとひどいじゃないですかぁぁぁぁ。なんですか今の言い様はぁぁぁああ!

A: えっ、いや、その。なんだ。今はとにかくあの怪しい奴の正体を突き止めないと。ちょっと後を追いかけるから。んじゃっ!(廊下へ飛び出す)

B: あ、こら、待て…っ! はあはあ。だめだ。力が出ない。まずはお腹を満たさないと。それにしてもこのお菓子美味しいなあ。むぐむぐ。むぐむぐむぐむぐ。