How to write GObject Introspection based Plugins (In Japanese).

優良な英語のドキュメントをガンガン翻訳していくシリーズ (?) の今回は、Lars Windolf 氏が自身のブログに投稿した「How to write GObject Introspection based Plugins」(2012年8月19日現在) の日本語訳です。
GObject イントロスペクションとプラグイン機能を提供するライブラリを使ってプラグインを実装していく手順を紹介しています。
氏は Liferea Feed Aggregator の開発者で、自分はメッセージ翻訳の協力で知り合いになりました。このたびは、氏が Google Plus に投稿していた記事を読んで、翻訳の許可をいただいた次第。何か誤訳や誤植があればこの投稿のコメントにどうぞ。


これは、GTK+ 3.0 のアプリケーション向けのプラグイン (ここでは Python のプラグイン) を作成する方法を簡単に紹介するものです。GTK+ 3.0 の主要な新しい機能の一つが GObject イントロスペクション (GObject Introspection) です。この技術を使えば、世にある全てのスクリプト言語からアプリケーションにランタイムでアクセスできるようになります。
この記事を投稿したきっかけは、Liferealibpeas ライブラリを使ったプラグインのサポート機能を追加しようとした時に、わずかなドキュメント (とはいえ、この時点では順をおって作業を進めていく方法を解説してくれた貴重なドキュメント) だけを頼りにちゃんと動作させるまでに三日も要してしまったことでした。ここでは、その際に得たノウハウをステップ毎に紹介しようと思います…
ステップ1. libpeas を使ってプラグインのエンジンを実装する
まず libpeas ライブラリとの統合では、たくさんのお決まりのコードを記述して初期化とプラグインのパスを登録します。ここでは、gtranslator というアプリケーションの gtr-plugins-engince.c というソースコードが実装の参考になりました。
そして最も重要なことは、peas_engine_add_search_path() 関数を使ってプラグインを格納するパスを登録することです:

peas_engine_add_search_path (PEAS_ENGINE (engine),
gtr_dirs_get_user_plugins_dir (),
gtr_dirs_get_user_plugins_dir ());
peas_engine_add_search_path (PEAS_ENGINE (engine),
gtr_dirs_get_gtr_plugins_dir (),
gtr_dirs_get_gtr_plugins_data_dir ());

アプリケーションを使うユーザが自分の権限で書き込みができる $HOME 配下のサブディレクトリと、パッケージのインストール時に作成する /usr/share/<アプリケーション名>/plugins/ といったディレクトリの二つをプラグイン・パスとして登録しておくと便利です。最後に、アプリケーションの初期化関数の中から、ここで記述したプラグインのエンジンが初期化されているか確認しておいて下さい。
ステップ2. libpeasgtk を使ってプラグインの設定ダイアログを実装する
さらに libpeas ライブラリにはユーザ・インタフェース用のライブラリが含まれており、プラグインの有効または無効を設定するユーザ・インタフェース (ノートブックのタブ) を自分のアプリケーションの設定ダイアログに追加することができるようになっています。次は Liferea の実装のスクリーンショットです:
libpeasgtk.png
ここでは “Plugins” というタブを自分の設定ダイアログに追加して、次のコードでプラグインの有効/無効を設定する画面を作成しています:

#include <libpeas-gtk/peas-gtk-plugin-manager.h>
[...]
/* ここで "plugins_box" が
* 既存のタブ型のコンテナ・ウィジェット (設定ダイアログ) と仮定します */
GtkWidget *alignment;
alignment = gtk_alignment_new (0., 0., 1., 1.);
gtk_alignment_set_padding (GTK_ALIGNMENT (alignment), 12, 12, 12, 12);
widget = peas_gtk_plugin_manager_new (NULL);
gtk_container_add (GTK_CONTAINER (alignment), widget);
gtk_box_pack_start (GTK_BOX (plugins_box), alignment, TRUE, TRUE, 0);

この時点で、テストのために全てのプログラムを一度コンパイルしてみて下さい。libpeas ライブラリのプラグイン・マネージャを持つ新しい空っぽのタブが表示されますが、ウィジェットとしてはちゃんと動作するはずです。
ステップ3. Activatable なクラスを定義する
既にステップ1でプラグイン・ライブラリ (libpeas) を初期化しています。次は、自分のアプリケーションに Activatable なフック (いわゆるイベント・ハンドラ) をいくつか追加する必要があります。アプリケーションのコードの中で、これらのフックを利用して PeasExtensionSet というインタフェース型のインスタンスを一つ生成します (これは、このインタフェースを提供する全てのプラグインを表します)。
例えば前述の gtranslator にはプラグインのために GtrWindowActivable という型のインタフェースが用意されており、gtranslator のウィンドウが生成された時に、そのインタフェースで定義したフックを呼び出すようになっています。このインタフェース型の定義は次のとおりです:

struct _GtrWindowActivatableInterface
{
GTypeInterface g_iface;
/* public な仮想メソッド */
void (*activate) (GtrWindowActivatable * activatable);
void (*deactivate) (GtrWindowActivatable * activatable);
void (*update_state) (GtrWindowActivatable * activatable);
};

activate() と deactivate() というメソッドは、PeasExtensionSet 型のインスタンスから発行された “extension-added” または “extension-removed” というシグナルのハンドラからそれぞれ呼び出されるようにします。最後の update_state() メソッドは、gtranslator の場合、ユーザがプラグインを操作してその結果を反映させる時に呼び出されるようになっていました。
プラグインがたくさん必要ならばたくさんのメソッドを追加することになりますが、それぞれアプリケーションが発行するシグナルに接続することになるので、これといって特別なメソッドが必要になることはありません。そのため、Activatable なインタフェース型はシンプルな構造になっています。
では、どれくらい Activatable な実装を追加する必要があるのかというと: アプリケーションが一個のメイン・ウィンドウしか持たないという一番簡単なケースであれば、(例え、そのメイン・ウィンドウを使って全てのプラグインを初期化することになっても) そのメイン・ウィンドウと全てのプラグインに対して Activatable 型を一つ実装するだけです。
ステップ4. Activatable なクラスを実装してインスタンスを生成する
実際に Activatable なクラスを定義したり、実装したり、対応するクラスを使う必要はありません。インタフェース型の実装自身がたくさんのお決まりのコードを提供してくれるからです。GtrWindowActivatable 型を実装している gtr-window-activatable.c を確認してみて下さい。
この Activable なクラスのインスタンスは PeasExtensionSet に属すことになるので、次のような初期化が必要です (ちなみに gtranslator の場合、GtrWindowActivatable 型は GtrWindow クラスに属しています):

window->priv->extensions = peas_extension_set_new (PEAS_ENGINE (gtr_plugins_engine_get_default ()),
GTR_TYPE_WINDOW_ACTIVATABLE,
"window", window,
NULL);
g_signal_connect (window->priv->extensions,
"extension-added",
G_CALLBACK (extension_added),
window);
g_signal_connect (window->priv->extensions,
"extension-removed",
G_CALLBACK (extension_removed),
window);

PeasExtensionSet 型のインスタンスは、そのクラスのインタフェースを実装する全てのプラグインを表しており、選択したプラグインまたは全てのプラグインでメソッドを呼び出す際に使用します。PeasExtensionSet 型のインスタンスを生成した後に、”extension-added” というシグナルを使って全てのプラグインを初期化します:

peas_extension_set_foreach (window->priv->extensions,
(PeasExtensionSetForeachFunc) extension_added,
window);

登録するプラグインが一個以上あるかもしれないので、それらのプラグインを処理するための PeasExtensionSetForeachFunc 型のメソッドをそれぞれ実装しておく必要があります。この型のメソッドは上で実装したインタフェースを利用します。gtranslator の場合だと次のような実装になっていました:

static void
extension_added (PeasExtensionSet *extensions,
PeasPluginInfo   *info,
PeasExtension    *exten,
GtrWindow        *window)
{
gtr_window_activatable_activate (GTR_WINDOW_ACTIVATABLE (exten));
}

注記: libpeas のバージョン 1.1 までは、次のように、呼び出すインタフェースのメソッドの名前を引数にして、単に peas_extension_call() メソッドを呼び出すだけでよかったのですが…

peas_extension_call (extension, "activate");

それが終わったら次を実施して下さい:

  1. まず、アプリケーションの起動時に peas_extension_set_foreach() を使って、登録したプラグイン毎に “extension-added” というシグナルのハンドラを呼び出す
  2. “extension-added” または “extension-removed” というシグナルのハンドラを実装して、それぞれシグナルに接続しておく
  3. ステップ3で追加で定義したインタフェース型のメソッドに対して一個の PeasExtensionSetForeachFunc 型のメソッドを実装する
  4. インタフェース型のメソッドに対し、それぞれ peas_extension_set_foreach() を実行するメソッドを提供する

ステップ5. いくつかの API を公開する
ここまででプラグイン単体を作成する準備がほぼ整いました。但し、プラグインがこのビジネスロジックにアクセスできるようにするには、アプリケーションからいくつかの API を公開しておいた方がよいでしょう。
これは具体的には、関数やインタフェースやクラスの定義の中にマークアップを付与し、それらのソース・ファイルを引数にして g-ir-scanner を実行して、一個の GObject イントロスペクションのメタデータ (一個の .gir ファイルと一個の .typelib ファイルのペア) を生成するということです。
マークアップについて詳細はイントロスペクションの注釈ガイド (Annotation Guide) や他の類似のプロジェクトを参照してみて下さい。なおコンパイル中、不完全または文法が間違っている箇所があればそれらを g-ir-scanner が警告してくれます。
ステップ6. プラグインを作成する
プラグイン本体を作成する時は、常に二つのものを生成する必要があります:

  • プラグインに関する情報を記述した .plugin ファイル
  • プラグインを実装した実行形式またはスクリプト (を少なくとも一つ)

これらのファイルをそれぞれソース・ファイルの “plugins” ディレクトリに格納し、追加でインストールするターゲットにします。ここで、作成するプラグインを “myplugin.py” という Python スクリプトと仮定して話しを進めます。従って、このスクリプトの他に作成するのは、次の内容を記述した “myplugin.plugin” というファイルになります:

[Plugin]
Module=myplugin
Loader=python
IAge=2
Name=My Plugin
Description=My example plugin for testing only
Authors=Joe, Sue
Copyright=Copyright © 2012 Joe
Website=...
Help=...

ここで Python のプラグインの中で、次のような GObject イントロスペクションのリポジトリからパッケージをいくつかインポートしておきます:

from gi.repository import GObject
from gi.repository import Peas
from gi.repository import PeasGtk
from gi.repository import Gtk
from gi.repository import <あなたのパッケージ (アプリケーション) の接頭子>

GObject や Peas、PeasGtk、そして自分のアプリケーションからなるパッケージのインポートは必須です。その他プラグインで必要となるパッケージは任意でインポートして下さい。そして、通常であれば Gtk と相互にやりとりすることになるでしょう。
次に、ステップ3で定義したインタフェース型の全てのメソッドを使って簡単なクラスを実装しておきます:

class MyPlugin(GObject.Object, <あなたのパッケージ (アプリケーション) の接頭子>.<型>Activatable):
__gtype_name__ = 'MyPlugin'
object = GObject.property(type=GObject.Object)
def do_activate(self):
print "activate"
def do_deactivate(self):
print "deactivate"
def do_update_state(self):
print "updated state!"

必ず自分のアプリケーションからなるパッケージの接頭子を付与してから、妥当で Activatable なクラスの名前 (例えば GtkWindowActivatable) を指定して下さい。ここからメソッドの詳細を実装します。
その他:

  • お使いの言語バインディングで名前空間を分離する書式を使用することになるでしょう。例えば Python の場合はドット文字でクラスの継承を表す階層ツリーの各要素を分離しています。どのように記述するか分からない場合は inofficial online API を参照してみて下さい。
  • Activatable なクラスを初期化している最中に文法エラーが見つかった場合、libpeas ライブラリは設定ダイアログの中から問題のあったプラグインを無効にします。そのような場合は手動で有効にする必要があります。
  • 自分のプラグインを複数回、有効または無効にして初期化中の問題をデバッグできます。
  • “make install” で無限ループになるのを回避するために、ホーム・ディレクトリにプラグイン・エンジンのディレクトリを登録して、そこでプラグインをテストして下さい。

ステップ7. autotools でインストールのフックを設定する
もし、次に示すのと似たような Makefile.am を automake を使ってソース・ディレクトリの中に展開しようとしているのであれば

if HAVE_INTROSPECTION
-include $(INTROSPECTION_MAKEFILE)
INTROSPECTION_GIRS = Gtranslator-3.0.gir
Gtranslator-3.0.gir: gtranslator
INTROSPECTION_SCANNER_ARGS = -I$(top_srcdir) --warn-all --identifier-prefix=Gtr
Gtranslator_3_0_gir_NAMESPACE = Gtranslator
Gtranslator_3_0_gir_VERSION = 3.0
Gtranslator_3_0_gir_PROGRAM = $(builddir)/gtranslator
Gtranslator_3_0_gir_FILES = $(INST_H_FILES) $(libgtranslator_c_files)
Gtranslator_3_0_gir_INCLUDES = Gtk-3.0 GtkSource-3.0
girdir = $(datadir)/gtranslator/gir-1.0
gir_DATA = $(INTROSPECTION_GIRS)
typelibdir = $(libdir)/gtranslator/girepository-1.0
typelib_DATA = $(INTROSPECTION_GIRS:.gir=.typelib)
CLEANFILES =	\
$(gir_DATA)	\
$(typelib_DATA)	\
$(BUILT_SOURCES) \
$(BUILT_SOURCES_PRIVATE)
endif

次の点を確認しておいて下さい:

  1. g-ir-scanner コマンドでスキャンしようとする全てのファイルを xxx_gir_FILES に渡す
  2. INTROSPECTION_SCANNER_ARGS–identifier-prefix=XXX (XXX はアプリケーションが持つ名前空間の接頭子) を渡す
  3. 一般的に使用する名前空間の接頭子が無い場合、INTROSPECTION_SCANNER_ARGS には –accept-unprefixed を追加する

次に、インストールするプラグインのターゲットを定義します:

plugindir = $(pkglibdir)/plugins
plugin_DATA = \
plugins/one_plugin.py \
plugins/one_plugin.plugin \
plugins/another_plugin.pl \
plugins/another_plugin.plugin

最後に、アプリケーションをビルドする際の依存関係と GIR のマクロを configure.ac に追加します:

pkg_modules="[...]
libpeas-1.0 >= 1.0.0
libpeas-gtk-1.0 >= 1.0.0"
GOBJECT_INTROSPECTION_CHECK([0.9.3])
GLIB_GSETTINGS

ステップ8. ビルドを試みる
“make” を実行して次を確認してみて下さい。

  1. 全てコンパイルできること
  2. g-ir-scanner から「ガミガミいわれないこと」(すなわち、たくさんのエラーや警告が出力されないこと)
  3. ソース・ディレクトリの中に .gir.typelib のファイルが生成されること

“make install” を実行して次を確認してみて下さい。

  1. .gir ファイルが <prefix>/share/<アプリケーション名>/gir-1.0/ 配下にインストールされること
  2. プラグインが <prefix>/lib/<アプリケーション名>/plugins/ 配下にインストールされること

アプリケーションを起動して次を確認してみて下さい。

  1. 初回にプラグイン設定ダイアログからプラグインを有効にできること
  2. 必要であれば、プラグインがまだ有効であるかどうかを確認する (初期化中に文法エラーなどで無効になる状態を確認できる)
  3. デバッグ文をプラグインにたくさん追加して、アプリケーションを起動したときに端末に出力されるログを監視する

以上です。もし間違いを見つけたりエラーに遭遇したらコメントにお願いします! このチュートリアルが多くの読者の助けになれば幸いです。

Translation of Python GTK+3 Tutorial (TAKE 2).

以前から始めている翻訳、ブログのコメントにもあるとおり、このドキュメントのベースになっている Sphinx というツールの gettext サポートを利用して、以前の Wiki ベースから (このツールを使って生成した) HTML ベースに変更した。
ひとまず Wiki で翻訳したもの po ファイルに移行中。ついでに、これらのファイルを本家にマージしてもらった。Thanks Sebastian!
日本語で HTML を生成する時は source/conf.py を下にあるように追加する必要あり:

$ git clone git://github.com/sebp/PyGObject-Tutorial.git PyGObject-Tutorial
$ tree PyGObject-Tutorial
PyGObject-Tutorial
├── COPYING
├── Makefile
├── README
├── examples/
├── get_stockitems.py
├── images/
├── source/
│   └── conf.py
└── translations/
├── ja/
│   ├── basics.po
│   ├── button_widgets.po
│   ├── entry.po
│   ├── index.po
│   ├── install.po
│   ├── introduction.po
│   ├── label.po
│   ├── layout.po
│   └── unicode.po
└── templates/
$ cd PyGObject-Tutorial
$ vi source/conf.py
$ make
Please use `make ' where  is one of
html       to make standalone HTML files
dirhtml    to make HTML files named index.html in directories
singlehtml to make a single large HTML file
pickle     to make pickle files
json       to make JSON files
htmlhelp   to make HTML files and a HTML help project
qthelp     to make HTML files and a qthelp project
devhelp    to make HTML files and a Devhelp project
epub       to make an epub
latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
latexpdf   to make LaTeX files and run them through pdflatex
text       to make text files
man        to make manual pages
changes    to make an overview of all changed/added/deprecated items
linkcheck  to check all external links for integrity
doctest    to run all doctests embedded in the documentation (if enabled)
$ mkdir -p translations/locale/ja/LC_MESSAGES/
$ make updatepo html
$ w3m build/html/index.html
--- /tmp/PyGObject-Tutorial/source/conf.py      2012-10-08 11:08:16.444664651 +0900
+++ source/conf.py      2012-09-24 20:13:15.308548365 +0900
@@ -43,8 +43,6 @@
project = u'Python GTK+ 3 Tutorial'
copyright = u'GNU Free Documentation License 1.3'
-locale_dirs = ["../translations/locale",]
-
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
@@ -57,6 +55,9 @@
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
+language = 'ja'
+locale_dirs = [ 'locale/' ]
+#gettext_compact = True
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -107,6 +108,7 @@
# The name for this set of Sphinx documents.  If None, it defaults to
# " v documentation".
#html_title = None
+html_title = u'Python GTK+ 3 チュートリアル 1.0 ドキュメント'
# A shorter title for the navigation bar.  Default is the same as html_title.
#html_short_title = None
@@ -217,3 +219,4 @@
('index', 'pygobjecttutorial', u'PyGObject Tutorial Documentation',
[u'Sebastian Pölsterl'], 1)
]
+

ひとまず翻訳済みの移行が完了しているところまで公開してみる:
See Also Python GTK+ 3 チュートリアル: Generated by Sphinx (移行中)
See Also Python GTK+ 3 チュートリアル: 以前の Wiki スタイル (更新停止)