LoginSignup
30
23

More than 3 years have passed since last update.

Cで作った関数をpythonで呼ぶ

Last updated at Posted at 2020-05-12

3行で

  • 普段はpythonを使うが、やむにやまれぬ事情でC言語で書かれたリソースを使う必要はまま生じる
  • pythonはCを使って新たなモジュールを追加することができるので、その機能を使えばいい
  • Python.hに用意された特別な構造体を使ってCでモジュール名、メソッド、引数を定義し、distutils.core.setup(pythonのメソッド!!)を使ってビルドする

必要な作業

  1. C言語で書かれたソースコードのベースを用意
  2. C言語でラッパーを記述
  3. pythonでsetupスクリプトを記述
  4. 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
// 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
// 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の関数の宣言と同じですね。

メソッドの定義

大まかに言って、処理の流れは
1. PyObject型(C言語でpythonのオブジェクトを扱う際の型)で引数を受け取り、PyObject型で返す関数を宣言する
2. PyObject型からC言語で扱える型にPyArg_ParseTupleなどで変換する
3. C言語で扱える型を使って処理を行う
4. Py_BuildValuePyObject型に戻す

といった感じになります。

関数定義

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というものがあります!!!!

setup.py
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

30
23
0

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
30
23