🐍

pybind11 で C++ のバインディングを書くメモ

2023/03/07に公開

pybind11, 機能はあるけど document はいろいろ書いてある割りには貧疎だし, exmaple コードなくてつらいね...

test コードはあるがいろいろ記述されているためわかりにくいし, pybind11 のコード自体template バリバリでようわからんし...

float, double の順

float と double の両方を取り得る同名メソッドの場合, 定義順で評価になるようです.

.def("set", float ...)
.def("set", double ...)

python は double が標準ですから, double なメソッドを先に定義するとよいでしょう.

明示的に C の型を指定したい場合は, ctypes を使うか

https://qiita.com/maiueo/items/b2093ba78cde988bb111

自前ライブラリで binding 書いて用意することになるでしょう(float16 などの型とか)

メソッドに対して def_readwrite とかしたい

class Myclass {
 private:
  std::vector<int> _myarr;
 public:
  std::vector<int> &myarr() { return _myarr; }
  const std::vector<int> &myarr() const {
    return _myarr;
  }

のような, メソッドしか公開されていない & setter, getter が同じ関数名の場合に, Python 側からは変数のようにアクセスしたい(MyClass.myarr = [1, 2, 3] みたいな).

まず, def_readwrite ではC++ メソッドを指定できない.
(なんかテンプレートとかの記述がんばればいけるかもだけどようわからん...)

https://github.com/pybind/pybind11/blob/3cc7e4258c15a6a19ba5e0b62a220b1a6196d4eb/include/pybind11/pybind11.h#L1662

def_readwrite 自体は def_property への wrapper でしたので, def_property で対応します.

方法1. ラムダ関数利用

C++ で setter, getter が同じ名前だとうまくいきません. 以下のように getter では明示的に const を返すように指定します!
(そうしないと Python 側でうまくいかない)

    py::class_<MyClass>(m, "MyClass")
      .def(py::init<>())
      .def_property("myarr", [](MyClass &self) -> const std::vector<int> & {
        py::print("intv get");
        return self.myarr();
      }, [](MyClass &self, const std::vector<int> &v) {
        py::print("intv set");
        self.myarr() = v;
      })

readonly にしたい場合は setter に nullptr, writeonly にしたい場合は getter に nullptr 指定でいけます.

方法2. 明示的型指定

https://pybind11.readthedocs.io/en/stable/classes.html#overloaded-methods

https://stackoverflow.com/questions/8343467/odd-syntax-asterisk-after-scope-operator

https://isocpp.org/wiki/faq/pointers-to-members#fnptr-vs-memfnptr-types

void (MyClass::*)(bool) などで, メソッド関数のシグネチャの関数ポインタのキャストでいけました.
(A::* とか C++ 歴 30 年くらいではじめてミタ! ややこしいね)

.def_property("myarr",
  static_cast<const std::vector<int>&(MyClass::*)() const>(&MyClass::myarr),   ..., py::return_value_policy::reference_internal)

py::return_value_policy::reference_internal をつけておくと安心かも
(getter で返るのは C++ 側インスタンスのリファレンス)

setter も同様ですが, 今回のケースでは対応する関数シグネチャ(void set_myarr(const std::vector<int> &v)が C++ クラス側に無いので, 今回の場合は引き続きラムダを使うことに
なるでしょう.

C++14 の場合は py::overload_cast でもうちょっと簡略化できます.
const な method の場合は py::const_ をつけます.

py::overload_cast<>(&MyClass::myattr, py::const_));

また, static method の場合はクラスの部分は (*) になります(C での関数ポインタと同じ).

class Dora {
  public:
   static void bora(bool v);
};

m.def_static(“bora", (void (*)(bool)) &Dora::bora);

std::vector

あとは, std::vector などつかっていて python list 的に処理したい場合は, opaque 型定義しておく必要があります.

https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html#making-opaque-types

これで以下のような感じで python 的にプロパティアクセス + リストアクセス(?)できます.

k = MyClass()
k.myarr.append(3)

ただ, std::vector を opaque type にすると, 明示的な型で配列(リスト)を作る必要があります.

k.myarr = [1, 3] # NG
k.myarr = VectorInt([1, 3]) # OK

より pythonic にするなら, 引数や返り値など C++ STL container 型ではなく py::list で扱うようにしたほうがいいのかも?

keepalive について

https://pybind11.readthedocs.io/en/stable/advanced/functions.html#additional-call-policies

keep_alive<Nurse, Patient> とややこしい名称になっている.
Nurse, Patient には, メソッドの場合引数の番号を指定する.

0 : 返り値型
1 : this
2 : 関数の第一引数

ふるまいとしては, Nurse が fee されるまでは Patient は保持されることを指定する.

通常のメソッドでは <1, 2> で(インスタンスthisが解放されるまで),
iterator の場合は <0, 1> (返したオブジェクトが解放されるまで)

https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html

となろうか.

不明点

k.myarr.append だとしかし実は const 返しの getter しか呼ばれていない(setter 関数は呼ばれない)

コンパイル遅い...

どうしようもないです...
C 関数用意して, Python C API で対応するか, ctypes で DLL 直叩きも検討してみましょう.

TODO

  • C++ の型によっては return value policy を適切に設定する必要があるであろう

Discussion