PythonからC言語の関数を呼び出す(基本編)

PythonからC言語の関数を呼び出す方法の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Python からC言語の関数を呼び出す方法について解説していきます。

Python からC言語の関数を呼び出すことができるようになれば、今まで開発してきたC言語のソースコード資産を Python から有効利用することもできますし、速度重視の部分はC言語・開発のしやすさ重視の部分は Python といったように、利用するプログラミング言語を適材適所に選ぶこともできるようになります。

Python からC言語の関数を呼び出す方法はいくつかありますが、今回はC言語で作成した関数を Python/C API を利用したラッパーを介して呼び出す方法について解説していきます。

今回紹介する方法は、Python が CPython であること(C言語から開発された Python であること)を前提とした解説になっていますので、その点はご注意ください。

また、このページで解説する内容は下記の Python の公式サイトでも解説されています。私の解説に不十分な点があるかもしれませんので、下記ページで情報を補いながら、解説を読み進めていただければ幸いです。

https://docs.python.org/ja/3/extending/extending.html

Python からC言語の関数を呼び出す手順

最初に、Python からC言語の関数を呼び出す際に必要になる手順についてまとめておきます。

今回の方法で Python からのC言語の関数を呼び出しを実現するにあたり、目指すべき構成は下の図のようになります。

PythonからのC言語の関数の呼び出しを実現する際の各ファイルの構成

要は、呼び出したいC言語の関数を含むソースコードから共有ライブラリ(動的ライブラリ)を生成し、それを Python(インタプリタ)から動的ロードさせることで Python からのC言語の関数の呼び出しを実現します。

この動的ロードは Python スクリプトから import が実行された際に行われることになります。

ただし、import した際にロードされるライブラリが Python のモジュールとして適していない場合、ライブラリのロードは成功したとしても import の処理で例外が発生して import に失敗することになります。

そのため、生成するライブラリが Python のモジュールとして適切なものとなるようモジュール化を行う必要があります。そして、このモジュール化を行うための実装がC言語のソースコードに必要になります(初期化関数の実装など)。

また、Python スクリプト上のデータとC言語のソースコード上のデータの扱いは大きく異なります。そのため、この違いを吸収するためのラッパー関数を設け、そのラッパー関数でデータの違いを吸収してから目的のC言語の関数を呼び出すようにする必要があります。

つまり、Python スクリプトからC言語の関数の呼び出しを行うためには、まずはC言語のソースコードに下記の3つを実装する必要があります。

  • C言語の関数の実装
  • ラッパー関数の実装
  • モジュール化のための実装

そして、上記を実装後に「ライブラリのビルド」を行ってライブラリの生成を行います。

さらに、そのライブラリをモジュールとして import を行い、そのモジュールの関数を実行するように「Python スクリプトの実装」を行います。

まとめると、Python スクリプトからのC言語の関数の呼び出しを実現するためには、下記の5つを行う必要があります。

  • C言語の関数の実装
  • ラッパー関数の実装
  • モジュール化のための実装
  • ライブラリのビルド
  • Python スクリプトの実装

ここからは、これら5つの手順について1つ1つ解説をしていきます。

具体例があった方が分かりやすいと思いますので、今回は下記の2つのC言語の関数を Python から呼び出す例を踏まえながら解説していきます。また、Python スクリプトから import するモジュール名は MyCalc として解説を進めていきたいと思います。

addとsub
int add(int, int);
int sub(int, int);

さらに、今回は calc.ccalc.hcalcWrapper.c の3つのファイルをソースコードファイル・ヘッダーファイルとして用意し、それぞれに対して下記を実装するものとして解説していきます。

  • calc.c:C言語の関数の実装
  • calc.hcalc.c で実装した関数のプロトタイプ宣言
  • calcWrapper.c:ラッパー関数の実装とモジュール化のための実装

ファイルをもっと分割しても良いですし、1つのファイルにまとめても良いです。また、ファイル名にも特に制限はないと思いますので、お好みで変更していただいても問題ありません。

今回紹介する例はかなり簡単な部類のものになりますが、Python からC言語の呼び出しを実現する上で必要になる手順の大枠はしっかり理解できると思います。

また、今後別途ページを新たに作成し、もっと複雑な例に関しても紹介していきたいと思います。

C言語の関数の実装

では、Python からC言語の呼び出しを行うために、まずは Python から呼び出ししたいC言語の関数を実装していきます。

と言っても、このC言語の関数の実装はいつも通りの書き方で実装して良いです。Python から呼び出されるからといって身構える必要もありません。

もちろん、このC言語の関数は、以前に実装したものを使い回すのでも良いです。

関数を Python から呼び出し可能にするために必要な処理は全て、後述で説明するラッパー関数側で実現します。

スポンサーリンク

C言語の関数の実装例

今回は、Python から呼び出す関数として下記の add 関数と sub 関数を用意したいと思います。

本当は不要ですが、動作確認時にこれらの関数が呼び出されている様子が分かりやすくなるよう、printf を実行するようにしています。

呼び出したいC言語の実装
#include <stdio.h>
#include "calc.h"

int add(int x, int y) {
    printf("call C function(add)!\n");
    return x + y;
}

int sub(int x, int y) {
    printf("call C function(sub)!\n");
    return x - y;
}

前述の通り、上記のC言語の関数は calc.c に実装します。

さらに、実装した add 関数と sub 関数を他のファイルからも参照できるように calc.h にプロトタイプ宣言を行います。

プロトタイプ宣言
int add(int, int);
int sub(int, int);

ラッパー関数の実装

続いて、先ほど実装したC言語の関数を Python から呼び出せるよう、ラッパー関数を実装していきます。

ラッパー関数の主な役割

このラッパー関数の役割は下記の2つとなります。

  • Python とC言語との違いを吸収する
  • 目的のC言語の関数を呼び出す

Python とC言語では扱うデータの形式が大きく異なります。そのため、Python のデータをC言語で扱うことのできる形式に変換してからC言語の関数の呼び出しを行う必要があります。そして、これらを行うことがラッパー関数の役割となります。

例えばですが、下記のように Python スクリプトを記述した場合(MyModule はC言語のソースコードから生成したモジュールという前提)、実行時に呼び出されるC言語の関数の引数の型や個数、さらには返却値の型はどのようになると思いますか?

関数呼び出し
import MyModule

ret = MyModule.func(100, 3.5, 'Hello')

引数の型や個数・返却値の型は、下記のようなものになると予想される方が多いのではないかと思います。

呼び出される関数の予想
static double
_func(int x, double y, char s[]) {
    /* 〜略〜 */
}

ですが、実際に実行時に呼び出されるのは、下記のような型の引数と返却値を持つ関数となります。

実際に呼び出される関数
static PyObject *
_func(PyObject *self, PyObject *args) {
    /* 〜略〜 */
}

予想したものと全然違ったという方も多いのではないかと思います。

Python インタプリタ内部においては、Python スクリプト上の全てのデータ(オブジェクト)は PyObject 構造体 or PyObject を拡張した構造体として扱われます(簡単のため、以降では PyObject を拡張した構造体も含めて PyObject 構造体と呼ばせていただきます)。

整数や浮動小数点数も文字列も全て PyObject 構造体で扱われます。

全てのオブジェトがPyObjectで扱われる様子

そして、Python スクリプトから関数が実行された際には、指定された引数は PyObject 構造体のアドレス args として渡されることになります。引数の個数に関わらず、受け取る引数は args から全て取得できるようになっています(第1引数の self はおそらくメソッド用の引数であり、今回は利用しません)。

MEMO

この説明は引数として位置引数のみを受け取ることを前提としたものになっています

キーワード引数を受け取る場合、args だけではなく他の引数から受け取った引数を取得することになります

このように、Python スクリプトから呼び出しされる関数の引数の型は通常のC言語の関数の型とはかけ離れているため、そのまま Python スクリプトからC言語の関数を呼び出すことができません。

そのため、Python スクリプトからC言語の関数を呼び出す際には、事前に PyObject 構造体のアドレス args から PyObject 構造体のデータを取得し、さらにそれをC言語の関数の引数の型に合わせて変換する必要があります。

そして、その変換後にC言語の関数を呼び出す必要があります。

ラッパーでPyObjectから通常のC言語で利用する型に変換する様子

また、C言語の関数から Python スクリプトに返却するデータも PyObject 構造体のアドレスとなります。これは、前述の通り、Python スクリプト上で扱うデータは全て PyObject 構造体として扱われるためです。

そのため、C言語の関数の返却値を Python スクリプト側に返却するためには、返却値を PyObject 構造体に変換してから、そのアドレスを返却する必要があります。

C言語の関数の返却値をPyObjectに変換する様子

そして、こういったデータの変換とC言語の関数の呼び出しを行うのが、ラッパー関数の役割となります。

スポンサーリンク

ラッパー関数で行う処理

前述の通り、Python スクリプトから呼び出しされるC言語の関数は下記のような形式のものになります。

ラッパー関数
static PyObject *
_func(PyObject *self, PyObject *args) {
    /* 〜略〜 */
}

以降、上記のような Python スクリプトから呼び出しされる関数をラッパー関数と呼ばせていただきます。

Python スクリプトからC言語の関数を呼び出す際には、このラッパー関数を介して関数の呼び出しを行うことになります。

ラッパー関数を介してC言語の関数を呼び出す様子

そのため、このラッパー関数では大きく分けて下記の3つを行う必要があります。

  1. PyObject *args からの各引数の取得とC言語の型への変換
  2. C言語の関数の実行
  3. C言語の関数の返却値の PyObject * への変換と返却

1. と 3. でデータの変換を行うことになりますが、この変換は Python/C API を利用することで簡単に実現することができます(Python/C API  を利用するためには Python.h をインクルードする必要があります)。

Python/C API については、下記ページで公式から詳細な解説が行われています。

https://docs.python.org/ja/3/c-api/index.html

それなりにボリュームがありますが、引数として受け取った PyObject * からC言語の型のデータへの変換を行う際にまず覚えておいた方が良いのが、下記ページで解説されている PyArg_ParseTuple となります。

https://docs.python.org/ja/3/c-api/arg.html

また、C言語の型のデータを PyObject * に変換を行う際には上記ページで紹介されている Py_BuildValue が便利です。

これらの2つの関数を利用することで、scanf 関数や printf 関数と同様の感覚で PyObject * とC言語の型との相互変換を行うことが可能です。

PyArg_ParseTuple

PyArg_ParseTuple 関数は、Python スクリプトから受け取った PyObject * 型の引数 args から書式文字列 format で指定したC言語の型のデータを取得する関数になります。

PyArg_ParseTuple
int PyArg_ParseTuple(PyObject *args, const char *format, ...);

前述の通り、書式文字列で指定した形式のデータを取得するという点で、scanf 関数に似ていると思います。

ただし、scanf 関数は標準入力で入力された文字列からデータを取得しますが、 PyArg_ParseTuple では Python スクリプトから受け取った PyObject * 型の引数 args からデータを取得します。

また、書式文字列の指定の仕方も異なるので注意してください。

この書式文字列 format は、Python スクリプトから受け取る引数の変換先の型に合わせて文字を組み合わせて設定する必要があります(この文字は書式単位と呼ばれます)。

例えば書式文字列 format"idi" と指定した場合、idi の順番で3つの書式単位の指定が行われており、iint 型のデータへの変換を、ddouble 型のデータへの変換を示す書式単位ですので、Python スクリプトから受け取った3つの引数を前から順番に int 型、double 型、int 型に変換して取得することができます。

もちろん取得したデータは変数に格納する必要があるため、第3引数以降に格納先の変数のアドレスを指定する必要があります。

例えば、下記のように PyArg_ParseTuple を実行した場合、

引数からint型の整数を1つ取得
int a;
double b;
int c;
PyArg_ParseTuple(args, "idi", &a, &b, &c);

Python スクリプトで関数実行時に指定された各引数を下記のように変換して変数に格納することができます。

  • a:第1引数を int 型に変換したデータ
  • b:第2引数を double 型に変換したデータ
  • c:第3引数を int 型に変換したデータ

このように PyArg_ParseTuple を利用して引数を変換するようにすれば、例えば下記のように Python スクリプトから関数の引数が指定された場合、変数 a10、変数 b2.5、変数 c100 を格納する形で受け取ることができます。

引数からint型の整数を1つ取得
import MyFunc
MyFunc.func(10, 2.5, 100);

変数に値を格納することができれば、あとは通常のC言語の時と同じように処理や関数呼び出し等を行えば良いだけです。

こんな感じで、Python スクリプトから受け取った引数をC言語の型のデータに変換して取得することができるため、ラッパー関数では PyArg_ParseTuple 関数を実行してからC言語の関数を呼び出す流れになることが多いです。

当然、この PyArg_ParseTuple の第2引数で指定する format は、Python スクリプトから引数として渡されるデータやラッパー関数から呼び出すC言語の引数に合わせて設定する必要があります。

format に設定可能な書式単位や、その書式単位を指定した際にどのようにデータが変換されるのかについては下記ページで詳細が解説されていますので、詳しくはこちらを参照していただければと思います。

https://docs.python.org/ja/3/c-api/arg.html

ちなみに、ラッパー関数の引数 args はタプルオブジェクトへの参照であり、このタプルに Python スクリプトで関数呼び出し時に引数で指定されたオブジェクトが要素として格納されています。

argsの説明図

PyArg_ParseTuple では、そのタプルの要素のオブジェクトの取得と、引数 format の書式単位で指定された型への変換処理が行われます。

タプルの要素のオブジェクトの取得を行う API や、オブジェクトを特定の型に変換する API も Python/C API に用意されているため、これらを利用すれば PyArg_ParseTuple を利用しなくても Python スクリプトから受け取った引数をC言語の型のデータに変換するようなことは可能ではあります。

ですが、PyArg_ParseTuple を利用した方が手順が少なくて楽ですので、PyArg_ParseTuple を利用することを前提に解説を進めさせていただいています。

Py_BuildValue

Py_BuildValue 関数は、PyArg_ParseTuple とは逆にC言語の型のデータから PyObject に変換する関数となります。

PyArg_ParseTuple 同様に、書式文字列 format の指定に応じてデータの変換が行われます。Py_BuildValue の返却値は変換後のデータ PyObject のアドレスとなります。

なので、ラッパー関数から呼び出したC言語の関数の返却値を Python スクリプトに返却したいような場合は、Py_BuildValue 関数の返却値をそのままラッパー関数から返却してやれば良いことになります。

Py_BuildValue
PyObject *Py_BuildValue(const char *format, ...);

例えば、format"d" を指定し、第2引数に double 型の変数や値を指定して Py_BuildValue を実行すれば、それが PyObject に変換され、その PyObject のアドレスを返却値として得ることができます。

ですので、例えば下記を実行すれば、2.5 という数値を Python スクリプトに返却することができます。

double型からPyObjectへの変換
return Py_BuildValue("d", 2.5);

こちらに関しても書式文字列に設定可能な書式単位は下記ページで解説されていますので、詳しくはこちらを参照していただければと思います。

https://docs.python.org/ja/3/c-api/arg.html

Py_BuildValue を使いこなすことで、例えばリスト型のデータを Python スクリプトに返却するようなこともできるようになります。

ラッパー関数の実装例

長々とラッパー関数について解説してきましたが、次は実際のラッパー関数の実装例を紹介したいと思います。

C言語の関数の実装例 で紹介した add 関数のラッパー関数 addWrapper と、sub 関数のラッパー関数 subWrapper の実装例は下記のようになります。

ラッパー関数の実装例
#define PY_SSIZE_T_CLEAN
#include <Python.h> /* Python/C API */
#include "calc.h" /* 呼び出したいC言語の関数 */

static PyObject *
addWrapper(PyObject *self, PyObject *args) {

    int x, y;

    /* argsからint型に変換したデータを2つ取得 */
    if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
        /* エラー時は例外発生 */
        return NULL;
    }

    /* 目的のC言語の関数を呼び出し */
    int ret = add(x, y);

    /* retをPybOject構造体に変換してアドレス返却 */
    return Py_BuildValue("i", ret);
}

static PyObject *
subWrapper(PyObject *self, PyObject *args) {

    int x, y;

    /* argsからint型に変換したデータを2つ取得 */
    if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
        /* エラー時は例外発生 */
        return NULL;
    }

    /* 目的のC言語の関数を呼び出し */
    int ret = sub(x, y);

    /* retをPybOject構造体に変換してアドレス返却 */
    return Py_BuildValue("i", ret);
}

前述の通り、上記のラッパー関数は calcWrapper.c に実装します。

まず、Python/C API や PyObject 型を利用するために Python.h のインクルード、さらに add 関数と sub 関数を利用するために C言語の関数の実装例 で作成した calc.h のインクルードがそれぞれ必要になります。

また、下記ページの解説にならい、Python.h をインクルードする前には #define PY_SSIZE_T_CLEAN を行うようにしています。

https://docs.python.org/ja/3/extending/extending.html

基本はここまで解説したきた通りに PyArg_ParseTuplePy_BuildValue およびC言語の関数の呼び出しを行なっているだけです。

ただ、ラッパー関数から呼び出す関数(addsub)が2つの int 型の整数を引数で受け取る関数となっていますので、PyArg_ParseTuple の第2引数 format には "ii" を指定しています。

また、ラッパー関数から呼び出す関数の返却値は int 型なので、int 型の値1つを PyObject に変換するために Py_BuildValue の第1引数 format には "i" を指定しています。

ポイントの1つになるのが NULL の返却になります。上記では PyArg_ParseTuple が失敗した時(0 を返却した時)に NULL を返却するようにしています。

このように、Python スクリプトから呼び出される関数から NULL を返却することで、Python 側で例外を発生させることができます。

ですので、PyArg_ParseTuple が失敗した時、例えば Python スクリプトから引数が1つしか指定されずに関数の呼び出しが行われたような場合には、Python 側で例外が発生し、問題があったことをユーザーに通知することができます。

モジュール化のための実装

ここまでで、Python スクリプトから実行可能な関数をラッパー関数として用意できたことになります。

ただし、現状のソースコードでビルドを行なったとしても生成されるのは単なるライブラリであり、Python スクリプトから import することができません(import に失敗する)。

次は、正常に import できるよう、ライブラリのモジュール化を行うための実装を行なっていきます。

このモジュール化のための実装とは、具体的には下記の3つの実装となります。

  • メソッド(関数)テーブル
  • モジュール定義
  • モジュールの初期化関数

実際に import を正常に動作させるために必要になるのは「モジュールの初期化関数」となります。ただし、モジュールの初期化関数を実装するために他の2つの実装も必要になります。

以降では、上記3つの具体的な実装手順を説明していきます。

スポンサーリンク

メソッドテーブルの実装

まずはメソッドテーブルの実装を行っていきます。

メソッドテーブルとは

メソッドテーブルとは、Python スクリプトに提供する関数名(メソッド名)と、それが呼び出された際に実行される関数(ラッパー関数)との紐付け等を行う配列となります。

メソッドテーブルの役割を示す図

より具体的には、メソッドテーブルは PyMethodDef 構造体型の配列となります。

この PyMethodDef 構造体は下記のように定義されています。

PyMethodDef
struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

ml_name で Python スクリプトに提供したい関数名、ml_meth には ラッパー関数の実装 で実装したラッパー関数のアドレスを指定します。

これにより、Python スクリプトから ml_name で指定した関数名の関数が呼び出された際に ml_meth で指定したラッパー関数が実行されるようになります。

また、ml_flags では ml_meth で指定するラッパー関数の引数の形式を指定します。ラッパー関数の実装 では位置引数を指定可能なラッパー関数の実装についてのみ解説しましたが、実はキーワード引数を指定可能なラッパー関数を実装するようなことも可能です。

こういった、指定可能な引数を示すために指定するのが ml_flags となります。

例えば、ml_meth で指定したラッパー関数が位置引数のみを受け取る場合は ml_flagsMETH_VARARGS を指定し、位置引数とキーワード引数を受け取る場合は ml_flagsMETH_VARARGS | METH_KEYWORDS を指定します。

さらに、ml_doc には関数の説明文の文字列を指定することもできます。

メソッドテーブルの実装例

例えば、Python スクリプトに対して MyAdd 関数と MySub 関数を提供し、それぞれが呼び出された際に ラッパー関数の実装例 で実装した addWrapper 関数と subWrapper 関数を実行するようにする場合は、下記のようにメソッドテーブルを実装することになります。

メソッドテーブルの実装例
static PyMethodDef myCalcMethods[] = {
    {"MyAdd", addWrapper, METH_VARARGS, NULL},
    {"MySub", subWrapper, METH_VARARGS, NULL},
    {NULL, NULL, 0, NULL} /* 番兵 */
};

上記の配列の宣言は、ラッパー関数の実装例 で実装したラッパー関数よりも下側で行うようにしてください。

上記では3つの要素を持つ PyMethodDef 構造体型の配列を定義しています。内側の {} が1つの PyMethodDef 構造体に対応し、, 区切りで前から順に ml_nameml_methml_flagml_doc のメンバに値が設定されることになります。

配列名は myCalcMethods としていますが、これは次に説明するモジュールの定義から参照可能な名前であればなんでも良いはずです(もちろんモジュール名と関連づけておいた方が分かりやすいと思います)。

また、配列の最後の要素は番兵(配列等の終端を示すもの)であり、上記のように {NULL, NULL, 0, NULL} と指定するのが一般的なようです。

ml_name に指定するのは文字列であり、ml_meth に指定するのは関数のアドレス(関数名)である点を混同しないように注意してください。

モジュール定義の実装

続いてモジュール定義の実装を行います。

モジュール定義とは

ここでいうモジュール定義とは、モジュールの各種設定を行う構造体 PyModuleDef の変数の宣言となります。

この PyModuleDef 構造体は下記のように定義されています。

PyModuleDef
typedef struct PyModuleDef{
  PyModuleDef_Base m_base;
  const char* m_name;
  const char* m_doc;
  Py_ssize_t m_size;
  PyMethodDef *m_methods;
  struct PyModuleDef_Slot* m_slots;
  traverseproc m_traverse;
  inquiry m_clear;
  freefunc m_free;
} PyModuleDef;

正直私も詳しくないので、詳細は下記ページを参照していただければと思います。

https://docs.python.org/ja/3/c-api/module.html

ポイントは、m_methodsメソッドテーブルの実装 で宣言した配列名を指定する点になります。これにより、PyModuleDef 構造体の変数にメソッドテーブルの情報(提供する関数の名前や実際に呼び出しされる関数の情報)を設定することができます。

モジュール定義の実装例

メソッドテーブルの実装例 で宣言した配列を設定する際のモジュール定義の実装例は下記のようになります。

モジュール定義の実装例
static PyModuleDef myCalcModule = {
    PyModuleDef_HEAD_INIT,
    "MyCalc",
    NULL,
    -1,
    myCalcMethods
};

上記の変数宣言は、メソッドテーブルの実装例 で示した配列の宣言よりも下側で行うようにしてください。

上記では、PyModuleDef 構造体の m_basem_methods のメンバにしか値を設定していませんが、これだけでもC言語の関数の呼び出しは実現可能です。

m_methods には メソッドテーブルの実装例 で用意した配列 myCalcMethods を指定しています。

モジュールの初期化関数の実装

最後にモジュールの初期化関数の実装を行います。

モジュールの初期化関数とは

モジュールの初期化関数とは、Python スクリプトから import された際に実行される関数です。この関数が無ければ import 時に例外が発生します。

この初期化関数の主な役割は、モジュールオブジェクトを生成し、そのオブジェクトのアドレスを返却することになります(他にも例外の設定なども行うことができるようです)。

このモジュールオブジェクトを Python が受け取ることで、以降でモジュールオブジェクトのメソッドテーブルに設定された関数を Python スクリプトから実行することができるようになります。

このモジュールオブジェクトは、モジュール定義の実装 で宣言した PyModuleDef 構造体の変数のアドレスを引数に指定して PyModule_Create 関数を実行することで生成することができます(PyModule_Create 関数の返却値としてそのオブジェクトのアドレスが得られる)。

モジュールの初期化関数の返却値の型は PyMODINIT_FUNC であり、関数名は PyInit_モジュール名 の形式とする必要があるので注意してください。また、モジュールの初期化関数は公開関数である必要があります(static 関数ではダメ)。

モジュールの初期化関数の実装例

例えば、モジュール名が MyCalc であるモジュールの初期化関数の実装例は下記のようになります。

モジュールの初期化関数の実装例
PyMODINIT_FUNC
PyInit_MyCalc(void) {
    return PyModule_Create(&myCalcModule);
}

上記の関数定義は モジュール定義の実装例 での変数宣言よりも下側で行ってください。

これにより、import MyCalc が Python スクリプトから実行された際に、上記の PyInit_MyCalc 関数が実行され、モジュール定義の実装例 で宣言した PyModuleDef 構造体の変数 myCalcModule に基づいてモジュールオブジェクトが生成されて返却されることになります。

以降、Python スクリプトでは、モジュール MyCalc が提供する関数を呼び出すことができるようになります。

より具体的には、メソッドテーブルで「提供する関数」として指定された関数を呼び出すことができるようになります。

モジュールが提供する関数の説明図

また、モジュール MyCalc が提供する関数が呼び出された際には、メソッドテーブルによって紐づけられたラッパー関数が実行されることになります。

モジュールが提供する関数が呼び出された際にラッパー関数が呼び出されることを示す図

そして、そのラッパー関数から元々目的としていたC言語の関数の呼び出しが行われることになります。

以上により、C言語のソースコードの準備が整ったことになります。

ここまで calcWrapper.c の実装の紹介を部分的に行ってきたため、ここで calcWrapper.c 全体のソースコードを紹介しておきます(ここまで紹介してきた実装を1つにまとめただけです)。

calcWrapper.c
#define PY_SSIZE_T_CLEAN
#include <Python.h> /* Python/C API */
#include "calc.h" /* 呼び出したいC言語の関数 */

static PyObject *
addWrapper(PyObject *self, PyObject *args) {

    int x, y;

    /* argsからint型に変換したデータを2つ取得 */
    if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
        /* エラー時は例外発生 */
        return NULL;
    }

    /* 目的のC言語の関数を呼び出し */
    int ret = add(x, y);

    /* retをPybOject構造体に変換してアドレス返却 */
    return Py_BuildValue("i", ret);
}

static PyObject *
subWrapper(PyObject *self, PyObject *args) {

    int x, y;

    /* argsからint型に変換したデータを2つ取得 */
    if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
        /* エラー時は例外発生 */
        return NULL;
    }

    /* 目的のC言語の関数を呼び出し */
    int ret = sub(x, y);

    /* retをPybOject構造体に変換してアドレス返却 */
    return Py_BuildValue("i", ret);
}

static PyMethodDef myCalcMethods[] = {
    {"MyAdd", addWrapper, METH_VARARGS, NULL},
    {"MySub", subWrapper, METH_VARARGS, NULL},
    {NULL, NULL, 0, NULL} /* 番兵 */
};

static PyModuleDef myCalcModule = {
    PyModuleDef_HEAD_INIT,
    "MyCalc",
    NULL,
    -1,
    myCalcMethods
};

PyMODINIT_FUNC
PyInit_MyCalc(void) {
    return PyModule_Create(&myCalcModule);
}

スポンサーリンク

ライブラリのビルドの実行

ソースコードの準備ができましたので、次はソースコードのコンパイルやリンクを行なってライブラリのビルドを行なっていきます。

通常のC言語での開発時同様に gcc コマンドでビルドすることも可能ですが、setup.py を実装して setup.py からビルドするのが手っ取り早いと思います。

これだとライブラリのビルドも setup.py を実行するだけで実現できますし、さらにモジュールを Python にインストールして pip に登録することもできます。

さらに、インクルードパスの設定も不要ですし、生成するライブラリ名も自動的に適切に設定してくれます(import 時にライブラリがロードされるようにするためには、OS や Python のバージョン等を考慮してライブラリ名を適切に設定する必要がある)。

そのため、今回は setup.py を利用してライブラリのビルドを行なっていきたいと思います。

ただ、少なくとも今回紹介する手順では、インストールしたモジュールを pip uninstall でアンインストールすることが出来ないので注意してください。ちょっと特殊な手順を踏んでアンインストールを行う必要があります(この手順も踏まえて解説していきます)。

setup.py の実装

まずは setup.py を実装していきます。

setup.py の実装の仕方

C言語のソースコードから Python のモジュールを生成する場合、下記のような形式で setup.py を実装することになります。

setup.pyの書き方
from distutils.core import setup, Extension

setup(
    name='パッケージ名',
    ext_modules = [
        Extension(
            'モジュール名', # PyInit_モジュール名に合わせる
            ['ソースコード1へのパス', 'ソースコード2へのパス'],
        )
    ]
)

setup 関数の引数により、生成するモジュールの設定を行うことになります。

上記では最低限の引数しか指定していないので、詳細を知りたい方は下記の Python の公式ページを参照していただければと思います。

https://docs.python.org/ja/3/distutils/setupscript.html

C言語のソースコードからモジュールを生成する際にポイントとなるのが ext_modules 引数となります。

上記スクリプトの通り、ext_modules には Extension() の返却値(Extension クラスのインスタンス)を要素とするリストを指定することになるのですが、この Extension() の引数に応じてビルドが行われてモジュールが生成されることになります。

この Extension() の第1引数には生成するモジュールのモジュール名を指定します。そして、第2引数のリストにはコンパイルを行うソースコードのパスを指定します。このパスは複数指定可能ですし、相対パスで指定することも可能です。ただし、相対パスを指定する場合は setup.py を実行するフォルダからの相対パスを指定する必要があるので注意してください。

このように引数を設定しておけば、setup.py を実行した際に第2引数で指定されたソースコードがコンパイルされ、それがリンクされて Python のモジュールとして import 可能なライブラリが生成されます。

そして、ライブラリを生成しておけば、このライブラリと同じフォルダ内に存在する Python スクリプトからモジュールとしてライブラリを import することができるようになります。

また、ライブラリをモジュールとして Python にインストールしておけば、Python スクリプトとライブラリの位置関係に関わらず、生成したモジュールを import することができるようになります。

setup.py の実装例

ここまでの手順の例にならってC言語のソースコードを作成してきた場合、モジュール名は MyCalc であり、C言語のソースコードは calc.ccalcWrapper.c の2つとなりますので、setup.py は下記のように実装することになります。

setup.pyの実装例
from distutils.core import setup, Extension

setup(
    name='MyCalcPkg',
    ext_modules = [
        Extension(
            'MyCalc',
            ['calc.c', 'calcWrapper.c'],
        )
    ]
)

name 引数はパッケージ名なのでなんでも良いのですが、Extension の第1引数のモジュール名に関しては、モジュールの初期化関数の実装 で実装した初期化関数の関数名に合わせて設定する必要があるので注意してください。

具体的には、初期化関数の関数名の PyInit_ の後ろ側で指定するモジュール名と、Extension の第1引数で指定するモジュール名とが一致している必要があります。

また、上記はあくまでも calc.ccalcWrapper.c が存在するフォルダ& setup.py を実行するフォルダが同じであることを前提とした実装例になっているので注意してください。

calc.ccalcWrapper.c が異なるフォルダにあったり、setup.py を実行するフォルダが異なる場合は、それに応じて Extension の第2引数は変更する必要があります。

ライブラリのビルド

続いて、setup.py を実行してライブラリのビルドを行なっていきます。

ライブラリのビルドは下記のコマンドから行うことができます。

% python setup.py build

このコマンドを実行することで、setup.py で指定したソースコードのコンパイルやリンクが行われます。当然ソースコードに問題がある場合はコンパイルエラーやリンクエラーが発生しますので、エラーが発生した場合は適宜ソースコードの修正を行ってください。

エラーが発生することなくコマンドが終了した場合、上記コマンドを実行したフォルダ内に build というフォルダが作成されているはずです。

さらに、その中に2つのフォルダが存在し、lib から始まるフォルダの中にライブラリが生成されているはずです(もしかしたら OS 等によってフォルダ構造は異なるかもしれません)。

ただし、上記コマンドでは単純にライブラリが生成されるだけなので、Python スクリプトから import を行うためには、その Python スクリプトと同じフォルダにライブラリをコピーする必要があります(他にも方法あるかも)。

ライブラリの位置に関係なく import できるようにしたい場合は、次の モジュールのインストール を行ってください。

とりあえずお試しで import してみたいだけであれば、次の モジュールのインストール はスキップして Python スクリプトの実装 まで進んでも問題ありません。

スポンサーリンク

モジュールのインストール

次は、生成したライブラリをモジュールとして Python にインストールする手順について解説します(パッケージのインストールと言った方が正しいかも)。

要は pip にモジュール(パッケージ)の登録を行います。

このインストールを行っておけば、普段利用しているモジュール同様に、モジュールのライブラリの位置を気にすることなく Python スクリプトからモジュールを import することが可能になります。

ただし、前述の通り、通常 pip に登録されたパッケージは pip uninstall の実行により自動的にアンインストールを行うことができますが、少なくとも今回用意した setup.py でインストールを行った場合は pip uninstall の実行によるアンインストールが上手く動作しません。

そのため、アンインストールは手動で行う必要があります。

このアンインストール手順も踏まえながら、インストールの手順を解説していきます。

インストール手順

setup.py からのインストールは、下記のコマンドで setup.py を実行することで行うことができます。

python setup.py install --record files.txt

--record files.txt の部分はアンインストールを楽に行うために指定しているオプションになります。

上記コマンドを実行すれば、Python に対して setup.py の実装内容に基づいてモジュール(パッケージ)のインストールが行われ、pip に登録されることになります。

MEMO

ライブラリのビルド の手順をスキップした場合は、上記コマンド実行時にライブラリのビルドも行われることになります

インストールが成功したかどうかは下記のコマンドで確認可能です。このコマンドの出力結果に、setup 関数の name 引数に指定した名前のパッケージが表示されていれば、インストールに成功したことになります。

% python -m pip list
Package                   Version
------------------------- ---------
〜略〜
MyCalPkg                  0.0.0
〜略〜

アンインストール手順

上記の手順でインストールした場合のモジュールのアンインストール手順についても解説しておきます。

アンインストールは、モジュールインストール時にインストールされたファイルを削除することで実現できます。

このインストールされたファイルのパスは、上記のようにコマンドを実行した場合、files.txt というファイルの中に記録されているはずです(このパスの記録を行うためのオプションが --record になります)。

なので、この files.txt に記録されているパスのファイルを削除してやれば、モジュールのアンインストールを行うことができます。

もちろん1つ1つ手動で削除しても良いですが、xargs コマンドが利用できるのであれば、下記のコマンドを実行することで files.txt に記録されているパスのファイルを全て削除することができます。

% xargs rm -rf <files.txt

手動 or 上記のコマンドでファイルを削除した後に再度下記コマンドを実行してみてください。今度は先ほどインストールしたパッケージが表示されなくなっているはずです。

% python -m pip list

これが確認できれば、モジュールのアンインストールが完了したことになります。

ちなみに、このアンインストールの手順は下記ページを参考にさせていただいています。

https://qiita.com/orion0616/items/dfe476067e499cca8535

Python スクリプトの実装

ここまでの手順により、Python からC言語の関数を呼び出すための準備が整ったことになります。

Python スクリプトでモジュールを import し、さらにメソッドテーブルに指定した関数を実行してやれば、ラッパー関数を介してC言語の関数を呼び出すことができます。

ということで、実際に Python スクリプトの実装&実行を行なってC言語の関数を呼び出してみましょう!

MEMO

モジュールのインストール をスキップされた方は、下記の2つのいずれかを行ない、ライブラリのビルド で生成したライブラリと Python スクリプトとが同じフォルダ内に存在するようにしてからスクリプトを実行してください

ここまでの解説の手順の例に従って実装を行なってきた場合、例えば下記のように Python スクリプトを実装して実行すれば、C言語の関数の実装例 で実装した関数 addsub を呼び出すことができます。

PythonからC言語の関数の呼び出し
import MyCalc

print(MyCalc.MyAdd(100, 200))
print(MyCalc.MySub(100, 200))

実行すれば、下記のように出力されるはずです。

call C function(add)!
300
call C function(sub)!
-100

この結果より、Python スクリプトから C言語の関数の実装例 で実装した関数が呼び出されていること、さらには Python スクリプトで指定した引数が C言語の関数の実装例 で実装した関数にまで渡されていることが確認できると思います。

まとめ

このページでは、Python からC言語の関数を呼び出す方法について解説しました!

下記の3つを含むソースコードからライブラリを生成することで、そのライブラリをモジュールとして Python スクリプトから利用できるようになります。

  • C言語の関数の実装
  • ラッパー関数の実装
  • モジュール化のための実装

結構手順としては多かったかもしれませんが、ラッパー関数の作成以外に関しては、呼び出すC言語が変わっても今回紹介した手順とほぼ同様の手順で Python からのC言語の関数の呼び出しを実現できると思います。

ただ、呼び出すC言語の関数の引数や返却値が変更された場合、それに合わせてラッパー関数も変更する必要があるので注意してください。

今回は引数が int 型2つ、返却値が int 型である関数の例を紹介しましたが、当然 int 型以外を引数・返却値とする関数の呼び出しを行うことも可能です。上手くラッパー関数を作成すれば、Python スクリプトからリストをC言語側に渡すようなことも可能です。

実際に、下記ページで Python スクリプトとC言語の関数とで様々なデータ(リストや辞書など)をやり取りするための実装方法や実装例を紹介していますので、こちらも是非読んでみてください(ラッパー関数と同じ位置付けの関数を IF 関数と呼んでいるので、その点のみご注意ください)。

PythonとC言語の関数との間で様々な型のデータのやりとりを行う方法の解説ページアイキャッチ 【Python/C API】PythonとC言語の関数とで様々なデータをやりとりする(リスト・辞書・キーワード引数など)

また、今回示したラッパー関数は簡単なものだったので不要でしたが、さまざまなラッパー関数を実現する上ではガベージコレクションを機能させるために、適切なタイミングで Py_INCREFPy_DECREF を実行することが重要になります。そして、この適切なタイミングを見極めるために、Python では「所有権」という考え方が用いられています。

この所有権については下記ページで解説していますので、詳しく知りたい方は是非こちらも読んでみてください!

所有権と参照カウントの解説ページアイキャッチ 【Python/C API】所有権と参照カウントについて解説

同じカテゴリのページ一覧を表示