Cython で Python ⇔ C++ のインタフェースを作る
以前、Python と C++ を繋ぐためには SWIG を使ってたが、
Cython 0.13 以降では Cython 自身で Python と C++ を繋ぐことができる。 (2018/05時点で 0.28)
この機能を試してみた。
ここまで来てようやく cythonize を知ったので、ビルドはそちらで。
環境前提
- Ubuntu 16.04
- Python 3.5
- Cython 0.28
- その他必要なものはインストール済みとする。
サンプル概要
2つのクラスを作成し、連携して動作させてみる簡単なサンプル。
1. file_reader ... 1行1整数のデータを読み込んでC++メモリ上で保持。
2. calculator ... file_reader をメンバとして持ち、中のデータを使って計算。
ファイル構成
- setup.py ... ビルド設定ファイル - file_read.pyx ... cython ソースコードファイル - FileReader.cpp ... C++ クラス実装 - FileReader.h ... C++ クラス定義 - test.txt ... サンプルデータファイル
実装
setup.py
cythonize を使ってビルドするための設定を定義
0.17 以降は
- 基本的な設定 → setup.py
- C++コードとのリンク情報 → .pyx
に区分けされている。
from distutils.core import setup from Cython.Build import cythonize setup( name = "file_read_app", ext_modules = cythonize("*.pyx"), extra_compile_args=["-std=c++11"], extra_link_args=["-std=c++11"], )
(2018/05/31追記)
実は上記C++11設定は効いていなかったことが判明。。
以下 export したのが効いてた様子。ビルド前に export で設定追加しておく。
上記設定は古いっぽい?
$ export CFLAGS='-std=c++11'
Tips
- extra_compile_args や extra_link_args でオプションを追加。C++11設定を追加しておく。
C++ コード
C++ 側で実際の処理を作る。
FileReader.h
#include <fstream> #include <iostream> #include <string> #include <vector> namespace reader { class FileReader { public : FileReader(std::string filePath); ~FileReader(); public : int getSize(); int getData(int index); void add(int datum); public : std::string filePath; private : std::vector<int> data; }; class DataCalc { public : DataCalc(FileReader *fileReader); ~DataCalc(); public : int sum(); private : FileReader *fileReader; }; }
FileReader.cpp
#include "FileReader.h" using namespace reader; FileReader::FileReader(std::string filePath) { // Read file and save data std::cout << "Read file : " << filePath << std::endl; this->data = std::vector<int>(); std::ifstream ifs(filePath.c_str()); if (ifs.fail()) { std::cout << "Error : Failed to open file." << std::endl; return; } std::string str; while (getline(ifs, str)) { data.push_back(std::stoi(str)); } } FileReader::~FileReader() {} int FileReader::getSize() { return this->data.size(); } int FileReader::getData(int index) { if (index >= this->data.size()) { std::cout << "Error: Index number too large (" << index << " < " << this->data.size() << ")." << std::endl; return -1; } return this->data[index]; } void FileReader::add(int datum) { this->data.push_back(datum); } DataCalc::DataCalc(FileReader *fileReader) { this->fileReader = fileReader; } DataCalc::~DataCalc() {} int DataCalc::sum() { int ret = 0; int size = this->fileReader->getSize(); for (int i = 0; i < size; i++) { ret += this->fileReader->getData(i); } return ret; }
Cython コード
Cython 上では C++ のクラスを呼ぶため、こちらでもクラス定義する必要ある (これだけちょっと面倒。。。)
Python ⇔ C++ のインタフェース定義もここで。
file_read.pyx
# distutils: language = c++ # distutils: sources = FileReader.cpp from libcpp.string cimport string cdef extern from "FileReader.h" namespace "reader" : cdef cppclass FileReader : FileReader(string) except + int getSize() int getData(int) void add(int) cdef cppclass DataCalc : DataCalc(FileReader*) except + int sum() cdef class PyFileReader : cdef FileReader *thisptr def __cinit__(self, bytes path) : cdef string s_path = path self.thisptr = new FileReader(s_path) def __dealloc__(self) : del self.thisptr def get_size(self) : return self.thisptr.getSize() def get_data(self, int index) : return self.thisptr.getData(index) def add(self, int datum) : self.thisptr.add(datum) cdef class PyCalc : cdef DataCalc *thisptr def __cinit__(self, PyFileReader file_reader) : self.thisptr = new DataCalc(file_reader.thisptr) def __dealloc__(self) : del self.thisptr def sum(self) : return self.thisptr.sum()
Tips :
- 「except +」はおまじない的に付けとけばよい (本家ページに理由書いてある)
- 文字列の扱い
- Python から入ってくるものは (型推論に期待しても良いがせっかくなので) bytes 型で受け取る。
- C++ には string で渡したいので、変換かける。
ビルド
ビルドしてエラー起きなければ OK 。
「-i」付けることで、ビルド実行したディレクトリ内に .so ファイルが生成される。
python3 setup.py build_ext
実行
test.txt
30 40 50 110
ipython で実行
$ ipython3 Python 3.5.2 (default, Nov 23 2017, 16:37:01) Type 'copyright', 'credits' or 'license' for more information IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help. In [1]: import file_read In [2]: reader = file_read.PyFileReader(b"test.txt") Read file : test.txt In [3]: reader.get_size() Out[3]: 4 In [4]: reader.get_data(2) Out[4]: 50 In [5]: c = file_read.PyCalc(reader) In [6]: c.sum() Out[6]: 230
できた!!
トラブルシューティング的な
- XXXX.pyx と XXXX.cpp (C++ソースコード) の名前を同じにしてはいけない。
- pyx コンパイル時に XXXX.cpp が作り出され、重複定義と言われてしまう。
- コンパイルに成功したが、python で import 時に「undefined symbol」のエラーで読み込み失敗。
- クラス内デストラクタを定義したはいいものの実装していなかったのが原因。定義と実装揃ってないとエラー出る。(わかりやすいエラー出してくれ。。。)
その他
参照
- 英語だけど最新の情報を見た方が参考になる。(有志による和訳版あるが0.17で止まってる)