Skip to content

Commit 58ed8fe

Browse files
committed
VectorTypedData : Support Python buffer protocol
1 parent 20550fb commit 58ed8fe

File tree

5 files changed

+279
-2
lines changed

5 files changed

+279
-2
lines changed

SConstruct

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,13 @@ o.Add(
9393
# https://developercommunity.visualstudio.com/content/problem/756694/including-windowsh-and-boostinterprocess-headers-l.html
9494
# /DBOOST_ALL_NO_LIB is needed to find Boost when it is built without
9595
# verbose system information added to file and directory names.
96+
# /DZc:strictStrings- fixes a compilation error when setting the `Py_buffer.format` member in `VectorTypedDataBinding.inl`
97+
# `getBuffer()` method. Python declares that member as `char *` but MSVC requires `const char *` for string literals.
98+
# Disabling strict strings relaxes that requirement.
9699
o.Add(
97100
"CXXFLAGS",
98101
"The extra flags to pass to the C++ compiler during compilation.",
99-
[ "-pipe", "-Wall", "-Wextra", "-Wsuggest-override" ] if Environment()["PLATFORM"] != "win32" else [ "/permissive-", "/D_USE_MATH_DEFINES", "/Zc:externC-", "/DBOOST_ALL_NO_LIB" ],
102+
[ "-pipe", "-Wall", "-Wextra", "-Wsuggest-override" ] if Environment()["PLATFORM"] != "win32" else [ "/permissive-", "/D_USE_MATH_DEFINES", "/Zc:externC-", "/DBOOST_ALL_NO_LIB", "/Zc:strictStrings-" ],
100103
)
101104

102105
o.Add(

include/IECorePython/VectorTypedDataBinding.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,35 @@
3737

3838
#include "IECorePython/Export.h"
3939

40+
#include "IECore/Data.h"
41+
#include "IECore/RefCounted.h"
42+
43+
4044
namespace IECorePython
4145
{
46+
47+
class Buffer : public IECore::RefCounted
48+
{
49+
public :
50+
IE_CORE_DECLAREMEMBERPTR( Buffer );
51+
52+
Buffer( IECore::Data *data, const bool writable );
53+
~Buffer() override;
54+
55+
IECore::DataPtr asData() const;
56+
57+
bool isWritable() const;
58+
59+
static int getBuffer( PyObject *object, Py_buffer *view, int flags );
60+
static void releaseBuffer( PyObject *object, Py_buffer *view );
61+
62+
private :
63+
IECore::DataPtr m_data;
64+
const bool m_writable;
65+
};
66+
67+
IE_CORE_DECLAREPTR( Buffer )
68+
4269
extern IECOREPYTHON_API void bindAllVectorTypedData();
4370
}
4471

include/IECorePython/VectorTypedDataBinding.inl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,21 @@
3636
#define IECOREPYTHON_VECTORTYPEDDATABINDING_INL
3737

3838
#include "boost/python.hpp"
39+
#include "Python.h"
3940

4041
#include "IECorePython/IECoreBinding.h"
4142
#include "IECorePython/RunTimeTypedBinding.h"
43+
#include "IECorePython/VectorTypedDataBinding.h"
44+
45+
#include "IECore/MessageHandler.h"
46+
#include "IECore/TypeTraits.h"
4247

4348
#include "boost/numeric/conversion/cast.hpp"
49+
4450
#include "boost/python/suite/indexing/container_utils.hpp"
4551

4652
#include <sstream>
53+
#include <typeindex>
4754

4855
namespace IECorePython
4956
{
@@ -546,6 +553,16 @@ class VectorTypedDataFunctions
546553
return &x.readable() == &y.readable();
547554
}
548555

556+
static IECorePython::BufferPtr asReadOnlyBuffer( ThisClass &x )
557+
{
558+
return new Buffer( &x, /* writable = */ false );
559+
}
560+
561+
static IECorePython::BufferPtr asReadWriteBuffer( ThisClass &x )
562+
{
563+
return new Buffer( &x, /* writable = */ true );
564+
}
565+
549566

550567
protected:
551568
/*
@@ -700,7 +717,7 @@ std::string str<IECore::TypedData<std::vector<TYPE> > >( IECore::TypedData<std::
700717
#define BASIC_VECTOR_BINDING(ThisClass, Tname) \
701718
typedef VectorTypedDataFunctions< ThisClass > ThisBinder; \
702719
\
703-
RunTimeTypedClass<ThisClass>( \
720+
RunTimeTypedClass<ThisClass>( \
704721
Tname "-type vector class derived from Data class.\n" \
705722
"This class behaves like the native python lists, except that it only accepts " Tname " values.\n" \
706723
"The copy constructor accepts another instance of this class or a python list containing " Tname \
@@ -742,6 +759,10 @@ std::string str<IECore::TypedData<std::vector<TYPE> > >( IECore::TypedData<std::
742759
; \
743760
}
744761

762+
#define BIND_BUFFER_PROTOCOL_METHODS \
763+
.def("asReadOnlyBuffer", &ThisBinder::asReadOnlyBuffer, "Returns a read-only Buffer object that can be passed to Python objects that support the buffer protocol." ) \
764+
.def("asReadWriteBuffer", &ThisBinder::asReadWriteBuffer, "Returns a writable Buffer object that can be passed to Python objects that support the buffer protocol." ) \
765+
745766
// bind a VectorTypedData class that supports simple Math operators (+=, -= and *=)
746767
#define BIND_SIMPLE_OPERATED_VECTOR_TYPEDDATA(T, Tname) \
747768
{ \
@@ -755,6 +776,7 @@ std::string str<IECore::TypedData<std::vector<TYPE> > >( IECore::TypedData<std::
755776
.def("__imul__", &ThisBinder::imul, "inplace multiplication (s *= v) : accepts another vector of the same type or a single " Tname) \
756777
.def("__cmp__", &ThisBinder::invalidOperator, "Raises an exception. This vector type does not support comparison operators.") \
757778
.def("toString", &ThisBinder::toString, "Returns a string with a copy of the bytes in the vector.")\
779+
BIND_BUFFER_PROTOCOL_METHODS \
758780
; \
759781
}
760782

@@ -775,6 +797,7 @@ std::string str<IECore::TypedData<std::vector<TYPE> > >( IECore::TypedData<std::
775797
.def("__itruediv__", &ThisBinder::idiv, "inplace division (s /= v) : accepts another vector of the same type or a single " Tname) \
776798
.def("__cmp__", &ThisBinder::invalidOperator, "Raises an exception. This vector type does not support comparison operators.") \
777799
.def("toString", &ThisBinder::toString, "Returns a string with a copy of the bytes in the vector.")\
800+
BIND_BUFFER_PROTOCOL_METHODS \
778801
; \
779802
}
780803

@@ -799,6 +822,7 @@ std::string str<IECore::TypedData<std::vector<TYPE> > >( IECore::TypedData<std::
799822
.def("__gt__", &ThisBinder::gt, "The comparison is element-wise, like a string comparison. \n") \
800823
.def("__ge__", &ThisBinder::ge, "The comparison is element-wise, like a string comparison. \n") \
801824
.def("toString", &ThisBinder::toString, "Returns a string with a copy of the bytes in the vector.")\
825+
BIND_BUFFER_PROTOCOL_METHODS \
802826
; \
803827
}
804828

src/IECorePython/VectorTypedDataBinding.cpp

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
#include "IECorePython/RunTimeTypedBinding.h"
4848
#include "IECorePython/VectorTypedDataBinding.inl"
4949

50+
#include "IECore/DataAlgo.h"
5051
#include "IECore/Export.h"
5152
#include "IECore/VectorTypedData.h"
5253

@@ -68,6 +69,29 @@ using namespace boost::python;
6869
using namespace Imath;
6970
using namespace IECore;
7071

72+
namespace
73+
{
74+
75+
template<typename T>
76+
struct PythonType
77+
{
78+
static const char *value();
79+
};
80+
81+
template<> struct PythonType<half>{ static const char *value(){ return "e"; } };
82+
template<> struct PythonType<float>{ static const char *value(){ return "f"; } };
83+
template<> struct PythonType<double>{ static const char *value(){ return "d"; } };
84+
template<> struct PythonType<int>{ static const char *value(){ return "i"; } };
85+
template<> struct PythonType<unsigned int>{ static const char *value(){ return "I"; } };
86+
template<> struct PythonType<char>{ static const char *value(){ return "b"; } };
87+
template<> struct PythonType<unsigned char>{ static const char *value(){ return "B"; } };
88+
template<> struct PythonType<short>{ static const char *value(){ return "h"; } };
89+
template<> struct PythonType<unsigned short>{ static const char *value(){ return "H"; } };
90+
template<> struct PythonType<int64_t>{ static const char *value(){ return "q"; } };
91+
template<> struct PythonType<uint64_t>{ static const char *value(){ return "Q"; } };
92+
93+
} // namespace
94+
7195
namespace IECorePython
7296
{
7397

@@ -124,6 +148,109 @@ std::string str<BoolVectorData>( BoolVectorData &x )
124148
return s.str();
125149
}
126150

151+
Buffer::Buffer( Data *data, const bool writable ) : m_data( data->copy() ), m_writable( writable )
152+
{
153+
154+
}
155+
156+
Buffer::~Buffer()
157+
{
158+
159+
}
160+
161+
DataPtr Buffer::asData() const
162+
{
163+
return m_data->copy();
164+
}
165+
166+
bool Buffer::isWritable() const
167+
{
168+
return m_writable;
169+
}
170+
171+
int Buffer::getBuffer( PyObject *object, Py_buffer *view, int flags )
172+
{
173+
// This method is a customized variation on Python's `PyBuffer_FillInfo` to suit our needs.
174+
if( view == NULL ) {
175+
PyErr_SetString( PyExc_ValueError, "getBuffer(): view==NULL argument is obsolete" );
176+
return -1;
177+
}
178+
179+
if( flags != PyBUF_SIMPLE )
180+
{
181+
if( flags == PyBUF_READ || flags == PyBUF_WRITE )
182+
{
183+
PyErr_BadInternalCall();
184+
return -1;
185+
}
186+
}
187+
188+
IECorePython::BufferPtr self = boost::python::extract<IECorePython::BufferPtr>( object );
189+
if( !self )
190+
{
191+
/// \todo reword this
192+
PyErr_SetString( PyExc_ValueError, "getBuffer(): Buffer type does not match expected type." );
193+
return -1;
194+
}
195+
196+
if( ( flags | PyBUF_WRITABLE ) == PyBUF_WRITABLE && !self->isWritable() )
197+
{
198+
return -1;
199+
}
200+
201+
try
202+
{
203+
dispatch(
204+
self->m_data.get(),
205+
[&flags, &view, &object, &self]( auto *bufferData ) -> void
206+
{
207+
using DataType = typename std::remove_const_t< std::remove_pointer_t< decltype( bufferData ) > >;
208+
209+
if constexpr( TypeTraits::HasBaseType<DataType>::value && TypeTraits::IsNumericBasedVectorTypedData<DataType>::value )
210+
{
211+
using ElementType = typename DataType::ValueType::value_type;
212+
if constexpr( std::is_arithmetic_v<ElementType> )
213+
{
214+
view->obj = Py_XNewRef( object );
215+
view->readonly = !self->isWritable();
216+
view->buf = view->readonly ? (void*)bufferData->baseReadable() : (void*)bufferData->baseWritable();
217+
view->len = bufferData->readable().size() * sizeof( ElementType );
218+
view->itemsize = sizeof( ElementType );
219+
view->format = ( ( flags & PyBUF_FORMAT ) == PyBUF_FORMAT ) ? const_cast<char *>( PythonType<ElementType>::value() ) : NULL;
220+
view->ndim = 1;
221+
view->internal = NULL;
222+
view->shape = ( ( flags & PyBUF_ND ) == PyBUF_ND ) ? (Py_ssize_t*)( new Py_ssize_t( bufferData->readable().size() ) ) : NULL;
223+
view->strides = ( ( flags & PyBUF_STRIDES ) == PyBUF_STRIDES ) ? &( view->itemsize ) : NULL;
224+
view->suboffsets = NULL;
225+
}
226+
}
227+
}
228+
);
229+
}
230+
catch( Exception &e )
231+
{
232+
view->obj = NULL;
233+
PyErr_SetString( PyExc_BufferError, e.what() );
234+
return -1;
235+
}
236+
237+
return 0;
238+
}
239+
240+
void Buffer::releaseBuffer( PyObject *object, Py_buffer *view )
241+
{
242+
if( view->shape != NULL )
243+
{
244+
delete view->shape;
245+
}
246+
// Python takes care of decrementing `object`
247+
}
248+
249+
static PyBufferProcs BufferProtocol = {
250+
(getbufferproc)Buffer::getBuffer,
251+
(releasebufferproc)Buffer::releaseBuffer,
252+
};
253+
127254
void bindAllVectorTypedData()
128255
{
129256
// basic types
@@ -189,6 +316,16 @@ void bindAllVectorTypedData()
189316
bindImathColorVectorTypedData();
190317
bindImathBoxVectorTypedData();
191318
bindImathQuatVectorTypedData();
319+
320+
auto c = RefCountedClass<Buffer, IECore::RefCounted>( "Buffer" )
321+
.def( init<Data *, bool>() )
322+
.def( "asData", &Buffer::asData )
323+
.def( "isWritable", &Buffer::isWritable )
324+
;
325+
PyTypeObject *o = (PyTypeObject *)c.ptr();
326+
o->tp_as_buffer = &BufferProtocol;
327+
PyType_Modified( o );
328+
192329
}
193330

194331

test/IECore/VectorData.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import math
3838
import os
3939
import unittest
40+
import struct
4041
import imath
4142

4243
import IECore
@@ -1366,6 +1367,91 @@ def test( self ) :
13661367
self.assertFalse( a._dataSourceEqual( ca2 ) )
13671368
self.assertTrue(ca2._dataSourceEqual( ca3 ) )
13681369

1370+
class TestBufferProtocol( unittest.TestCase ) :
1371+
1372+
def testSimpleTypes( self ) :
1373+
1374+
for elementType, vectorType, elementFormat in [
1375+
# It would be nice to test for `IECore.HalfVectorData: "e"`
1376+
# but that is not supported in our Python.
1377+
( float, IECore.FloatVectorData, "f" ),
1378+
( float, IECore.DoubleVectorData, "d" ),
1379+
( int, IECore.IntVectorData, "i" ),
1380+
( int, IECore.UIntVectorData, "I" ),
1381+
( chr, IECore.CharVectorData, "b" ),
1382+
( int, IECore.UCharVectorData, "B" ),
1383+
( int, IECore.ShortVectorData, "h" ),
1384+
( int, IECore.UShortVectorData, "H" ),
1385+
( int, IECore.Int64VectorData, "q" ),
1386+
( int, IECore.UInt64VectorData, "Q" ),
1387+
] :
1388+
with self.subTest( elementType = elementType, vectorType = vectorType ) :
1389+
v = vectorType( [ elementType( 1 ), elementType( 2 ), elementType( 3 ) ] )
1390+
1391+
b = v.asReadOnlyBuffer()
1392+
m = memoryview( b )
1393+
1394+
# `memoryview` returns `int` for C `char` / Python `chr` types. Cast to `chr`
1395+
self.assertEqual( list( m ) if elementType != chr else [ chr(i) for i in m ], list( v ) )
1396+
self.assertIs( m.obj, b )
1397+
self.assertTrue( m.readonly )
1398+
self.assertEqual( m.format, elementFormat )
1399+
self.assertEqual( m.ndim, 1 )
1400+
self.assertEqual( m.shape, ( len( v ), ) )
1401+
self.assertEqual( m.itemsize, struct.calcsize( elementFormat ) )
1402+
self.assertEqual( m.strides, ( m.itemsize, ) )
1403+
self.assertTrue( m.c_contiguous )
1404+
self.assertTrue( m.contiguous )
1405+
1406+
def testReadOnlyBuffer( self ) :
1407+
1408+
v = IECore.FloatVectorData( [ 1, 2, 3 ] )
1409+
buffer = v.asReadOnlyBuffer()
1410+
bufferData = buffer.asData()
1411+
self.assertFalse( buffer.isWritable() )
1412+
self.assertTrue( v._dataSourceEqual( bufferData ) )
1413+
self.assertTrue( bufferData._dataSourceEqual( buffer.asData() ) )
1414+
1415+
m = memoryview( buffer )
1416+
1417+
self.assertTrue( m.readonly )
1418+
self.assertTrue( v._dataSourceEqual( bufferData ) )
1419+
self.assertTrue( bufferData._dataSourceEqual( buffer.asData() ) )
1420+
1421+
# We can modify `v` without affecting our buffer.
1422+
v.append( 99 )
1423+
self.assertEqual( list( m ), [ 1, 2, 3 ] )
1424+
self.assertFalse( v._dataSourceEqual( bufferData ) )
1425+
self.assertTrue( bufferData._dataSourceEqual( buffer.asData() ) )
1426+
1427+
# We can modify `bufferData`. It's a copy so writing to it results
1428+
# in a deep copy of the source data.
1429+
bufferData[0] = 42
1430+
self.assertEqual( list( bufferData ), [ 42, 2, 3 ] )
1431+
self.assertFalse( bufferData._dataSourceEqual( buffer.asData() ) )
1432+
self.assertEqual( list( buffer.asData() ), [ 1, 2, 3 ] )
1433+
1434+
def testReadWriteBuffer( self ) :
1435+
1436+
v = IECore.FloatVectorData( [ 1, 2, 3 ] )
1437+
buffer = v.asReadWriteBuffer()
1438+
bufferData = buffer.asData()
1439+
self.assertTrue( buffer.isWritable() )
1440+
self.assertTrue( v._dataSourceEqual( bufferData ) )
1441+
self.assertTrue( bufferData._dataSourceEqual( buffer.asData() ) )
1442+
1443+
# Creating a writable `memoryview` makes `bufferData` unique.
1444+
m = memoryview( buffer )
1445+
self.assertFalse( m.readonly )
1446+
self.assertFalse( v._dataSourceEqual( buffer.asData() ) )
1447+
self.assertFalse( bufferData._dataSourceEqual( buffer.asData() ) )
1448+
1449+
# Modify the memoryview, which is reflected in the buffer.
1450+
m[0] = 42
1451+
self.assertEqual( list( m ), [ 42, 2, 3 ] )
1452+
self.assertEqual( list( buffer.asData() ), list( m ) )
1453+
1454+
13691455
if __name__ == "__main__":
13701456
unittest.main()
13711457

0 commit comments

Comments
 (0)