概要
c++ で書いているコードを、どうしてもpythonで呼べる形のライブラリにしたい。
ただ、そこにはcythonという大きな壁が立ちはだかっていた。
闇の深いライブラリの依存関係、発狂したくなる環境構築。
どうすればいいかわからない型の変換。
自分がpythonを書いているのか、c++を書いているのか、何をやっているのかわからなくなるあの絶望感。
少しだけcythonを触る時間が増えたので、少しずつまとめて行きたいと思います。
なお、私はpython, c++, cythonどれも初心者レベルなので、間違いなどありましたらどうぞご指摘よろしくお願いいたします。
内容
今回は、とても簡単なc++のコードを、cythonizeしてライブラリとしてpythonから呼ぶことを目標にしてみます。
ミニマムなフォルダ構成は、以下になります。
hoge@~/Documents/cython_test> tree .
.
├── cython
│ ├── library_cython.pyx
│ ├── test_library.pxd
│ └── test_library.pyx
├── my_library
│ ├── test.cpp
│ └── test.h
└── setup.py
2 directories, 6 files
setup.py
まず、ライブラリのビルドに必要なsetup.pyです。
test_library
がpythonで読み込むライブラリの名前になります。
つまり、ビルドした後は import test_library
とかけるわけです。
from setuptools import setup, Extension, find_packages
from Cython.Build import cythonize
from Cython.Distutils import build_ext
from distutils import sysconfig
ext_modules = [
Extension(
"test_library", sources=[
"./cython/test_library.pyx",
"./my_library/test.cpp"
],
language="c++"
)
]
setup(
name = "test_library",
cmdclass = {"build_ext": build_ext},
ext_modules= cythonize(ext_modules)
)
このとき、
Extension(
"test_library", sources=[
"./cython/test_library.pyx",
setup(
name = "test_library",
で出てくる3つのtest_library
のところの名前は一緒にしておきましょう。ここでハマりました。
c++側のコード
c++側に、TestClass
というクラスを作っておきます。これをcython経由でpythonから呼びます。
簡単のため、TestClassはコンストラクタと、test_function1()
というメソッドだけ持たせておきます。
このとき、namespaceをきちんとつけておきましょう。理由はよくわかりませんが、cython化するときにこの方が都合が良くなります。ここでは、my_library
というnamespaceにしました。
namespace my_library
{
class TestClass{
public:
TestClass();
void test_function1();
};
} // namespace my_library
ヘッダで定義された関数の中身を書きます。test_function1()
は、単にプリントをする関数にしました。
#include <iostream>
#include "test.h"
using namespace std;
namespace my_library{
TestClass::TestClass(){};
void TestClass::test_function1(){
cout << "printed from cpp function" << endl;
}
}
cython側のコード
setup.pyにも出てくるように、test_library.pyx
が、メインのcythonファイルになります。
後述のlibrary_cython.pyx
をインクルードしています。
import cython
cimport cython
include "library_cython.pyx"
def test():
return TestClassCython()
pxd
ファイルは、test_library.pyxに必要な情報をc++から渡すための定義ファイルのようなものです。ここで実際に、c++のヘッダで定義されたことを再度、cythonの形で書く必要があります。
cdef extern from "../my_library/test.h" namespace "my_library":
cdef cppclass TestClass:
TestClass()
void test_function1()
この、library_cython.pyx
で、python側で実際に呼び出すpythonの(cythonの?)クラスを定義して行きます。最初戸惑うのは、クラスのメンバ変数として、c++で定義されたクラスのポインタを保持しておくことです。コンストラクタでは、c++側のクラスをコンストラクトし、そのアドレスをこのメンバ変数のptrに渡します。デストラクタは、delでこのptrを消去しています。
また、test_function1_cython()
メソッドは、 ptr.test_function1()
とすることで、c++側で定義されたメソッドを内部的に呼び出しています。
import cython
cimport cython
cdef class TestClassCython:
cdef TestClass* ptr
def __cinit__(self):
self.ptr = new TestClass()
def __deadaloc(self):
del self.ptr
def test_function1_cython(self):
self.ptr.test_function1()
ビルドできたら、自分を褒めてあげよう。
以上が準備できた段階で、
python setup.py install
とします。うまくビルドできたら、pythonのインタプリタに入り、
Python 3.6.7 (default, Nov 16 2019, 21:57:19)
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.11.45.5)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import test_library
>>> a = test_library.test()
>>> a.test_function1_cython()
printed from cpp function
>>>
とできたら、ちょっと嬉しいよね。
まとめ
というわけで、cythonを使って、c++で書いたクラスを、pythonからライブラリを経由して呼ぶことができるところまでやってみました。
これはまだ序の口で、例えばc++でコンパイルの必要なライブラリを使っている時など、依存関係がややこしくなったりしますが、もうすこし闇の深いケースに関してはまた書きたいと思います。
参考になった方がいましたら、「いいね」や「コメント」をお待ちしております!!
おわり。