概要
C++ で書いているコードを、どうしてもPythonで呼べる形のライブラリにしたい。
ただ、そこにはCythonという大きな壁が立ちはだかっていた。
これは、 Pythonを爆速にする「Cython」チュートリアル: C++のコードがライブラリに依存しているとき。まずはCMake。 の続きになります。
ここまでで、gmpライブラリに依存したC++のプログラムを、cmakeを使って快適にコンパイルするところまでを見てきました。ここからやっと本題ですが、このC++プログラムをcythonizeするコードを書き、それをsetup.pyでコンパイルしていきます。
ここで使用したコードは、ここのgithub にありますので、興味のある方はご覧ください!
状況説明
前回の記事では、まずそもそもsetup.pyを書くために自分のC++のプログラムの依存関係についてきちんと理解しておく必要があったため、
- C++のプログラムを書くときにつまづきやすい、依存関係について
- CMakeを使ったC++のコンパイルについて
を解説しました。今回は、実際にC++の関数をCython化するコードを書き、前回のCMakeLists.txtを参考にsetup.pyを書き、Pythonから呼べるようにコンパイルします。
C++側のコードもおさらいとして載せます。
#include <gmp.h>
namespace my_library
{
class TestClass1{
public:
TestClass1();
void test_function1();
static void gmp_print_test();
};
} // namespace my_library
#include <iostream>
#include "TestClass1.h"
using namespace std;
namespace my_library{
TestClass1::TestClass1(){};
void TestClass1::test_function1(){
cout << "printed from cpp function" << endl;
}
void TestClass1::gmp_print_test(){
mpz_t test;
mpz_init(test);
mpz_set_ui(test, 1);
gmp_printf("print : %Zd \n", test);
}
}
仮に、このようなtest.cpp
があった場合、以下のCMakeLists.txt
でコンパイルできることを確かめましょう。
#include "TestClass1.h"
using namespace my_library;
int main(){
TestClass1::gmp_print_test();
}
cmake_minimum_required(VERSION 3.10)
project(TEST VERSION 1.1.0 LANGUAGES CXX)
# Executable will be in ../bin
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
set(CMAKE_CXX_FLAGS "-g -O0 -lgmp")
add_executable(test1
test.cpp
TestClass1.cpp
)
target_sources(test1
PRIVATE
)
target_include_directories(test1 PRIVATE /usr/local/gmp/6_1_2/include/)
target_link_libraries(test1 gmp /usr/local/gmp/6_1_2/lib)
とりあえずCython化
まず、cythonのコードを以下のように書いて、C++関数をラップします。
以前やったように、pxd ファイルはcythonのヘッダーファイル、pyxファイルはcythonのプログラム本体のファイルでした。
以下のようにC++をラップするように書きます。
cdef extern from "../cpp_library/TestClass1.h" namespace "my_library":
cdef cppclass TestClass1:
TestClass1()
void test_function1()
void gmp_print_test()
import cython
cimport cython
cdef class TestClass1Cython:
cdef TestClass1* ptr
def __cinit__(self):
self.ptr = new TestClass1()
def __deadaloc(self):
del self.ptr
def test_function1_cython(self):
self.ptr.test_function1()
@staticmethod
def gmp_print_test():
cdef TestClass1 testclass1
testclass1.gmp_print_test()
cdef extern from "../cpp_library/TestClass1.h" namespace "my_library":
cdef cppclass TestClass1:
TestClass1()
void test_function1()
void gmp_print_test()
import cython
cimport cython
include "test_class1.pyx"
def test():
return TestClass1Cython()
setup.py
依存するライブラリはsetup.pyでどのように書かれるのか。
ここから、setup.pyを、gmpライブラリに依存していることを明記して記述します。
実際、もうCMakeLists.txt
の書き方がわかっていれば、簡単な作業です。
見ていただければわかりますが、
libraries=["gmp"],
library_dirs=["/usr/local/lib", "/usr/local/gmp/6_1_2/lib"],
include_dirs=["/usr/local/gmp/6_1_2/include"],
のように、
- 依存するライブラリ
- ライブラリのオブジェクトファイルの場所
- ヘッダーファイルの場所
をCMakeLists.txtを書いた時のように書くだけです。
結果、以下のようなsetup.pyを作成しました。
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(
"my_library", sources=["./cython/my_library.pyx",
"./cpp_library/TestClass1.cpp"
],
libraries=["gmp"],
library_dirs=["/usr/local/lib", "/usr/local/gmp/6_1_2/lib"],
include_dirs=["/usr/local/gmp/6_1_2/include"],
language="c++",
extra_compile_args=['-std=c++1z',"-lgmp"]
),
]
setup(
name = 'my_library',
cmdclass = {'build_ext': build_ext},
ext_modules = cythonize(ext_modules)
)
以上が終われば、無事にgmpライブラリに依存したC++のプログラムをcython化出来ています。
python setup.py install
で試してみると、コンパイルが通りました。python側からきちんと呼べるか試してみます。
(myenv) root@e96f489c2395:/from_local/cython_practice# python
Python 3.6.3 (default, Jan 30 2020, 06:37:54)
[GCC 7.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import my_library
>>> my_library.TestClass1Cython.gmp_print_test()
print : 1
すばらしい。。
まとめ
今回は、C++側のコードがなんらかのライブラリ(例では gmpライブラリ)に依存している時の、Cythonize のやり方について解説し、実際にコンパイルすることができました。
次は、C++側のクラスオブジェクトを、Pythonのクラスオブジェクトに渡すやり方について解説していきます。
自分的には、この渡し方までわかれば大抵のことが出来てくると思うので、是非ご覧ください!!
参考になった、という方がおりましたら、ぜひLGTM、コメント、お待ちしております。
今回はこの辺で。
おわり。