LoginSignup
0
0

pybind11のReturn value policiesについて

Last updated at Posted at 2023-11-27

はじめに

pybind11とはC++とpythonの相互呼び出しを実現するためのライブラリです。
boost::pythonと比較し以下の特徴があります。

  • ヘッダファイルのみで利用可能
  • 簡潔に記述できる(特にデフォルト引数周りで余分にマクロを書かなくてよいのが良いです)
  • 古い環境はサポートされていない(Python3.6以上、C++11以上)
  • 構造やキーワードがboost::pythonと似ており移行が比較的容易

本稿ではドキュメントの説明だけではわかりづらかったReturn value policiesに絞って検証を行い、理解した内容をまとめておきます。

各Return value policiesを検証

pybind11ではpython側にメソッドなどを公開する際にReturn value policiesを指定可能です。
現状用意されている値は以下の通りです。

enum値 説明
return_value_policy::copy 新しいオブジェクトを作成し内容をコピーし、Pythonによって管理される。これはC++側と生存期間が分離されるため比較的安全である。
return_value_policy::move 新しいオブジェクトを作成しstd::moveを用いて内容をムーブし、Pythonによって管理される。これはC++側と生存期間が分離されるため比較的安全である。
return_value_policy::take_ownership 既存のオブジェクトを参照し所有権を取得する(新規にオブジェクトを作成しない)。Pythonは参照カウントが0になったときにデストラクタとdeleteを行う。C++側で同様の事を行った場合やオブジェクトが動的確保されていない場合の動作は未定義となる。
return_value_policy::reference 既存のオブジェクトを参照するが所有権を取得しない。C++側でオブジェクトの生存~破棄を管理する責任がある。
※Python側で使用中にも関わらずC++で破棄した場合は未定義の動作となる。
return_value_policy::reference_internal 親オブジェクトの生存期間と結びつける。内部的にはreturn_value_policy::referenceとkeep_alive<0, 1>を同時に適用した挙動となる。ちなみにkeep_alive<0, 1>とは戻り値(0)が生存している限り親オブジェクト(1)を延命させるものである。これはdef_property,def_readwrite等で作成されたプロパティのgetterのデフォルトポリシーである。
return_value_policy::automatic 戻り値がポインタの場合return_value_policy::take_ownershipとなり、それ以外は右辺値参照/左辺値参照によってmove/copyとなる。これはpy::class_でラップされた型のデフォルトポリシーである。
return_value_policy::automatic_reference 戻り値がポインタの場合return_value_policy::referenceとなる点以外はautomaticと同じ。Python関数をC++側から呼び出す際(handle::operator()、pybind11/stl.hなど)の関数の引数のデフォルトポリシーとして使用する。明示的に使う必要はおそらくない。

順にコードを書いて挙動を確認していきます。
検証環境は以下とします。

  • Visual Studio 2022 (プロジェクト設定はC++17 x64 DebugBuild)
  • Python 3.7.9 x64
  • pybind11 ver2.11

まず各検証でpython側に引き渡すオブジェクトのクラスTestClassを定義します。
id()によってオブジェクトの同一性を確認する為に自身のアドレスとシーケンス番号を返します。

class TestClass {
public:
  TestClass() { seq_ = seq_issuer_++; }
  TestClass(const TestClass& src) {
    printf("copy constructor\n");
    seq_ = seq_issuer_++;
  }
  TestClass(TestClass&& src) {
    printf("move constructor\n");
    seq_ = src.seq_;
    src.seq_ = -1;
  }
  virtual ~TestClass() { printf("destructor %s\n", id().c_str()); }

  std::string id() {
    std::stringstream ss;
    ss << std::hex << this << "-" << seq_;
    return ss.str();
  }
  int seq_;
  static int seq_issuer_;
};
int TestClass::seq_issuer_ = 0;
TestClass* g_test_class_ptr = nullptr;
TestClass  g_test_class;

// Python側にTestClass生ポインタを渡す関数
TestClass* get() {
  printf("addr(get)=%s\n", g_test_class_ptr->id().c_str());
  return g_test_class_ptr;
}

// Python側にTestClass生ポインタを渡すメンバ関数を持つクラス
class GetClass {
public:
  virtual ~GetClass() { printf("destructor GetClass\n"); }

  // Python側にTestClass生ポインタを渡すメンバ関数
  TestClass* get() {
    printf("addr=%s\n", g_test_class_ptr->id().c_str());
    return g_test_class_ptr;
  }

  // Python側にTestClassの左辺値を返す
  cls15& lvalue() { return g_cls15; }

  // Python側にTestClassの右辺値を返す
  cls15  rvalue() { return cls15(); }
};

PYBIND11_EMBEDDED_MODULE(test_module, m) {
  using namespace pybind11::literals;

  py::class_<TestClass>(m, "TestClass")
    .def("id", &TestClass::id);

  py::class_<GetClass>(m, "GetClass")
    .def(py::init<>())
    .def("get_ref_intr", &get_class::get, py::return_value_policy::reference_internal)
    .def("get_auto_ptr", &get_class::get, py::return_value_policy::automatic)
    .def("get_auto_lvalue", &get_class::lvalue, py::return_value_policy::automatic)
    .def("get_auto_rvalue", &get_class::rvalue, py::return_value_policy::automatic);

  m.def("get_copy", &get, py::return_value_policy::copy);
  m.def("get_move", &get, py::return_value_policy::move);
  m.def("get_take_ownership", &get, py::return_value_policy::take_ownership);
  m.def("get_ref", &get, py::return_value_policy::reference);
}

return_value_policy::copy

  • python
import test_module

def test_copy():
  tmp = test_module.get_copy()
  print('addr(py)=', tmp.id())
  • C++
g_test_class_ptr = new TestClass();
m.attr("test_copy")();
printf("addr(end)=%s\n", g_test_class_ptr->id().c_str());
  • 動作結果
addr=0000024792031710-1                        // C++側でget()時点でのg_test_class_ptrの状態
copy ctor 0000024792030EA0 <= 0000024792031710 // C++ → py側に渡される際にコピーが発生している
addr(py)= 0000024792030EA0-2                   // py側でget_copy()から受け取ったオブジェクトの状態
destructor 0000024792030EA0-2                  // 上記オブジェクトの破棄
addr(end)=0000024792031710-1                   // python呼び出しが完了した時点でのg_test_class_ptrの状態
destructor 0000024792031710-1                  // g_test_class_ptrの破棄

2行目で判るようにC++側からpy側にポインタを渡す際にコピーが渡されており、
各々のデストラクタの起動タイミングから生存期間も独立していることが判ります。

return_value_policy::move

  • python
import test_module

def test_move():
  tmp = test_module.get_move()
  print('addr(py)=', tmp.id())
  • C++
g_test_class_ptr = new TestClass();
m.attr("test_move")();
printf("addr(end)=%s\n", g_test_class_ptr->id().c_str());
  • 動作結果
addr=0000024791FF5790-3                        // C++側でget()時点でのg_test_class_ptrの状態
move ctor 0000024792031A80 <= 0000024791FF5790 // C++ → py側に渡される際にムーブが発生している
addr(py)= 0000024792031A80-3                   // py側でget_move()から受け取ったオブジェクトの状態
destructor 0000024792031A80-3                  // 上記オブジェクトの破棄
addr(end)=0000024791FF5790-ffffffff            // python呼び出しが完了した時点でのg_test_class_ptrの状態
destructor 0000024791FF5790-ffffffff           // g_test_class_ptrの破棄

2行目で判るようにC++側からpy側にポインタを渡す際にムーブが発生しています。今回のサンプルではseq_をpython側へ引き渡し、自身は無効化(-1 == 0xffffffff)してあります。
それ以外の挙動はcopyと同じです。

return_value_policy::take_ownership

  • python
import test_module

def test_take_ownership():
  tmp = test_module.get_take_ownership()
  print('addr(py)=', tmp.id())
  • C++
g_test_class_ptr = new TestClass();
m.attr("test_take_ownership")();
printf("addr(end)=%s\n", g_test_class_ptr->id().c_str());
  • 動作結果
addr=0000024792030EA0-4             // C++側でget()時点でのg_test_class_ptrの状態
addr(py)= 0000024792030EA0-4        // py側でget_take_ownership()から受け取ったオブジェクトの状態
destructor 0000024792030EA0-4       // 上記オブジェクトの破棄
addr(end)=0000024792030EA0-dddddddd // python呼び出しが完了した時点でのg_test_class_ptrの状態

1~2行目から判るようにC++側からpy側にポインタを渡す際にC++側で作成した物をそのまま渡しており、pythonオブジェクトのスコープを抜けたタイミングでデストラクタが呼ばれています。
そのため、python呼び出しが完了した4行目時点でのg_test_class_ptrの内容が破棄されていることが判ります。
(※VisualStudioのデバッグビルドでは破棄されたメモリ領域が0xddでFillされます)

return_value_policy::reference

  • python
import test_module

def test_reference():
  tmp = test_module.get_reference()
  print('addr(py)=', tmp.id())
  • C++
g_test_class_ptr = new TestClass();
m.attr("test_reference")();
printf("addr(end)=%s\n", g_test_class_ptr->id().c_str());
  • 動作結果
addr=0000024791FF5970-5
addr(py)= 0000024791FF5970-5
addr(end)=0000024791FF5970-5

C++側からpy側にポインタを渡す際にC++側で作成した物をそのまま渡しており、こちらはpythonオブジェクトのスコープを抜けたタイミングでデストラクタが呼ばれていません。

return_value_policy::reference_internal

import test_module

def test_reference_internal():
  def sub():
    get_cls = test_module.GetClass()
    return get_cls.get_ref_intr()
  sub()
  print('-----')
  tmp = sub()
  print('addr(py)=', tmp.id())
  • C++
g_test_class_ptr = new TestClass();
m.attr("test_reference_internal")();
printf("addr(end)=%s\n", g_test_class_ptr->id().c_str());
  • 動作結果
addr=00000247920312B0-6
destructor GetClass
-----
addr=00000247920312B0-6
addr(py)= 00000247920312B0-6
destructor GetClass
addr(end)=00000247920312B0-6
destructor 00000247920312B0-6

1度目のsub()では関数を抜ける際にGetClassのデストラクタが実行されています。
対して2度目のsub()ではpythonプログラムの終了時点でGetClassのデストラクタが実行されています。
これは2度目のsub()ではtmpにget_ref_intr()から取得したオブジェクトを格納しているため、reference_internalポリシーによってその親であるget_clsのデストラクタが延命されている為です。

ちなみにこのポリシーを適用する為には親オブジェクトが必須となるので通常の関数で使用すると
「RuntimeError: Could not activate keep_alive!」のランタイムエラーが発生します。

return_value_policy::automatic

import test_module

def test_automatic():
  get_cls = my_module15.get_class()
  print('--- pointer')
  tmp = get_cls.get_auto_ptr()
  print('addr(py)=', tmp.id())
  del tmp
  
  print('--- lvalue')
  tmp = get_cls.get_auto_lvalue()
  print('addr(py)=', tmp.id())
  del tmp
  
  print('--- rvalue')
  tmp = get_cls.get_auto_rvalue()
  print('addr(py)=', tmp.id())
  del tmp
  • C++
g_test_class_ptr = new TestClass();
m.attr("test_automatic")();
printf("addr(end)=%s\n", g_test_class_ptr->id().c_str());
  • 動作結果
------automatic
--- pointer
addr=0000024792030EA0-8
addr(py)= 0000024792030EA0-8
destructor 0000024792030EA0-8
--- lvalue
copy ctor 00000247920317B0 <= 00007FF7C2CE0120
addr(py)= 00000247920317B0-9
destructor 00000247920317B0-9
--- rvalue
move ctor 0000024792031710 <= 000000BC7B8FE218
destructor 000000BC7B8FE218-ffffffff
addr(py)= 0000024792031710-a
destructor 0000024792031710-a

ポインタの場合はtake_ownership、左辺値の場合はcopy、右辺値の場合はmoveとなっており、
いずれもドキュメント通りの挙動を確認出来ました。

return_value_policy::automatic_reference

使う必要は無いと態々記載があるのと、説明文からおおよそ動きが想像できるので省略します。

はまったこと

pybind11ではC++からpython側に渡した生存中のオブジェクトをすべて管理していて、同一のオブジェクトが再度渡された場合、指定したポリシーに関わらずpybind11側で管理しているオブジェクトを返す仕様になっています。
しばらくこれに気が付かず期待した動きにならずソースを追う羽目になりました。

よく見たら公式のドキュメントにちゃんと記載がありました。

One important aspect of the above policies is that they only apply to instances which pybind11 has not seen before, in which case the policy clarifies essential questions about the return value’s lifetime and ownership. When pybind11 knows the instance already (as identified by its type and address in memory), it will return the existing Python object wrapper rather than creating a new copy.

まとめ

状況ごとに用途をまとめてみます。
毎回記述するのも煩わしいので基本デフォルトで、要所で以下方針で明示的に使用していく感じになると思います。

  • C++側で使用しており、python実行中に破棄されない → reference
  • C++側で使用しており、python実行中に破棄される可能性がある → copy + shared_ptr
  • C++側で使用せず、pythonで使用するためにnewしたもの → take_ownership
  • 親オブジェクトのメンバ変数等(それ以外にある?) → reference_internal
  • 値渡し → copy/move

実際の移行作業はまだ途中なので進めていくと色々見直すべき部分が出るかも知れません…。

0
0
0

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
0
0