Python バインディングを書いてみよう

この記事は The Qt BlogWrite your own Python bindings を翻訳したものです。
執筆: Alexandru, 2018年05月31日

こんにちは。

前回の記事では Qt ライブラリの Python バインディングを生成する方法について簡単に紹介しました。

今回は、自分のプロジェクトでバインディングを作る方法をちら見せしたいと思います。

Qt for Python には Shiboken というバインディング生成ツールが含まれることになりました。

以下の説明を読んでいただくことで、簡単な C++ のライブラリから Python バインディングが生成する方法を理解することができるでしょう。みなさんがお持ちのカスタムライブラリも、この手法でバインディング化していただけたらいいなと思っています。

Qt 関連の様々なプロジェクト同様、Shiboken に対するコントリビューションも大歓迎です。みんなの幸せにつながる改善をお待ちしております。

ライブラリのサンプル

icecream

今回の記事では、あまり中身のない Universe と呼ばれるカスタムライブラリを使用します。このライブラリは IcecreamTruck という2つのクラスを提供します。

アイスクリームには様々な味があります。トラックは近所の子供たちにアイスクリームを配達するための乗り物です。とっても簡単ですね?

このクラスを Python から使いたいわけです。例えば、新しい味のアイスクリームを発明したり、配達が問題なく完了したかを確認したいと思います。

というわけで、これから Icecream と Truck の Python バインディングを作って、自分の Python スクリプトから使えるようにしたいと思います。

わかりやすく伝えるために説明を省略することもありますが、すべてのソースコードは pyside-setup/examples/samplebinding から入手可能になっていますので、ぜひご確認ください。

C++ のライブラリの詳細

まずは、Icecream のヘッダーファイルを見てみましょう。

class Icecream
{
public:
Icecream(const std::string &flavor);
virtual Icecream *clone();
virtual ~Icecream();
virtual const std::string getFlavor();

private:
std::string m_flavor;
};

次は、Truck のヘッダーファイルです。

class Truck {
public:
Truck(bool leaveOnDestruction = false);
Truck(const Truck &other);
Truck& operator=(const Truck &other);
~Truck();

void addIcecreamFlavor(Icecream *icecream);
void printAvailableFlavors() const;

bool deliver() const;
void arrive() const;
void leave() const;

void setLeaveOnDestruction(bool value);
void setArrivalMessage(const std::string &message);

private:
void clearFlavors();

bool m_leaveOnDestruction = false;
std::string m_arrivalMessage = "A new icecream truck has arrived!\n";
std::vector<Icecream *icecream> m_flavors;
};

ほとんどの API は簡単に理解できると思いますが、重要な点をまとめます。

  • Icecream は派生クラスを用いて拡張できるように設計しています
  • getFlavor() は派生クラス側で実際の味を提供する予定です
  • Truck は値型で内部でポインタを保持するため、コピーコンストラクタ等も実装されています
  • Truck は運んでいるアイスクリームの一覧を保持し、addIcecreamFlavor() 経由で追加も可能です
  • Truck の到着メッセージは setArrivalMessage() でカスタマイズが可能です
  • deliver() は、アイスクリームの配達が問題なく完了したかを知らせます

Shiboken の型システム

バインディングをしたい API を shiboken に伝えるために、関連する型を一通り提供するヘッダーファイルを用意します。

#ifndef BINDINGS_H
#define BINDINGS_H
#include "icecream.h"
#include "truck.h"
#endif // BINDINGS_H

これに加え、型情報を定義するためのXMLファイルも必要で、ここで C++ の型と Python の型の関係を定義します。

<?xml version="1.0"?>
<typesystem package="Universe">
<primitive-type name="bool"/>
<primitive-type name="std::string"/>
<object-type name="Icecream">
<modify-function signature="clone()">
<modify-argument index="0">
<define-ownership owner="c++"/>
</modify-argument>
</modify-function>
</object-type>
<value-type name="Truck">
<modify-function signature="addIcecreamFlavor(Icecream*)">
<modify-argument index="1">
<define-ownership owner="c++"/>
</modify-argument>
</modify-function>
</value-type>
</typesystem>

まず、最初に重要な点は、"bool""std::string"" を プリミティブ型 として宣言していることです。

いくつかの C++ のメソッドで引数や返り値の型として使われているため、それを定義する必要があります。これにより、C++ と Python の変換のコードが生成されます。

ほとんどの C++ のプリミティブ型は追加のコードなしに shiboken で処理がなされます。

次に、前述の2つのクラスを宣言します。片方は「オブジェクト型」として、もう片方は「値型」として宣言します。

この二つの主な違いは、生成されるコード内で、オブジェクト型はポインタとして扱われ、値型はコピーされます。

この型情報定義ファイルでクラスの名前を指定することで、shiboken は自動的にそのクラスが提供するすべてのメソッドのバインディングを生成するため、すべてのメソッド名を手動で記述する必要はありません。

この関数を修正したい場合があるかもしれませんが、とりあえず次のトピックである オーナーシップルール に移りましょう。

Shiboken は、Python で確保された C++ のオブジェクトの解放の責任の所在を知る術は持っていません。推定は可能ですが、常に正しいとは限りません。

色々なパターンが存在します。Python 側で、Python オブジェクトの参照カウントがゼロになった場合に C++ のメモリを解放したいというケース。C++ のライブラリで適切に解放処理がなされるため、Python 側ではとくに何もする必要がない場合。もしくは、(QWidgets のように)別の親オブジェクトが解放の責任者になる場合。

今回の場合、clone() メソッドは C++ のライブラリ内部でのみ呼ばれるため、C++ が複製したオブジェクトの解放の面倒をみるということにしましょう。

addIcecramFlavor() では、Truck が Icecream オブジェクトを保持し、Truck 自体が破棄された場合にそれらを削除するようになっています。このため、オーナーシップは "c++" に設定します。

もしオーナーシップのルールを設定しない場合は、対応する Python の名前がスコープ外に行ったタイミングで C++ のオブジェクトは破棄されるでしょう。

ビルド

Universe ライブラリをビルドしバインディングを生成するために、詳細なドキュメントと、みなさんも利用できるようなごく一般的な CMakeLits.txt ファイルを提供しています。

これにより "cmake ." を実行するだけで、プロジェクトの設定を行い、選択したツールチェイン( '(N)Makefile' を推奨します) を使ってビルドします。

プロジェクトのビルド結果として、libuniverse.(so/dylib/dll)Universe.(so/pyd) の2つの共有ライブラリが生成されます。

前者は C++ のライブラリで、後者は Python スクリプトから import するための Python のモジュールになります。

もちろん、shiboken は(バインディングを生成するのに必要な .h / .cpp といった)中間ファイルも生成します。何かしらの問題でコンパイルが通らないとか、振る舞いがおかしいといった場合以外にはこれらのファイルを気にする必要は特にありません。そういう場合にはバグレポートも送ってくださいね!

詳細なビルド手順やつまづきそうなところ(特に Windows で)に関しては README.md ファイルをご覧ください。

そして、いよいよ Python 側です。

ビルドした Python モジュールを使う

以下の短いコードで、Universe モジュールを使用し、Icecream の派生クラスで仮想メソッドを実装し、インスタンスを生成してみましょう。

from Universe import Icecream, Truck

class VanillaChocolateIcecream(Icecream):
def __init__(self, flavor=""):
super(VanillaChocolateIcecream, self).__init__(flavor)

def clone(self):
return VanillaChocolateIcecream(self.getFlavor())

def getFlavor(self):
return "vanilla sprinked with chocolate"

class VanillaChocolateCherryIcecream(VanillaChocolateIcecream):
def __init__(self, flavor=""):
super(VanillaChocolateIcecream, self).__init__(flavor)

def clone(self):
return VanillaChocolateCherryIcecream(self.getFlavor())

def getFlavor(self):
base_flavor = super(VanillaChocolateCherryIcecream, self).getFlavor()
return base_flavor + " and a cherry"

if __name__ == '__main__':
leave_on_destruction = True
truck = Truck(leave_on_destruction)

flavors = ["vanilla", "chocolate", "strawberry"]
for f in flavors:
icecream = Icecream(f)
truck.addIcecreamFlavor(icecream)

truck.addIcecreamFlavor(VanillaChocolateIcecream())
truck.addIcecreamFlavor(VanillaChocolateCherryIcecream())

truck.arrive()
truck.printAvailableFlavors()
result = truck.deliver()

if result:
print("All the kids got some icecream!")
else:
print("Aww, someone didn't get the flavor they wanted...")

if not result:
special_truck = Truck(truck)
del truck

print("")
special_truck.setArrivalMessage("A new SPECIAL icecream truck has arrived!\n")
special_truck.arrive()
special_truck.addIcecreamFlavor(Icecream("SPECIAL *magical* icecream"))
special_truck.printAvailableFlavors()
special_truck.deliver()
print("Now everyone got the flavor they wanted!")
special_truck.leave()

Universe モジュールからクラスをインポートし、2つの異なる "風味" の Icecream の派生クラスを作成しました。

それからトラックを生成し、普通の味のアイスクリームと、2つの特別な味のアイスクリームを積載しました。

そしてそのアイスクリームを配達します。

もし配達に失敗した場合には、新しいトラックを生成し、古いトラックからのアイスクリームをコピーし、新たな「特別な味」のアイスクリームをお詫びとして追加します。

上記のコードで、C++ のクラスの派生の仕方や仮想メソッドのオーバーライド、オブジェクトの生成と破棄を簡潔に紹介できたのではないかと思います。

前述の通り、すべてのソースコードと詳細なビルド手順は pyside-setup/examples/samplebinding のリポジトリから入手してください。

今回の一連の手順で、Shiboken の便利さが伝わりましたでしょうか。これを利用して私たちは Qt for Python を作っていますし、みなさんも同じような事が可能になりますよ!

Happy binding!


Blog Topics:

Comments