2 years ago
#54936

Naum
How to bind a method that returns std::shared_ptr< uint8_t > with pybind11
From C++ via pybind11 to Python project I've met with an issue and I don't know how to proceed.
Intro, "Holder classes are only supported for custom types"
There is an abstract C++ class Page
with a pure virtual method Render
that returns a heap buffer guarded by a smart pointer:
class Page {
// ...
std::shared_ptr< uint8_t > Render( /*...*/ ) const = 0;
// ...
};
To solve the abstraction - I added a trampoline:
class PageTr : public Page {
//...
std::shared_ptr< uint8_t > Render( /*...*/ ) const override {
PYBIND11_OVERRIDE_PURE( std::shared_ptr< uint8_t >, Page, Render,
/*...*/ );
}
//...
};
The c++17 compiler (from within pybind11.h
) complained with: Holder classes are only supported for custom types.
After a deep study (based on discussion on binding std::shared_ptr<void>
) I realized that I must define an opaque custom type like:
using UInt8_Ptr = std::shared_ptr< uint8_t >;
PYBIND11_MAKE_OPAQUE( UInt8_Ptr );
inside appropriate .h
file, and, in a .cpp
file:
py::class_< UInt8_Ptr >( m, "UInt8_Ptr" );
And all was fine. The binding is:
void BindPage( py::module_& m ) {
py::class_< Page, PageTr, std::shared_ptr< Page >> cls( m, "Page" );
cls.def( "Render", &Page::Render, "ToDo", return_value_policy::reference )
// ...
}
The code compiles but was neither checked nor used yet.
Side Effect "TypeError: a bytes-like object is required, not 'UInt8_Ptr'"
But, after some time, at a totally independent place in project, an issue is raised.
I needed an access from Python to a C++ heap-allocated buffer, containing RGBA uint8_t
image samples in an instance of this class:
class Bitmap {
public:
//...
BitmapSamplesPtr GetSamples() {
return this->m_SamplesPtr;
}
private:
BitmapSamplesPtr m_SamplesPtr;
};
where BitmapSamplesPtr
is defined as:
using BitmapSamplesPtr = std::shared_ptr< uint8_t >;
It is bound as:
void Bind_Bitmap( py::module_& m ) {
py::class_< Bitmap, std::shared_ptr< Bitmap >> cls( m, "Bitmap" );
cls.def( "GetSamples", &Bitmap::GetSamples, "ToDo",
py::return_value_policy::reference );
//...
}
and I managed to get, within a test suite, an instance of it on Python side (as later pageBmp
). But when I tried to create a GIL.Image.frombuffer()
in this way:
w = pageBmp.GetWidth()
h = pageBmp.GetHeight()
pilImg = Image.frombuffer('RGBA', (w, h), pageBmp.GetSamples(), 'raw')
the Python complained with:
[...]
File "[...]\tests\test_Doc.py", line 87, in OnPageDone
pilImg = Image.frombuffer('RGBA', (w, h), pageBmp.GetSamples(), 'raw')
File "D:\Python38\lib\site-packages\PIL\Image.py", line 2796, in frombuffer
return frombytes(mode, size, data, decoder_name, args)
File "D:\Python38\lib\site-packages\PIL\Image.py", line 2742, in frombytes
im.frombytes(data, decoder_name, args)
File "D:\Python38\lib\site-packages\PIL\Image.py", line 807, in frombytes
s = d.decode(data)
TypeError: a bytes-like object is required, not 'UInt8_Ptr'
Thinking...
Then I realized that BitmapSamplesPtr
is treated as an alias of UInt8_Ptr
(once it is being propagated as opaque through the whole project) - and it is OK.
But how to solve the PIL Image issue? Should not un-opaque it since abstract Page still needs it. I am considering several scenarios/solutions:
- numpy, ndarray
- py::buffer_protocol
- py::bytes
but not sure on the propper way to go. It looks to me that, the most promising way (3.) is to replace UInt8_Ptr
with py::bytes
. Seems for that, I'll have to wrap the bindings with lambda to return py::bytes
.
And here are my questions...
- How to ensure that
std::shared_ptr< uint8_t >
exists as aPage::Render()
return value? Or in other words, was the opaque type of it a correct decision? - If opaque
UInt8_Ptr
is a must, how to expose theuint8_t*
from it on Python side? A getter method? - What are the bytes-like objects which will make
PIL.Image
happy? - If
py::bytes
is a solution, how the lambda function should look like? - Since
PIL.Image
is cappable to use aconst uint8_t*
with and without making a copy - what would be the optimal but enough secure way to go? (I would prefer a copy and decouple the ownership regardless it is not expected the bitmap-samples-buffer to be modified on Python side.)
Edit
I made an attempt that gave me first result.
First, (according to pybind11 PDF doc 12.2.7 Memory view) I used lambda function to expose the Bitmap samples via memoryview
as:
cls.def( "GetSamples",
[]( const Bitmap& bmp ) {
const uint8_t* bmpBuf = bmp.GetSamples().get();
uint32_t height = bmp.GetHeight();
uint32_t stride = bmp.GetStride();
return py::memoryview::from_memory( bmpBuf, height * stride );
},
"ToDo" );
But that produced another error:
TypeError: a bytes-like object is required, not `memoryview`
Then I investigated how to get bytes-like
from 'memoryview' and it was unexpectedly simple - wrap with bytes()
on Python side:
pilImg = Image.frombytes( 'RGBA', ( w, h ), bytes( pageBmp.GetSamples() ), 'raw' )
I switched to Image.frombytes
instead of Image.frombuffer
since the first makes a copy which ensures that the original buffer could be freely released once we return from the test code.
Saved PIL.Image to file looks fine.
Maybe there is a better way, thus I still appreciate comments.
python
c++
python-imaging-library
shared-ptr
pybind11
0 Answers
Your Answer