在Boost Python中公开一个非常量但不可复制的成员

问题描述 投票:4回答:2

这是我的问题:

我有两个这样的课程:

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使成员可分配,因此它不能是不可复制的。但是对于我的用法,我不想分配成员,我只是不想调用一个方法。

[我考虑过将connectSignal方法设为const,即使它改变了对象,但后来我无法从该方法中调用sig.connect(),所以也不可行。

有什么想法吗?

c++ boost boost-python
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::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中的绑定。对最终用户透明:

  • 将C ++绑定模块名称从>>> 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_ptrMyClass删除。
  • [shared_ptr可以制作连接对象的其他副本,就像调用信号boost::signals2::signal时一样。
  • [调用通过concurrently连接的Python对象。回调肯定会影响python对象。例如,提供给boost::signals2::signal方法的self参数将增加和减少对象的引用计数。
  • 支持从C ++线程中删除__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添加了一个公共副本构造函数,现在它可以工作。


1
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.