3行で
- 普段はpythonを使うが、やむにやまれぬ事情でC言語で書かれたリソースを使う必要はまま生じる
- pythonはCを使って新たなモジュールを追加することができるので、その機能を使えばいい
- Python.hに用意された特別な構造体を使ってCでモジュール名、メソッド、引数を定義し、distutils.core.setup(pythonのメソッド!!)を使ってビルドする
必要な作業
- C言語で書かれたソースコードのベースを用意
- C言語でラッパーを記述
- pythonでsetupスクリプトを記述
- setupスクリプトを実行
細かいことはいいから答えを教えろ
ソースコードと使い方だけ書いて下記のリポジトリにアップロードしときました。
https://github.com/nagiton/c-python-api
目標
自作のモジュールhelloをC言語で作成し、下記のような出力を実現する。
sh-4.2$ python
Python 3.6.10 |Anaconda, Inc.| (default, Mar 23 2020, 23:13:11)
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>> hello.add(2,3)
5
>>> hello.out('tokyo','koike')
こんにちは、私は tokyo の koike です。
>>>
C言語で書かれたソースコードのベースを用意
今回ではシンプルなものだけを使います。
まずはシンプルに始めるのがよいかと。
// hello.c
int add(int x, int y)
{
return x + y;
}
void out(const char* adrs, const char* name)
{
printf("こんにちは、私は %s の %s です。\n", adrs, name);
}
2つの関数を定義しているだけのソースコードです。
- add: 入力される2つのint型の数値の和を返します。戻り値はintです。
- out: 入力される2つの文字列adrs、nameをとり、標準出力に「こんにちは、私は(adrsの中身)の(nameの中身)です。」を表示します。戻り値なし。
char*
ってなんだよこれだからC言語はよお!💢💢💢💢💢という人は
→10-3.ポインタと文字列
C言語でラッパーを記述
まず、ソースコード例を示し、順に解説します。
// helloWrapper.c
#include "Python.h"
extern int add(int, int);
extern void out(const char*, const char*);
//definition of add method
static PyObject* hello_add(PyObject* self, PyObject* args)
{
int x, y, g;
if (!PyArg_ParseTuple(args, "ii", &x, &y))
return NULL;
g = add(x, y);
return Py_BuildValue("i", g);
}
//definition of out method
static PyObject* hello_out(PyObject* self, PyObject* args, PyObject* kw)
{
const char* adrs = NULL;
const char* name = NULL;
static char* argnames[] = {"adrs", "name", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kw, "|ss",
argnames, &adrs, &name))
return NULL;
out(adrs, name);
return Py_BuildValue("");
}
//definition of all methods of my module
static PyMethodDef hellomethods[] = {
{"add", hello_add, METH_VARARGS},
{"out", hello_out, METH_VARARGS | METH_KEYWORDS},
{NULL},
};
// hello module definition struct
static struct PyModuleDef hello = {
PyModuleDef_HEAD_INIT,
"hello",
"Python3 C API Module(Sample 1)",
-1,
hellomethods
};
//module creator
PyMODINIT_FUNC PyInit_hello(void)
{
return PyModule_Create(&hello);
}
ラッパーで必要な処理は大きく分けて、以下のようなブロックに分かれます
- Python.hのinclude
- pythonで使いたい外部関数の宣言
- メソッドの定義
- pythonオブジェクトを入出力にもつ関数の宣言
- pythonオブジェクトからC言語の変数に変換
- 処理
- C言語の変数からpythonオブジェクトに変換して出力
- モジュールに入れる全てのメソッドのリストアップ
- モジュールの作成
include
必ず最初にPython.h
をインクルードします。公式によると
注釈 Python は、システムによっては標準ヘッダの定義に影響するようなプリプロセッサ定義を行っているので、 Python.h をいずれの標準ヘッダよりも前にインクルード せねばなりません 。
Python.h をインクルードする前に、常に PY_SSIZE_T_CLEAN を定義することが推奨されます。 このマクロの解説については 拡張モジュール関数でのパラメタ展開 を参照してください。
https://docs.python.org/ja/3/extending/extending.html#a-simple-example
とのことです。
extern hogehoge
helloWrapper.cの外で定義された関数を使うときにこのように表現します。
global
/private
の関係のようなものがextern
/static
にあります。
(実はextern
はつけてもつけなくても勝手にextern
になるようです。これだからC言語はよお💢)
hogehoge部分はhello.cの関数の宣言と同じですね。
メソッドの定義
大まかに言って、処理の流れは
-
PyObject
型(C言語でpythonのオブジェクトを扱う際の型)で引数を受け取り、PyObject
型で返す関数を宣言する -
PyObject
型からC言語で扱える型にPyArg_ParseTuple
などで変換する - C言語で扱える型を使って処理を行う
-
Py_BuildValue
でPyObject
型に戻す
といった感じになります。
関数定義
static PyObject* 関数名
で、PyObject
型の参照を返す関数であることを宣言しています。PyObject
とは
全てのオブジェクト型はこの型を拡張したものです。 この型には、あるオブジェクトを指すポインタをオブジェクトとして Python から扱うのに必要な情報が入っています。 通常の 「リリース」 ビルドでは、この構造体にはオブジェクトの参照カウントとオブジェクトに対応する型オブジェクトだけが入っています。 実際には PyObject であることは宣言されていませんが、全ての Python オブジェクトへのポインタは PyObject* へキャストできます。 メンバにアクセスするには Py_REFCNT マクロと Py_TYPE マクロを使わなければなりません。
https://docs.python.org/ja/3.5/c-api/structures.html#c.PyObject
ということで、C言語内でpythonに渡すオブジェクトはみんなこの型を拡張した型を使います。
ここで定義した関数の中でPyObject
型の構造体を作り、その構造体への参照を最終的に外に渡すわけですね。参照渡しです。
hello_add(PyObject* self, PyObject* args)
なので、中身も参照渡しです。
self
はpythonから実行する時のモジュール自身、args
はメソッドに渡される位置引数を表します。
(参考: 用語集)
関数の処理とpythonオブジェクトからCの変数への変換
int x, y, g;
if (!PyArg_ParseTuple(args, "ii", &x, &y))
return NULL;
g = add(x, y);
if
文以外は、C言語の入門まででもやったことがあれば見慣れた感じです。
if
でやっていることはpythonから入力される変数の型を調べ、可能ならCで扱う変数に変換し、不可能なら例外を吐くという処理です。
pythonから値を受け取るときはPyArg_ParseTuple
でC言語の変数に変換できます。
(参考:引数の解釈と値の構築)
python上でhello.add(2,3)
のように渡される引数が文字列'ii'
、つまり2つのint型と解釈できるとき、x
y
に代入します。
その後関数add
に渡して答えg
を得ます。
Cの型からpythonのオブジェクトに戻して出力
return Py_BuildValue("i", g);
Py_BuildValue
でC言語の変数であるg
をpythonのオブジェクトPyObject
に変換して返しています。
int
型なので'i'
を指定していますが、型に合った文字列を指定してください。
別の例
hello_out
はキーワード変数を使う例になっています。
PyArg_ParseTuple
の代わりにPyArg_ParseTupleAndKeywords
を使うなどの違いがありますが
モジュールに入れる全てのメソッドのリストアップ
//definition of all methods of my module
static PyMethodDef hellomethods[] = {
{"add", hello_add, METH_VARARGS},
{"out", hello_out, METH_VARARGS | METH_KEYWORDS},
{NULL},
};
ここでモジュールに入れる全てのメソッドをリストアップして、そのインターフェイスをPyMethodDef
型で記述します。
構造体に値を入れるときは定義された順番と同じ順番で並べる必要があります。
PyMethodDef
上記によれば
メソッド名、C 実装へのポインタ、呼び出しをどのように行うかを示すフラグビット、docstring の内容を指すポインタ
の順で並べておけばいいみたいですね。
最後のNULLは・・・・、ごめんなさい(これだからC言語はよお💢)
モジュールの作成
// myModule definition struct
static struct PyModuleDef hello = {
PyModuleDef_HEAD_INIT,
"hello",
"Python3 C API Module(Sample 1)",
-1,
hellomethods
};
//module creator
PyMODINIT_FUNC PyInit_hello(void)
{
return PyModule_Create(&hello);
}
PyMethodDef
型でモジュールの情報を格納します。
構造体に値を入れるときは定義された順番と同じ順番で並べる必要があるのでドキュメントを確認すると
PyModuleDef
PyModuleDef_HEAD_INIT、モジュール名、Docstring、モジュールの状態、PyMethodDef
型で定義したメソッド
の順で入れておけばOKです。
そして最終的にPyMODINIT_FUNC
型を返す関数を作ります。
PyModule_Create
ここは単にPyMethodDef
型で定義されたモジュールの定義を使って実際にモジュールを作る関数を叩けばOKです。
pythonでsetupスクリプトを記述
pythonを普段使っていると、makefileを見るたびに蕁麻疹がでます・・・。
ここで書いたラッパーをgccでコンパイルすることも・・・できなかないですが・・・
私はあいつが嫌いなので、違う方法をとります。
幸いに、pythonにはモジュール開発者のためにDistutilsというものがあります!!!!
from distutils.core import setup, Extension
module1 = Extension('hello',
sources = ['helloWrapper.c','hello.c'],
#extra_objects = ['hello.o'],
)
setup(name = 'hello', version = '1.0.0', ext_modules = [module1])
細かく説明しなくても、pythonならわかるよね!!!!!やっぱりpythonなんだよなあ。この実家のような安心感よ。
ちょっとだけ補足しておくと、.cファイルが複数に渡る場合、sourcesでリストで指定してください。作業ディレクトリからの相対パスでも絶対パスでもOKです。
もしあなたがそうしたければ、gccかなにかで作った中間生成物の.o(オブジェクトファイル)を使うこともできます。
この場合はextra_objectsに指定してください。
他のオプションはclass distutils.core.Extension参照のこと。
makefileでやってることはここで指定できるのでは?(makefileに詳しくない)
setupスクリプトを実行
hello.c、helloWrapper.c、setup.pyがあるディレクトリに移動し、
python setup.py install
するとpythonでhelloがimportできるようになります。
やったね!!!
参考
C や C++ による Python の拡張(公式)
setup スクリプトを書く(公式)
API リファレンス(distutils)
【Python C API入門】C/C++で拡張モジュール作ってPythonから呼ぶ -前編-
PythonからCプログラムを呼び出す
本記事のソースコード
https://github.com/nagiton/c-python-api