チラシの裏は意外と白くない

最近忘れっぽくなったので調べたことをチラシの裏に書きます

pythonからC++を呼び出して、jsonファイルをやり取りする①

背景

  • 新しいデータベースの形としてjsonを採用した
  • jsonといえばpython
  • 過去資産はC++で書かれているし、bit精度を考慮するとC++を使いたい

というようなせめぎあいの結果、とりあえず方式を調査することに。 あとで絶対わからなくなるので、ここにまとめることにする。

なお、Pythonは3.x系を対象とする。手元の環境ではPython 3.6.8。

方針

大きく2点の課題を解決する必要がある。

  1. pythonからC++の関数を呼び出す方法
  2. C++jsonを解釈する方法

調べたところ、それぞれいくつかの方法はあるようだが、どれをとっても面倒な感じだ。とりあえず、

  • 課題1に対してはpython公式のAPI(Python.h)を利用
  • 課題2に対してはjassonというC++用のjsonパーサを利用

という作戦で進めたい。

まずはpythonからAPIを介してC++関数を呼び出す方式の調査と、プロトタイプ実装から。

pythonからC++を呼ぶ方法:Python.hを利用

大きな流れは、

  1. C++側(core.cpp)でPython APIであるPython.hをインクルード
  2. 処理本体と(Pythonの)モジュール化のためのwrapper記述を追加
  3. 共有ライブラリ(.so)としてコンパイル
  4. python側でモジュールとしてimportして利用

f:id:samurai375:20210120130025j:plain

ということのようだ。大きいのは2番と3番。

Python.hのインクルード

手元の環境ではPython.hは以下のパスに存在した。

/usr/include/python3.6m/Python.h

ただ、後述するsetup.pyを用いてコンパイルを実行することで、このパスを明示的に指定する必要はない。

python用wrapper記述

C処理本体

int c_add(int num1, int num2){
    int res = num1 + num2;
    return res;
}

int c_sub(int num1, int num2){
    int res = num1 - num2;
    return res;
}

int c_mul(int num1, int num2){
    int res = num1 - num2;
    return res;
}

int c_div(int num1, int num2){
    int res = int(num1 / num2);
    return res;
}
  • 処理本体は基本的にそのままC++で記述すればOK
  • 引数や戻り値については、wrapper部を対応した記述に変更する必要がある
    • あらゆる型を取ることができるかは要調査
    • ユーザ定義した構造体のポインタとかも利用可能?

python wrapper

static PyObject *py_add(PyObject* self, PyObject* args){
    int num1, num2, res;

    if(!PyArg_ParseTuple(args, "ii", &num1, &num2)) return NULL;
    res = c_add(num1,num2);
    return Py_BuildValue("i",res);
}
  • cpp関数を呼び出すpython wrapper
    • py_addという名称にしている
  • 4行目で引数のチェックを行う
    • "ii"はint型引数を2つ取り、
    • それぞれがnum1, num2に対応する
    • 引数が不正ならNULLを返して抜ける
  • 5行目でc_addにnum1,num2を渡して、結果をresで受ける
  • 6行目でint型のresを返す
  • py_sub,py_mul,py_divも同様のため省略

メソッド登録

static PyMethodDef my_arith_methods[]={
    {"add", py_add, METH_VARARGS, "return num1 + num2"},
    {"sub", py_sub, METH_VARARGS, "return num1 - num2"},
    {"mul", py_mul, METH_VARARGS, "return num1 * num2"},
    {"div", py_div, METH_VARARGS, "return int(num1 / num2)"},
    {NULL}
};
  • 各メソッドの
    • 要素1,2番目:上記でwarpした関数"py_add"を"add"というメソッド名で登録
    • 要素3番目:引数によって変わる。いくつかあるので継続調査
      • METH_NOARGS: 引数無し
      • METH_VARARGS: 可変長引数
    • 要素4番目:メソッドの説明。任意
  • 最終メソッドはNULL

モジュール定義

static PyModuleDef my_arith = {
    PyModuleDef_HEAD_INIT,
    "my_arith",                 // モジュール名
    "arithmetic operations",    // モジュールの説明
    -1,
    my_arith_methods
};
  • 1行目がモジュール名となる。今回my_arith。
  • 構造体のメンバーで
    • 1番目はおまじない
    • 2番目はモジュール名。1行目と合わせておく
    • 3番目はモジュールの説明
    • 4番目はおまじない
    • 5番目はメソッドリスト名と合わせておく

初期化処理

PyMODINIT_FUNC PyInit_my_arith(void){
    return PyModule_Create(&my_arith);
}
  • 初期化処理で関数名はPyInit_<module名>
  • 戻り値はPyModule_Create($<module名>

共有ライブラリ化

  • g++ ...でビルド記述をべたに書くこともできるが、setup.pyを使うのが楽そう

setup.py記述

from setuptools import setup, Extension

setup(
    name = 'my_arith', 
    version = '1.0.0', 
    ext_modules = [Extension('my_arith',['core.cpp'])]
)
  • 基本的にモジュール名を変更するだけ
  • my_arith.soができる
  • ソースコードが複数ある場合は、['core.cpp', 'srcA.cpp', 'srcB.cpp'…]と続ける

setup.py実行

$ python3 setup.py build
  • setup.pyと同じ階層にbuild/ディレクトリが生成される
  • 以下のような階層構造となり、my_arith.cpython-36m-x86_64-linux-gnu.soが共有ライブラリ
  • いろいろついてしまうので、上位階層に持ってきてシンプルにmy_arith.soに名前を変えておく
.
├── build
│   ├── lib.linux-x86_64-3.6
│   │   └── my_arith.cpython-36m-x86_64-linux-gnu.so
│   └── temp.linux-x86_64-3.6
│       └── core.o
├── core.cpp
└── setup.py

pythonから呼び出し

ソースコード

from core import my_arith

def main():
  num1 = 6
  num2 = 3

  print(my_arith.add(num1,num2))
  print(my_arith.sub(num1,num2))
  print(my_arith.mul(num1,num2))
  print(my_arith.div(num1,num2))

  return 1

if __name__=='__main__':
  main()
  • coreディレクトリ以下のmy_arithモジュールをインポート
  • my_arithモジュールのadd,sub,mul,divメソッドを呼び出し
    • wrap関数名ではなく、メソッド登録時の名称を使用する

実行結果

$ ./top.py 
9
3
3
2
  • というので、無事四則演算ができている

ちなみに

  • pythonからhelp(my_arith)をすると、以下の通りモジュールのヘルプが表示される
  • モジュール定義の章で記載した説明が、モジュールの説明として表示される
  • メソッド登録時の説明が、各FUNCTIONに表示される
Help on module core.my_arith in core:

NAME
    core.my_arith - arithmetic operations

FUNCTIONS
    add(...)
        return num1 + num2
    
    div(...)
        return int(num1 / num2)
    
    mul(...)
        return num1 * num2
    
    sub(...)
        return num1 - num2

FILE
   arith/core/my_arith.so

まとめ

  • とりあえず単純なint型引数2つを四則演算して、int型戻り値を返すC++関数をpythonモジュール化してpythonから呼び出し利用した
  • C++関数として、どこまでの引数や戻り値に対応しているか。特にポインタを取り扱えるか、は継続調査
  • pythonに関して、今回でいえば特にモジュール周りの体系的な知識がないので時間があるときに勉強しておく
  • 続いて、C++用のjsonパーサであるjanssonの調査を行う