Python で GObject をラップする
C の専門家でなくても Python 用にモジュールをラップすることはできます
Python は、グラフィカルなインターフェースをコーディングするには素晴らしい言語です。Python を使用すれば、実際に動作するコードを短時間で作成できる上に、時間のかかるコンパイル作業も不要であるため、数分でインターフェースを開発することができ、その後ほどなくして実際に利用できるものにすることができます。Python では、このような特徴を持つことに加え、ネイティブ・ライブラリーを簡単に利用できることから、素晴らしい環境が実現されます。
GNOME および関連する Python 用ライブラリーをラップしたパッケージに gnome-python
があります。このパッケージを使用すれば、GNOME のコア・アプリケーションとまったく同じルック・アンド・フィールのアプリケーションを Python で作成することができ、しかも要する時間は C で作成する場合の何分の一かで済みます。
その一方で、C でコーディングしないことによる欠点もあります。GNOME は大半が C で記述されているため、Python でウィジェットを使用するにはラップをしなければなりません。この作業は、ラップ処理がどのように行われるかを理解している人にとっては簡単ですが、自動的に行われるわけではありません。また、ウィジェットが GNOME のコア・ライブラリーに含まれていない場合や、非常に役立つウィジェットというわけではない場合、それらのウィジェットがラップされて提供されることもありません。C でコーディングを行う場合、かなり複雑なコードを記述しなければならないかもしれませんが、最初からこのようなウィジェットをすべて利用できるのです!
しかし、この一般論が必ずしも当てはまるというわけでもありません。ウィジェットをラップする作業は、従来、限られた数少ない人たちだけができるものとされてきましたが、実際にはそれほど難しいことではないのです。新しく見つけてきたウィジェットをラップできれば、Python プログラムでもすぐにそれを利用することができます。
この記事では、C 言語でコーディングされた GObject (GTK+ のすべてのウィジェットおよび関連する数多くのオブジェクトの大元の基底クラス) をラップして、Python のコードから利用できるようにする方法を説明します。その前提として、gnome-python
バージョン 1.99.x が皆さんのマシンにインストールされているものとします (インストールされてない場合には、「参考文献」に示してあるリンクを参照してください)。パッケージを使用する場合には、開発パッケージをインストールしておいてください。また、Python 2.2 とそのヘッダーもインストールしておく必要があります。Make、Python、GTK+ 2、およびある程度の C は理解しているものとします。
実際にラップ作業を行う例として、この記事では、画面の通知スペースのアイコンを抽象化する GTK+ ウィジェット EggTrayIcon
をラップします。このライブラリーは、GNOME CVS の libegg
モジュールに含まれています。記事の最後までには、TrayIcon オブジェクトを包含する trayicon
という Python ネイティブのモジュールができているはずです。
まずは、eggtrayicon.c と eggtrayicon.h を入手し (これらのファイルへのリンクは、記事の終りにある「参考文献」に示してあります)、新しいディレクトリーに保存してください。このソースは、automake 環境でビルドすることを前提に作られているので (この記事では automake 環境でのビルドは行いません)、ファイルから #include <config.h> を削除するか、config.h という名前で空のファイルを作成し、空の makefile を作成するかのいずれかを行ってください。ファイルの中身は、作業を進める中で記述していきます。
インターフェース定義の作成
このオブジェクトをラップする作業の最初のステップでは、このオブジェクトの API を定義する trayicon.defs ファイルを作成します。定義ファイルは、Scheme 系の言語で記述するため、小規模なインターフェースなら簡単に作成できますが、大規模なインターフェースであったり、初心者が作成したりする場合には、作成作業は厄介なものになる可能性があります。
gnome-python
には、h2def
というツールが用意されています。このツールは、ヘッダー・ファイルを構文解析して、おおまかな定義ファイルを作成してくれます。ただし、実際には C のコードを構文解析するわけではなく、正規表現を使用しているだけなので、昔ながらのフォーマットで記述された GObject を大前提としており、変わったフォーマットで記述された C のコードは、正しく構文解析できない可能性があります。
定義ファイルの最初のバージョンを作成するために、以下のようにして h2def
を呼び出します。
python /usr/share/pygtk/2.0/codegen/h2def.py eggtrayicon.h > trayicon.defs
h2def.py を /usr にインストールしていない場合には、それが保存されている場所を指すようにパスを変更する必要があります。
生成された定義ファイルの中身を見てみると、なるほどと思うはずです。このファイルには、EggTrayIcon
クラス、コンストラクター、そして send_message
と cancel_message
という 2 つのメソッドが定義されており、その内容に明白な誤りはなく、削除したいメソッドやフィールドもないため、このファイルに手を加える必要はありません。このファイルは、Python 専用のものではなく、他の言語のバインディングからも利用することができます。
ラッパーの作成
インターフェースの定義を作成できたので、次は Python ラッパーの大部分を作成します。この作業では、最初にオーバーライド・ファイルを作成します。オーバーライド・ファイルとは、どのヘッダーをインクルードし、モジュールをどんな名前にするか、等々をコード・ジェネレーターに指示するためのファイルです。
オーバーライド・ファイルは、%% によって (lex/yacc のスタイルで) 複数のセクションに区切られます。これらのセクションには、インクルードするヘッダー、モジュールの名前、インクルードする Python モジュール、無視する関数、手作業で記述する関数などを定義します。以下に示すのは、今回の trayicon
モジュール用のオーバーライド・ファイルの最初のバージョンです。
リスト1. trayicon.override
%% headers #include <Python.h> #include "pygobject.h" #include "eggtrayicon.h" %% modulename trayicon %% import gtk.Plug as PyGtkPlug_Type %% ignore-glob *_get_type %%
このコードについても、詳しく見てみたいと思います。
headers
#include <Python.h>
#include "pygobject.h"
#include "eggtrayicon.h"
このセクションには、ラッパーをビルドする際にインクルードするヘッダー・ファイルが記述されています。Python.h と pygobject.h は、必ずインクルードする必要があります。また、ラップする対象である eggtrayicon.h もインクルードする必要があります。modulename trayicon
modulename
の指定では、モジュールを含める対象となるラッパーが記述されています。import gtk.Plug as PyGtkPlug_Type
このセクションには、ラッパーにインポートする Python のモジュールが記述されています。モジュールをインポートする際の名前の付け方に注意してください。コンパイル対象とするモジュールの名前は、この命名規則に従う必要があります。通常は、作成するオブジェクトのスーパークラスをインポートしておけば十分です。例えば、オブジェクトが GObject から直接継承されたものであれば、以下のように指定します。import gobject.GObject as PyGObject_Type
ignore-glob
*_get_type
このセクションには、無視する関数の名前が glob パターン (シェル・スタイルの正規表現) で記述されています。型コードは、Python が処理してくれるので、*_get_type
にマッチする関数は無視します。無視しないと、これらの関数はラップされることになります。
これでオーバーライド・ファイルを作成できたので、今度はこのファイルを使用してラッパーを作成します。gnome-python
バインディングには、ラッパーを作成するために簡単に使用できる魔法のツールが用意されています。そこで、以下のコードを makefile に追加します。
リスト2. makefile の最初のバージョン
DEFS='pkg-config --variable=defsdir pygtk-2.0' trayicon.c: trayicon.defs trayicon.override pygtk-codegen-2.0 --prefix trayicon \ --register $(DEFS)/gdk-types.defs \ --register $(DEFS)/gtk-types.defs \ --override trayicon.override \ trayicon.defs > $@
これも、詳しく見てみたいと思います。
DEFS='pkg-config --variable=defsdir pygtk-2.0'
DEFS
には、Python GTK+ バインディングの定義ファイルを格納してある場所のパスが指定されます。trayicon.c: trayicon.defs trayicon.override
生成される C のコードは、定義ファイルとオーバーライド・ファイルに依存します。pygtk-codegen-2.0 --prefix trayicon \
ここで、コード・ジェネレーターgnome-python
が呼び出されます。引数prefix
で指定した名前は、生成されるコードの中の変数名に付けるプレフィックスとして使用されます。これは、どんな名前にしてもよいのですが、モジュール名にすれば、シンボル名を統一することができます。--register $(DEFS)/gdk-types.defs \
--register $(DEFS)/gtk-types.defs \
今回のモジュールでは、Glib と GTK+ の型を使用するので、コード・ジェネレーターに対しても、それらの型をロードするよう指示する必要があります。--override trayicon.override \
この引数で、先ほど作成したオーバーライド・ファイルをコード・ジェネレーターに渡します。trayicon.defs > $@
コード・ジェネレーターに対するこの最後のオプションで、定義ファイルそのものを指定します。コード・ジェネレーターからの出力は、標準出力に対して行われるため、この出力をターゲットの trayicon.c にリダイレクトします。
ここで make trayicon.c
を実行し、生成されたファイルの中身を見てみると、C のコードで EggTrayIcon
の各関数がラップされているのがわかります。「No ArgType for GdkScreen*」という警告が表示されても、正常なので気にする必要はありません。
ご覧のとおり、ラップ・コードは複雑に見えます。コード・ジェネレーターがコードのすべての行を生成してくれることは、ありがたいことです。ラップ・コードに若干の変更を加える必要がある場合に、個々のメソッドを手作業でラップする方法を後ほど説明しますが、この場合もすべてのラッパーを自分で作成する必要はありません。
モジュールの作成
ラッパーの大部分を作成できたので、今度はそれを起動する方法が必要になります。そのために、Python モジュールにとっての main()
関数とみなせる trayiconmodule.cを作成します。このファイルは (オーバーライド・ファイルと同様) ボイラープレート・コードであり、ここでは少しだけ修正を加えます。以下に示すのが、ここで使用する trayiconmodule.c です。
リスト3. trayIcon モジュールのコード
#include <pygobject.h> void trayicon_register_classes (PyObject *d); extern PyMethodDef trayicon_functions[]; DL_EXPORT(void) inittrayicon(void) { PyObject *m, *d; init_pygobject (); m = Py_InitModule ("trayicon", trayicon_functions); d = PyModule_GetDict (m); trayicon_register_classes (d); if (PyErr_Occurred ()) { Py_FatalError ("can't initialise module trayicon"); } }
上記コードには trayicon
という語が名前の一部に使われているところが何箇所もあるので、それぞれの違いを説明しておきたいと思います。inittrayicon という関数の名前と、モジュールを初期化する際に指定される名前は、Python モジュールの実際の名前であり、最終的な共有オブジェクトの名前でもあります。配列 trayicon_functions
と関数 trayicon_register_classes
の名前は、コード・ジェネレーターに対して --prefix
引数で指定された名前に基づいて付けられています。先ほど触れたように、このファイルのコーディング作業があまりややこしくならないように、これらの名前は統一しておくに越したことはありません。
使われている名前が原因で混乱を招く可能性はあるものの、この C のコードは非常に簡明なものになっています。GObject と trayicon
モジュールを初期化した後、クラスを Python に登録しています。
これですべてのピースが揃ったので、次は共有オブジェクトを作成します。以下の内容を makefile に追加します。
リスト4. makefile へ追加する内容
CFLAGS = 'pkg-config --cflags gtk+-2.0 pygtk-2.0' -I/usr/include/python2.2/ -I. LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0' trayicon.so: trayicon.o eggtrayicon.o trayiconmodule.o $(CC) $(LDFLAGS) -shared $^ -o $@
これについても、1 行ずつ見ていきたいと思います。
CFLAGS = 'pkg-config --cflags gtk+-2.0 pygtk-2.0' -I/usr/include/python2.2/ -I.
この行は、C のコンパイル・フラグを定義しています。pkg-config
は、GTK+ と PyGTK のインクルード・パスを指定するためのものです。LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0'
この行は、リンカー・フラグを定義しています。ここでも、正しいライブラリー・パスを指定するためにpkg-config
が使用されています。trayicon.so: trayicon.o eggtrayicon.o trayiconmodule.o
共有オブジェクトは、生成されたコードと、先ほど作成したモジュール・コード、およびEggTrayIcon
の実装コードを基に作成されます。暗黙のルールで、作成した .cファイルから .oファイルが作成されます。$(CC) $(LDFLAGS) -shared $^ -o $@
これで、最終的な共有ライブラリーがビルドされます。
ここで make trayicon.so
を実行すると、定義ファイルから C コードが生成され、3 つの C ファイルがコンパイルされて、最終的にそれらがリンクされるはずです。これで、最初のネイティブ Python モジュールがビルドされました。コンパイルやリンクがうまくいかなかった場合は、これまでの手順をもう一度チェックし、最初のほうで出された警告が後々のエラーにつながっていないか確認してください。
これで trayicon.so が作成されたので、Python プログラムの中で実際に使用してみることにします。まずは、このファイルをロードして、そのメンバーを一覧表示させてみるのがよいでしょう。シェルで「python
」と入力し、対話型インタープリターを起動して、以下に示すコマンドを入力してください。
リスト5. 対話型インタープリターによる TrayIcon のテスト
$ python Python 2.2.2 (#1, Jan 18 2003, 10:18:59) [GCC 3.2.2 20030109 (Debian prerelease)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import pygtk >>> pygtk.require("2.0") >>> import trayicon >>> dir (trayicon) ['TrayIcon', '__doc__', '__file__', '__name__']
dir
で、このリストと同じ結果が得られたでしょうか。今度はもっと規模を大きくした例で試してみたいと思います。
リスト6. Hello サンプル
#! /usr/bin/python import pygtk pygtk.require("2.0") import gtk import trayicon t = trayicon.TrayIcon("MyFirstTrayIcon") t.add(gtk.Label("Hello")) t.show_all() gtk.main()
1 行ずつ細かく分けて見ていきたいと思います。
#! /usr/bin/python
import pygtk
pygtk.require("2.0")
import gtk
import trayicon
ここでは最初に、GTK+ のバインディングを必要なものとしてインポートしており、それから先ほど作成した新しいモジュールをインポートしています。t = trayicon.TrayIcon("MyFirstTrayIcon")
次に trayicon.TrayIcon のインスタンスを作成します。コンストラクターは、アイコンの名前を指定する文字列引数を 1 つとります。t.add(gtk.Label("Hello"))
TrayIcon の要素は GTK+ のコンテナーなので、その中にはどんなものでも追加することができます。ここでは、ラベル・ウィジェットを追加しています。t.show_all()
gtk.main()
ここでは、ウィジェットが表示されるように設定し、GTK+ のメインのイベント・ループを開始しています。
まだ GNOME パネルに「Notification Area (通知スペース)」アプレットを追加していないのなら、追加してください (パネル上で右クリックし、「Add to Panel (パネルに追加)」 -> 「Utility (ユーティリティ)」 -> 「Notification Area (通知スペース)」の順に選択します)。テスト・プログラムを実行すると、トレイに「Hello」と表示されるはずです。なかなか良くありませんか?
図1. Hello プログラムの実行例

通知スペースでは、他にどんなことができるのでしょうか?例えば、通知スペースにプログラムでメッセージを表示させることができます。このメッセージが実際にどんな風に表示されるかは、実装次第です。現在、GNOME の通知スペースにはツールチップが表示されるようになっています。メッセージを送信するには、send_message()
関数を呼び出します。API を少し調べてみると、この関数はタイムアウト値とメッセージを引数にとることになっているので、以下のコードでうまくいくはずです。
... t = trayicon.TrayIcon("test") ... t.send_message(1000, "My First Message")
しかし、うまくいきませんでした。C のプロトタイプは send_message(int timeout, char* message, int length)
となっているので、Python の API でも文字型のポインターと長さを指定する必要があります。以下のコードだと、うまくいきます。
... t = trayicon.TrayIcon("test") ... message = "My First Message" t.send_message(1000, message, len(message))
今度はうまくいきましたが、少し見苦しくなってしまいます。これは Python です。プログラミングは簡潔でなければなりません。この調子でコーディングを続けると、セミコロンがないだけの C のコードになってしまいます。幸い、コード・ジェネレーター gnome-python
を使用する場合、個々のメソッドを手作業でラップできるようになっています。
インターフェースに対する若干の修正
現時点では、使用しているインターフェースは send_message(int timeout, char *message, int length)
関数ですが、EggTrayIcon
の Python API から send_message(timeout, message)
を呼び出せるようになると望ましいです。幸いこれは、さほど難しいことではありません。
このインターフェースの変更を行うには、再度 trayicon.override に手を加えます。このことは、ファイルの名前から察しがつくことです。このファイルには、主に、手作業で変更したラッパー関数が含められます。これらが機能する仕組みを説明するのは、サンプル・コードを示して中身を追っていくよりも遥かに大変なので、ここでは手作業でラップした send_message
のコードを示すことにします。
リスト7. 手作業での変更
override egg_tray_icon_send_message kwargs static PyObject* _wrap_egg_tray_icon_send_message(PyGObject *self, PyObject *args, PyObject *kwargs) { static char *kwlist[] = {"timeout", "message", NULL}; int timeout, len, ret; char *message; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "is#:TrayIcon.send_message", kwlist, &timeout, &message, &len)) return NULL; ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj), timeout, message, len); return PyInt_FromLong(ret); }
ここでも、コードを 1 行 1 行分解して、説明したいと思います。
override egg_tray_icon_send_message kwargs
この行はコード・ジェネレーターに対して、egg_tray_icon_send_message
は手作業で定義するので、生成しないように指示しています。static PyObject*
_wrap_egg_tray_icon_send_message(PyGObject *self,
PyObject *args, PyObject *kwargs)
これらの行は、Python から C へのブリッジのプロトタイプです。このプロトタイプは、メソッドの呼び出し元となる GObject へのポインター、引数の配列、キーワード引数の配列で構成されています。Python の値は (整数も含め) すべてオブジェクトなので、戻り値は常にPyObject*
です。{
static char *kwlist[] = {"timeout", "message", NULL};
int timeout, len, ret;
char *message;
この配列には、この関数で解釈可能なキーワード引数の名前が定義されています。必ずキーワード引数を使用できるようにしなければならないわけではありませんが、キーワード引数を使用することで、引数を数多く使用するコードを非常に明快なものにすることができます。また、キーワード引数を使用できるようにするのは、大した手間ではありません。if (!PyArg_ParseTupleAndKeywords(args, kwargs,
"is#:TrayIcon.send_message", kwlist,
&timeout, &message, &len))
return NULL;
この非常に複雑な関数呼び出しでは、引数の解析を行っています。引数に、私たちが理解できるキーワード引数のリストと、渡されてきたすべての引数を渡すと、最後の 3 つの引数が指している値が設定されます。暗号めいた文字列は、要求される変数の型を宣言したものであり、これについては後ほど説明します。ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj),
timeout, message, len);
return PyInt_FromLong(ret);
}
ここでは、実際にegg_tray_icon_send_message
を呼び出し、返されたint
をPyObject
に変換しています。
一見、少し手ごわそうにみえますが、元々は trayicon.c に生成されたコードをコピーしたものです。関数がとる引数に少し手を加えたいだけであれば、たいていは、これでまったく問題ありません。生成された C コードから変更したい関数をコピー・アンド・ペーストし、魔法の override 行を追加し、望みどおりの動作になるようにコードを編集すればよいのです。
最も重要な変更内容は、関数がとる引数を変更することです。PyArg_ParseTupleAndKeywords
関数に引数として指定されている、わかりにくそうな文字列は、関数が要求する引数を定義しています。この文字列は元々、isi:TrayIcon.send_message
であり、この isi
は、引数の型が int
(int の i)、char*
(文字列を意味する string の s)、int
(int の i) であることを意味しています。そして、例外がスローされると、この TrayIcon.send_message
という名前の関数が呼び出されることを意味しています。Python のコードで文字列の長さを指定しなくてもよいようにしたいため、isi
を is#
に変更します。s
ではなく s#
としているのは、PyArg_ParseTupleAndKeywords
が文字列の長さを自動的に計算して、別の変数にセットしてくれること (これがまさに望んでいたことです) を意味しています。
新しいラッパーを使用するには、共有オブジェクトを再度ビルドして、テスト・プログラムの send_message
呼び出しを、以下のように変更します。
t.send_message(1000, message)
すべて計画どおりであれば、修正後のコードも同じ動作をするはずです。しかもすっきりしたコードになっています。
まとめ
この記事では、小さいながらも便利な C の GObject を取り上げ、それをラップして Python で利用できるようにしました。さらに、要求を満たすように、ラッパーに手作業での修正も加えました。今回紹介した手法は、さまざまなオブジェクトに数多く適用できるので、見かけた GObject はどんなものでも Python で利用できるようにすることができます。
ダウンロード可能なリソース
- このコンテンツのPDF
- Sample code (l-wrap.zip | 7KB)
関連トピック
- eggtrayicon.c や実際に動作するサンプル・コードが含まれている (この記事で説明したソースの) tarball をダウンロードしてください。
gnome-python
のホームページから gnome-python のバージョン 1.99.x をダウンロードしてください。- この記事で取り上げた GNOME のプログラムはすべて、GNOME の FTP サーバーからダウンロードすることができます。
- developerWorks Linux ゾーンで Linux 開発者および Python 開発者向けの参考資料を見つけてください。