Skip to content

Commit ac1ba07

Browse files
committed
Fix #439 Added support for the buffer protocol.
New Demo35 demonstrates how to access fast numpy arrays using the buffer protocol.
1 parent f1a4945 commit ac1ba07

File tree

3 files changed

+224
-7
lines changed

3 files changed

+224
-7
lines changed

Demos/Demo35/PyBufferDemo.dpr

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
program PyBufferDemo;
2+
3+
{$APPTYPE CONSOLE}
4+
5+
{$R *.res}
6+
7+
uses
8+
System.SysUtils,
9+
System.Diagnostics,
10+
System.Variants,
11+
PythonEngine,
12+
VarPyth;
13+
14+
var
15+
PythonEngine: TPythonEngine;
16+
17+
procedure CreatePyEngine;
18+
begin
19+
PythonEngine := TPythonEngine.Create(nil);
20+
PythonEngine.Name := 'PythonEngine';
21+
PythonEngine.LoadDll;
22+
end;
23+
24+
procedure DestroyEngine;
25+
begin
26+
PythonEngine.Free;
27+
end;
28+
29+
const
30+
N = 100000;
31+
32+
type
33+
PIntArray = ^TIntArray;
34+
TIntArray = array[0..N - 1] of Integer;
35+
36+
var
37+
SW: TStopwatch;
38+
Sum: Int64;
39+
np: Variant;
40+
arr: Variant;
41+
np_arr: PPyObject;
42+
PyBuffer: Py_buffer;
43+
V: Variant;
44+
I: Integer;
45+
begin
46+
try
47+
CreatePyEngine;
48+
try
49+
// Import numpy and create an array
50+
np := Import('numpy');
51+
arr := np.array(BuiltinModule.range(N));
52+
53+
// This is the slow way to iterate the array
54+
WriteLn('Lazy but slow:');
55+
SW := TStopwatch.StartNew;
56+
Sum := 0;
57+
for V in VarPyIterate(arr) do
58+
Sum := Sum + BuiltinModule.int(V);
59+
SW.Stop;
60+
WriteLn(Format('Sum from 0 to %d = %d', [N, Sum]));
61+
WriteLn('Elapsed ms: ' + SW.ElapsedMilliseconds.ToString);
62+
WriteLn;
63+
64+
WriteLn('Using Py_Buffer:');
65+
SW := TStopwatch.StartNew;
66+
np_arr := ExtractPythonObjectFrom(arr);
67+
PythonEngine.PyObject_GetBuffer(np_arr, @PyBuffer, PyBUF_CONTIG);
68+
PythonEngine.CheckError;
69+
try
70+
Sum := 0;
71+
for I := 0 to N - 1 do
72+
Sum := Sum + PIntArray(PyBuffer.buf)^[I];
73+
SW.Stop;
74+
WriteLn(Format('Sum from 0 to %d = %d', [N, Sum]));
75+
finally
76+
PythonEngine.PyBuffer_Release(@PyBuffer);
77+
end;
78+
WriteLn('Elapsed ms: ' + SW.ElapsedMilliseconds.ToString);
79+
WriteLn;
80+
81+
// test write access
82+
PIntArray(PyBuffer.buf)^[0] := 999;
83+
if BuiltinModule.int(arr.GetItem(0)) = 999 then
84+
WriteLn('Successfully modified the numpy array using Py_buffer');
85+
finally
86+
DestroyEngine;
87+
end;
88+
except
89+
on E: Exception do
90+
Writeln(E.ClassName, ': ', E.Message);
91+
end;
92+
ReadLn;
93+
end.

Demos/readme.txt

+1
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ Demo31 Using WrapDelphi to access Delphi Form attributes
3131
Demo32 Demo08 revisited using WrapDelphi
3232
Demo33 Using Threads inside Python
3333
Demo34 Dynamically creating, destroying and recreating PythonEngine. Uses PythonVersions
34+
Demo35 Fast access to numpy arrays using the buffer protocol

Source/PythonEngine.pas

+130-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
unit PythonEngine;
1818

19-
{ TODO -oMMM : implement tp_as_buffer slot }
2019
{ TODO -oMMM : implement Attribute descriptor and subclassing stuff }
2120

2221
{$IFNDEF FPC}
@@ -189,6 +188,7 @@ TPythonVersionProp = record
189188
WCharTString = UnicodeString;
190189
{$ENDIF}
191190

191+
PPy_ssize_t = PNativeInt;
192192
Py_ssize_t = NativeInt;
193193

194194
const
@@ -307,7 +307,7 @@ TPythonVersionProp = record
307307
T_UINT = 11;
308308
T_ULONG = 12;
309309

310-
//* Added by Jack: strings contained in the structure */
310+
//* strings contained in the structure */
311311
T_STRING_INPLACE= 13;
312312

313313
T_OBJECT_EX = 16;{* Like T_OBJECT, but raises AttributeError
@@ -326,6 +326,32 @@ TPythonVersionProp = record
326326
mtStringInplace, mtObjectEx);
327327
TPyMemberFlag = (mfDefault, mfReadOnly, mfReadRestricted, mfWriteRestricted, mfRestricted);
328328

329+
// Constants from pybuffer.h
330+
const
331+
PyBUF_MAX_NDIM = 64; // Maximum number of dimensions
332+
// Flags for getting buffers. Keep these in sync with inspect.BufferFlags.
333+
PyBUF_SIMPLE = 0;
334+
PyBUF_WRITABLE = 1;
335+
336+
PyBUF_FORMAT = $0004;
337+
PyBUF_ND = $0008;
338+
PyBUF_STRIDES = $0010 or PyBUF_ND;
339+
PyBUF_C_CONTIGUOUS = $0020 or PyBUF_STRIDES;
340+
PyBUF_F_CONTIGUOUS = $0040 or PyBUF_STRIDES;
341+
PyBUF_ANY_CONTIGUOUS = $0080 or PyBUF_STRIDES;
342+
PyBUF_INDIRECT = $0100 or PyBUF_STRIDES;
343+
PyBUF_CONTIG = PyBUF_ND or PyBUF_WRITABLE;
344+
PyBUF_CONTIG_RO = PyBUF_ND;
345+
PyBUF_STRIDED = PyBUF_STRIDES or PyBUF_WRITABLE;
346+
PyBUF_STRIDED_RO = PyBUF_STRIDES;
347+
PyBUF_RECORDS = PyBUF_STRIDES or PyBUF_WRITABLE or PyBUF_FORMAT;
348+
PyBUF_RECORDS_RO = PyBUF_STRIDES or PyBUF_FORMAT;
349+
PyBUF_FULL = PyBUF_INDIRECT or PyBUF_WRITABLE or PyBUF_FORMAT;
350+
PyBUF_FULL_RO = PyBUF_INDIRECT or PyBUF_FORMAT;
351+
352+
PyBUF_READ = $100;
353+
PyBUF_WRITE = $200;
354+
329355
//#######################################################
330356
//## ##
331357
//## Non-Python specific constants ##
@@ -610,7 +636,35 @@ TPythonVersionProp = record
610636
m_free : inquiry;
611637
end;
612638

639+
// pybuffer.h
640+
641+
PPy_buffer = ^Py_Buffer;
642+
Py_buffer = record
643+
buf: Pointer;
644+
obj: PPyObject; (* owned reference *)
645+
len: Py_ssize_t;
646+
itemsize: Py_ssize_t; (* This is Py_ssize_t so it can be
647+
pointed to by strides in simple case.*)
648+
readonly: Integer;
649+
ndim: Integer;
650+
format: PAnsiChar;
651+
shape: PPy_ssize_t ;
652+
strides: PPy_ssize_t;
653+
suboffsets: PPy_ssize_t;
654+
internal: Pointer;
655+
end;
656+
657+
getbufferproc = function(exporter: PPyObject; view: PPy_buffer; flags: Integer): Integer; cdecl;
658+
releasebufferproc = procedure(exporter: PPyObject; view: PPy_buffer); cdecl;
659+
660+
PPyBufferProcs = ^PyBufferProcs;
661+
PyBufferProcs = record
662+
bf_getbuffer: getbufferproc;
663+
bf_releasebuffer: releasebufferproc;
664+
end;
665+
613666
// object.h
667+
614668
PyTypeObject = {$IFDEF CPUX86}packed{$ENDIF} record
615669
ob_refcnt: NativeInt;
616670
ob_type: PPyTypeObject;
@@ -643,7 +697,7 @@ TPythonVersionProp = record
643697
tp_setattro: setattrofunc;
644698

645699
// Functions to access object as input/output buffer
646-
tp_as_buffer: Pointer; // PPyBufferProcs - not implemented
700+
tp_as_buffer: PPyBufferProcs;
647701
// Flags to define presence of optional/expanded features
648702
tp_flags: C_ULong;
649703

@@ -1075,6 +1129,7 @@ FutureWarning = class (EPyWarning);
10751129
EPySyntaxWarning = class (EPyWarning);
10761130
EPyRuntimeWarning = class (EPyWarning);
10771131
EPyReferenceError = class (EPyStandardError);
1132+
EPyBufferError = class (EPyException);
10781133
{$IFDEF MSWINDOWS}
10791134
EPyWindowsError = class (EPyOSError);
10801135
{$ENDIF}
@@ -1326,6 +1381,7 @@ TPythonInterface=class(TDynamicDll)
13261381
PyExc_UnicodeDecodeError: PPPyObject;
13271382
PyExc_UnicodeEncodeError: PPPyObject;
13281383
PyExc_UnicodeTranslateError: PPPyObject;
1384+
PyExc_BufferError: PPPyObject;
13291385

13301386
PyCode_Type: PPyTypeObject;
13311387
PyType_Type: PPyTypeObject;
@@ -1427,8 +1483,20 @@ TPythonInterface=class(TDynamicDll)
14271483
PySys_SetArgv: procedure( argc: Integer; argv: PPWCharT); cdecl;
14281484

14291485
PyCFunction_NewEx: function(md:PPyMethodDef;self, ob:PPyObject):PPyObject; cdecl;
1430-
// Removed. Use PyEval_CallObjectWithKeywords with third argument nil
1431-
// PyEval_CallObject: function(callable_obj, args:PPyObject):PPyObject; cdecl;
1486+
1487+
PyBuffer_GetPointer: function(view: PPy_buffer; indices: PPy_ssize_t): Pointer; cdecl;
1488+
PyBuffer_SizeFromFormat: function(format: PAnsiChar): Py_ssize_t; cdecl; // New in Python 3.9
1489+
PyBuffer_ToContiguous: function(buf: Pointer; view: PPy_buffer; len: Py_ssize_t; order: AnsiChar): Integer; cdecl;
1490+
PyBuffer_FromContiguous: function(view: PPy_buffer; buf: Pointer; len: Py_ssize_t; order: AnsiChar): Integer; cdecl;
1491+
PyBuffer_IsContiguous: function(view: PPy_buffer; fort: AnsiChar): Integer; cdecl;
1492+
PyBuffer_FillContiguousStrides: procedure(ndims: Integer; shape: Py_ssize_t;
1493+
strides: PPy_ssize_t; itemsize: Integer; fort: AnsiChar); cdecl;
1494+
PyBuffer_FillInfo: function(view: PPy_buffer; o: PPyObject; buf: Pointer;
1495+
len: Py_ssize_t; readonly: Integer; flags: Integer): Integer; cdecl;
1496+
PyBuffer_Release: procedure(view: PPy_buffer); cdecl;
1497+
1498+
// Removed. Use PyEval_CallObjectWithKeywords with third argument nil
1499+
// PyEval_CallObject: function(callable_obj, args:PPyObject):PPyObject; cdecl;
14321500
PyEval_CallObjectWithKeywords:function (callable_obj, args, kw:PPyObject):PPyObject; cdecl;
14331501
PyEval_GetFrame:function :PPyObject; cdecl;
14341502
PyEval_GetGlobals:function :PPyObject; cdecl;
@@ -1546,6 +1614,9 @@ TPythonInterface=class(TDynamicDll)
15461614
PyObject_GC_Del:procedure (ob:PPyObject); cdecl;
15471615
PyObject_GC_Track:procedure (ob:PPyObject); cdecl;
15481616
PyObject_GC_UnTrack:procedure (ob:PPyObject); cdecl;
1617+
PyObject_CheckBuffer: function(obj: PPyObject): Integer; cdecl;
1618+
PyObject_GetBuffer: function(obj: PPyObject; view: PPy_buffer; flags: Integer): Integer; cdecl;
1619+
PyObject_CopyData: function (dest: PPyObject; src: PPyObject): Integer; cdecl;
15491620
PySequence_Check:function (ob:PPyObject):integer; cdecl;
15501621
PySequence_Concat:function (ob1,ob2:PPyObject):PPyObject; cdecl;
15511622
PySequence_Count:function (ob1,ob2:PPyObject):integer; cdecl;
@@ -2427,6 +2498,8 @@ TPyObject = class
24272498
function Iter : PPyObject; virtual;
24282499
function IterNext : PPyObject; virtual;
24292500
function Init( args, kwds : PPyObject ) : Integer; virtual;
2501+
function GetBuffer(view: PPy_buffer; flags: Integer): Integer; virtual;
2502+
procedure ReleaseBuffer(view: PPy_buffer); virtual;
24302503

24312504
// Number services
24322505
function NbAdd( obj : PPyObject) : PPyObject; virtual;
@@ -2495,7 +2568,8 @@ TPyObjectClass = class of TPyObject;
24952568
// since version 2.1
24962569
bsRichCompare,
24972570
// since version 2.2
2498-
bsIter, bsIterNext);
2571+
bsIter, bsIterNext,
2572+
bsBuffer);
24992573
TNumberServices = set of (nsAdd, nsSubtract, nsMultiply,
25002574
nsRemainder, nsDivmod,
25012575
nsPower, nsNegative, nsPositive,
@@ -2571,6 +2645,7 @@ TPythonType = class(TGetSetContainer)
25712645
FTypeFlags : TPFlags;
25722646
FCreateFunc : PPyObject;
25732647
FCreateFuncDef : PyMethodDef;
2648+
FBufferProcs: PyBufferProcs;
25742649
FGenerateCreateFunction: Boolean;
25752650

25762651
procedure Notification( AComponent: TComponent;
@@ -3550,6 +3625,8 @@ procedure TPythonInterface.MapDll;
35503625
PyExc_UnicodeDecodeError := Import('PyExc_UnicodeDecodeError');
35513626
PyExc_UnicodeEncodeError := Import('PyExc_UnicodeEncodeError');
35523627
PyExc_UnicodeTranslateError:= Import('PyExc_UnicodeTranslateError');
3628+
PyExc_BufferError := Import('PyExc_BufferError');
3629+
35533630
PyType_Type := Import('PyType_Type');
35543631
PyCFunction_Type := Import('PyCFunction_Type');
35553632
PyCode_Type := Import('PyCode_Type');
@@ -3639,7 +3716,17 @@ procedure TPythonInterface.MapDll;
36393716
PySys_SetArgv := Import('PySys_SetArgv');
36403717
Py_Exit := Import('Py_Exit');
36413718

3642-
PyCFunction_NewEx := Import('PyCFunction_NewEx');
3719+
PyCFunction_NewEx := Import('PyCFunction_NewEx');
3720+
3721+
PyBuffer_GetPointer := Import('PyBuffer_GetPointer');
3722+
PyBuffer_ToContiguous := Import('PyBuffer_ToContiguous');
3723+
PyBuffer_FromContiguous := Import('PyBuffer_FromContiguous');
3724+
PyBuffer_IsContiguous := Import('PyBuffer_IsContiguous');
3725+
PyBuffer_FillContiguousStrides := Import('PyBuffer_FillContiguousStrides');
3726+
PyBuffer_FillInfo := Import('PyBuffer_FillInfo');
3727+
PyBuffer_Release := Import('PyBuffer_Release');
3728+
if (FMajorVersion > 3) or (FMinorVersion > 9) then
3729+
PyBuffer_SizeFromFormat := Import('PyBuffer_SizeFromFormat');
36433730

36443731
PyEval_CallObjectWithKeywords:= Import('PyEval_CallObjectWithKeywords');
36453732
PyEval_GetFrame := Import('PyEval_GetFrame');
@@ -3757,6 +3844,9 @@ procedure TPythonInterface.MapDll;
37573844
PyObject_GC_Del := Import('PyObject_GC_Del');
37583845
PyObject_GC_Track := Import('PyObject_GC_Track');
37593846
PyObject_GC_UnTrack := Import('PyObject_GC_UnTrack');
3847+
PyObject_CheckBuffer := Import('PyObject_CheckBuffer');
3848+
PyObject_GetBuffer := Import('PyObject_GetBuffer');
3849+
PyObject_CopyData := Import('PyObject_CopyData');
37603850
PySequence_Check := Import('PySequence_Check');
37613851
PySequence_Concat := Import('PySequence_Concat');
37623852
PySequence_Count := Import('PySequence_Count');
@@ -5263,6 +5353,8 @@ procedure TPythonEngine.RaiseError;
52635353
raise Define( EPyValueError.Create(''), s_type, s_value )
52645354
else if (PyErr_GivenExceptionMatches(err_type, PyExc_ReferenceError^) <> 0) then
52655355
raise Define( EPyReferenceError.Create(''), s_type, s_value )
5356+
else if (PyErr_GivenExceptionMatches(err_type, PyExc_BufferError^) <> 0) then
5357+
raise Define( EPyBufferError.Create(''), s_type, s_value )
52665358
else if (PyErr_GivenExceptionMatches(err_type, PyExc_SystemError^) <> 0) then
52675359
raise Define( EPySystemError.Create(''), s_type, s_value )
52685360
else if (PyErr_GivenExceptionMatches(err_type, PyExc_MemoryError^) <> 0) then
@@ -7589,6 +7681,16 @@ function TPyObject.GetAttrO( key: PPyObject) : PPyObject;
75897681
Result := GetPythonEngine.PyObject_GenericGetAttr(GetSelf, key);
75907682
end;
75917683

7684+
function TPyObject.GetBuffer(view: PPy_buffer; flags: Integer): Integer;
7685+
// Default implementation that raises an exception
7686+
// Subclass implementing the buffer protocol will need to override this
7687+
begin
7688+
view^.obj := nil;
7689+
with GetPythonEngine do
7690+
PyErr_SetObject(PyExc_BufferError^, PyUnicodeFromString(''));
7691+
Result := -1;
7692+
end;
7693+
75927694
function TPyObject.SetAttrO( key, value: PPyObject) : Integer;
75937695
begin
75947696
Result := GetPythonEngine.PyObject_GenericSetAttr(GetSelf, key, value);
@@ -7884,6 +7986,11 @@ class procedure TPyObject.RegisterMethods( APythonType : TPythonType );
78847986
begin
78857987
end;
78867988

7989+
procedure TPyObject.ReleaseBuffer(view: PPy_buffer);
7990+
begin
7991+
// Do nothing. Subclasses may provide an implementation.
7992+
end;
7993+
78877994
class procedure TPyObject.RegisterMembers( APythonType : TPythonType );
78887995
begin
78897996
end;
@@ -8183,6 +8290,16 @@ function TPythonType_InitSubtype( pSelf, args, kwds : PPyObject) : Integer; cde
81838290
Result := PythonToDelphi(pSelf).Init(args, kwds);
81848291
end;
81858292

8293+
function TPythonType_GetBuffer(exporter: PPyObject; view: PPy_buffer; flags: Integer): Integer; cdecl;
8294+
begin
8295+
Result := PythonToDelphi(exporter).GetBuffer(view, flags);
8296+
end;
8297+
8298+
procedure TPythonType_ReleaseBuffer(exporter: PPyObject; view: PPy_buffer); cdecl;
8299+
begin
8300+
PythonToDelphi(exporter).ReleaseBuffer(view);
8301+
end;
8302+
81868303
function TPythonType.NewSubtypeInst( aType: PPyTypeObject; args, kwds : PPyObject) : PPyObject;
81878304
var
81888305
obj : TPyObject;
@@ -8483,6 +8600,12 @@ procedure TPythonType.InitServices;
84838600
tp_iter := TPythonType_Iter;
84848601
if bsIterNext in Services.Basic then
84858602
tp_iternext := TPythonType_IterNext;
8603+
if bsBuffer in Services.Basic then
8604+
begin
8605+
FBufferProcs.bf_getbuffer := TPythonType_GetBuffer;
8606+
FBufferProcs.bf_releasebuffer := TPythonType_ReleaseBuffer;
8607+
tp_as_buffer := @FBufferProcs;
8608+
end;
84868609
if tpfBaseType in TypeFlags then
84878610
begin
84888611
tp_init := TPythonType_InitSubtype;

0 commit comments

Comments
 (0)