結果の再現に問題がありますが、問題の解決に役立つ可能性のある情報を以下に示します。
単純なクラスの場合:
class Signal
{
public:
void connect() { std::cout << "connect called" << std::endl; }
private:
boost::signals2::signal<void()> signal_;
};
class MyClass
{
public:
Signal on_event;
};
そして基本的なバインディング:
namespace python = boost::python;
python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
.def("connect", &Signal::connect)
;
python::class_<MyClass>("MyClass")
.def_readonly("on_event", &MyClass::on_event)
;
コードはコンパイルに失敗します。クラスを公開するとき、コンバーターを登録する Boost.Python のデフォルトの動作。これらのコンバーターには、C++ クラス オブジェクトを Python オブジェクトで管理できるストレージにコピーする手段として、コピー コンストラクターが必要です。この動作はboost::noncopyable
、型に引数として指定することにより、クラスに対して無効にすることができますclass_
。
この場合、MyClass
バインドはコピー コンストラクターを抑制しません。Boost.Python はバインディング内でコピー コンストラクターを使用しようとしますが、メンバー変数on_event
がコピー可能でないため、コンパイラ エラーで失敗します。 から継承するSignal
の型のメンバー変数が含まれているため、 はコピーできません。boost::signal2::signal
boost::noncopyable
boost:::noncopyable
のバインディングに引数の型として追加するMyClass
と、コードをコンパイルできます。
namespace python = boost::python;
python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
.def("connect", &Signal::connect)
;
python::class_<MyClass, boost::noncopyable>("MyClass")
.def_readonly("on_event", &MyClass::on_event)
;
使用法:
>>> import example
>>> m = example.MyClass()
>>> m.on_event.connect()
connect called
>>> m.on_event = None
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>
この設定により、必要なバインドと呼び出し構文が可能になりますが、最終目標の最初のステップであるかのように見えます。
これがあまりにも傲慢である場合は、申し訳ありません。しかし、他の最近の質問に基づいて、時間をかけて最初の例を拡張し、最終的な目標と思われるもの、つまり Python コールバックを に接続できるようにすることをカバーしたいと思いますsignal2::signal
。メカニズムと複雑さのレベルが異なるため、2 つの異なるアプローチについて説明しますが、最終的なソリューションで考慮する必要がある詳細についての洞察を提供する場合があります。
Python スレッドのみ。
この最初のシナリオでは、Python スレッドのみがライブラリとやり取りしていると仮定します。
比較的単純な方法の 1 つは、継承を使用することです。に接続できるヘルパーSlot
クラスを定義することから始めSignal
ます。
class Slot
: public boost::python::wrapper<Slot>
{
public:
void operator()()
{
this->get_override("__call__")();
}
};
このクラスは、Python クラスが基本クラスの関数をオーバーライドできるようにするためのフックを控えめに提供するクラスSlot
から継承します。boost::python::wrapper
呼び出し可能な型が に接続するboost::signals2::signal
と、シグナルは引数を内部リストにコピーする場合があります。Slot
したがって、インスタンスが に接続されている限り、ファンクターがインスタンスの寿命を延ばすことができることが重要ですsignal
。これを実現する最も簡単な方法はSlot
、boost::shared_ptr
.
結果のSignal
クラスは次のようになります。
class Signal
{
public:
template <typename Callback>
void connect(const Callback& callback)
{
signal_.connect(callback);
}
void operator()() { signal_(); }
private:
boost::signals2::signal<void()> signal_;
};
また、ヘルパー関数はSignal::connect
、他の C++ 型がそれに接続する必要がある場合に備えて、汎用性を維持するのに役立ちます。
void connect_slot(Signal& self,
const boost::shared_ptr<Slot>& slot)
{
self.connect(boost::bind(&Slot::operator(), slot));
}
これにより、次のバインディングが生成されます。
BOOST_PYTHON_MODULE(example) {
namespace python = boost::python;
python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
.def("connect", &connect_slot)
.def("__call__", &Signal::operator())
;
python::class_<MyClass, boost::noncopyable>("MyClass")
.def_readonly("on_event", &MyClass::on_event)
;
python::class_<Slot, boost::shared_ptr<Slot>,
boost::noncopyable>("Slot")
.def("__call__", python::pure_virtual(&Slot::operator()))
;
}
そして、その使用法は次のとおりです。
>>> from example import *
>>> class Foo(Slot):
... def __call__(self):
... print "Foo::__call__"
...
>>> m = MyClass()
>>> foo = Foo()
>>> m.on_event.connect(foo)
>>> m.on_event()
Foo::__call__
>>> foo = None
>>> m.on_event()
Foo::__call__
成功している一方で、pythonic ではないという残念な特徴があります。例えば:
>>> def spam():
... print "spam"
...
>>> m = MyClass()
>>> m.on_event.connect(spam)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Boost.Python.ArgumentError: Python argument types in
Signal.connect(Signal, function)
did not match C++ signature:
connect(Signal {lvalue}, boost::shared_ptr<Slot>)
呼び出し可能なオブジェクトをシグナルに接続できれば理想的です。これを行う簡単な方法の 1 つは、Python でバインディングにモンキー パッチを適用することです。エンド ユーザーに透過的にするには:
- C++ バインディング モジュール名を から
example
に変更し_example
ます。ライブラリ名も必ず変更してください。
- から継承する型に引数をラップ
example.py
するパッチを作成します。Signal.connect()
Slot
example.py
次のようになります。
from _example import *
class _SlotWrap(Slot):
def __init__(self, fn):
self.fn = fn
Slot.__init__(self)
def __call__(self):
self.fn()
def _signal_connect(fn):
def decorator(self, slot):
# If the slot is not an instance of Slot, then aggregate it
# in SlotWrap.
if not isinstance(slot, Slot):
slot = _SlotWrap(slot)
# Invoke the decorated function with the slot.
return fn(self, slot)
return decorator
# Patch Signal.connect.
Signal.connect = _signal_connect(Signal.connect)
パッチ適用は、エンド ユーザーにとってシームレスです。
>>> from example import *
>>> def spam():
... print "spam"
...
>>> m = MyClass()
>>> m.on_event.connect(spam)
>>> m.on_event()
spam
このパッチを使用すると、任意の呼び出し可能な型がSignal
から明示的に継承しなくても に接続できSlot
ます。そのため、最初のソリューションよりもはるかに Pythonic になります。バインディングをシンプルで非 pythonic に保つ利点を決して過小評価しないでください。ただし、python で pythonic になるようにパッチを適用してください。
Python および C++ スレッド。
次のシナリオでは、C++ スレッドが Python とやり取りしているケースを考えてみましょう。たとえば、一定時間後にシグナルを呼び出すように C++ スレッドを設定できます。
この例はかなり複雑になる可能性があるため、基本から始めましょう: Python のGlobal Interpreter Lock (GIL)。つまり、GIL はインタプリタの周りのミューテックスです。スレッドが Python 管理対象オブジェクトの参照カウントに影響を与える何かを実行している場合、そのスレッドは GIL を取得している必要があります。前の例では、C++ スレッドがなかったため、GIL が取得されている間にすべてのアクションが発生しました。これはかなり単純ですが、すぐに複雑になる可能性があります。
まず、モジュールでは、スレッド化のために Python で GIL を初期化する必要があります。
BOOST_PYTHON_MODULE(example) {
PyEval_InitThreads(); // Initialize GIL to support non-python threads.
...
}
便宜上、GIL の管理に役立つ単純なクラスを作成してみましょう。
/// @brief RAII class used to lock and unlock the GIL.
class gil_lock
{
public:
gil_lock() { state_ = PyGILState_Ensure(); }
~gil_lock() { PyGILState_Release(state_); }
private:
PyGILState_STATE state_;
};
スレッドはMyClass
のシグナルを呼び出します。MyClass
したがって、スレッドが生きている間の寿命を延ばす必要があります。これを達成するための良い候補はMyClass
、shared_ptr
.
C++ スレッドがいつ GIL を必要とするかを特定しましょう。
MyClass
によって削除されていshared_ptr
ます。
boost::signals2::signal
信号が同時に呼び出されたときに行われるように、接続されたオブジェクトの追加のコピーを作成できます。
- を介して接続された Python オブジェクトを呼び出し
boost::signals2::signal
ます。コールバックは確かに python オブジェクトに影響します。たとえば、メソッドにself
提供される引数は__call__
、オブジェクトの参照カウントを増減します。
MyClass
C++ スレッドからの削除のサポート。
C++ スレッド内でMyClass
が削除されたときに GIL が保持されるようにするには、カスタムのデリータが必要です。これには、既定のコンストラクターを抑制し、代わりにカスタム コンストラクターを使用するバインディングも必要です。shared_ptr
/// @brief Custom deleter.
template <typename T>
struct py_deleter
{
void operator()(T* t)
{
gil_lock lock;
delete t;
}
};
/// @brief Create Signal with a custom deleter.
boost::shared_ptr<MyClass> create_signal()
{
return boost::shared_ptr<MyClass>(
new MyClass(),
py_deleter<MyClass>());
}
...
BOOST_PYTHON_MODULE(example) {
...
python::class_<MyClass, boost::shared_ptr<MyClass>,
boost::noncopyable>("MyClass", python::no_init)
.def("__init__", python::make_constructor(&create_signal))
.def_readonly("on_event", &MyClass::on_event)
;
}
スレッド自体。
スレッドの機能はかなり基本的なものです。スレッドはスリープしてからシグナルを呼び出します。ただし、GIL のコンテキストを理解することは重要です。
/// @brief Wait for a period of time, then invoke the
/// signal on MyClass.
void call_signal(boost::shared_ptr<MyClass>& shared_class,
unsigned int seconds)
{
// The shared_ptr was created by the caller when the GIL was
// locked, and is accepted as a reference to avoid modifying
// it while the GIL is not locked.
// Sleep without the GIL so that other python threads are able
// to run.
boost::this_thread::sleep_for(boost::chrono::seconds(seconds));
// We do not want to hold the GIL while invoking C++-specific
// slots connected to the signal. Thus, it is the responsibility of
// python slots to lock the GIL. Additionally, the potential
// copying of slots internally by the signal will be handled through
// another mechanism.
shared_class->on_event();
// The shared_class has a custom deleter that will lock the GIL
// when deletion needs to occur.
}
/// @brief Function that will be exposed to python that will create
/// a thread to call the signal.
void spawn_signal_thread(boost::shared_ptr<MyClass> self,
unsigned int seconds)
{
// The caller owns the GIL, so it is safe to make copies. Thus,
// spawn off the thread, binding the arguments via copies. As
// the thread will not be joined, detach from the thread.
boost::thread(boost::bind(&call_signal, self, seconds)).detach();
}
そして、MyClass
バインディングが更新されます。
python::class_<MyClass, boost::shared_ptr<MyClass>,
boost::noncopyable>("MyClass", python::no_init)
.def("__init__", python::make_constructor(&create_signal))
.def("signal_in", &spawn_signal_thread)
.def_readonly("on_event", &MyClass::on_event)
;
boost::signals2::signal
Python オブジェクトとのやり取り。
boost::signals2::signal
呼び出されたときにコピーを作成できます。さらに、C++ スロットがシグナルに接続されている可能性があるため、シグナルが呼び出されている間は GIL をロックしないことが理想的です。ただし、signal
スロットのコピーを作成したりスロットを呼び出したりする前に GIL を取得できるようにするためのフックは提供しません。
複雑さを増すために、スマート ポインターではない C++ クラスを受け入れる C++ 関数をバインドが公開する場合、HeldType
Boost.Python は参照カウントされた Python オブジェクトから非参照カウント C++ オブジェクトを抽出します。Python の呼び出しスレッドには GIL があるため、これを安全に行うことができます。Python から接続しようとしているスロットへの参照カウントを維持し、呼び出し可能な型が接続できるようにするために、 の不透明型を使用できますboost::python::object
。
signal
提供された のコピーを作成することを避けるためにboost::python::object
、 のコピーを作成しboost::python::object
て参照カウントを正確に保ち、 を介してコピーを管理することができますshared_ptr
。これにより、GIL なしで作成する代わりに、 のsignal
コピーを自由に作成できます。shared_ptr
boost::python::object
この GIL セーフティ スロットは、ヘルパー クラスにカプセル化できます。
/// @brief Helper type that will manage the GIL for a python slot.
class py_slot
{
public:
/// @brief Constructor that assumes the caller has the GIL locked.
py_slot(const boost::python::object& object)
: object_(new boost::python::object(object), // GIL locked, so copy.
py_deleter<boost::python::object>()) // Delete needs GIL.
{}
void operator()()
{
// Lock the gil as the python object is going to be invoked.
gil_lock lock;
(*object_)();
}
private:
boost::shared_ptr<boost::python::object> object_;
};
ヘルパー関数が Python に公開され、型を適応させるのに役立ちます。
/// @brief Signal connect helper.
void signal_connect(Signal& self,
boost::python::object object)
{
self.connect(boost::bind(&py_slot::operator(), py_slot(object)));
}
そして、更新されたバインディングはヘルパー関数を公開します:
python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
.def("connect", &signal_connect)
.def("__call__", &Signal::operator())
;
最終的な解決策は次のようになります。
#include <boost/bind.hpp>
#include <boost/python.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/signals2/signal.hpp>
#include <boost/thread.hpp>
class Signal
{
public:
template <typename Callback>
void connect(const Callback& callback)
{
signal_.connect(callback);
}
void operator()() { signal_(); }
private:
boost::signals2::signal<void()> signal_;
};
class MyClass
{
public:
Signal on_event;
};
/// @brief RAII class used to lock and unlock the GIL.
class gil_lock
{
public:
gil_lock() { state_ = PyGILState_Ensure(); }
~gil_lock() { PyGILState_Release(state_); }
private:
PyGILState_STATE state_;
};
/// @brief Custom deleter.
template <typename T>
struct py_deleter
{
void operator()(T* t)
{
gil_lock lock;
delete t;
}
};
/// @brief Create Signal with a custom deleter.
boost::shared_ptr<MyClass> create_signal()
{
return boost::shared_ptr<MyClass>(
new MyClass(),
py_deleter<MyClass>());
}
/// @brief Wait for a period of time, then invoke the
/// signal on MyClass.
void call_signal(boost::shared_ptr<MyClass>& shared_class,
unsigned int seconds)
{
// The shared_ptr was created by the caller when the GIL was
// locked, and is accepted as a reference to avoid modifying
// it while the GIL is not locked.
// Sleep without the GIL so that other python threads are able
// to run.
boost::this_thread::sleep_for(boost::chrono::seconds(seconds));
// We do not want to hold the GIL while invoking C++-specific
// slots connected to the signal. Thus, it is the responsibility of
// python slots to lock the GIL. Additionally, the potential
// copying of slots internally by the signal will be handled through
// another mechanism.
shared_class->on_event();
// The shared_class has a custom deleter that will lock the GIL
// when deletion needs to occur.
}
/// @brief Function that will be exposed to python that will create
/// a thread to call the signal.
void spawn_signal_thread(boost::shared_ptr<MyClass> self,
unsigned int seconds)
{
// The caller owns the GIL, so it is safe to make copies. Thus,
// spawn off the thread, binding the arguments via copies. As
// the thread will not be joined, detach from the thread.
boost::thread(boost::bind(&call_signal, self, seconds)).detach();
}
/// @brief Helepr type that will manage the GIL for a python slot.
struct py_slot
{
public:
/// @brief Constructor that assumes the caller has the GIL locked.
py_slot(const boost::python::object& object)
: object_(new boost::python::object(object), // GIL locked, so copy.
py_deleter<boost::python::object>()) // Delete needs GIL.
{}
void operator()()
{
// Lock the gil as the python object is going to be invoked.
gil_lock lock;
(*object_)();
}
private:
boost::shared_ptr<boost::python::object> object_;
};
/// @brief Signal connect helper.
void signal_connect(Signal& self,
boost::python::object object)
{
self.connect(boost::bind(&py_slot::operator(), py_slot(object)));
}
BOOST_PYTHON_MODULE(example) {
PyEval_InitThreads(); // Initialize GIL to support non-python threads.
namespace python = boost::python;
python::class_<Signal, boost::noncopyable>("Signal", python::no_init)
.def("connect", &signal_connect)
.def("__call__", &Signal::operator())
;
python::class_<MyClass, boost::shared_ptr<MyClass>,
boost::noncopyable>("MyClass", python::no_init)
.def("__init__", python::make_constructor(&create_signal))
.def("signal_in", &spawn_signal_thread)
.def_readonly("on_event", &MyClass::on_event)
;
}
およびテスト スクリプト ( test.py
):
from time import sleep
from example import *
def spam():
print "spam"
m = MyClass()
m.on_event.connect(spam)
m.on_event()
m.signal_in(2)
m = None
print "Sleeping"
sleep(5)
print "Done sleeping"
結果は次のとおりです。
スパム
睡眠
スパム
寝ました
結論として、オブジェクトが Boost.Python レイヤーを通過するときは、時間をかけてその寿命と使用されるコンテキストを管理する方法を検討してください。これには多くの場合、使用されている他のライブラリがオブジェクトをどのように処理するかを理解する必要があります。これは簡単な問題ではなく、pythonic ソリューションを提供することは困難な場合があります。