CPythonをC言語で拡張(2): Pythonの型を増やす

前回に引き続き、今回はstr型やlist型のようなCPythonの「型」をC言語で拡張する。

以下を参照してほしい:

概要

基本的に定義するものは以下のようになる:

CPythonの型をC言語で拡張するときのコンポーネント

(ただし、__newなどのアンダーバーは新しい型名などで書く。例えばNewType_newなど。)

左側は前回定義したModuleと同じだ。さらに型を定義するには、図右側のType関連の関数やテーブル:

  1. PyMemberDef: 外側からアクセスできるメンバ変数のテーブル
  2. ライフサイクルの管理用の関数(__dealloc, __new, __init

を追加すれば良い。(詳細は上記チュートリアルを参考にしてほしい。)

コンパイル

setuptoolsにC言語の拡張を扱う機能がある。以下の2つのファイルを書くとpip install -e .だけでC拡張をインストールできるようになる。(これは実は前回にも使える。)

以下は上記チュートリアルのすべてのファイルをコンパイルする場合の処理: setup.py:

from setuptools import Extension, setup
setup(ext_modules=[
  Extension("custom", ["custom.c"]),    # Pure type
  Extension("custom2", ["custom2.c"]),  # Type + its operators
  Extension("custom3", ["custom3.c"]),  # Adding finer control of class variables
  Extension("custom4", ["custom4.c"]),  # GC
  Extension("sublist", ["custom5.c"])   # Inheritence of List
])

pyproject.toml:

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "custom"
version = "1"

Gabage Collection (GC)

ライフサイクルは循環参照も考慮した Cyclic Gabage Collection を使うこともできる。そのために必要なのは以下の3つを追加、書き換えが必要になる。

static int __traverse(CustomObject *self, visitproc visit, void *arg);
static int __clear(CustomObject *self)
static PyObject *Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)

基本的には依存関係をtraverseに伝えるだけにする、ルーチン的な処理ですむ。後でIRのグラフの探索ができるようにするようだ。

CPythonをC言語で拡張(1): モジュールを作る

PythonからC言語で書いた関数を呼び出すには、CPythonにCのコードを追加する。 具体的には以下の公式記事を参考にすればいい:

しかし、微妙に詰まった点があったのでここでは具体的なステップで拡張の書き方を紹介する。

1. 関数を書く

まずは関数の中身を書く

// 以下の2行は必ず必要
#define PY_SSIZE_T_CLEAN
#include <Python.h>


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

  if (!PyArg_ParseTuple(args, "s", &command))
    return NULL;
  sts = system(command);
  return PyLong_FromLong(sts);
}

この部分は、PythonAPIを使うこと以外は比較的素直に書ける。

2. 関数を登録情報を書く

インタプリタがアクセスできるように、関数の情報を"メソッドテーブル"に書く必要がある。

static PyMethodDef SpamMethods[] = {
  // METH_VARARGS: C関数が使う呼び出し規約をインタプリタに教える
  // METH_VAARGS もしくは METH_VAARGS | METH_KEYWORDS
  {"system", spam_system, METH_VARARGS,
    "Execute a shell command."},
  {NULL, NULL, 0, NULL}
};

基本的には、名前、関数、呼び出し規約、説明(任意) を入れれば良い。 最後の行は番兵として必要なだけらしい。

3. モジュール定義の構造体を作る

以下のようにモジュール定義用の構造体を作り、メソッドテーブルを参照させる:

static struct PyModuleDef spammodule = {
  PyModuleDef_HEAD_INIT,
  "spam",
  NULL,
  -1,
  SpamMethods
};

4. Pythonインタプリタの初期化時にモジュールを登録する

インタプリタの初期化時に、モジュールを登録する。

PyMODINIT_FUNC PyInit_spam(void)
{
  return PyModule_Create(&spammodule);
}

上記までのプログラムをspammodule.cのファイルとして保存しておく。

5. CPythonのコンパイルとリンク

ここがさらにトリッキーで、CPythonのModule/ディレクトリに上記のプログラムを入れる必要がある。 具体的には、以下のようにコンパイルする:

git clone --depth 1 https://github.com/python/cpython.git
cp spammodule.c  cpython/Modules/
vim Modules/setup.local

setup.localは以下のような新しいモジュールの説明を書く:

spam spammodule.o

このファイルはPythonコンパイル時に自動で見つけてくれる。

そしてコンパイルする:

cd cpython
./configure --prefix=`pwd`/built
make -j 4
make install

6. 新しいモジュールを試す

ここまで行けば、あとは単純に試せる

$ cpython/built/bin/python3
Python 3.14.0a0 on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import spam
>>> spam.system("echo hi")
hi
0

終わり

PythonからC言語で書いたmoduleを呼び出せた。もっと高度なAPIは以下に書いてある。

Python/C API リファレンスマニュアル — Python 3.12.5 ドキュメント