Samuel SILVESTER Samuel SILVESTER - 6 months ago 96
Python Question

Set a python variable to a C++ object pointer with boost-python

I want to set a Python variable from C++ so that the C++ program can create an object

Game* game = new Game();
in order for the Python code to be able to reference this instance (and call functions, etc). How can I achieve this?

I feel like I have some core misunderstanding of the way Python or Boost-Python works.

The line
main_module.attr("game") = game
is in a try catch statement, and the error (using PyErr_Fetch) is "No to_python (by-value) converter found for C++ type: class Game".

E.g.

class_<Game>("Game")
.def("add", &Game::add)
;

object main_module = import("__main__");
Game* game = new Game();
main_module.attr("game") = game; //This does not work


From Python:

import testmodule

testmodule.game.foo(7)

Answer

When dealing with language bindings, one often has to be pedantic in the details. By default, when a C++ object transgresses the language boundary, Boost.Python will create a copy, as this is the safest course of action to prevent dangling references. If a copy should not be made, then one needs to be explicit as to the ownership of the C++ object:

  • To pass a reference to a C++ object to Python while maintaining ownership in C++, use boost::python::ptr() or boost::ref(). The C++ code should guarantee that the C++ object's lifetime is at least as long as the Python object. When using ptr(), if the pointer is null, then the resulting Python object will be None.
  • To transfer ownership of a C++ object to Python, one can apply the manage_new_object ResultConverterGenerator, allowing ownership to be transferred to Python. C++ code should not attempt to access the pointer once the Python object's lifetime ends.
  • For shared ownership, one would need to expose the class with a HeldType of a smart pointer supporting shared semantics, such as boost::shared_ptr.

Once the Python object has been created, it would need to be inserted into a Python namespace to be generally accessible:

  • From within the module definition, use boost::python::scope to obtain a handle to the current scope. For example, the following would insert x into the example module:

    BOOST_PYTHON_MODULE(example)
    {
      boost::python::scope().attr("x") = ...; // example.x
    }
    
  • To insert into the __main__ module, one can import __main__. For example, the following would insert x into the __main__ module:

    boost::python::import("__main__").attr("x") = ...;
    

Here is an example demonstrating how to directly construct the Python object from C++, transfer ownership of a C++ object to Python, and construct a Python object that references a C++ object:

#include <iostream>
#include <boost/python.hpp>

// Mockup model.
struct spam
{
  spam(int id)
    : id_(id)
  {
    std::cout << "spam(" << id_ << "): "  << this << std::endl;
  }

  ~spam()
  {
    std::cout << "~spam(" << id_ << "): " << this << std::endl;
  }

  // Explicitly disable copying.
  spam(const spam&) = delete;
  spam& operator=(const spam&) = delete;

  int id_;
};

/// @brief Transfer ownership to a Python object.  If the transfer fails,
///        then object will be destroyed and an exception is thrown.
template <typename T>
boost::python::object transfer_to_python(T* t)
{
  // Transfer ownership to a smart pointer, allowing for proper cleanup
  // incase Boost.Python throws.
  std::unique_ptr<T> ptr(t);

  // Use the manage_new_object generator to transfer ownership to Python.
  namespace python = boost::python;
  typename python::manage_new_object::apply<T*>::type converter;

  // Transfer ownership to the Python handler and release ownership
  // from C++.
  python::handle<> handle(converter(*ptr));
  ptr.release();

  return python::object(handle);
}

namespace {
spam* global_spam;
} // namespace

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  // Expose spam.
  auto py_spam_type = python::class_<spam, boost::noncopyable>(
      "Spam", python::init<int>())
    .def_readonly("id", &spam::id_)
    ;

  // Directly create an instance of Python Spam and insert it into this
  // module's namespace.
  python::scope().attr("spam1") = py_spam_type(1);

  // Construct of an instance of Python Spam from C++ spam, transfering
  // ownership to Python.  The Python Spam instance will be inserted into
  // this module's namespace.
  python::scope().attr("spam2") = transfer_to_python(new spam(2));

  // Construct an instance of Python Spam from C++, but retain ownership of
  // spam in C++.  The Python Spam instance will be inserted into the
  // __main__ scope.
  global_spam = new spam(3);
  python::import("__main__").attr("spam3") = python::ptr(global_spam);
}

Interactive usage:

>>> import example
spam(1): 0x1884d40
spam(2): 0x1831750
spam(3): 0x183bd00
>>> assert(1 == example.spam1.id)
>>> assert(2 == example.spam2.id)
>>> assert(3 == spam3.id)
~spam(1): 0x1884d40
~spam(2): 0x1831750

In the example usage, note how Python did not destroy spam(3) upon exit, as it was not granted ownership of the underlying object.

Comments