Python で GObject をラップする

C の専門家でなくても Python 用にモジュールをラップすることはできます

GTK+ の C モジュールを Python で使用できるようにラップする方法がわかれば、いつでも好きなときに、特段 C 言語に長けていなくても、C 言語でコーディングされた GObject を Python で利用できるようになります。

Ross Burton, Software Engineer, OneEighty Software Ltd

Ross Burton は、昼間はJavaと組み込みシステムをコーディングする凡庸なコンピューターサイエンス学士取得者です。その恐怖から逃避するために、夜な夜なPython、C、そしてGTK+に浮気をします。



2012年 12月 13日 (初版 2003年 3月 11日)

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_messagecancel_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                            
%%

このコードについても、詳しく見てみたいと思います。

  1. headers
    #include <Python.h>
    #include "pygobject.h"
    #include "eggtrayicon.h"
    このセクションには、ラッパーをビルドする際にインクルードするヘッダー・ファイルが記述されています。Python.h と pygobject.h は、必ずインクルードする必要があります。また、ラップする対象である eggtrayicon.h もインクルードする必要があります。
  2. modulename trayicon
    modulename の指定では、モジュールを含める対象となるラッパーが記述されています。
  3. import gtk.Plug as PyGtkPlug_Type
    このセクションには、ラッパーにインポートする Python のモジュールが記述されています。モジュールをインポートする際の名前の付け方に注意してください。コンパイル対象とするモジュールの名前は、この命名規則に従う必要があります。通常は、作成するオブジェクトのスーパークラスをインポートしておけば十分です。例えば、オブジェクトが GObject から直接継承されたものであれば、以下のように指定します。
    import gobject.GObject as PyGObject_Type
  4. 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 > $@

これも、詳しく見てみたいと思います。

  1. DEFS='pkg-config --variable=defsdir pygtk-2.0'
    DEFS には、Python GTK+ バインディングの定義ファイルを格納してある場所のパスが指定されます。
  2. trayicon.c: trayicon.defs trayicon.override
    生成される C のコードは、定義ファイルとオーバーライド・ファイルに依存します。
  3. pygtk-codegen-2.0 --prefix trayicon \
    ここで、コード・ジェネレーター gnome-python が呼び出されます。引数 prefix で指定した名前は、生成されるコードの中の変数名に付けるプレフィックスとして使用されます。これは、どんな名前にしてもよいのですが、モジュール名にすれば、シンボル名を統一することができます。
  4. --register $(DEFS)/gdk-types.defs \
    --register $(DEFS)/gtk-types.defs \
    今回のモジュールでは、Glib と GTK+ の型を使用するので、コード・ジェネレーターに対しても、それらの型をロードするよう指示する必要があります。
  5. --override trayicon.override \
    この引数で、先ほど作成したオーバーライド・ファイルをコード・ジェネレーターに渡します。
  6. 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 行ずつ見ていきたいと思います。

  1. CFLAGS = 'pkg-config --cflags gtk+-2.0 pygtk-2.0' -I/usr/include/python2.2/ -I.
    この行は、C のコンパイル・フラグを定義しています。pkg-config は、GTK+ と PyGTK のインクルード・パスを指定するためのものです。
  2. LDFLAGS = 'pkg-config --libs gtk+-2.0 pygtk-2.0'
    この行は、リンカー・フラグを定義しています。ここでも、正しいライブラリー・パスを指定するために pkg-config が使用されています。
  3. trayicon.so: trayicon.o eggtrayicon.o trayiconmodule.o
    共有オブジェクトは、生成されたコードと、先ほど作成したモジュール・コード、および EggTrayIcon の実装コードを基に作成されます。暗黙のルールで、作成した .cファイルから .oファイルが作成されます。
  4. $(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 行ずつ細かく分けて見ていきたいと思います。

  1. #! /usr/bin/python
    import pygtk
    pygtk.require("2.0")
    import gtk
    import trayicon
    ここでは最初に、GTK+ のバインディングを必要なものとしてインポートしており、それから先ほど作成した新しいモジュールをインポートしています。
  2. t = trayicon.TrayIcon("MyFirstTrayIcon")
    次に trayicon.TrayIcon のインスタンスを作成します。コンストラクターは、アイコンの名前を指定する文字列引数を 1 つとります。
  3. t.add(gtk.Label("Hello"))
    TrayIcon の要素は GTK+ のコンテナーなので、その中にはどんなものでも追加することができます。ここでは、ラベル・ウィジェットを追加しています。
  4. t.show_all()
    gtk.main()
    ここでは、ウィジェットが表示されるように設定し、GTK+ のメインのイベント・ループを開始しています。

まだ GNOME パネルに「Notification Area (通知スペース)」アプレットを追加していないのなら、追加してください (パネル上で右クリックし、「Add to Panel (パネルに追加)」 -> 「Utility (ユーティリティ)」 -> 「Notification Area (通知スペース)」の順に選択します)。テスト・プログラムを実行すると、トレイに「Hello」と表示されるはずです。なかなか良くありませんか?

図1. 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 行分解して、説明したいと思います。

  1. override egg_tray_icon_send_message kwargs
    この行はコード・ジェネレーターに対して、egg_tray_icon_send_message は手作業で定義するので、生成しないように指示しています。
  2. static PyObject*
    _wrap_egg_tray_icon_send_message(PyGObject *self,
    PyObject *args, PyObject *kwargs)
    これらの行は、Python から C へのブリッジのプロトタイプです。このプロトタイプは、メソッドの呼び出し元となる GObject へのポインター、引数の配列、キーワード引数の配列で構成されています。Python の値は (整数も含め) すべてオブジェクトなので、戻り値は常に PyObject* です。
  3. {
    static char *kwlist[] = {"timeout", "message", NULL};
    int timeout, len, ret;
    char *message;
    この配列には、この関数で解釈可能なキーワード引数の名前が定義されています。必ずキーワード引数を使用できるようにしなければならないわけではありませんが、キーワード引数を使用することで、引数を数多く使用するコードを非常に明快なものにすることができます。また、キーワード引数を使用できるようにするのは、大した手間ではありません。
  4. if (!PyArg_ParseTupleAndKeywords(args, kwargs,
    "is#:TrayIcon.send_message", kwlist,
    &timeout, &message, &len))
    return NULL;
    この非常に複雑な関数呼び出しでは、引数の解析を行っています。引数に、私たちが理解できるキーワード引数のリストと、渡されてきたすべての引数を渡すと、最後の 3 つの引数が指している値が設定されます。暗号めいた文字列は、要求される変数の型を宣言したものであり、これについては後ほど説明します。
  5. ret = egg_tray_icon_send_message(EGG_TRAY_ICON(self->obj),
    timeout, message, len);
    return PyInt_FromLong(ret);
    }
    ここでは、実際に egg_tray_icon_send_message を呼び出し、返された intPyObject に変換しています。

一見、少し手ごわそうにみえますが、元々は trayicon.c に生成されたコードをコピーしたものです。関数がとる引数に少し手を加えたいだけであれば、たいていは、これでまったく問題ありません。生成された C コードから変更したい関数をコピー・アンド・ペーストし、魔法の override 行を追加し、望みどおりの動作になるようにコードを編集すればよいのです。

最も重要な変更内容は、関数がとる引数を変更することです。PyArg_ParseTupleAndKeywords 関数に引数として指定されている、わかりにくそうな文字列は、関数が要求する引数を定義しています。この文字列は元々、isi:TrayIcon.send_message であり、この isi は、引数の型が int (int の i)、char* (文字列を意味する string の s)、int (int の i) であることを意味しています。そして、例外がスローされると、この TrayIcon.send_message という名前の関数が呼び出されることを意味しています。Python のコードで文字列の長さを指定しなくてもよいようにしたいため、isiis# に変更します。s ではなく s# としているのは、PyArg_ParseTupleAndKeywords が文字列の長さを自動的に計算して、別の変数にセットしてくれること (これがまさに望んでいたことです) を意味しています。

新しいラッパーを使用するには、共有オブジェクトを再度ビルドして、テスト・プログラムの send_message 呼び出しを、以下のように変更します。

t.send_message(1000, message)

すべて計画どおりであれば、修正後のコードも同じ動作をするはずです。しかもすっきりしたコードになっています。


まとめ

この記事では、小さいながらも便利な C の GObject を取り上げ、それをラップして Python で利用できるようにしました。さらに、要求を満たすように、ラッパーに手作業での修正も加えました。今回紹介した手法は、さまざまなオブジェクトに数多く適用できるので、見かけた GObject はどんなものでも Python で利用できるようにすることができます。


ダウンロード

内容ファイル名サイズ
Sample codel-wrap.zip7KB

参考文献

  • eggtrayicon.c や実際に動作するサンプル・コードが含まれている (この記事で説明したソースの) tarball をダウンロードしてください。
  • gnome-python のホームページから gnome-python のバージョン 1.99.x をダウンロードしてください。
  • GObject GTK+ を紹介している過去の developerWorks の記事を読んでください。
  • GTK+ のホームページには、ドキュメントへのリンクや、ダウンロードへのリンク、コード・サンプルへのリンクがあります。
  • Python のホームページ (または日本 Python ユーザ会) から Python 2.2 をダウンロードしてください。同サイトには、Python に関するドキュメントも数多くあります。
  • この記事で取り上げた GNOME のプログラムはすべて、GNOME の FTP サーバーからダウンロードすることができます。このサーバーは、ミラー・サイトの一覧も公開しています。
  • The Camel and the Snake, or "Cheat the Prophet"」では、Perl、Python、および DB2 を使用したオープンソースの開発を取り上げています。
  • developerWorks Linux ゾーンで Linux 開発者および Python 開発者向けの参考資料を見つけてください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=231486
ArticleTitle=Python で GObject をラップする
publish-date=12132012