Cython(9) Pythonの拡張モジュールをCで作る方法


2022年 09月 22日

Cython(5)で、Cの関数をPythonから呼び出して円周率を計算した。ただし、このとき、ctypesというPythonのための外部関数ライブラリを用いた。

今回は、C言語だけで、Pythonから普通に呼び出せるPythonnモジュールをつくり、その中にPythonの関数として動く関数を作ってみよう。
はやい話が、「C/C++でPythonの拡張モジュールを作る」ことを紹介する。

C/C++でPythonの拡張モジュールを書けるようになる必要はかならずしも無いが、ここで紹介するサンプルコードを眺めることにより、Pythonの内側の仕掛けがどうなっているかを少し理解できるようになると思う。文法だけを勉強してPythonを使っているのと、Pythonの内側の仕掛けを少しでも知っているのとでは、プログラミング言語の理解という点では大差がある。プログラミング言語自体を作成したり修正したりする可能性は少ないと思うが、このあたりが分かると、プログラミングの理解が一段と深まる。

ところで、とても参考になる、例も多いWebページがあったので紹介しよう。

「はやぶさの技術ノート」の中の、【Python C API入門】の中に、今回説明しようとしていることがしっかり描かれていたので、参考にしよう。
  【Python C API入門】C/C++で拡張モジュール作ってPythonから呼ぶ -前編-
  【Python C API入門】C/C++で拡張モジュール作ってPythonから呼ぶ -後編-

細かい説明は「はやぶさの技術ノート」を見ていただくことにして、まずCのソース全体を見てみよう。Pythonのモジュールとするために、色々なことが書かれているが、ざっくりと見ていこう。

// primes_array_c.c

#include <Python.h>
#include <stdio.h>

// Pythonから呼べる関数
static PyObject* c_primes(PyObject* self, PyObject* args) {
	int p_num;
	int* p_array;
	int p_len = 0;
	int n = 2;
	int idx, p;
	PyObject* c_list;

	// 引数の処理
	if (!PyArg_ParseTuple(args, "i", &p_num)){
    	return NULL;
	}

	p_array = (int*)malloc(sizeof(int) * p_num);    // 素数配列の確保
	if( p_array == NULL ) {
    	return NULL;
	}
    
	// 素数の計算の本体
	while( p_len < p_num ) {
    	for( idx=0; idx < p_len; ++idx ) {
        	p = p_array[idx];
        	if( n % p == 0 )
            	break;
    	}
    	if( idx >= p_len ) {
        	p_array[p_len++] = n;
    	}
      	++n;
	}

	// リストを確保し、配列の内容をリストにコピー
	c_list = PyList_New(p_num);
	for( idx=0; idx<p_num; ++idx ) {
    	PyList_SET_ITEM(c_list, idx, PyLong_FromLong(p_array[idx]));
	}

	free(p_array);   				 // 素数配列の解放

	return c_list;
}

// モジュールに関数を登録
static PyMethodDef primesMethods[] = {
	{ "primes", c_primes, METH_VARARGS, "make prime list"},
	{ NULL }
};

// モジュールの定義構造体への設定
static struct PyModuleDef primeModule = {
	PyModuleDef_HEAD_INIT,
	"primeModule",
	"Python3 C API Module(make prime list)",
	-1,
	primesMethods
};

// モジュールの初期化
PyMODINIT_FUNC PyInit_primeModule(void)
{
	return PyModule_Create(&primeModule);
}

関数の宣言は、以下のようになっていて、引数の型も、戻り値の型も、全部PyObjectへのポインタになっている。

static PyObject* c_primes(PyObject* self, PyObject* args) 

引数は、PyObjectへのポインタを通して、PyArg_ParseTuple()にてローカル変数に取り出している。

配列は、mallocを使うことでサイズは自由に指定できる。なお、最後でちゃんとメモリを開放するのを忘れないように。

素数の計算自体についてはコメントの必要はないだろう。
配列に素数を求めた後、この関数は素数のリストを返すので、リストを確保してから、配列の中身をリストにコピーしている。
最後に、mallocで確保したメモリを開放してから、最後にリストを返り値としている。

その他の関数については説明を省略する。

これをコンパイルするのも、次のようなsetupを用意するだけでできる。
Cのファイル名と、Pythonのモジュール名だけを指定している。

##  compile command:
##  	python3 prime_array_setup.py build_ext --inplace

from distutils.core import setup, Extension

setup(name = 'primeModule',
  	version = '1.0.0',
  	ext_modules = [ Extension('primeModule', ['primes_array_c.c']) ]
)

これでコンパイルすると、シェアード・オブジェクトprimes_array.cpython-38-x86_64-linux-gnu.soができる。

実行は、Pythonのモジュールの関数を呼び出すのと同じようにできる。

In [1]: import primes_array

In [2]: primes_array.primes(10) 
Out[2]: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

In [3]: primes_array.primes(10000)[-10:] 
Out[3]:
[104677,
 104681,
 104683,
 104693,
 104701,
 104707,
 104711,
 104717,
 104723,
 104729]

In [4]: timeit(primes_array.primes(10000)) 
188 ms ± 658 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Cython版と比較して、約5ミリ秒、3%弱ほど速くなっているようだが、速度差を感じるほどではない。
つまり、CythonはCとほぼ同等の性能が出ると言えるだろう。