コンテンツにスキップ

Top

自作のC言語の関数をPythonから呼び出す方法

C言語でいろいろ作った関数をPythonで使いたい!

Python用に関数を書き換えるの嫌!

というあなた。

実はそのCの関数、Pythonでも使えますよ。

Pythonで使えるCの共有ライブラリを作成する

どういう仕組みなのか知らんけどPythonは一定のルールに従って作成したC言語の.so(共有ライブラリ)を普通にimportできる。

細かい仕様とかは知らんし、動けば別にいいので、以下のC言語の関数をPythonでimportできる共有ライブラリにする手順を述べる。

以下は呼び出したら"Hello World!"と言ってくれるお決まりのやつである。

hello_world.c

#include <stdio.h>

void hello_world() { 
    printf("Hello World!\n");
}

当然このままだとどうしようもないのでPythonから呼び出せるようなラッパーとなるコードを作成する。

c_wrapper.c

#include "Python.h"

extern void hello_world(); // hello_world.cのhello_world関数をexternする

PyObject* hello_world_wrapper(PyObject* self, PyObject* args) {
    hello_world(); // hello_world.cのhello_world関数を呼ぶ
    return Py_BuildValue(""); // 戻り値がないときも""をセットしないといけない。
}

// メソッドの定義。第一引数で書いた名前がpythonで使うときの名前。cの関数名と一致させる必要はなく、hello_worldじゃなくでunkoとかでもいい   
// 第二引数はラッパー関数。(PyCFunction)で型をキャストしないと以下のようなwarningでるので注意。  
//     hello_wrapper.c:31:13: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
// 第三引数はお作法。引数の種類についての定義。今回のように引数無しの場合はNULLでも良いのかもしれん。METH_VARARGSだとPython側ではタプルで渡す感じになる(今回はないので空) 
static PyMethodDef c_methods[] = { 
    {"hello_world", (PyCFunction)hello_world_wrapper, METH_VARARGS},
    {NULL}
};

// PyModule_Createにぶっこむ引数の構造体。上のほうはお作法。一番最後に上記で作成したメソッドの定義を入れる。
static struct PyModuleDef c_modules =
{
    PyModuleDef_HEAD_INIT, // 決めごと
    "c_modules", // なんか名前。多分空文字でも行ける。
    "", // しらん
    -1, // しらん
    c_methods // 上記で定義した関数定義
};

// pythonが共有ライブラリをimportしたときに最初に呼ばれる関数
// 関数名は PyInit_ + 共有ライブラリ名 + () でないといけないので注意すること!  
PyMODINIT_FUNC PyInit_c_modules() {
    return PyModule_Create(&c_modules);
}

できた2つのソースコードを元に共有ライブラリをつくる。

$ rm *.o *.so
rm: '*.o' を削除できません: そのようなファイルやディレクトリはありません
rm: '*.so' を削除できません: そのようなファイルやディレクトリはありません
$ gcc -fPIC -o hello_world.o -c hello_world.c
$ gcc -fPIC -Wall -c -o c_wrapper.o c_wrapper.c $(python3-config --cflags --ldflags)
$ gcc -fPIC -Wall -shared -o c_modules.so hello_world.o c_wrapper.o $(python3-config --cflags --ldflags)
$ ls
c_modules.so  hello_world.c  hello_world.o  c_wrapper.c  c_wrapper.o

で、本当にPythonから呼び出せるか確認。なおpython3での確認となる。今時python2使う人もあれだろうし。

$ python3
Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import c_modules
>>> c_modules.hello_world()
Hello, world!
>>> 

呼び出せた!!

ラッパーコードで大事なのは、一番最後の PyMODINIT_FUNC PyInit_c_modules() の関数名。

python側はimportするとき共有ライブラリ名を指定する。

そして、importのタイミングで、PyInit_ + 共有ライブラリ名() の関数をコールする。

具体的には上記の場合、 c_modules.so という名前の共有ライブラリだったら、

import c_modules

でimportし、その時に、 PyInit_c_modules() が呼び出される、ということ。

なので、ここの名前が一致していないと実行時にエラーになる。から合わせること!

呼び出したい関数に引数を渡したり結果を受け取る必要がある場合

先ほどのHello, World!は標準出力しただけで、引数も渡せなければ値も返ってきてない。
ので引数を2つ渡して足し算をした結果を返却する関数の場合どうなるかを例示する。
(当然、先ほど作成したhello_world.c内にadd関数をaddして良いのだが別ファイルにしてみた)

add.c

#include <stdio.h>

int add(int a, int b) { 
    return a + b;
}

c_wrapper.c

#include "Python.h"

extern void hello_world();
extern int  add(int, int); // 追加

PyObject* hello_world_wrapper(PyObject* self, PyObject* args) {
    hello_world();
    return Py_BuildValue("");
}

PyObject* add_wrapper(PyObject* self, PyObject* args) {
    int a   = 0;
    int b   = 0;
    int ret = 0;

    if (!PyArg_ParseTuple(args, "ii", &a, &b)) { // 引数の型がintとintなので"ii"と指定。文字列ならs。このあたりは仕様確認すること
        return NULL;
    }
    ret = add(a, b); 
    return Py_BuildValue("i", ret); // ここも戻り値の型はintなので"i"
}

static PyMethodDef c_methods[] = { 
    {"hello_world", (PyCFunction)hello_world_wrapper, METH_VARARGS},
    {"add",         (PyCFunction)add_wrapper,         METH_VARARGS}, // ここ追加
    {NULL}
};

static struct PyModuleDef c_modules =
{
    PyModuleDef_HEAD_INIT,
    "c_modules",
    "",
    -1,
    c_methods
};

PyMODINIT_FUNC PyInit_c_modules() {
    return PyModule_Create(&c_modules);
}

引数の型はint だと i、文字列だと s となる。

指定する文字 Cにおける型
c char
i int
l long
f float
d double
s char*

んで、表にはないが、 |(パイプ。longのlじゃないお)みたいな変な奴がある。オプションの開始とかいうらしいがさっぱりわからん。
このあたりの型の仕様はどっか書いてあるだろうからググれ。

動作確認

$ python3
Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import c_modules
>>> c_modules.add(1,2)
3

でけた!

注意点

コンパイル時に以下のエラーが出る場合はpython3のインストール時に過不足があるみたい。

c_wrapper.c:1:10: fatal error: Python.h: そのようなファイルやディレクトリはありません
 #include "Python.h"

パスが通ってないせいだが、以下のコマンドでパスが何も出なかったら知らん。 python-devあたりが足りてないんだろう。多分。
ぐぐれ。

$ python3-config --cflags --ldflags
-I/usr/include/python3.6m -I/usr/include/python3.6m  -Wno-unused-result -Wsign-compare -g -fdebug-prefix-map=/build/python3.6-e9shER/python3.6-3.6.9=. -specs=/usr/share/dpkg/no-pie-compile.specs -fstack-protector -Wformat -Werror=format-security  -DNDEBUG -g -fwrapv -O3 -Wall
-L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu -L/usr/lib -lpython3.6m -lpthread -ldl  -lutil -lm  -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions

注意点

Python2は結構ちがうみたい。同じようにしてもエラーになるらしいよ。

その他

なんか、Boost使うともっと簡単にできるって誰かが言ってた。

以上!