C言語を使ってPython拡張についてもう少し…
今回の事例のようにC言語でPythonの拡張するには、次のような順序で作業を進めていくことなると思います。(当たり前か…)
1. C言語でのアプリケーション開発。
2. Pythonから扱えるようにするための、C言語ソースへのラッピンコード追記 & setup.pyの追加。
3. コンパイルとテスト。
第1,第3ステップは、一般的な開発と大差がないので今回のスコープ外ということにし第2ステップに注目して解説を進めていくことにします。
尚、Raspberry Piの場合は、ビルド環境も簡単にインストールできるので実機でビルドしながら開発を進めるという方法をとるのがいいと思います。C言語のソースのサイズにもよると思いますが、今回のようにセンサーを初期化する関数と計測した値を読み込む関数の2つ程度ならビルド時間も許容できるのではないかと思います。
(実開発はしないので、次の開発環境準備部分は読み飛ばしても大丈夫です。)
まずは、apt-getコマンドで、gcc及び一般的な開発環境をインストールします。
1
| |
次に、GPIOを制御しているBCM2835のheaderファイルもインストールします。 インストール方法に関しては、C library for Broadcom BCM 2835 as used in Raspberry Piのドキュメントを参考にcm2835-1.36.tar.gzをDLし、次のようにビルド&インストールします。
1 2 3 4 5 6 | |
尚、bcm2835のheaderファイルで使われている各モジュールの詳細はhttp://www.airspayce.com/mikem/bcm2835/modules.htmlを参照してください。(大元の資料が一番ですよね…)
ここれで、Raspberry Pi上でのPython拡張モジュールの開発の準備は完了です。
C言語ソースへのラッピンコード追記
C言語のコードを拡張していくには次の4項目を書き足すことになります。
1. Python用のheaderファイル宣言 (#include <Python.h>)
2. モジュールに含む関数へのPythonラッパー (static PyObject * モジュール名_関数名()の部分)
3. モジュール内の関数のリスト (static PyMethodDef モジュール名Methods[]の部分)
4. モジュールをイニシャライズする関数 (void initModule()の部分)
前回のポストで紹介したadafruitが提供してくれているリポジトリの中のAdafruit_DHT_Driver_Pythonディレクトリにある、dhtreader.cというファイルを項目に従って見ていくことにします。
1. Python用のheaderファイル宣言
29行目で、ヘッダーファイルの宣言をしていますね。
1
| |
2. モジュールに含む関数へのPythonラッパー
128~132行目にかけてPython環境からC言語のbcm2835に初期化のできるようPythonのラッパーを書いていますね。
ここでPyObjectを定義し、モジュール名と関数名を”_”(アンダースコア)で繋げて書くことで、当該モジュールをインポートした時にPython Scriptからdhtreader.init()みたいな方法で実行できるようにしています。
1
| |
Pythoラッパーの一般的な役割は、Pythonの値を受け取ってCの値に変換し、Cの適切ば関数を実行することです。そして、Cの関数が実行された後でCの値をPythonの値に戻してあげることです。
CからPythonには、Py_BuildValue()を使って、単体値かタプル形式のPythonオブジェクトを戻すことができます。
1 2 3 4 5 | |
Py_BuildValue()では、第1引数の文字のフォーマットに合わせて、第2引数の値をオブジェクトに変換します。
フォーマット文字は、次の表の仕様になっています。
| [フォーマット文字] | [Pythonタイプ] | [C/C++ タイプ] | |
|---|---|---|---|
| s, s# | str/unicode, len() | char*(, int) | |
| z, z# | str/unicode/None, len() | char*/NULL(, int) | |
| u, u# | unicode, len() | (Py_UNICODE*, int) | |
| i | int | int | |
| b | int | char | |
| h | int | short | |
| l | int | long | |
| k | int or long | unsigned long | |
| I | int or long | unsigned int | |
| B | int | unsigned char | |
| H | int | unsigned short | |
| L | long | long long | |
| K | long | unsigned long long | |
| c | str | char | |
| d | float | double | |
| f | float | float | |
| D | complex | Py_Complex* | |
| O | (any) | PyObject* | |
| S | str | PyStringObject | |
| Nb | (any) | PyObject* | |
| O& | (any) | (any) |
先のPy_BuildValue()例では、bcm2835_init()の結果がint値になるので、iを指定してPythonオブジェクトに戻しています。
さて、次のブロックでは、Pythonから受け取った値を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 25 | |
139行目のPyArg_ParseTuple()とある部分がPythonから渡ってきた値をCの値に置き換えています。この関数でフォーマット文字の列をつかって渡ってくる値を形式を指定し変換します。
計測には、C言語で書かれたソースの前半(31~91行)で指定しているreadDHT()関数を使い、結果はPy_BuildValue()で、floatの数値をタプルとしてPythonオブジェクにしているのが分かりますね。
それ以外のDEBUG用のコードは、readDHT()の戻り値によって、プリントディバッグできるようになっているのでしょうね…。
3. モジュール内の関数のリスト
次の部分のstatic PyMethodDef DHTReaderMethods[]ではモジュールをimportした後に、PythonインタプリターがそれぞれのメソッドとCの関数との対応表を提供しています。
DHTReaderMethodsは、関数リストを定義するための関数の名前です。モジュール名にMethodsを続けて書きます。この名前は、次の初期化のブロックでインタープリタに伝える関数リストの指定しに使います。
1 2 3 4 5 6 7 | |
内容は、次の通りです。
1
| |
尚、[Pythonから渡す引数の形式指定]の指定には、METH_VARARGSとMETH_KEYWARDがあります。(METH_KEYWARDについては、PyArg_ParseTupleKeywards()とのセットで指定するは分かっているのですが詳細な用法が…。)
169行のNULLのは、リストの終わりを示す記号になります。
1
| |
4. モジュールをイニシャライズする関数
最後の部分は、モジュールがインポートされた時にPythonインタープリタによって実行される関数です。
1 2 3 4 5 6 7 8 9 | |
1
| |
以上で、dhtreader.c内のコードでpython拡張に関わる部分はの解説は全てです。
試しに取得しているレポジトリーのAdafruit_DHT_Driver_Pythonディレクトリで、次のコマンドを実行すると拡張モジュールがビルドされているはずです。~/Adafruit-Raspberry-Pi-Python-Code/Adafruit_DHT_Driver_Python/build/lib.linux-armv6l-2.7以下を見てみてください。(自分で書いているわけではないので、できるに決まってますよね〜)
1
| |
感想:
調べてみて感じたことは、「CソースからPython拡張モジュールにするための追記自体はそれほど複雑ではない」ということです。僕にとっては、むしろ元のCソースの処理を理解し、通信の方式やバイナリをCでハンドリングする部分を理解するのに頭を使ったような気がします。
今後モノのインターネットが進むと、Pythonでのプロトタイピングなんてケースが多々出てくると思います。Cとの兼ね合いでちょっと困ったという時には、この手法は十分使えるのではないでしょうか。Python本家のドキュメントを一読しておくと、いざという時に慌てなくすむかもしれませんね。
みちくさ:
僕は、未だ試していませんが、py-libbcm2835というCtypesのバインディングもあるようです。そちらも合わせて参考にしてみてください。