我在重现您的结果时遇到问题,但这里有一些信息可能有助于解决问题。
使用简单的类:
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 线程。
对于第一个场景,我们假设只有 Python 线程与库交互。
一种使其相对简单的技术是使用继承。首先定义一个Slot
可以连接到Signal
.
class Slot
: public boost::python::wrapper<Slot>
{
public:
void operator()()
{
this->get_override("__call__")();
}
};
该类Slot
继承自boost::python::wrapper
,一个非侵入式地提供钩子以允许 Python 类覆盖基类中的函数的类。
当可调用类型连接到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>)
如果任何可调用对象都可以连接到信号,那将是理想的。一种简单的方法是猴子修补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 的全局解释器锁(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
可以制作连接对象的额外副本,就像同时调用信号时所做的那样。
- 调用通过
boost::signals2::signal
. 回调肯定会影响 python 对象。例如,self
提供给该__call__
方法的参数将增加和减少对象的引用计数。
支持MyClass
从 C++ 线程中删除。
为了保证 GIL 在从 C++ 线程MyClass
中删除时被保留,需要自定义删除器。这也需要绑定来抑制默认构造函数,并改用自定义构造函数。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++ 函数,该函数接受带有HeldType
非智能指针的 C++ 类时,Boost.Python 将从引用计数的 python 对象中提取非引用计数的 C++ 对象。它可以安全地执行此操作,因为 Python 中的调用线程具有 GIL。为了保持对尝试从 Python 连接的插槽的引用计数,并允许任何可调用类型进行连接,我们可以使用不透明类型的boost::python::object
.
为了避免signal
创建所提供boost::python::object
的副本,可以创建一个副本以boost::python::object
使引用计数保持准确,并通过 管理副本shared_ptr
。这允许signal
自由创建副本shared_ptr
而不是在boost::python::object
没有 GIL 的情况下创建。
这个 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 的解决方案可能是一个挑战。