Python/C APIを用いた、拡張モジュールの作成方法を、GimpのAPIを対象にしてまとめてみました。
なお、Pythonから既存のCライブラリのAPIを呼び出す方法はいろいろなものがあります。今回Pythonに標準的に用意されているPython/C APIを用いたのは、Gimp-Python(GimpのプラグインをPythonで呼べるようにする機能、Python-Fuとも呼ばれる)がPython/C APIにより書かれているように読めたためです。このような制約がなければ、SWIGのように半自動で拡張モジュールを作成してくれるやり方のほうが便利な場合も多いかと思います。この辺は状況に応じて便利なものを選べばよいかと思います。
対象とするAPIと拡張モジュール
今回作成する拡張モジュールで呼び出したいGimpのAPIは、
gimp_get_color_configuration
という、カラーマネージメント関係の設定情報を取得する関数です。この関数の戻り値は、
GimpColorConfig
という構造体になります。従いまして、作成する拡張モジュールでは、上記のAPI関数を呼べるようにし、戻り値の構造体もPythonのオブジェクトとしてアクセスできるようにする、ということが目標になります。
拡張モジュールの関数
API関数を呼び出すためには拡張モジューにラッパー間数を追加します。
static PyObject * gimprc_get_color_configuration(PyObject* self) { GimpColorConfig *tmp; PyGimpColorConfig *colcfg; tmp = gimp_get_color_configuration(); if (tmp == NULL) { return NULL; } colcfg = PyObject_NEW(PyGimpColorConfig, &gimprc_PyGimpColorConfigType); if (colcfg == NULL) { return NULL; } colcfg->colorconfig = tmp; return (PyObject *)colcfg; }
ラッパー間数はわかりやすいように、API関数と1対1で作成しました(といっても1つだけです)。この間数は、
import gimprc colcfg = gimprc.get_color_configuration()
のように使うことを想定しています(さらにこの戻り値がgimprc.GimpColorConfig 型として扱えるようにします)。
ラッパー関数の処理は、呼びだされたら、CのAPI関数 gimp_get_color_configurationを呼び出し、戻り値をPyGimpColorConfig*の変数に代入して戻すだけです。PyGimpColorConfig型の定義方法については後で説明します。
関数の定義ができたら、これらの関数をモジュールのメソッドとして登録するためのメソッドテーブルを作成します。
static PyMethodDef gimprc_methods[] = { {"get_color_configuration", gimprc_get_color_configuration, METH_VARARGS}, {NULL}, };
このメソッドテーブルを初期化関数内で処理することにより、この拡張モジュールの関数として呼ぶことが可能になります。初期化処理については、C構造体に対するPython型の定義の説明のあとに説明します。
C構造体に対応するPythonの型の定義
Pythonのオブジェクトは、Cの世界ではPyObject*型の変数で扱われているようです(こちらの記述から推測)。PyObjectはリファレンスカウンタとタイプオブジェクトへのポインタからなります。このため実質的にその型の振る舞いはタイプオブジェクトが決めています。新しい型を定義する場合は、PyObject型(に変換可能な)構造体と対応するタイプオブジェクトを定義すればよいことになります。
最初はPyObject型(に変換可能な)構造体の定義です。
typedef struct { PyObject_HEAD GimpColorConfig * colorconfig; } PyGimpColorConfig;
構造体の名前はPyGimpColorConfigにしました。Pythonからアクセスする際の名前は後で決めることになります。
最初のPyObject_HEADはCのプリプロセッサマクロで、 リファレンスカウンタとタイプオブジェクトへのポインタになります。この文末にはセミコロンをつけないように注意してください(マクロ内に含まれています)。
二つめのGimpColorConfig*が今回独自に追加しているメンバです。これはGimpColorConfigへのポインタを保持して、各メンバ変数の読み出しがあった際に使うことを想定しています。
(このような構造のため、Cのキャストを行っても問題なくPyObject*として扱うことができることが読み取れます。Cなので、直接継承とかを用いて、PyObjectの派生クラスを定義するのではないところが分かりにくいところです。)
次にタイプオブジェクトを定義します。長くなりますが、今回定義したのはこういうものです。
static PyTypeObject gimprc_PyGimpColorConfigType = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "gimprc.GimpColorConfig", /*tp_name*/ sizeof(PyGimpColorConfig), /*tp_basicsize*/ 0, /*tp_itemsize*/ (destructor)colcfg_dealloc, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ 0, /*tp_repr*/ 0, /*tp_as_number*/ 0, /*tp_as_sequence*/ 0, /*tp_as_mapping*/ 0, /*tp_hash */ 0, /*tp_call*/ 0, /*tp_str*/ 0, /*tp_getattro*/ 0, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT, /*tp_flags*/ "GimpColorConfig objects", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ colcfg_methods, /* tp_methods */ colcfg_members, /* tp_members */ colcfg_getsets, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ 0, /* tp_alloc */ 0, /* tp_new */ };
代表的な各スロット(メンバ)についてだけ説明します。詳細は参考サイト等をご覧ください。
型の定義と聞いて真っ先に思い浮かぶのが、メンバ関数やメンバ変数の定義だと思います。これらは、tp_methodsスロットおよびtp_membersスロットに変数を割り当てることで行っています。tp_nameスロットには、モジュール名.型名、の形式でこの型名を入力しています。tp_deallocスロットには解放時に呼ばれる関数を定義しています(デストラクタに相当すると思います)。もちろんコンストラクタに相当する処理も記述でき、tp_newスロットに関数を割り当てます。今回は特別な処理は不要であったので、既存の関数を後述する初期化関数内で割り当てています。
今回のGimpColorConfig構造体に対する処理は設定値の読み取りだけにしようと思います。そのため、実際にはメンバ関数およびメンバ変数の定義はなにもなくて、
メンバ関数
static PyMethodDef colcfg_methods[] = { {NULL} /* sentinel */ };
メンバ変数
static PyMemberDef colcfg_members[] = { {NULL} /* sentinel */ };
のようになっています。
構造体の各メンバへのアクセスはゲッターセッター関数を定義し、tp_getsetスロットに定義することで行っています。
static PyGetSetDef colcfg_getsets[] = { { "cmyk_profile", (getter)colcfg_get_cmyk_profile, (setter)0, "cmyk profile name", NULL }, { "display_module", (getter)colcfg_get_display_module, (setter)0, "display module", NULL }, { "display_profile", (getter)colcfg_get_display_profile, (setter)0, "display profile name", NULL }, { "display_profile_from_gdk", (getter)colcfg_get_display_profile_from_gdk, (setter)0, "flag for using system display profile", NULL }, { "display_rendering_intent", (getter)colcfg_get_display_intent, (setter)0, "rendering intent for display", NULL }, { "mode", (getter)colcfg_get_mode, (setter)0, "mode", NULL }, { "out_of_gamut_color", (getter)colcfg_get_out_of_gamut_color, (setter)0, "out of gamut color, rgb value", NULL }, { "printer_profile", (getter)colcfg_get_printer_profile, (setter)0, "printer profile name", NULL }, { "rgb_profile", (getter)colcfg_get_rgb_profile, (setter)0, "rgb profile name", NULL }, { "simulation_gamut_check", (getter)colcfg_get_simulation_gamut_check, (setter)0, "flag for gamut check at simulation", NULL }, { "simulation_rendering_intent", (getter)colcfg_get_simulation_intent, (setter)0, "rendering internt for simulation", NULL }, {NULL} /* sentinel */ };
各メンバ変数の定義の詳細は割愛します(ソースコードをご覧ください)。ここで、セッターには関数を定義しないことでオブジェクトへの書き込みを抑制しています。
初期化関数の作成
拡張モジュール内のAPI関数呼び出しと構造体の定義が終わったら、初期化関数を作成します。初期化関数はPythonでその拡張モジュールをimportした際に呼ばれる関数です。この初期化関数の内部で、拡張モジュールで呼び出したい関数等を登録します。
#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ #define PyMODINIT_FUNC void #endif PyMODINIT_FUNC initgimprc(void) { PyObject* m; gimprc_PyGimpColorConfigType.tp_new = PyType_GenericNew; if (PyType_Ready(&gimprc_PyGimpColorConfigType) < 0) return; /* for pygimp_rgb_new function */ init_pygimpcolor(); m = Py_InitModule3("gimprc", gimprc_methods, "Example module for gimprc."); Py_INCREF(&gimprc_PyGimpColorConfigType); PyModule_AddObject(m, "GimpColorConfig", (PyObject *)&gimprc_PyGimpColorConfigType); gimprc_add_enums(m); /* Check for errors */ if (PyErr_Occurred()) { Py_FatalError("can't initialize module gimprc"); } }
まず、拡張モジュールの名前がgimprcだとすると、初期化関数の名前は、initgimprcとなる必要があります。
この初期化関数内部の処理は次のような流れになっています。
- タイプオブジェクト変数に既存の初期化メソッドを割り当てます(gimprc_PyGimpColorConfigType.tp_newの行)
- Gimp-Pythonで定義済みのRGB型を利用するための初期化関数、init_pygimpcolorを呼び出します。これは今回の拡張モジュール独自の処理になります。
- Py_InitModule3を呼び出し、gimprcの名前でモジュールを初期化します。このタイミングでメソッドテーブルの関数が登録されます。モジュール登録の関数は何種類かあるようなので必要なものを使ってください。
- PyModule_AddObjectでGimpColorConfig型を初期化します。
- gimprc_add_enumsを呼び出し、enumに関する処理を行います。
最後のgimprc_add_enumsはGimpColorConfigが使用する定数を登録するために、今回独自に作成した関数で、
static void gimprc_add_enums(PyObject *m) { /* GimpColorManagementMode */ PyModule_AddIntConstant(m, "COLOR_MANAGEMENT_OFF", GIMP_COLOR_MANAGEMENT_OFF); PyModule_AddIntConstant(m, "COLOR_MANAGEMENT_DISPLAY", GIMP_COLOR_MANAGEMENT_DISPLAY); PyModule_AddIntConstant(m, "COLOR_MANAGEMENT_SOFTPROOF", GIMP_COLOR_MANAGEMENT_SOFTPROOF); /* GimpColorRenderingIntent */ PyModule_AddIntConstant(m, "COLOR_RENDERING_INTENT_PERCEPTUAL", GIMP_COLOR_RENDERING_INTENT_PERCEPTUAL); PyModule_AddIntConstant(m, "COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC", GIMP_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC); PyModule_AddIntConstant(m, "COLOR_RENDERING_INTENT_SATURATION", GIMP_COLOR_RENDERING_INTENT_SATURATION); PyModule_AddIntConstant(m, "COLOR_RENDERING_INTENT_ABSOLUTE_COLORIMETRIC", GIMP_COLOR_RENDERING_INTENT_ABSOLUTE_COLORIMETRIC); }
のようになっています。
拡張モジュールのコンパイル方法
ここまでで拡張モジュールの定義は完了です。次に、実際にこれをコンパイルして拡張モジュールを作成します。
コンパイル方法は例えば、
gcc -Wall -I/usr/include/gimp-2.0 -I/usr/include/python2.6 -I/usr/include/glib-2.0
-I/usr/lib/glib-2.0/include -I../../../../source/gimp-2.6.6//plug-ins/pygimp/
-fPIC -o pygimp-gimprc.o -c pygimp-gimprc.c
gcc -shared -L/usr/lib pygimp-gimprc.o -lgimp-2.0 -o gimprc.so
のようにすればOKです。拡張モジュールの内容に合わせて、インクルードディレクトリやリンクするライブラリを変更してください。
これ以外にも、Pythonのdistutilsモジュールを使って拡張モジュールを作成する方法があります。この場合、setup.pyというインクルードディレクトリやコンパイル対象ファイルの情報を書いたファイルを作成し、pythonから呼び出すことで拡張モジュールを作成できます。この場合のサンプルは、本サイトの色空間での距離表示プラグインver0.0.2に含まれていますので、興味があればご参考にしてください。
ソースコード
pygimp-gimprc.c:本サイトの色空間での距離表示プラグインver0.0.2にも含まれています。
参考にしたサイト
Python インタプリタの拡張と埋め込み:特に第1章と第2.1節が役に立ちました