cython による C++ ラッピング

Last Change: 13-Nov-2015.
author : qh73xe

このページでは、 cython を使用して C++ のラッパーを書く方法を記述します。 とはいえ、書いていく内容は大抵公式サイトにあることですが、ここでは基本的なことも 省略せずに書いていこうと考えています。

細かいことは一旦おいて置き、 とりあえず、 C++ で宣言をしたコードが python で使えることを確認して行きましょう。

サンプル C++

サンプルに使用するのは以下の C++ コードです。

Rectangle.cpp
 #include "Rectangle.h"

 using namespace shapes;

 Rectangle::Rectangle(int X0, int Y0, int X1, int Y1)
 {
     x0 = X0;
     y0 = Y0;
     x1 = X1;
     y1 = Y1;
 }

 Rectangle::~Rectangle()
 {
 }

 int Rectangle::getLength()
 {
     return (x1 - x0);
 }

 int Rectangle::getHeight()
 {
     return (y1 - y0);
 }

 int Rectangle::getArea()
 {
     return (x1 - x0) * (y1 - y0);
 }

 void Rectangle::move(int dx, int dy)
 {
     x0 += dx;
     y0 += dy;
     x1 += dx;
     y1 += dy;
 }

凄く単純な四角形を定義しています。 この四角形クラスは x0, x1 と y0, y1 を持ち、 インスタンス化されると、 長さとか、高さとか、面積を計算できます。 ついでに move を使用すると、座標軸を移動することも可能です。

で ヘッダーファイルは以下のように作成します。 namespace (c++ にあまり詳しくない場合はとりあえずパッケージ化していると思ってください)、 shapes に 上で作成しているクラスや、関数を読み込ませておきます。

Rectangle.h
namespace shapes {
    class Rectangle {
    public:
        int x0, y0, x1, y1;
        Rectangle(int x0, int y0, int x1, int y1);
        ~Rectangle();
        int getLength();
        int getHeight();
        int getArea();
        void move(int dx, int dy);
    };
}

setup.py の準備

で、ここからが cython の話です。 まずは、 build 用の setup.py を書いて行きましょう。 setup.py の書き方は2通りあります。 一番ベーシックな書き方は以下の通りです

setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize(
        "rect.pyx",                 # Cython ソース
        sources=["Rectangle.cpp"],  # その他のソースファイル
        language="c++",             # C++ コードを生成させる
    )
)

ここで重要なのは、cythonize 関数です。 ここにラッパーとなる pyx ファイル名、C++ のソースファイル、そして C++ をコンパイルするという宣言を記述します。

一方で、distutils へのオプションは、ソースファイルを経由して直接渡すことも可能です。 これを利用するとより簡潔に setup.py を記述可能です。

setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(
    name = "rectangleapp",
    ext_modules = cythonize('*.pyx'),
)

この場合、.pyx(前者の setup.py では rect.pyx と宣言しています。一方後者の場合、そのディレクトリにある全ての .pyx がコンパイルの対象になります) の中に以下のような記述が必要になります。

# distutils: language = c++
# distutils: sources = Rectangle.cpp

このサイトでは基本的に後者の方法を使ってコードを記述していきます。

ラッパーの作成

最後に cython のラッパーコードを書いていきましょう。 ここで行う作業は大きく分て、2つあります。

  • C++ のクラスインタフェースの宣言
  • Cython のラッパクラス の作成

それぞれ順番に見ていきます。

インターフェースの宣言

まず、そもそも C++ のコードから何を cython で使用したいのかを考え、宣言を行います。 これをインターフェースの宣言と呼ぶのだと思えばよいかと。

今回の場合、以下の3つを cython から扱いたいわけです。

  • Rectangle というクラス
  • Rectangle がもつ属性値
  • Rectangle がもつ関数

これは cython では以下のように表現します。

rect.pyx
# distutils: language = c++
# distutils: sources = Rectangle.cpp
cdef extern from "Rectangle.h" namespace "shapes":
    cdef cppclass Rectangle:
        Rectangle(int, int, int, int) except +
        int x0, y0, x1, y1
        int getLength()
        int getHeight()
        int getArea()
        void move(int, int)

まず一行目ですが、ここで、どのヘッダーファイルのどの名前空間を使用するのかを宣言しています。 そして今回対象になる Rectangle はクラスなので、二行目では cppclass を宣言しています。 このクラス内で後に使用したい属性や関数を宣言します。

except + が気になるかもしれません。 この宣言によりコンストラクタの C++ コードの中や、オブジェクトのメモリアロケーションの過程で、何らかの障害によって例外が送出されると、Cythonは適切な Python の例外を安全に送出します。

ラッパクラス の作成

ではラッパクラスを作成していきます。 これを作成することにより(ようやく) python から Rectangle クラスにアクセスできるようになります。 この手のプログラミングでよくあるのが、 Cython の拡張型を作っておき、 C++ のインスタンスポインタを thisptr のようなアトリビュートに持たせて、 メソッド呼び出しを転送する一連のメソッド群をつくるというものです。 Python の拡張型の実装は、下記のようになります。

rect.pyx
...

cdef class PyRectangle:
    cdef Rectangle *thisptr  # ラップ対象の C++ インスタンスを保持する

    def __cinit__(self, int x0, int y0, int x1, int y1):
        self.thisptr = new Rectangle(x0, y0, x1, y1)

    def __dealloc__(self):
        del self.thisptr

    def getLength(self):
        return self.thisptr.getLength()

    def getHeight(self):
        return self.thisptr.getHeight()

    def getArea(self):
        return self.thisptr.getArea()

    def move(self, dx, dy):
        self.thisptr.move(dx, dy)

... の部分には先に記述したインターフェースの宣言が入ると思ってください。

buld と python からの使用方法

でここまで作成が終われば、あとはビルドをすると、 一般的な python ライブラリと同様 import を行うことが可能になります。 build は以下のコマンドを使用します。

python setup.py build_ext -i

これを実行すると、 build ディレクトリと .so ファイルが作成されます。 これで Rectangle.cpp のラッパーが作成できました。

例えば以下のように使用可能です。

test.py
from rect import PyRectangle

rect = PyRectangle(1, 1, 2, 2)
print(rect.getArea())