pybind / pybind/pybind11

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

Open
#2,120 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
18k
Forks
2.3k
Avg merge
5d 17h
Merged PRs (30d)
10

Description

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)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the attached example.cpp and its Python reproduction, then run the example to compare direct Image passing with Image stored in AnnotatedImage.image. Trace how the shared_ptr is converted in both paths. Done means the derived Python Image can be stored indirectly and dump_annotated_image can use it without the reported RuntimeError.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.