LoginSignup
27
21

More than 5 years have passed since last update.

年末が近づくと Python/C API を無駄に使いたくなるので準備

Last updated at Posted at 2018-12-02

KLab Engineer Advent Calendar 2018 の 3 日目

これは?

年末になると Python マニュアルだけでなく CPython のソースを読んでみたり改変してみたり C でライブラリを書いてみたりして遊ぶのが自分の中で恒例となっていまして。今年もそうやって過ごすつもりなので毎年のように調べなおしている Python C/API の事柄のうち記憶がしっかりしているものを事前に書き出しておきます。そして本来自分用のものだけれども隠すものでもないと考え直したので見えるところにおいてみます。

C/C++ で CPython のモジュールを書くことができる、 C/C++ アプリに CPython を組み込むこともできる Python C/API 。これは役に立つのか? 先達によって書かれた Python ライブラリ、 Cython, ctypes など既存ツールが手厚く存在するので C/C++ と Python C/API で頑張らなくても大抵のケースでは事足りるよなぁ、というのが実際のところ。というわけで目的と手段の逆転、使ってみたかったから使うといった感じで。いや CPython の実装の理解には役に立っている、はず、たぶん、きっと。

0. 準備

とにかく C 言語を使える環境を用意する。必須ではないがせっかくなので少し手を伸ばして Python をソースからビルドしてみる。そのやりかたもマニュアルが用意されているので開発者・先駆者の方々に感謝しつつ実行。今回は Python 3.7.1 を使用する。

Windows なら Visual Stodio 2017 を Python development と Python native development tools 入りでインストールして PCBuild/build.bat を実行。 OS X や Linux ではマニュアルの通り前準備して ./configure, make, make install 。たとえば Ubuntu 18.04 なら

# パッケージの準備
$ sudo vim /etc/apt/sources.list
# deb-src http://archive.ubuntu.com/ubuntu/ bionic universe とか
# Japanese Team の日本語 Remix イメージなら
# deb-src http://jp.archive.ubuntu.com/ubuntu/ bionic universe と設定されている行のコメントを外す

$ sudo apt build-dep python3.7

# ソースの準備
# python.org をブラウザで見る、 wget, curl などなど
$ wget https://www.python.org/ftp/python/3.7.1/Python-3.7.1.tar.xz
$ tar --xz -xvf Python-3.7.1.tar.xz
# もしくは github から v3.7.1 を clone
$ git clone --depth 1 --single-branch -b v3.7.1 https://github.com/python/cpython.git

# build & install
$ ./configure --prefix=${HOME}/python371 --enable-optimizations
$ make
$ make install

# さっそく venv 。 ipython, pytest くらいは入れておく。
$ ~/python371/bin/python -m venv ~/py371
$ . ~/py371/bin/activate
$ pip install ipython pytest

以下のコードの動作確認はこの Ubuntu 18.04 にて行っている。一部だけだが Windows 10 1083, MacOS High Sierra でも試してみて無事だったので CPython が動く環境であれば動くはず。

1. C で空のモジュールを作る

Python なら 0 byte の .py ファイルを用意すればこれを import することができる。空でもモジュールには違いない。まずはこれを目指す。0 byte の spam.so という名のファイルを用意して import を試みると ModuleNotFoundError ではなく ImportError になることからもわかるように CPython は共有ライブラリを見つけるとこれをモジュールであると期待して読み込もうとする。

In [1]: import spam
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-1-bdb680daeb9f> in <module>
----> 1 import spam

ModuleNotFoundError: No module named 'spam'

In [2]: !touch spam.so

In [3]: import spam
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
<ipython-input-3-bdb680daeb9f> in <module>
----> 1 import spam

ImportError: /home/fgshun/src/advent2018/spam.so: file too short

どのような共有ライブラリを作ればよいか? それは PyObject * PyInit_<モジュール名>(void) という関数を公開した共有ライブラリ。 PyObject * と直接書かずに PyMODINIT_FUNC マクロを使うのは環境の違いを吸収するため。たとえば Visual Studio では __declspec(dllexport) が追加で必要だったりするがこのマクロが対応してくれる。たとえば C ではなく C++ を使うと extern "C" が要るがこれも同様。

spam.c
#include <Python.h>

PyMODINIT_FUNC PyInit_spam(void) {
    return NULL;
}

この後 setup.py を用意。これができてしまえば pure python のライブラリと同じように python setup.py install もしくは pip install . でビルドしつつインストールできるようになる。 python setup.py develop もしくは pip install -e . でインストールしきらずに開発を続行できるのも同様。

setup.py
from setuptools import setup, Extension

extensions = [Extension('spam', sources=['spam.c'])]

setup(
    name='spam',
    ext_modules=extensions,
    )

あらためてビルド、 import 。 NULL を返したにもかかわらず例外がセットされていないゆえの SystemError 。

In [1]: import spam
---------------------------------------------------------------------------
SystemError                               Traceback (most recent call last)
<ipython-input-1-bdb680daeb9f> in <module>
----> 1 import spam

SystemError: initialization of spam failed without raising an exception

モジュールオブジェクトを PyModule_Create でつくるのがチュートリアルにある方法。でも今回はあえて PyModuleDef_Init多段階初期化でやってみる。関連する PEP 489 を読みつつ。ついでに Py_LIMITED_API を入れておく。これは非公開の API や構造体、構造体のメンバを隠してくれるもの。環境が許せば再ビルド不要なバイナリパッケージをつくることが可能となるが Windows ではやっぱり再ビルド必要なのとできることが減るのでよいことばかりではない、いやデメリットのほうが目立つ? ともかく今回は limited API 環境で。 PyModuleDef_Init が追加されたのは Python 3.5 からなので 0x03050000 を対応バージョンの下限に設定する。 もうひとつ、 PyArg_ParseTuple で長さの型に int を使ってしまっていたのを Py_ssize_t にする修正をいれる PY_SSIZE_T_CLEAN を入れておく。

これで空のモジュールが C で書けた。そして、 setup.py に tests_require=['pytest'] などを加えて py.test でユニットテストができるようにして。書き加えて遊ぶ準備は完了。

spam.c
#define Py_LIMITED_API 0x03050000
#define PY_SSIZE_T_CLEAN
#include <Python.h>


static struct PyModuleDef spam_module = {
    PyModuleDef_HEAD_INIT,
    .m_name = "spam",
};


PyMODINIT_FUNC PyInit_spam(void) {
    return PyModuleDef_Init(&spam_module);
}
setup.py
from setuptools import setup, Extension

extensions = [Extension('spam', sources=['spam.c'])]

setup(ext_modules=extensions)
setup.cfg
[metadata]
name = spam
version = 0.1.0

[options]
python_requires = >=3.5
setup_requires = pytest-runner
tests_requires = pytest

[aliases]
test=pytest
test_spam.py
import pytest

import spam


def test_import():
    assert spam.__name__ == 'spam'

2. モジュール構築

2.1. 前知識

Python/C API リファレンスマニュアル に必要なことは書いてあるので感謝しつつ読む。モジュールの完成例が見たくなったら標準ライブラリ。今回の limited API 環境でモジュールを書くにあたり直接参照できるのは xxlimited.c 。その他ももちろん参考になる。

「はじめに - オブジェクト、型および参照カウント」は必読。以下概要。

  • CPython におけるオブジェクトは C から見ると PyObject 構造体。
  • PyObject 構造体は参照カウントを持つ。これを増減するのはプログラマの責任。自動ではなされない。
    • 参照カウントが 0 になるとそのオブジェクトのメモリ解放関数が呼ばれる。メモリ開放処理が別のメモリ開放関数を呼び出すといった連鎖が起こることがあり、これによりメモリ解放の連鎖に巻き込まれたオブジェクトをまだ使えると誤解・誤操作することが起こりえる。どうしても消されたくないオブジェクトがあるときは事前に参照カウントを増やしておく。
    • Py_DECREF(PyObject *o) で減らし、 Py_INCREF(PyObject *o) で増やす。 NULL を渡すのは禁止。
    • Py_XDECREF(PyObject *o) で減らし、 Py_XINCREF(PyObject *o) で増やす。 X なしとの違いは NULL チェックをしてくれるところ。
    • Py_CLEAR(PyObject *o) で減らす。 Py_XDECREF との違いは変数を参照カウンタを減らす「前」に NULL にしてくれるところ。結果として Py_CLEAR 後に o は NULL となる。減らす前なのはメモリ解放処理が連鎖したときの事故に備えている。
    • PyObject * を返す API が成功したときの返り値の所有権を持っているかどうかは API 次第。ドキュメントに Return Value: New reference とあれば所有権あり、 Borrow reference とあれば所有権なし、借り物。日本語マニュアルでは前者を所有参照、後者を借用参照と訳している。
    • 所有参照は不要になったら参照カウントを減らさなければならない。借用参照はなにがあっても参照カウントを減らしてはならない。
    • 引数として渡された参照を盗むとされている API がごく少数存在する。将来なんらかのしくみで参照カウントが 1 減らされることが約束されるので減らし過ぎに注意。たとえば PyModule_AddObject でモジュールに追加されたオブジェクトの参照カウントはモジュールがメモリ解放されはじめたときに 1 減らされることとなる。当然ながら借り物をわたしてはならない、又貸し禁止。
  • PyObject * を返す API は失敗すると NULL を、 int を返す API は -1 を返す。
    • マニュアルに特記ある API 以外は。
    • 失敗したときにはなんらかの例外がセット済み。
    • 失敗していなくとも -1 が得られて区別がつかない場合がある API もある。こういったものに対しては PyErr_Occurred を併用する。 if (ret == -1 && PyErr_Occurred()) のようにする。
  • PyObject * を引数として受け取る API には NULL を渡しても良いものとだめなものがある。要マニュアル参照。
    • NULL を渡しても良い API の引数には失敗したとき NULL を返す API を直接書くことができる。たとえば PyModule_AddObject(module, "zero", PyLong_FronLong(0))

2.2. モジュールオブジェクトへのオブジェクトの追加

PyModuleDef の m_slots に PyModuleDef_Slot[] を登録し、 Py_mod_exec として関数を登録。関数には module オブジェクトが渡ってくるので編集する。成功したら 0 を、失敗したら module オブジェクトの参照カウントを減らして -1 を返す。 module オブジェクトへのオブジェクトの追加には専用の関数 が便利。モジュールオブジェクトもオブジェクトには違いないので汎用のオプジェクトプロトコルでの操作も可能。

static int spam_exec(PyObject *module) {
    if (PyModule_AddIntConstant(module, "one", 1)) { goto error; }
    if (PyModule_AddStringConstant(module, "s", "string")) { goto error; }
    if (PyModule_AddObject(module, "spam", Py_BuildValue("sss", "spam", "ham", "eggs"))) { goto error; }
    return 0;
error:
    Py_DECREF(module);
    return -1;
}


static PyModuleDef_Slot spam_slots[] = {
    {Py_mod_exec, spam_exec},
    {0, NULL}
};


static struct PyModuleDef spam_module = {
    PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_slots = spam_slots,
};

2.3. モジュールオブジェクトへの関数の追加

PyMethodDef[] を定義して PyModuleDef の m_methods に登録。もっとも簡単なのは次のようなもの。これで def spam(*args): return None 相当の関数ができる。PyMethodDef は名前、実装である C 関数への参照、フラグ、 docstring の 4 つのメンバからなる構造体。 (なお PyMethodDef はクラスをつくってメソッドを追加するときにも使う、登録先が異なるだけで作り方は似ている。そもモジュールの関数というものはモジュールクラスのインスタンスのメソッドでしかないので。)

C 関数の型は PyCFunction であり、その引数は PyObject * 2 つ。1つめにはモジュールオブジェクトが、 2 つめには引数を表す tuple が入ってくる。 (インスタンスメソッドならば 1 つめはインスタンスオブジェクト、クラスメソッドならばクラスオブジェクト、スタティックメソッドであれば NULL になるがこれは別の話) 。なんらかの処理をして PyObject * を返せばそれが Python 呼び出し側への戻り値となる。例外が設定された、もしくは設定したのであれば NULL を返さなくてはならない。

static PyObject *
spam_none(PyObject *self, PyObject *args) {
    // Py_INCREF(Py_None); return Py_None; 相当のマクロ
    // Py_None は Python からは None に見える PyObject へのポインタ
    Py_RETURN_NONE;
}


static PyMethodDef spam_methods[] = {
    {"none", spam_none,
     METH_VARARGS,
     "return None."},
    {NULL, NULL, 0, NULL}
};


static struct PyModuleDef spam_module = {
    PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_methods = spam_methods,
};

引数を解釈する

ここで第 2 引数 PyObject *args には tuple が入ってくるので PyArg_ParseTuple で処理する。第 2 引数のフォーマット文字列でこの関数がどのような引数なら受け付けるのかを指定する。フォーマット文字列に使える文字は PyObject * をそのまま借り受ける O の他、 C の int 型に変換を試みる i とか str, bytes, byte like object を Py_buffer に変換を試みる s* など多数用意されている。ただし limited API 環境だと buffer プロトコルが使用不可であり、 s*y* など可変 byte like object の抱えている生のメモリ領域に触れる手段は塞がれている。

実行時、フォーマットに一致しない引数が与えられた場合は PyArg_ParseTuple が例外をセットしつつ NULL を返してくるのでエラー処理をした後 NULL を返す。初手 PyArg_ParseTuple としたのであれば即 return NULL で問題ない。

static PyObject *
spam_echo(SpamObject *self, PyObject *args) {
    PyObject *o;

    if (!PyArg_ParseTuple(args, "O", &o)) {
        return NULL;
    }

    // 借り物なのでオウム返しするには所有権を持つ必要がある
    Py_INCREF(o);
    return o;
}

引数を一つも取らないのであれば PyArg_ParseTuple(args, "") を呼ぶか PyMethodDef のフラグとして METH_NOARGS を指定すればよい。引数が一つだけであるのであれば METH_O でも。詳しい説明およびその他のフラグは PyMethodDef にある。

static PyObject *
spam_echo(SpamObject *self, PyObject *arg) {
    Py_INCREF(arg);
    return arg;
}

spam_echo
static PyMethodDef spam_methods[] = {
    {"echo", spam_echo,
     METH_O},
    {NULL, NULL, 0, NULL}
}

初期値持ちのオプション引数を用意したいときはフォーマット文字列を | で区切る。これ以降はあってもなくてもよい引数として解釈され、与えられなかったときには対応する C 変数に変更は加えられないことになる。これで def add(i, j=2018): return i + j のような関数を作ることができる。

static PyObject *
spam_add(SpamObject *self, PyObject *args) {
    int i;
    int j = 2018;

    if (!PyArg_ParseTuple(args, "i|i", &i, &j)) {
        return NULL;
    }

    return PyLong_FromLong(i + j);
}

名前付き引数

いままでの関数の作り方には Pure python で関数を書いたときのと違いがある。キーワード引数に対応していないのだ。 add(1, 2) という呼び出し方はできても add(i=1, j=2) とすることはできない。これを可能にしたいのであれば。まず PyMethodDef のフラグに METH_VARARGS | METH_KEYWORDS を指定。次に PyCFunction の代わりに PyCFunctionWithKeywords を使う。 PyCFunction の 2 引数に加えて名前付き引数を保持した dict が加わる。これを PyArg_ParseTupleAndKeywords で処理する。最後に PyMethodDef には PyCFunction が必要なのでコンパイラに警告・エラーをだされないようキャストする。

static PyObject *
spam_add(SpamObject *self, PyObject *args, PyObject *kw) {
    int i;
    int j = 2018;
    static char *keywords[] = {"i", "j", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kw, "i|i", keywords, &i, &j)) {
        return NULL;
    }

    return PyLong_FromLong(i + j);
}

static PyMethodDef spam_methods[] = {
    {"add", (PyCFunction)spam_add,
     METH_VARARGS | METH_KEYWORDS},
    {NULL, NULL, 0, NULL}
};

2.4 モジュールオブジェクトに追加のメモリ領域をもたせる

PyModuleDef の m_size に確保したいメモリ領域のサイズを指定するとそれだけのメモリがモジュールオブジェクトごとに与えられる。アクセスするには PyModule_GetState を使う。 PyObject * を保持させることも可能だけれどもメモリリークしないようモジュールのメモリ解放処理であわせて開放されるよう準備しておく必要がある。 PEP 3121 に実装例あり。 PyModule_AddObject でモジュールの属性辞書に持たせるのと比べると辞書を介さずポインタ演算だけで所定のメモリにたどり着けるので加速はする。手間に見合うかどうかは用途による。

#define Py_LIMITED_API 0x03050000
#define PY_SSIZE_T_CLEAN
#include <Python.h>


typedef struct {
    int year;
    int month;
    int day;
} SpamState;


static PyObject *
spam_use_state_sample(PyObject *self, PyObject *args) {
    SpamState *s = PyModule_GetState(self);
    if (s == NULL) { return NULL; }

    return Py_BuildValue("iii", s->year, s->month, s->day);
}


static int spam_exec(PyObject *module) {
    SpamState *s = PyModule_GetState(module);
    if (s == NULL) {
        Py_DECREF(module);
        return -1;
    }
    s->year = 2018;
    s->month = 12;
    s->day = 3;
    return 0;
}


static PyModuleDef_Slot spam_slots[] = {
    {Py_mod_exec, spam_exec},
    {0, NULL}
};


static PyMethodDef spam_methods[] = {
    {"use_state_sample", spam_use_state_sample,
     METH_NOARGS},
    {NULL, NULL, 0, NULL}
};


static struct PyModuleDef spam_module = {
    PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_slots = spam_slots,
    .m_methods = spam_methods,
    .m_size = sizeof(SpamState),
};


PyMODINIT_FUNC PyInit_spam(void) {
    return PyModuleDef_Init(&spam_module);
}

3. Pure Python でよくあるあれこれ、 C だとどう書く?

Python だと数タイプで済むものが数行のコードになったり。

3.1. 基本の型のインスタンス作成

具象オブジェクトレイヤ にある型にはオブジェクトを得るための API が用意されているのでそれを用いる。もしくは Py_BuildValue を使う。後者は複数の基本型を組み合わせた tuple, list, dict を作るときに便利。

// 1
PyLong_FromLong(1);
// 2
Py_BuildValue("i", 2);
// ('spam', 'ham', 'eggs')
Py_BuildValue("sss", "spam", "ham", "eggs");
// {1: 'one', 2: 'two', 3: 'three'}
Py_BuildValue("{isisis}", 1, "one", 2, "two", 3, "three");

これに限った話ではないが。すべての API は失敗しうるので使用時には NULL が返ってきていないかのエラーチェックが必要。

    PyObject *one;
    if (!(one = PyLong_FromLong(1))) { return NULL; }

3.2. 演算子など

抽象オブジェクトレイヤ を参照。

// a.b
PyObject_GetAttrString(a, "b");

// a.b = c
PyObject_SetAttrString(a, "b", c);

// a[b]
PyObject_GetItem(a, b);

// -a
PyNumber_Negative(a);

// a + b
PyNumber_Add(a, b);

// a == b
PyObject_RichCompare(a, b, Py_EQ);
// Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE が
// <, <=, ==, !=, >, >= に対応

// len(a)
PyObject_Size(a);

// isinstance(a, b)
PyObject_IsInstance(a, b);

// repr(a)
PyObject_Repr(a);

// a()
PyObject_CallFunctionObjArgs(a, NULL);

関数呼び出しは亜種がたくさんあるので PyObject_Call など似た名前の API を確認。

3.3. for 文、イテレータの操作

イテレータプロトコル の説明が簡潔かつ完璧すぎてこのコードを引用もといコピペですんでしまう。

PyObject *iterator = PyObject_GetIter(obj);
PyObject *item;

if (iterator == NULL) {
    /* propagate error */
}

while (item = PyIter_Next(iterator)) {
    /* do something with item */
    ...
    /* release reference when done */
    Py_DECREF(item);
}

Py_DECREF(iterator);

if (PyErr_Occurred()) {
    /* propagate error */
}
else {
    /* continue doing useful work */
}

3.4 組み込み関数へのアクセス

リフレクション 参照。 PyEval_GetBuiltins は借用参照を、 PyMapping_GetItemString や PyObject_GetItem は所有参照を返してくるので後始末の Py_DECREF は後者にだけかける。

PyObject *builtins;
if (!(builtins = PyEval_GetBuiltins())) { return NULL; }
PyObject *printfunc;
if (!(printfunc = PyMapping_GetItemString(builtins, "print"))) { return NULL; }

// 使用

Py_DECREF(printfunc);

3.5. import

sys モジュールは専用の PySys_GetObject が存在。

PySys_GetObject("version_info");

sys 以外は モジュールのインポート を参照。 PyImport_ImportModule で概ね事足りる。 PyImport_ImportModuleLevelObject の説明が組み込み関数 __import__ にリダイレクトされているのが興味深い。

PyImport_ImportModule("itertools");

3.6. モジュール変数へのアクセス

モジュール関数の第 1 引数 self はモジュールオブジェクト。なのでこれの属性をとるだけ。 PyObject_GetAttrString を使う。

static PyObject *
spam_spam(PyObject *self, PyObject *args) {
    return PyObject_GetAttrString(self, "spam");
}

3.7 例外のセット

def raise_error(): raise ValueError("spam") のようなものを作るには。例外の送出 を参照。一番手っ取り早いのは PyErr_SetString 。 C から組み込み例外は PyExc_ を頭につけた変数名で見える。一覧は標準例外を参照。


static PyObject *
spam_raise_error(PyObject *self, PyObject *args) {
    PyErr_SetString(PyExc_ValueError, "spam");
    return NULL;
}

4. クラスの作成

4.1. とにかく作ってみる

クラスの作り方もモジュール同様にいくつか方法がある。シングルトンな PyTypeObject を static に確保する方法はチュートリアルにある。今回は limited API 環境ゆえに PyTypeObject のメンバが隠されていてこの方法はとれないので、型定義 PyType_Spec から動的に作る PyType_FromSpec を使う方法を取る。詳しくは PEP 384 Type Objects を参照。 PyTypeObject のメンバは似た名前の slot id を用いて PyType_Slot として設定することで間接に設定できる。例えば Py_tp_iter を設定すれば tp_iter に iter(o) された時に呼び出される関数を登録できる。 PEP にできないと書かれている tp_dict, tp_mro, tp_cache, tp_subclasses, tp_weaklist, tp_print, tp_weaklistoffset, tp_dictoffset は limited API 環境では使用不可。

tp_dictoffset が使用不可ということは、これでつくられたインスタンスは __dict__ を持たず、デフォルトでは実行時に任意の属性を追加することができない。どうしても必要であれば xxlimited モジュールの Xxo クラスの Xxo_getattroXxo_setattr のように自力でデスクリプタを書く。もしくは type を 3 引数で呼び出してクラスオブジェクトを作る。ここまで来ると C で書く意義が怪しくなる。 C だけで書ききらず Python で作りかけのクラスを継承して完成させるほうが無難。

#define Py_LIMITED_API 0x03050000
#define PY_SSIZE_T_CLEAN
#include <Python.h>


typedef struct {
    PyObject_HEAD
} SpamObject;


static PyType_Slot SpamType_slots[] = {
    {0, 0},
};


static PyType_Spec SpamType_spec = {
    .name = "spam.Spam",
    .basicsize = sizeof(SpamObject),
    .flags = Py_TPFLAGS_DEFAULT,
    .slots = SpamType_slots,
};


static int spam_exec(PyObject *module) {
    PyObject *spam_type;

    spam_type = PyType_FromSpec(&SpamType_spec);
    if (!spam_type) {
        Py_DECREF(module);
        return -1;
    }
    if (PyModule_AddObject(module, "Spam", spam_type)) {
        Py_DECREF(spam_type);
        Py_DECREF(module);
        return -1;
    }

    return 0;
}


static PyModuleDef_Slot spam_slots[] = {
    {Py_mod_exec, spam_exec},
    {0, NULL}
};


static struct PyModuleDef spam_module = {
    PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_slots = spam_slots,
};


PyMODINIT_FUNC PyInit_spam(void) {
    return PyModuleDef_Init(&spam_module);
}

4.2 メソッドの追加

メソッドを追加するには PyMethodDef[] を tp_methods に設定する。モジュールに関数を書くのとほぼ同じ。第 1 引数 self がこのクラス由来であることは明らかなのでキャストする手間が面倒ならクラス用の構造体型で宣言してしまって構わない。ただし PyCFunction ではなくなってしまうので PyMethodDef に登録するときにキャストがいる。フラグに METH_CLASS を設定すればクラスメソッドを作ることができる。 __init__ は tp_method ではなく tp_init で設定する。

#include <structmember.h>

typedef struct {
    PyObject_HEAD
    long value;
} SpamObject;


int
Spam_init(SpamObject *self, PyObject *args) {
    long v;
    if (!PyArg_ParseTuple(args, "l", &v)) {
        return -1;
    }
    self->value = v;

    return 0;
}


static PyObject *
Spam_get_value(SpamObject *self, PyObject *args) {
    return PyLong_FromLong(self->value);
}


static PyObject *
Spam_set_value(SpamObject *self, PyObject *args) {
    if (Spam_init(self, args)) { return NULL; }

    Py_RETURN_NONE;
}


static PyMethodDef Spam_methods[] = {
    {"get_value", (PyCFunction)Spam_get_value,
     METH_NOARGS},
    {"set_value", (PyCFunction)Spam_set_value,
     METH_VARARGS},
    {NULL, NULL, 0, NULL}
};


static PyType_Slot SpamType_slots[] = {
    {Py_tp_methods, Spam_methods},
    {Py_tp_init, (initproc)Spam_init},
    {0, 0},
};

4.3 インスタンス属性の追加

属性を足すには tp_members を設定する。内容は PyMemberDef[] 。

PyMemberDef のために structmember.h ヘッダのインクルードがいる。忘れずに。

#include <structmember.h>


typedef struct {
    PyObject_HEAD
    long x;
    long y;
} SpamObject;


int
Spam_init(SpamObject *self, PyObject *args) {
    long x, y;
    if (!PyArg_ParseTuple(args, "ll", &x, &y)) {
        return -1;
    }
    self->x = x;
    self->y = y;

    return 0;
}


static PyMemberDef Spam_members[] = {
    {"x", T_LONG, offsetof(SpamObject, x), READONLY},
    {"y", T_LONG, offsetof(SpamObject, y), 0},
    {NULL}
};


static PyType_Slot SpamType_slots[] = {
    {Py_tp_init, (initproc)Spam_init},
    {Py_tp_members, Spam_members},
    {0, 0},
};

property のようになんらかの処理を入れるには tp_getsetPyGetsetDef を設定する。

static PyObject *
Spam_get_point(SpamObject *self, void *Py_UNUSED(ignored)) {
    return Py_BuildValue("ll", self->x, self->y);
}


int Spam_set_point(SpamObject *self, PyObject *arg, void *Py_UNUSED(ignored)) {
    long v[2];
    PyObject *o;

    for (int i = 0; i < 2; ++i) {
        if (!(o = PySequence_GetItem(arg, i))) {
            return -1;
        }
        if (!PyLong_Check(o)) {
            Py_DECREF(o);
            PyErr_SetString(PyExc_TypeError, "int required");
            return -1;
        }
        v[i] = PyLong_AsLong(o);
        Py_DECREF(o);
        if (v[i] == -1 && PyErr_Occurred()) {
            return -1;
        }
    }

    self->x = v[0];
    self->y = v[1];

    return 0;
}


static PyGetSetDef Spam_getset[] = {
    {"point", (getter)Spam_get_point, (setter)Spam_set_point},
    {NULL}
};


static PyType_Slot SpamType_slots[] = {
    {Py_tp_init, (initproc)Spam_init},
    {Py_tp_members, Spam_members},
    {Py_tp_getset, Spam_getset},
    {0, 0},
};

5. 終わり

とりあえずここまで。 async 関連など C から扱ったことがないものもまだまだあるので、年末にはこれらに触れてみたいと思っているところです。では、よい Python/C API ライフ(?)を。

27
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
21