Cython から C++ を使う

概要

Cython v0.13 から、大抵の C++ 言語に対するネイティブサポートが入りまし た。すなわち、これまで C++ クラスのラップに対して行なってきたトリック (http://wiki.cython.org/WrappingCPlusPlus_ForCython012AndLower で説明 しているような方法) が不要になりました。

Cython による C++ クラスのラップは、以前よりずっと素直にできます。 このドキュメントでは、新しく登場した C++ のコードのラップ方法について 詳しく説明します。

C++ に関する Cython v0.13 の新しい機能

以前のバージョンの Cython を使っている人のために、バージョン 0.13 で登 場した C++ サポートの主な機能を挙げます:

  • newdel といったキーワードで、 C++ のオブジェクトを動的に アロケートできます。
  • C++ オブジェクトをスタックアロケートできます。
  • 新しいキーワード cppclass を使って C++ クラスを宣言できます。
  • テンプレートクラスをサポートしました。
  • オーバロード関数をサポートしました。
  • C++ 演算子のオーバロード (operator+, operator[],...) をサポートしま した。

C++ のラップ手順概要

C++ ファイルをラップするための一般的な手順は、以下の通りです:

  • setup.py スクリプト全体か、ソースファイル指定オプションで、 言語を C++ に指定する。
  • .pxd ファイルを作成する。ファイルには cdef extern from ブロッ クと (あれば) C++ 名前空間名を指定する。ブロック中で、
    • cdef cppclass ブロックでクラスを宣言する。
    • public にする名前 (変数、メソッド、コンストラクタ) を宣言する。
  • 拡張モジュールを書き、 .pxd ファイルを cimport して、その中 の宣言を使う。

簡単なチュートリアル

C++ API の例

以下に小さな C++ API を示します。本節では、この API を例に使います。さ て、ヘッダファイルの名前は、 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);
    };
}

また、以下の実装を 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;
}

少々間抜けなプログラムですが、手順を示していくには充分な内容です。

setup.py で C++ 言語を指定する

Cython コードを setup.py でビルドするには、 cythonize() 関 数を使うのが最適です。distutils を使って Cython に C++ コードを生成・ コンパイルさせるには、 language="c++" オプションを渡す必要がありま す:

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

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

Cython は、まず、 rect.cpp というファイルを (rect.pyx から) 生成してコンパイルし、次に Rectangle.cpp (Rectangle ク ラスの実装) をコンパイルしてから、二つのオブジェクトファイルを rect.so にリンクします。 rect.so は Python から import rect で import できます (Rectangle.o のリンクを忘れ ると、ライブラリの import 中に、「シンボルが見つかりません (missing symbols)」というエラーを起こします)。

distutils へのオプションは、ソースファイルを経由して直接渡せます。この 方法が望ましいことが、よくあります。バージョン 0.17 以降、 Cython はソー スファイルを経由する方法で、外部のソースファイルを cythonize() に 渡せます。以下に、より簡単に書いた setup.py ファイルを示します:

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

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

上のように setup.py を書いておいて、 .pyx に中には以下のような 内容を、ファイル先頭のコメントブロックとして記述します。コメントブロッ クは、 Rectange.cpp に対して静的にリンクする必要のあるソースファ イルで、かつ C++ モードでコンパイルせねばならないコード全てに記述しま す。:

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

(make などを使って) 手動でコンパイルするには、 cython コマンド ラインユーティリティを使って C++ の .cpp ファイルを生成し、それを Python の拡張モジュールにコンパイルします。 cython コマンドの C++ モードをオンにするには、 --cplus オプションを使います。

C++ のクラスインタフェースを宣言する

C++ クラスをラップする手順は、 C の構造体をラップする手順によく似てい て、二つほど追加のポイントがあります。まずは、基本の cdef extern from ブロックから書き始めます:

cdef extern from "Rectangle.h" namespace "shapes":

このブロックの中で、 C++ クラス Rectangle を宣言できます。 namespace の宣言に注意してください。名前空間は、オブジェクトを完全 指定の名前で識別するためのもので、 ("outer::inner" のように) 入れ 子にしたり、クラスを指定したり ("namespace::MyClass" で、MyClass の静的メンバを宣言する) もできます。

cdef cppclass でクラスを宣言する

さて、 Rectangle クラスを extern from ブロックに追加しましょう。 クラス名を Rectangle.h からコピーしてきて Cython の書法に合わせる と、以下のようになります:

cdef extern from "Rectangle.h" namespace "shapes":
    cdef cppclass Rectangle:

public アトリビュートを追加する

次に、Cython で使うアトリビュートとメソッドを宣言する必要があります:

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)

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

ラップした C++ クラス型の変数を宣言する

さあ、 cdef でクラス型の変数を宣言して、 C++ の new 文と同時に使っ てみます:

cdef Rectangle *rec = new Rectangle(1, 2, 3, 4)
try:
    recLength = rec.getLength()
    ...
finally:
    del rec     # ヒープアロケートしたオブジェクトを削除する

「デフォルトの」コンストラクタを持っていれば、スタックアロケート型のオ ブジェクトも宣言できます:

cdef extern from "Foo.h":
    cdef cppclass Foo:
        Foo()

def func():
    cdef Foo foo
    ...

C++ と同様、クラスがただひとつしかコンストラクタを持たなければ、それが デフォルトコンストラクタになるので、特に宣言する必要はありません。

Cython のラッパクラスを作成する

ここまでで、 pyx ファイルの名前空間と、 C++ の Rectangle 型の インタフェースを公開できました。次は、 (本題の) このクラスを Python のコードからアクセス可能にする作業です。

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

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)

よし、これでできました。 Python から見れば、この拡張型は、普通に定義し た Rectangle クラスと同じルック&フィールを持つはずです。アトリビュー トアクセスの機能をつけたければ、プロパティも実装できます:

property x0:
    def __get__(self): return self.thisptr.x0
    def __set__(self, x0): self.thisptr.x0 = x0
...

高度な C++ 機能

この節では、上のチュートリアルで解説しなかった C++ 機能全てについて説 明します。

オーバロード

オーバロードはとても簡単です。メソッドを異なるパラメタで宣言して、使う だけです:

cdef extern from "Foo.h":
    cdef cppclass Foo:
        Foo(int)
        Foo(bool)
        Foo(int, bool)
        Foo(int, int)

演算子オーバロード

Cython は演算子のオーバロードに C++ を使います:

cdef extern from "foo.h":
    cdef cppclass Foo:
        Foo()
        Foo* operator+(Foo*)
        Foo* operator-(Foo)
        int operator*(Foo*)
        int operator/(int)

cdef Foo* foo = new Foo()
cdef int x

cdef Foo* foo2 = foo[0] + foo
foo2 = foo[0] - foo[0]

x = foo[0] * foo2
x = foo[0] / 1

cdef Foo f
foo = f + &f
foo2 = f - f

del foo, foo2

入れ子のクラス定義

C++ では、入れ子のクラス宣言が可能です。Cython でも、クラス宣言をネス トできます:

cdef extern from "<vector>" namespace "std":
    cdef cppclass vector[T]:
        cppclass iterator:
            T operator*()
            iterator operator++()
            bint operator==(iterator)
            bint operator!=(iterator)
        vector()
        void push_back(T&)
        T& operator[](int)
        T& at(int)
        iterator begin()
        iterator end()

cdef vector[int].iterator iter  # iter は vector<int>::iterator 型で宣言されている

入れ子の内側のクラスにも cppclass を使いますが、 cdef は必要な いので注意してください。

Python の文法と互換性のない C++ 演算子

Cython では、可能な限り標準の Python に近い文法を使おうとしています。 そのため、一部の C++ 演算子、例えば事前インクリメント演算子 ++foo や、参照渡し演算子 *foo には、 C++ と同じ文法を使えません。 Cython では、こうした演算子の記述を置き換えるための関数を、 cython.operator という特殊なモジュールで提供しています。 例えば、以下のような関数が定義されています:

  • cython.operator.dereference: デリファレンスです。 dereference(foo) は C++ のコード *(foo) を生成します。
  • cython.operator.preincrement: 事前インクリメントです preincrement(foo) は C++ のコード ++(foo) を生成します。
  • ...

これらの関数は cython.operator から cimport する必要があります。 もちろん、関数名を短く読みやすくするために、 from ... cimport as の形式を使えます。 例えば: from cython.operator cimport dereference as deref

テンプレート

Cython はテンプレートの表現にブラケット書法を使います。 C++ の vector クラスを以下に示します:

# import dereference and increment operators
from cython.operator cimport dereference as deref, preincrement as inc

cdef extern from "<vector>" namespace "std":
    cdef cppclass vector[T]:
        cppclass iterator:
            T operator*()
            iterator operator++()
            bint operator==(iterator)
            bint operator!=(iterator)
        vector()
        void push_back(T&)
        T& operator[](int)
        T& at(int)
        iterator begin()
        iterator end()

cdef vector[int] *v = new vector[int]()
cdef int i
for i in range(10):
    v.push_back(i)

cdef vector[int].iterator it = v.begin()
while it != v.end():
    print deref(it)
    inc(it)

del v

複数のテンプレートパラメタは、 [T, U, V] や [int, bool, char] のように、 リストで定義できます。

標準ライブラリ

/Cython/Includes/libcpp 下の pxd ファイルで、 C++ 標準ライブラリの コンテナクラスのほとんどを宣言しています。コンテナは、 deque, list, map, pair, queue, set, stack, vector です。

例えば:

from libcpp.vector cimport vector

cdef vector[int] vect
cdef int i
for i in range(10):
    vect.push_back(i)
for i in range(10):
    print vect[i]

/Cython/Includes/libcpp 下の pxd ファイルは、 C++ クラスの宣言方法 の良い例でもあります。

Cython 0.17 から、 STL コンテナは、対応する Python の組み込み型との間 で相互に型強制できます。変換は、型付けされた変数 (型付けされた関数の引 数を含む) への代入や、明示的なキャストによってトリガされます:

from libcpp.string cimport string
from libcpp.vector cimport vector

cdef string s = py_bytes_object
print(s)
cpp_string = <string> py_unicode_object.encode('utf-8')

cdef vector[int] vect = xrange(1, 10, 2)
print(vect)              # [1, 3, 5, 7, 9]

cdef vector[string] cpp_strings = b'ab cd ef gh'.split()
print(cpp_strings.get(1))   # b'cd'

以下の型強制が使えます:

Python 型 => C++ 型 => Python 型
bytes std::string bytes
iterable std::vector list
iterable std::list list
iterable std::set set
iterable (len 2) std::pair tuple (len 2)

どの変換でも、新しいコンテナを生成して、データをコピーします。 コンテナ内の要素は、適切な型に自動的に変換されます。 例えば string の map からなる vector のように、入れ子のコンテナに 対しては再帰的に変換を行います。

例外

Cython は C++ の例外を throw できず、 try-except でキャッチすることも できません。ただし、関数を定義する際に、 C++ の例外が送出されうること を宣言したり、例外を Python の例外に変換するよう宣言できます:

cdef extern from "some_file.h":
    cdef int foo() except +

このように宣言すると、 C++ のエラーを適切な Python の例外に翻訳しよう と試みます。翻訳は、以下の表の規則に従います (C++ の識別子から std:: プレフィクスを省略しています)。

C++ Python
bad_alloc MemoryError
bad_cast TypeError
domain_error ValueError
invalid_argument ValueError
ios_base::failure IOError
out_of_range IndexError
overflow_error OverflowError
range_error ArithmeticError
underflow_error ArithmeticError
(その他) RuntimeError

what() メッセージがある場合、その内容は維持されます。 C++ の ios_base_failure 例外は EOF を示している場合がありますが Cython から識別できるような充分な情報を伴わないので、 IO ストリームに 例外マスクを仕掛けて監視してください。

以下のコードは、何らかの C++ エラーを捕捉すると、 Python の MemoryError に置き換えて送出します (ここには、Python の例外なら何 でも置けます):

cdef int bar() except +MemoryError

下のコードは、 something_dangerous が C++ の例外を送出すると、 raise_py_error を呼び出します:

cdef int raise_py_error()
cdef int something_dangerous() except +raise_py_error

こうすると、 raise_py_error で C++ から Python の例外に「翻訳」す る処理をカスタマイズできます。 raise_py_error が何の例外も送出しな ければ、 RuntimeError を代わりに送出します。

静的メンバメソッド

Rectangle クラスに静的メンバがある場合:

namespace shapes {
    class Rectangle {
    ...
    public:
        static void do_something();

    };
}

以下のように、クラスの名前空間にある関数として宣言できます:

cdef extern from "Rectangle.h" namespace "shapes::Rectangle":
    void do_something()

注意点と、制限事項

C のみの関数へのアクセス

C++ のコードを生成する際、 Cython は関数を C++ の関数とみなして (extern "C" {...} を使わずに) 宣言し、呼び出します。 C の関数に C++ のエントリポイントがある場合には問題ありませんが、そうでない場合に はつまづくことでしょう。 C++ の Cython モジュールから pure C の関数を 呼び出したいなら、C++ のシム(くさび) モジュールを作成して、以下のよう な処理を書く必要があるでしょう:

  • extern "C" ブロックで 必要な C ヘッダを include する。
  • 最小限のフォワード用関数を C++ で書き、各々で pure C の関数を呼び出 す。

C++ の左辺値

C++ では、左辺値に使える参照を返す関数を定義できます。現状、Cython は この機能をサポートしていません。 cython.operator.dereference(foo) も、左辺値とはみなしません。