pybind / pybind11

Seamless operability between C++11 and Python
https://pybind11.readthedocs.io/
Other
15.76k stars 2.11k forks source link

C++ interface, Python derived instance in struct to C++ not working #2120

Open andioz opened 4 years ago

andioz commented 4 years ago

Hi,

First, thank you for this great library, it saves many weeks of work for me! I could solve most things in a simple and direct way, but with this construction I'm struggling. I found some hints in issues, stackoverflow, etc, but no direct answer to this problem, therefore let me give you a concrete example here.

I have a C++ interface Image, which is meant to be implemented on Python side. I want to pass an instance or this to C++, which works when it is passed directly as a parameter to a C++ function or method. But more complicated, I want to create more complex data structures, which contains references (shared pointer in this case) to such images. Now using this images passed into C++ function or method doesn't work, I guess the magic for the transfer is not done because the image reference is passed indirectly.

Example code: here I paste in the C++ and Python code, the complete files are attached here derrived-in-struct-problem.zip

Do I miss something, does a clean solution exist? Or do I need some kine of adapter for a workaround?

Thank you in advance! Andi

example.cpp:

#include <pybind11/pybind11.h>
#include <iostream>
#include <memory>

namespace py = pybind11;

class Image {
public:
  virtual ~Image() = default;
  virtual unsigned long width() const = 0;
  virtual unsigned long height() const = 0;
  virtual unsigned long channels() const = 0;
  virtual const void* data() const = 0;
};

struct AnnotatedImage {
  std::shared_ptr<Image> image;
  unsigned long dpi;
};

void dumpImage(const std::shared_ptr<Image>& image) {
  std::cout << "Image(";

  if (!image) {
    std::cout << "None";
  } else {
    std::cout << image->width() 
        << "x" << image->height() 
        << "x" << image->channels()
        << "@" << image->data();
  }

  std::cout << ")" << std::endl;
}

void dumpAnnotatedImage(const AnnotatedImage& image) {
  std::cout << "AnnotatedImage(";

  if (!image.image) {
    std::cout << "None";
  } else {
    std::cout << image.image->width() 
        << "x" << image.image->height() 
        << "x" << image.image->channels()
        << "@" << image.image->data();
  }

  std::cout << ", " << image.dpi;  
  std::cout << ")" << std::endl;
}

class ImageTrampoline : public Image {
public:
  using Image::Image;

  virtual unsigned long width() const override {
    PYBIND11_OVERLOAD_PURE(unsigned long, Image, width);
  }

  virtual unsigned long height() const override {
    PYBIND11_OVERLOAD_PURE(unsigned long, Image, height);
  }

  virtual unsigned long channels() const override {
    PYBIND11_OVERLOAD_PURE(unsigned long, Image, channels);
  }

  virtual const void* data() const override {
    PYBIND11_OVERLOAD_PURE(const void*, Image, data);
  }

};

void* dataFromBuffer(const py::buffer& buffer) {
  return buffer.request().ptr;
}

py::memoryview memoryviewFromImage(const Image& image) {
  ssize_t itemSize = sizeof(unsigned char);
  ssize_t size = image.width() * image.height() * image.channels();
  py::buffer_info info{};
  info.ptr = const_cast<void*>(image.data());
  info.itemsize = itemSize;
  info.size = size;
  info.format = py::format_descriptor<unsigned char>::format();
  info.ndim = 1;
  info.shape = {size};
  info.strides = {itemSize};
  return py::memoryview{info};
}

AnnotatedImage initializeAnnotatedImage(
    const std::shared_ptr<Image>& image,
    unsigned long dpi
) {
  return AnnotatedImage{image, dpi};
}

PYBIND11_MODULE(example, m) {
  py::class_<Image, std::shared_ptr<Image>, ImageTrampoline>(
      m,"Image","Image interface."
  ).def(
      py::init<>()
  ).def(
      "width", &Image::width,"Returns the width for the image in pixels."
  ).def(
      "height",&Image::height, "Returns the height for the image in pixels."
  ).def(
      "channels", &Image::channels, "Returns the number of color channels for the image."
  ).def(
      "data", &Image::data, "Returns the data buffer pointer for the image."
  ).def_static(
      "data_from_buffer", &dataFromBuffer, "Get native data pointer from buffer.", py::arg("buffer")
  ).def_static(
      "image_to_memoryview", &memoryviewFromImage, "Get memoryview from image.", py::arg("image")
  );

  py::class_<AnnotatedImage>(
      m, "AnnotatedImage", "AnnotatedImage data structure."
  ).def(
      py::init<>(&initializeAnnotatedImage), py::arg("image") = std::shared_ptr<Image>(), py::arg("dpi") = 0, "Initializer with default arguments."
  ).def_readwrite(
      "image", &AnnotatedImage::image, "The image itself."
  ).def_readwrite(
      "dpi", &AnnotatedImage::dpi, "The image's dots per inch."
  );

  m.def("dump_image", &dumpImage, "Dump image to console.");  
  m.def("dump_annotated_image", &dumpAnnotatedImage, "Dump annotated image to console.");  
}
import numpy as np

from example import *

class NumpyImage(Image):

    def __init__(self, source, copy=False):
        Image.__init__(self)
        self._array = None
        self._image = None
        if isinstance(source, np.ndarray):
            self._initialize_from_array(source, copy)
        elif isinstance(source, Image):
            self._initialize_from_image(source, copy)
        else:
            raise TypeError("Invalid source argument")
        return

    def _initialize_from_array(self, array, copy):
        self._array = np.copy(array) if copy else array
        return

    def _initialize_from_image(self, image, copy):
        shape = [image.height(), image.width()]
        if image.channels() != 1:
            shape.append(image.channels())
        array = np.frombuffer(
            buffer=image.memoryview_from_image(image),
            dtype=np.uint8
        ).reshape(shape)
        if copy:
            self._array = np.copy(array)
        else:
            self._array = array
            self._image = image
        return

    def width(self):
        return self._array.shape[1]

    def height(self):
        return self._array.shape[0]

    def channels(self):
        return self._array.shape[2] if self._array.ndim > 2 else 1

    def data(self):
        return self.data_from_buffer(self._array)

    def array(self):
        return self._array

array = np.full((480, 640, 3), ord("."), dtype=np.uint8)

image = NumpyImage(array, copy=True)
print("*** image:", image, type(image))
dump_image(image)

print("*** dump annotated image ***")
annotated_image = AnnotatedImage(dpi=500)
print("*** annotated_image.image (1):", annotated_image.image, type(annotated_image.image))
annotated_image.image = NumpyImage(array, copy=True)
print("*** annotated_image.image (2):", annotated_image.image, type(annotated_image.image))

try:
    dump_image(annotated_image.image)
except RuntimeError as e:
    print("*** runtime error:", e)

try:
    dump_annotated_image(annotated_image)
except RuntimeError as e:
    print("*** runtime error:", e)
andioz commented 4 years ago

OK, I was able to track down the problem towards a keep_alive problem. Reading the manual more carefully I found this:

Keep alive

In general, this policy is required when the C++ object is any kind of container and another objects being added to the container. keep_alive<Nurse, Patient> indicates that the argument with index Patient should be kept alive at least until the argument with index Nurse is freed by the garbage collector.

Conclusion: I need to use keep_alive for instances held in C++. But now I have 2 issues:

Is it possible to use keep_alive with other definition types like def_readwrite and def_property? And is it possible to use the variable holding the reference as nurse instead of the whole container structure?

Here another, simplified example code:

#include <pybind11/pybind11.h>
#include <iostream>
#include <memory>

namespace py = pybind11;

class Interface {
public:
  virtual ~Interface() = default;
  virtual unsigned long value() const = 0;
};

struct Structure {
  std::shared_ptr<Interface> instance;
};

class InterfaceTrampoline : public Interface {
public:
  using Interface::Interface;

  virtual unsigned long value() const override {
    PYBIND11_OVERLOAD_PURE(unsigned long, Interface, value);
  }

};

PYBIND11_MODULE(example, m) {
  py::class_<Interface, std::shared_ptr<Interface>, InterfaceTrampoline>(m, "Interface")
      .def(py::init<>())
      .def("value", &Interface::value);

  py::class_<Structure>(m, "Structure")
      .def(
          py::init<const std::shared_ptr<Interface>&>(), 
          py::keep_alive<1, 2>(),
          py::arg("instance") = nullptr
      )
      .def_readwrite(
          "instance", 
          &Structure::instance,
          py::keep_alive<1, 2>()
      )
      .def_property(
          "instance2",
          [](Structure& structure){ return structure.instance; },
          [](Structure& structure, const std::shared_ptr<Interface>& instance){ structure.instance = instance; },
          py::keep_alive<1, 2>()
      )
      .def(
          "instance3", 
          [](Structure& structure, const std::shared_ptr<Interface>& instance){ structure.instance = instance; },
          py::keep_alive<1, 2>()
      )
  ;
}
import sys

from example import *

class Derived(Interface):

    def __init__(self):
        Interface.__init__(self)
        print("*** init ***", self)
        return

    def __del__(self):
        print("*** del ***", self)
        return

    def value(self):
        return 42

d = Derived()
print("d refcount (initial):", sys.getrefcount(d))

s = Structure(d)
print("d refcount (after s = Structure(d)):", sys.getrefcount(d))
s.instance = d
print("d refcount (after s.instance = d):", sys.getrefcount(d))
s.instance2 = d
print("d refcount (after s.instance2 = d):", sys.getrefcount(d))
s.instance3(d)
print("d refcount (after s.instance3(d)):", sys.getrefcount(d))
s.instance = None
print("d refcount (after s.instance = None):", sys.getrefcount(d))
del s
print("d refcount (after del s):", sys.getrefcount(d))
del d
print("finished")
andioz commented 4 years ago

I found the solution for def_property here https://gitter.im/pybind/Lobby?at=5da73ece2d59854e7f13faa8AC

I have to wrap the setter function with py::cpp_function() and append the py::keep_alive<1, 2>() part as second argument.

Only left the question using it in def_readwrite and the second question about releasing the object on overwriting the attribute.