pythonc++referencepybind11

pybind11: Python callback executed in C++ with parameter modification


I'm working on Python bindings of my C++ library (a mathematical optimization solver) and I'm stuck at a point where I create a Python callback evaluate_constraints() that takes two arguments, pass it to the C++ library and evaluate it with C++ arguments. The callback modifies its second parameter constraints based on its first parameter x.

// C++ code
#include "Vector.hpp"
#include <pybind11/pybind11.h>
namespace py = pybind11;

void solve(const std::function<void(const Vector&, Vector&)>& evaluate_constraints) {
  const Vector x = ...;
  Vector constraints = ...;
  evaluate_constraints(x, constraints);
}

PYBIND11_MODULE(myCppModule, module) {
   py::class_<Vector>(module, "Vector")
      .def(py::init<size_t>(), "Constructor")
      .def("__getitem__", [](const Vector& vector, size_t index) {
         return vector[index];
      })
      .def("__setitem__", [](Vector& vector, size_t index, double value) {
         vector[index] = value;
      });
   module.def("solve", &solve);
}
# Python code
import myCppModule

def evaluate_constraints(x, constraints):
  constraints[0] = function of x
  constraints[1] = function of x
  ...

myCppModule.solve(evaluate_constraints)

Unfortunately, some copy must happen somewhere, because the C++ object constraints is not modified. I'm not sure whether I missed something totally obvious (I've stumbled upon suggestions to use py::return_value_policy::reference_internal, but to no avail) or whether it is indeed a bit tricky to address. Hope you can crack it!

Note: the second parameter is a Vector here, but for other callbacks, it could be a C++ matrix type.


Solution

  • pybind11 stores the C++ object by value in the python object, think of the python object as a struct whose first member is a control block and the second member is your C++ class, so in order to construct the python object your C++ object has to be copied.

    pybind11 also allows you to store shared_ptr inside your python object .... this shared_ptr can have an empty deleter in case you don't want python to manage the lifetime of your object, but it will still allocate the control block which is wasteful, and is very unsafe as the user can hold onto your C++ object and cause UB, so from a safety point of view, just allocate your object with make_shared and let python control its lifetime.

    another solution if you want to reinvent numpy is to create a "reference-like" object, call it VectorRef that only stores a pointer to your Vector similar to a span and wrap that instead, but be very careful of lifetimes, remember python is expecting to own this object and can hold onto it. it will also help to create a VectorCRef that provides a const view to avoid any copies.

    numpy can be used to wrap your matrix and can provide both a read-only and a read-write views over the data using the buffer protocol, but the code shown will only create the buffer from the python object, which requires copying the C++ object into the python object, and you cannot store it on the stack, so to avoid this copy you'll have to manually create a numpy array if you want it to point to stack memory and make your code less safe ... or just give up a create the python object by casting to a py::object then read and write from the copy.

    you can also just use Eigen instead which has out-of-the-box integration but is subject to a similar limitation requiring Eigen::Ref to be used to pass matricies by reference, which is just the VectorRef i described above.