1.12 拡張モジュールに C API を提供する

多くの拡張モジュールは単に Python から使える新たな関数や型を 提供するだけですが、時に拡張モジュール内のコードが他の拡張 モジュールでも便利なことがあります。例えば、あるモジュールでは 順序概念のないリストのように動作する ``コレクション (collection)'' クラスを実装しているかもしれません。 ちょうどリストを生成したり操作したりできる C API を備えた標準の Python リスト型のように、この新たなコレクション型も他の 拡張モジュールから直接操作できるようにするには一連の C 関数を 持っていなければなりません。

一見するとこれは簡単なこと: 単に関数を (もちろんstatic などとは宣言せずに) 書いて、適切なヘッダファイルを提供し、C API を書けばよいだけ、に思えます。そして実際のところ、全ての 拡張モジュールが Python インタプリタに常に静的にリンクされている 場合にはうまく動作します。 ところがモジュールが共有ライブラリの場合には、一つのモジュールで 定義されているシンボルが他のモジュールから不可視なことがあります。 可視性の詳細はオペレーティングシステムによります; あるシステムは Python インタプリタと全ての拡張モジュール用に単一のグローバルな 名前空間を用意しています (例えば Windows)。別のシステムはモジュールの リンク時に取り込まれるシンボルを明示的に指定する必要があります (AIX がその一例です)、また別のシステム (ほとんどの Unix) では、 違った戦略を選択肢として提供しています。 そして、たとえシンボルがグローバル変数として可視であっても、 呼び出したい関数の入ったモジュールがまだロードされていないこと だってあります!

従って、可搬性の点からシンボルの可視性には何ら仮定をしてはならない ことになります。つまり拡張モジュール中の全てのシンボルは static と宣言せねばなりません。例外はモジュールの初期化関数 で、これは (1.4 で述べたように) 他の拡張モジュールとの間で 名前が衝突するのを避けるためです。 また、他の拡張モジュールからアクセスを受けるべきではない シンボルは別のやり方で公開せねばなりません。

Python はある拡張モジュールの C レベルの情報 (ポインタ) を別の モジュールに渡すための特殊な機構: CObject を提供しています。 CObject はポインタ (void*) を記憶する Python のデータ型です。 CObject は C API を介してのみ生成したりアクセスしたりできますが、 他の Python オブジェクトと同じように受け渡しできます。 とりわけ、CObject は拡張モジュールの名前空間内にある名前に代入 できます。他の拡張モジュールはこのモジュールを import でき、次に名前を 取得し、最後にCObject へのポインタを取得します。

拡張モジュールの C API を公開するために、様々な方法で CObject が 使われます。エクスポートされているそれぞれの名前を使うと、CObject 自体や、CObject が公表しているアドレスで示される配列内に収められた 全ての C API ポインタを得られます。 そして、ポインタに対する保存や取得といった様々な作業は、コードを 提供しているモジュールとクライアントモジュールとの間では異なる 方法で分散できます。

以下の例では、名前を公開するモジュールの作者にほとんどの負荷が 掛かりますが、よく使われるライブラリを作る際に適切なアプローチを 実演します。 このアプローチでは、全ての C API ポインタ (例中では一つだけですが!) を、 CObject の値となるvoid ポインタの配列に保存します。 拡張モジュールに対応するヘッダファイルは、モジュールの import と C API ポインタを取得するよう手配するマクロを提供します; クライアントモジュールは、C API にアクセスする前にこの マクロを呼ぶだけです。

名前を公開する側のモジュールは、1.1 節のspam モジュールを修正したものです。関数spam.system() は C ライブラリ関数system() を直接呼び出さず、 PySpam_System() を呼び出します。この関数はもちろん、 実際には (全てのコマンドに ``spam'' を付けるといったような) より込み入った処理を行います。 この関数 PySpam_System() はまた、他の拡張モジュール にも公開されます。

関数PySpam_System() は、他の全ての関数と同様に static で宣言された通常の C 関数です。

static int
PySpam_System(const char *command)
{
    return system(command);
}

spam_system() には取るに足らない変更が施されています:

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = PySpam_System(command);
    return Py_BuildValue("i", sts);
}

モジュールの先頭にある以下の行

#include "Python.h"

の直後に、以下の二行:

#define SPAM_MODULE
#include "spammodule.h"

を必ず追加してください。

#define は、ファイルspammodule.h をインクルードして いるのが名前を公開する側のモジュールであって、クライアントモジュール ではないことをヘッダファイルに教えるために使われます。 最後に、モジュールの初期化関数は C API のポインタ配列を初期化するよう 手配しなければなりません:

PyMODINIT_FUNC
initspam(void)
{
    PyObject *m;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    m = Py_InitModule("spam", SpamMethods);

    /* C API ポインタ配列を初期化する */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* API ポインタ配列のアドレスが入った CObject を生成する */
    c_api_object = PyCObject_FromVoidPtr((void *)PySpam_API, NULL);

    if (c_api_object != NULL)
        PyModule_AddObject(m, "_C_API", c_api_object);
}

PySpam_APIstatic と宣言されていることに注意して ください; そうしなければ、initspam() が終了したときに ポインタアレイは消滅してしまいます!

からくりの大部分はヘッダファイル spammodule.h 内にあり、 以下のようになっています:

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* spammodule のヘッダファイル */

/* C API 関数 */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (char *command)

/* C API ポインタの総数 */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* この部分は spammodule.c をコンパイルする際に使われる */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* この部分は spammodule の API を使うモジュール側で使われる */

static void **PySpam_API;

#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

/* エラーによる例外の場合には -1 を、成功すると 0 を返す */
static int
import_spam(void)
{
    PyObject *module = PyImport_ImportModule("spam");

    if (module != NULL) {
        PyObject *c_api_object = PyObject_GetAttrString(module, "_C_API");
        if (c_api_object == NULL)
            return -1;
        if (PyCObject_Check(c_api_object))
            PySpam_API = (void **)PyCObject_AsVoidPtr(c_api_object);
        Py_DECREF(c_api_object);
    }
    return 0;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */

PySpam_System() へのアクセス手段を得るために クライアントモジュール側がしなければならないことは、初期化関数内 でのimport_spam() 関数 (またはマクロ) の呼び出しです:

PyMODINIT_FUNC
initclient(void)
{
    PyObject *m;

    Py_InitModule("client", ClientMethods);
    if (import_spam() < 0)
        return;
    /* さらなる初期化処理はここに置ける */
}

このアプローチの主要な欠点は、spammodule.h がやや難解に なるということです。とはいえ、各関数の基本的な構成は公開される ものと同じなので、書き方を一度だけ学べばすみます。

最後に、CObject は、自身に保存されているポインタをメモリ確保したり 解放したりする際に特に便利な、もう一つの機能を提供しているという ことに触れておかねばなりません。詳細は Python/C API リファレンスマニュアル の ``CObjects '' の節、および CObjects の実装部分 (Python ソースコード配布物中のファイル Include/cobject.h およびObjects/cobject.c に述べられています。

ご意見やご指摘をお寄せになりたい方は、 このドキュメントについて... をご覧ください。