LoginSignup
19
19

More than 5 years have passed since last update.

C++しか書かずにPythonライブラリを作る

Last updated at Posted at 2018-12-08

前日の記事は@seshimaruさんによる「GTK+3で別スレッドから画面を更新する」、
翌日の記事は@Gacchoさんによる「C言語とC++の同時比較リファレンス」です。

Introduction

C++アドベントカレンダーを読んでいる方はC++が大好きなのでご存じないかもしれませんが、人の世ではC++は矮小な人類の理解を超越した深淵から来たりし闇の言語と呼ばれ恐れられています。しかし邪教の力は凄まじく、中には人の身ながらその力を借りたいと願う者もいると聞きます。そういうわけで、人の世で受け入れられているPythonにも、C++の闇の力を纏えるようにしようと思うわけです。

こういう多方面から怒られそうなノリは最初の1パラグラフだけです。

今回は、pybind11を使ってC++で書かれたコードをpythonから呼び出していけるようにしたいわけですが、インストール方法やチュートリアル、他のバインディング方法との比較などはあまり触れません。というのも、その辺りは参考にリストした記事や当然ながら公式ドキュメントで既によくまとまっているからです。ここではもう少し細かいTips集を、記事として体裁を整えて書いていきます。

説明を簡単にするため、画像ファイルを読み込むライブラリのバインディングを書いているということにしましょう。非常に大雑把に、以下のような構成要素があることにします。実装の細かいところは、本筋ではないので飛ばします。

enum class pixel_tag {rgb, rgba};

template<typename T> basic_rgb_pixel  {T r, g, b;};
template<typename T> basic_rgba_pixel {T r, g, b, a;};

using  rgb_pixel = basic_rgb_pixel <std::uint8_t>;
using rgba_pixel = basic_rgba_pixel<std::uint8_t>;

struct header; // header informations
template<typename Pixel> class image {header hdr; std::vector<Pixel> data;/*その他便利関数*/};

header read_header(const std::string&);
template<typename Pixel> image<Pixel> read(const std::string&);
template<typename Pixel> void        write(const std::string&, const image<Pixel>&);

あと、namespace py = pybind11;をしてあるとして進みます。

関数のバインディング

普通に関数をバインディングしたいときは、以下のようにします。

#include <pybind11/pybind11.h>
namespace py = pybind11;

PYBIND11_MODULE(/*モジュール名*/ example, /*モジュールオブジェクト*/ m) {
    m.def(/*pythonでの関数名*/ "read_header",
          /*バインドする関数*/  &read_header,
          /*named argument*/  py::arg("filename"),
          /*docstring*/       "read header information from file");
}

別の関数に分けることもできます。

void bind_readheader(py::module& mod) {
    mod.def(/*pythonでの関数名*/ "read_header",
            /*バインドする関数*/  &read_header,
            /*named argument*/  py::arg("filename"),
            /*docstring*/       "read header information from file");
    return
}

PYBIND11_MODULE(example, m) {
    bind_readheader(m);
}

注:ここでは当然のように書いていますが、先に戻り値型のクラスをバインディングしておきましょう。

テンプレート関数をバインディングする

残念ながらこれはできません。templateは一般の型について実装のテンプレートを作るための機能なので、template関数の実体は型が決まる前には存在していません。存在しないものをバインディングできるわけがないですね。

というわけで型を指定して実体化するとバインディングできるようになります。

PYBIND11_MODULE(example, m) {
    m.def("read_rgb",
          read<rgb_pixel>, // 型を指定して実体化する
          py::arg("filename"),
          "reads image composed of rgb pixel");
    m.def("read_rgba",
          read<rgba_pixel>, // 型を指定して実体化する
          py::arg("filename"),
          "reads image composed of rgba pixels");
}

オーバーロードが可能な場合、つまり実体化によって引数の型が変わる場合(C++では引数からの推論でtemplateの型が決まる場合)は関数のオーバーロードと同様のことができます。

PYBIND11_MODULE(example, m) {
    m.def("write", write<rgb_pixel>,  py::arg("filename"), py::arg("image"));
    m.def("write", write<rgba_pixel>, py::arg("filename"), py::arg("image"));
}

その場でラッパー関数を実装する & 返す型を動的に変える

さて、いちいちヘッダ情報を読み取ってピクセルのフォーマットを調べてからread_rgbとかread_rgbaとかを呼び分けるのは面倒過ぎますね。この2つは戻り値型が違うので(image<rgb_pixel>image<rgba_pixel>)、C++では何らかの形でコンパイル時に型を決めておく必要があり、動的にファイルを読んでから型を変えることはできません。いや継承してればできますが。

ただ、Pythonでは型がそもそも動的に決まるので余裕で型を動的に決められます。なのでそういう関数としてラップしたいですよね。

pybind11ではラムダをラップすることができるので、C++側に関数を漏らすことなくPython用の関数を実装できます。戻り値型はpy::objectにしておきましょう。クラスをバインディングしておけば、py::castによってpy::objectに変換できます。

PYBIND11_MODULE(example, m) {
    m.def("read", [](const std::string& fname) -> py::object {
        const auto header = read_header(fname);
        if(header.pixel_format == pixel_tag::rgb) {
            return py::cast(read<rgb_pixel>(fname));
        } else if(header.pixel_format == pixel_tag::rgba) {
            return py::cast(read<rgba_pixel>(fname));
        } else {
            throw std::runtime_error("error: unknown pixel format");
        }
    }, py::arg("filename"));
}

便利。

クラスのバインディング

メンバ変数をさらけ出す

クラスのメンバ変数は、書き換え可能かどうかなどを指定してバインディングできます。また、第三引数にその変数の説明を書けます。helpではこのメッセージが見られるようになります。

// template<typename Pixel> class image {header hdr; std::vector<Pixel> data;};

PYBIND11_MODULE(example, m) {
    py::class_<image<rgb_pixel>>(m, "image_rgb", "image composed of rgb pixels")
        .def_readwrite("header", &image<rgb_pixel>::hdr, "header struct");
}

メンバ関数をその場で実装する

例えば__repr__などがあると嬉しいでしょう。ちなみにC++では__を含む名前は予約されているので使ってはいけません。あと、_の後に大文字から始まる(_M_dataなど)名前も予約されています。標準ライブラリがこれを使うのは当然ですが、標準ライブラリ以外が使ってはいけません。

さて、メンバ関数は自分を最初の引数として取るので、そのような関数を作ってバインディングすればokです。

PYBIND11_MODULE(example, m) {
    py::class_<header>(m, "header", "header information")
        .def("__repr__", [](const header& self) {
            std::ostringstream oss;
            oss << "x_pixels = " << self.x_pixels;
            oss << "y_pixels = " << self.y_pixels;
            return oss.str();
        });
}

変数と関数を同名にするとどうなるか

普通やらないと思いますが、同名のメンバ関数とメンバ変数を登録してしまうと、ビルドが通ります。普通にライブラリができてしまいますが、インポートした時に以下のようなエラーが出ます。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: Cannot overload existing non-function object "data" with a function of the same name

C++しか書かずに、と言ったものの、それだとこういうのは見つけづらいので、pythonコードのテストも自動でやっておきましょう。

enum class

ドキュメントでenumを検索すると、クラス内で定義されたenumの説明が出てきますが、それに限らずenum classも同様にバインディングできます。

// enum class pixel_tag {rgb, rgba};

PYBIND11_MODULE(example, m) {
    py::enum_<pixel_tag>(m, "pixel_tag", py::arithmetic())
        .value("rgb",  pixel_tag::rgb)
        .value("rgba", pixel_tag::rgba)
        .export_values();
}

まったく同じですね。

ビルド

Pybind11がCMakeを使っているので、CMakeを使っていきます。多分一番簡単なのは、CMakeスクリプトでpybind11をダウンロードして、add_subdirectoryすることでしょう。それだけで全部できるようになります。

あとは、ソースコードのところで

set(PYBIND11_CPP_STANDARD -std=c++11)
pybind11_add_module(example SHARED example.cpp)

とすればよいです。

pipでインストールできるようにする

インストールがユーザーにとって一番の鬼門であるというのはよく知られた事実です。エラーが出やすい上に、知識がないと理由がわからず、解決不能に陥ることもあります。とくに、「〇〇と××のバージョンこれこれを先にインストールしておいて、ビルドしたあとPYTHONPATHを通す」などと言われると発狂間違いなしです。

そこで、READMEのインストール方法に

$ git clone https://github.com/hoge/fuga.git
$ pip install --user ./fuga

とだけ書いてあればカッコいいでしょう。Star即ポチ間違いなしです。

要するにsetup.pyがあればそれでいいわけですが、pybind11は優しいのでサンプルレポジトリに全部置いてくれています。

基本的にCMakeでビルドするようにしておいて、このサンプルレポジトリにあるsetup.pyを適宜改変すれば全て上手いこと行きます。最高。

参考

英語

日本語

基本的にこの辺のを見ると英語情報の大半をスキップできます。

19
19
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
19