4

这是我的问题:

我有两个这样的课程:

class Signal {
public:
    void connect(...) { sig.connect(...); }
private:
    boost::signal2::signal sig;
};

class MyClass {
public:
    Signal on_event;
};

我想公开MyClass::on_event以便我可以my_class_instance.on_event.connect(...)从 Python 调用。

这就是我包装这些类的方式:

class_<Signal, boost::noncopyable> ("Signal", noinit)
    .def("connect", &some_helper_function);

class_<MyClass> ("MyClass")
    .def_readonly("on_event", &MyClass::on_event);

这可以编译,但是当我尝试connect从 Python 调用时,我得到:AttributeError: can't set attribute. 这在这里解释:http: //www.boost.org/doc/libs/1_53_0/libs/python/doc/tutorial/doc/html/python/exposing.html,所以我改为.def_readwritefor on_event

但现在我得到了一个编译时错误。它几乎不可能阅读 C++ 模板错误消息,但据我了解,它是因为不可boost::signals2::signal复制。由于.def_readwrite使成员可分配,因此它不能是不可复制的。但是对于我的用法,我不想分配成员,我只是不想调用一种方法。

我考虑过制作 const 的connect方法Signal,即使它改变了对象,但是我不能sig.connect()从那个方法调用,所以那也是不行的..

有任何想法吗?

4

2 回答 2

18

我在重现您的结果时遇到问题,但这里有一些信息可能有助于解决问题。

使用简单的类:

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 的解决方案可能是一个挑战。

于 2013-02-27T19:29:47.960 回答
1

写完这个问题后,我向 Signal 添加了一个公共复制构造函数,现在它可以工作了。

于 2013-02-27T08:44:50.393 回答