MRが楽しい

MRやVRについて学習したことを書き残す

Pythonの自作モジュールを作成してPythonにインストールする その2(C言語で作成する拡張モジュール)

本日は Python の技術調査枠です。
Python で自作モジュールを作成してPythonにインストールする手順を記事にします。
f:id:bluebirdofoz:20191124023005j:plain

今回はC言語を使って拡張モジュールを作成してみます。
前回記事の続きです。
bluebirdofoz.hatenablog.com

C言語で書いたPythonモジュールの作成

インストールを行うモジュールとして、以下のC言語プログラムを作成しました。
"Hello World"を標準出力する関数と、受け取った数値の足し算結果を返す関数、オプションで文字列を受け取る関数を持ちます。
・hello_module.c

// Python関連の構造体を利用するため、Python.h をインポート
#include <Python.h>

// "Hello World"を標準出力する関数
static PyObject* print_str (PyObject *self, PyObject *args) {
    // "Hello World"を標準出力する
    printf("Hello_world\n");
    // 戻り値がない場合は関数の最後に Py_RETURN_NONE を記述する
    Py_RETURN_NONE;
}

// 数値の引数を受け取り足し算結果を返す関数
static PyObject* sum_arg(PyObject *self, PyObject *args){
    // 数値を受け取るための変数を作成する
    int p_argx, p_argy;
    // 受け取った args を展開する今回は2つの数値(int)に分解する
    if(!PyArg_ParseTuple(args, "ii", &p_argx, &p_argy))
    {
        // 引数の取得に失敗した場合はエラー終了
        return NULL;
    }
    // 受け取った数値を足し算して int32 型に格納する
    int32_t result_val = p_argx + p_argy;
    // 足し算結果を返却する
    // 返却の際は必ず Python の型への変換を行う
    return PyLong_FromLong(result_val);
}

// 文字列の引数をオプションで受け取り標準出力する関数
static PyObject* print_arg(PyObject *self, PyObject *args){
    // 文字列を受け取るための変数を作成する
    // 引数はオプションとし、引数がない場合はデフォルト値を表示する
    const char *p_argstr = "Default";
    // 受け取った args を展開する今回は文字列(char[])に分解する
    // '|'以降のフォーマットはオプションとして扱われる
    if(!PyArg_ParseTuple(args, "|s", &p_argstr))
    {
        // 引数の取得に失敗した場合はエラー終了
        return NULL;
    }
    // 受け取った文字列を標準出力する
    printf(p_argstr);
    printf("\n");
    // 出力結果を文字列としても返却する
    // 返却の際は必ず Python の型への変換を行う
    return PyUnicode_FromString(p_argstr);
}

// 外部から参照できるメソッドの定義
static PyMethodDef HelloextMethods[] = {
    // 以下のフォーマットで Python から呼び出すことのできるメソッドを定義する
    // {(参照時の関数名), (C言語内の関数名), (引数の定義), (関数の説明文)}
    // 引数を受け取らない場合は引数の定義に METH_NOARGS を設定する
    {"printstrext", (PyCFunction)print_str, METH_NOARGS, "print_str description"},
    {"sumargext", (PyCFunction)sum_arg, METH_VARARGS, "sum_arg description"},
    {"printarg", (PyCFunction)print_arg, METH_VARARGS, "print_arg description"},
    // 終端を示す
    {NULL, NULL, 0, NULL}
};

//モジュールの定義
static struct PyModuleDef helloextmodule = {
    PyModuleDef_HEAD_INIT,  // 固定値(常に PyModuleDef_HEAD_INIT で初期化する)
    "helloext",             // モジュールの名前
    "helloext description", // モジュールの説明文
    -1,                     // メモリ確保の指定(-1は静的な領域を確保しない)
    HelloextMethods         // メソッドの定義への参照
};

// モジュールの初期化関数
PyMODINIT_FUNC PyInit_helloext (void) {
    // 構造体の初期化やモジュールの作成を行う
    // モジュールの作成
    return PyModule_Create(&helloextmodule);
}

上記のモジュールを登録する際は以下のような setup.py を用意します。
・setup.py

from distutils.core import setup, Extension

setup(
    name="helloext",              # モジュールの名前
    version="1.0.0",              # モジュールのバージョン
    author="holomon",             # モジュールの作者
    url="",                       # モジュールのホームページ
    description="description",    # モジュールの説明(help(hello)で確認可能)
    install_requires=[],          # 依存パッケージ([pip install -e .]実行時に一緒にインストール)
    extras_require={
        "develop" : [],           # 拡張依存パッケージ([pip install -e . develop]実行時に一緒にインストール)
    },
    ext_modules=[                 # パッケージのソースコード
        Extension('helloext', ['hello_module.c'])
    ]
)

C言語で拡張モジュールを作成する場合、Extension に記述された helloext がモジュールの名前となります。

モジュールの関数について

関数の戻り値は PyObject* 、引数は PyObject *self, PyObject *args である必要があります。

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

受け取った引数は PyArg_ParseTuple でパースを行います。

PyArg_ParseTuple(args, "ii", &p_argx, &p_argy)

パースを行う際は第二引数で、受け取る引数のフォーマットを指定します。
フォーマット指定の識別子は以下のページに詳細な説明があります。
docs.python.org

以下に幾つかの凡例があります。
docs.daemon.ac

戻り値は必ず Python のオブジェクト型で返却を行います。
docs.python.org

例えば、数値は以下の整数型オブジェクトに変換して返却します。
docs.python.org

インストールの実行

作成した setup.py と hello_module.c を同じディレクトリに配置します。
f:id:bluebirdofoz:20191124023149j:plain

今回はテストのため、インストールを行う Python を直接指定します。
例えば以下のパスの Python.exe を利用して setup.py を実行すれば、実行した Python ディレクトリにモジュールがインストールされます。

D:\Python\SetupPythonTest\Python37\python.exe

setup.py を配置したディレクトリで、Shiftキー+右クリックから[PowerShellウィンドウをここで開く]を実行します。
f:id:bluebirdofoz:20191124023158j:plain

PowerShell が起動したら、以下のコマンドを実行して hello_module.c をビルドします。

python .\setup.py build

f:id:bluebirdofoz:20191124023206j:plain

ビルドが成功すると build ディレクトリが生成され、helloext.(ビルド環境).pyd ファイルが出力されます。
f:id:bluebirdofoz:20191124023217j:plain

続けて、以下のコマンドを実行するとモジュールのインストールが実行されます。

python .\setup.py install

f:id:bluebirdofoz:20191124023226j:plain

成功すると、実行した Python ディレクトリの以下のパスに pyd ファイルと helloext-(バージョン)-(ビルド環境).egg-info ファイルが追加されます。

(Pythonディレクトリ)\Lib\site-packages

f:id:bluebirdofoz:20191124023236j:plain

モジュールの実行確認

python を起動して以下のコマンドを実行します。

import helloext
print(helloext)

インストールしたモジュールへのパスが表示されればインストールは成功しています。
f:id:bluebirdofoz:20191124023245j:plain

続けて以下の通り、関数を実行してみます。

helloext.printstrext()
helloext.sumargext(2, 4)

それぞれ"Hello World"の出力と、足し算の結果が出力されれば成功です。
f:id:bluebirdofoz:20191124023255j:plain

更に文字列の関数も実行してみます。
引数を渡すパターンと、渡さないパターンでそれぞれ戻り値を確認してみます。

ret_str = helloext.printarg()
print(ret_str)
ret_str = helloext.printarg("Hello Test")
print(ret_str)

引数を渡さない場合は"Default"、引数を渡した場合はその文字列が出力されれば成功です。
f:id:bluebirdofoz:20191124224001j:plain