写完这个问题后,我向Signal添加了一个公共副本构造函数,现在它可以工作。
这是我的问题:
我有两个这样的课程:
class Signal {
public:
void connect(...) { sig.connect(...); }
private:
boost::signal2::signal sig;
};
class MyClass {
public:
Signal on_event;
};
我想公开MyClass::on_event
,以便可以从Python调用my_class_instance.on_event.connect(...)
。
这就是我包装这些类的方式:
class_<Signal, boost::noncopyable> ("Signal", noinit)
.def("connect", &some_helper_function);
class_<MyClass> ("MyClass")
.def_readonly("on_event", &MyClass::on_event);
这会编译,但是当我尝试从Python调用connect
时会得到:AttributeError: can't set attribute
。此处说明:http://www.boost.org/doc/libs/1_53_0/libs/python/doc/tutorial/doc/html/python/exposing.html,所以我将.def_readwrite
更改为on_event
。
但是现在我收到了编译时错误。它几乎是不可能读取C ++模板错误消息的,但是据我了解,这是因为boost::signals2::signal
是不可复制的。由于.def_readwrite
使成员可分配,因此它不能是不可复制的。但是对于我的用法,我不想分配成员,我只是不想调用一个方法。
[我考虑过将connect
的Signal
方法设为const,即使它改变了对象,但后来我无法从该方法中调用sig.connect()
,所以也不可行。
有什么想法吗?
我在重现您的结果时遇到问题,但是这里有一些信息可能有助于解决问题。
使用简单的类:
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
。我将介绍两种不同的方法,因为其机制和复杂程度有所不同,但是它们可以提供对最终解决方案应考虑的细节的深入了解。
对于第一种情况,假设只有Python线程正在与库进行交互。
使它相对简单的一种技术是使用继承。首先定义一个可以连接到Slot
的助手Signal
类。
class Slot : public boost::python::wrapper<Slot> { public: void operator()() { this->get_override("__call__")(); } };
Slot
类继承自boost::python::wrapper
,该类非侵入地提供了钩子,以允许Python类重写基类中的函数。
当可调用类型连接到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_; };
通用,以防其他C ++类型需要连接到它。
Signal::connect
这将导致以下绑定:
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())) ; }
虽然成功,但是它具有不使用Python的不幸特征。例如:
>>> 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__
如果有任何可调用对象可以连接到信号,那将是理想的。一种简便的方法是猴子修补Python中的绑定。对最终用户透明:
>>> 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>)
更改为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
。这样,它变得比初始解决方案更具Python风格。永远不要低估保持绑定简单和非pythonic的好处,而是在python中将它们修补为pythonic。Python和C ++线程。
在下一种情况下,请考虑C ++线程与Python交互的情况。例如,可以将C ++线程设置为在一段时间后调用信号。
此示例可能会涉及很多,所以让我们从以下基础知识入手:Python的Slot
(GIL)。简而言之,GIL是解释器周围的互斥体。如果线程正在执行任何会影响python受管理对象的引用计数的操作,则它需要获取GIL。在前面的示例中,由于没有C ++线程,因此所有操作都在获取GIL时发生。尽管这很简单,但很快就会变得复杂。
首先,模块需要让Python初始化GIL以便进行线程化。
Global Interpreter Lock为了方便起见,让我们创建一个简单的类来帮助管理GIL:
BOOST_PYTHON_MODULE(example) { PyEval_InitThreads(); // Initialize GIL to support non-python threads. ... }
该线程将调用
/// @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
。让我们确定C ++线程何时需要GIL:
shared_ptr
被MyClass
删除。shared_ptr
可以制作连接对象的其他副本,就像调用信号boost::signals2::signal
时一样。boost::signals2::signal
方法的self
参数将增加和减少对象的引用计数。__call__
。为了确保当C0线程中的MyClass
删除MyClass
时保持GIL,需要shared_ptr
。这还要求绑定禁止显示默认构造函数,而改用自定义构造函数。
custom deleter线程本身。
该线程的功能是相当基本的:它先休眠然后调用信号。但是,了解GIL的上下文很重要。
/// @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) ; }
[
/// @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) ;
与python对象进行交互。
boost::signals2::signal
可能在被调用时进行复制。另外,可能有C ++插槽连接到信号,因此在调用信号时不要锁定GIL是理想的。但是,boost::signals2::signal
没有提供钩子来允许我们在创建插槽副本或调用插槽之前获取GIL。
为了增加复杂性,当绑定公开一个C ++函数,该函数接受带有signal
的C ++类(不是智能指针)时,Boost.Python将从引用计数中提取未引用计数的C ++对象python对象。它可以安全地执行此操作,因为在Python中,调用线程具有GIL。为了维护对尝试从Python连接的插槽的引用计数,并允许任何可调用的类型进行连接,我们可以使用HeldType
的不透明类型。
[为了避免让boost::python::object
创建所提供的signal
的副本,可以创建boost::python::object
的副本,以使引用计数保持准确,并通过boost::python::object
管理该副本。这允许shared_ptr
自由创建signal
的副本,而不是在没有GIL的情况下创建shared_ptr
。
此GIL安全插槽可以封装在帮助程序类中。
boost::python::object
辅助函数将在Python中公开,以帮助修改类型。
/// @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_; };
并且更新的绑定公开了辅助函数:
/// @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
结果如下:
垃圾邮件睡眠垃圾邮件睡觉了总之,当对象通过Boost.Python层传递时,请花一些时间来考虑如何管理其寿命以及使用该对象的上下文。这通常需要了解其他正在使用的库如何处理该对象。这不是一个容易的问题,提供pythonic解决方案可能是一个挑战。
写完这个问题后,我向Signal添加了一个公共副本构造函数,现在它可以工作。
写完这个问题后,我向Signal添加了一个公共副本构造函数,现在它可以工作。