Skip to content

Commit 120e7f3

Browse files
Improved ref keeping of memoryview return values
A single weakref to the memoryview also stores a reference to the data owning Python object by using one of its methods as a weakref callback.
1 parent e7da106 commit 120e7f3

File tree

18 files changed

+660
-618
lines changed

18 files changed

+660
-618
lines changed

USAGE.rst

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -372,19 +372,36 @@ In some cases this includes writing to the data.
372372
buf = thumb.copy()
373373
thumb_im = PIL.Image.open(io.BytesIO(buf.data()))
374374
375-
Since version 0.18.0 python-exiv2 releases the memoryview (when the memory block is resized or deleted) to prevent problems such as segmentation faults:
375+
Since version 0.18.0 python-exiv2 releases the memoryview_ if the memory is invalidated (e.g. if the memory block is resized) to prevent problems such as segmentation faults:
376376

377377
.. code:: python
378378
379379
>>> buf = exiv2.DataBuf(b'fred')
380380
>>> data = buf.data()
381381
>>> print(bytes(data))
382382
b'fred'
383-
>>> del buf
383+
>>> buf.resize(128)
384384
>>> print(bytes(data))
385385
Traceback (most recent call last):
386386
File "<stdin>", line 1, in <module>
387387
ValueError: operation forbidden on released memoryview object
388+
>>>
389+
390+
Although memoryview_ objects can be used in a with_ statement this has no benefit with python-exiv2.
391+
The memory view's ``release`` method does nothing.
392+
Releasing any associated resources only happens when the memory view is deleted:
393+
394+
.. code:: python
395+
396+
with buf.data() as data:
397+
file.write(data)
398+
del data
399+
400+
is equivalent to
401+
402+
.. code:: python
403+
404+
file.write(buf.data())
388405
389406
Buffer interface
390407
----------------
@@ -400,6 +417,8 @@ For example, you could save a photograph's thumbnail in a separate file like thi
400417
with open('thumbnail.jpg', 'wb') as out_file:
401418
out_file.write(thumb.copy())
402419
420+
Use of this buffer interface is deprecated (since python-exiv2 v0.18.0) and the ``data()`` methods described above should be used instead.
421+
403422
Image data in memory
404423
--------------------
405424

@@ -410,32 +429,9 @@ The buffered data isn't actually read until ``Image::readMetadata`` is called, s
410429
When ``Image::writeMetadata`` is called exiv2 allocates a new block of memory to store the modified data.
411430
The ``Image::io`` method returns an `Exiv2::BasicIo`_ object that provides access to this data.
412431

413-
The ``BasicIo::mmap`` method allows access to the image file data without unnecessary copying.
414-
However it is rather error prone, crashing your Python program with a segmentation fault if anything goes wrong.
415-
416-
The ``Exiv2::BasicIo`` object must be open when ``mmap()`` is called.
417-
A Python `context manager`_ can be used to ensure that the ``open()`` and ``mmap()`` calls are paired with ``munmap()`` and ``close()`` calls:
418-
419-
.. code:: python
420-
421-
from contextlib import contextmanager
422-
423-
@contextmanager
424-
def get_file_data(image):
425-
exiv_io = image.io()
426-
exiv_io.open()
427-
try:
428-
yield exiv_io.mmap()
429-
finally:
430-
exiv_io.munmap()
431-
exiv_io.close()
432-
433-
# after setting some metadata
434-
image.writeMetadata()
435-
with get_file_data(image) as data:
436-
rsp = requests.post(url, files={'file': io.BytesIO(data)})
437-
438-
Since v0.18.0 python-exiv2 has a ``exiv2.BasicIo.data()`` method that is easier to use:
432+
The ``BasicIo::mmap`` and ``BasicIo::munmap`` methods allows access to the image file data without unnecessary copying.
433+
However they are rather error prone, crashing your Python program with a segmentation fault if anything goes wrong.
434+
Since python-exiv2 v0.18.0 it is much easier to use the ``data()`` method:
439435

440436
.. code:: python
441437
@@ -444,26 +440,17 @@ Since v0.18.0 python-exiv2 has a ``exiv2.BasicIo.data()`` method that is easier
444440
exiv_io = image.io()
445441
rsp = requests.post(url, files={'file': io.BytesIO(exiv_io.data())})
446442
447-
The ``exiv2.BasicIo`` Python type also exposes a `buffer interface`_.
448-
It allows the ``exiv2.BasicIo`` object to be used anywhere that a `bytes-like object`_ is expected:
449-
450-
.. code:: python
451-
452-
# after setting some metadata
453-
image.writeMetadata()
454-
exiv_io = image.io()
455-
rsp = requests.post(url, files={'file': io.BytesIO(exiv_io)})
456-
457-
Since python-exiv2 v0.15.0 this buffer can be writeable:
443+
The ``data()`` method can also return a writeable memoryview_:
458444

459445
.. code:: python
460446
461447
exiv_io = image.io()
462-
with memoryview(exiv_io) as data:
463-
data[23] = 157 # modifies data buffer
448+
data = exiv_io.data(True)
449+
data[23] = 157 # modifies data buffer
450+
del data # writes modified data to the buffer
464451
image.readMetadata() # reads modified buffer data
465452
466-
The modified data is written back to the file or memory buffer when the memoryview_ is released.
453+
The modified data is written back to the file or memory buffer when the memoryview_ is deleted.
467454

468455
.. _bytearray:
469456
https://docs.python.org/3/library/stdtypes.html#bytearray
@@ -513,3 +500,5 @@ The modified data is written back to the file or memory buffer when the memoryvi
513500
https://docs.python.org/3/library/stdtypes.html#memoryview
514501
.. _PyPI:
515502
https://pypi.org/project/exiv2/
503+
.. _with:
504+
https://docs.python.org/3/reference/compound_stmts.html#with

src/interface/basicio.i

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ UNIQUE_PTR(Exiv2::BasicIo);
5757
%thread Exiv2::BasicIo::seek;
5858
%thread Exiv2::BasicIo::transfer;
5959
%thread Exiv2::BasicIo::write;
60-
%thread Exiv2::BasicIo::_release;
60+
%thread Exiv2::BasicIo::_view_deleted_cb;
6161

6262
// Some calls don't raise exceptions
6363
%noexception Exiv2::BasicIo::eof;
@@ -158,16 +158,14 @@ RETURN_VIEW(Exiv2::byte* mmap, $1 ? arg1->size() : 0,
158158

159159
// Release memoryviews when some other functions are called
160160
%typemap(ret, fragment="memoryview_funcs")
161-
(int close), (int munmap), (long write), (size_t write),
162-
(void _release) %{
161+
(int close), (int munmap), (long write), (size_t write) %{
163162
release_views(self);
164163
%}
165164

166165
// Add data() method for easy access
167166
// The callback is used to call munmap when the memoryview is deleted
168-
RETURN_VIEW_CB(Exiv2::byte* data, $1 ? arg1->size() : 0,
169-
_global_writeable ? PyBUF_WRITE : PyBUF_READ,
170-
PyObject_GetAttrString(self, "_release"),)
167+
RETURN_VIEW(Exiv2::byte* data, $1 ? arg1->size() : 0,
168+
_global_writeable ? PyBUF_WRITE : PyBUF_READ,)
171169
%feature("docstring") Exiv2::BasicIo::data
172170
"Easy access to the IO data.
173171

@@ -183,7 +181,7 @@ munmap() and close() are called when the memoryview object is deleted.
183181
self->open();
184182
return self->mmap(isWriteable);
185183
};
186-
void _release(PyObject* ref) {
184+
void _view_deleted_cb(PyObject* ref) {
187185
self->munmap();
188186
self->close();
189187
};

src/interface/preview.i

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,12 @@ RETURN_VIEW(Exiv2::byte* pData, arg1->size(), PyBUF_READ,
9696
// Add data() alias of pData()
9797
RETURN_VIEW(Exiv2::byte* data, arg1->size(), PyBUF_READ,
9898
Exiv2::PreviewImage::data)
99+
%noexception Exiv2::PreviewImage::_view_deleted_cb;
99100
%extend Exiv2::PreviewImage {
100-
const Exiv2::byte* data() {
101-
return $self->pData();
102-
};
101+
const Exiv2::byte* data() {
102+
return $self->pData();
103+
};
104+
void _view_deleted_cb(PyObject* ref) {};
103105
}
104106

105107
// Deprecate pData() in favour of data() since 2025-07-02

src/interface/shared/buffers.i

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -70,71 +70,69 @@
7070
%}
7171
%enddef // OUTPUT_BUFFER_RW
7272

73-
// Import exiv2.utilities.view_manager
74-
%fragment("_import_view_manager_decl", "header") {
75-
static PyObject* view_manager = NULL;
76-
}
77-
%fragment("_import_view_manager", "init",
78-
fragment="_import_view_manager_decl") {
79-
{
80-
PyObject* mod = PyImport_ImportModule("exiv2.utilities");
81-
if (!mod)
82-
return INIT_ERROR_RETURN;
83-
view_manager = PyObject_GetAttrString(mod, "view_manager");
84-
if (!view_manager) {
85-
PyErr_SetString(
86-
PyExc_RuntimeError,
87-
"Import error: exiv2.utilities.view_manager not found.");
88-
return INIT_ERROR_RETURN;
89-
}
90-
}
91-
}
9273

9374
// Functions to store references to memoryview objects and release them
94-
%fragment("memoryview_funcs", "header", fragment="private_data",
95-
fragment="_import_view_manager") {
96-
static int store_view(PyObject* py_self, PyObject* view,
97-
PyObject* callback=NULL) {
98-
PyObject* view_ref = PyWeakref_NewRef(view, callback);
99-
if (!view_ref)
100-
return -1;
101-
PyObject* marker = private_store_get(py_self, "marker");
102-
if (!marker) {
103-
// Marker is any weakrefable object.
104-
marker = PySet_New(NULL);
105-
if (!marker)
75+
%fragment("memoryview_funcs", "header", fragment="private_data") {
76+
static int store_view(PyObject* py_self, PyObject* view) {
77+
PyObject* view_list = private_store_get(py_self, "view_list");
78+
if (!view_list) {
79+
view_list = PyList_New(0);
80+
if (!view_list)
10681
return -1;
107-
int error = private_store_set(py_self, "marker", marker);
108-
Py_DECREF(marker);
82+
int error = private_store_set(py_self, "view_list", view_list);
83+
Py_DECREF(view_list);
10984
if (error)
11085
return -1;
11186
}
112-
PyObject* OK = PyObject_CallMethod(
113-
view_manager, "store_view", "(OO)", marker, view_ref);
114-
Py_DECREF(view_ref);
115-
if (!OK)
87+
PyObject* callback = PyObject_GetAttrString(py_self, "_view_deleted_cb");
88+
if (!callback)
11689
return -1;
117-
Py_DECREF(OK);
118-
return 0;
90+
PyObject* view_ref = PyWeakref_NewRef(view, callback);
91+
Py_DECREF(callback);
92+
if (!view_ref)
93+
return -1;
94+
int result = PyList_Append(view_list, view_ref);
95+
Py_DECREF(view_ref);
96+
return result;
11997
};
12098
static int release_views(PyObject* py_self) {
121-
private_store_del(py_self, "marker");
99+
PyObject* view_list = private_store_get(py_self, "view_list");
100+
if (!view_list)
101+
return 0;
102+
PyObject* view_ref = NULL;
103+
PyObject* view = NULL;
104+
for (Py_ssize_t idx = PyList_Size(view_list); idx > 0; idx--) {
105+
view_ref = PyList_GetItem(view_list, idx - 1);
106+
view = PyWeakref_GetObject(view_ref);
107+
if (view != Py_None)
108+
Py_XDECREF(PyObject_CallMethod(view, "release", NULL));
109+
PyList_SetSlice(view_list, idx - 1, idx, NULL);
110+
}
122111
return 0;
123112
};
124113
}
125114

126-
// Macro to convert byte* return value to memoryview
127-
// WARNING: return value does not keep a reference to the data it points to
128-
%define RETURN_VIEW_CB(signature, size_func, flags, callback, doc_method)
115+
/* Macro to convert byte* (or similar) return value to memoryview
116+
*
117+
* We can't store a reference to the data owner in the memoryview result
118+
* so we store a weak reference to the memoryview in the data owner. To
119+
* prevent the data owner being deleted while the memoryview exists we
120+
* use a method of the Python data owner as the weakref callback. This
121+
* increments the data owner's ref count, preventing it from being deleted,
122+
* then decrements it when the memoryview is deleted (and the callback is
123+
* called). The callback doesn't have to do anything, but it can be used
124+
* for cleanup (e.g. calling BasicIo::munmap).
125+
*/
126+
%define RETURN_VIEW(signature, size_func, flags, doc_method)
129127
%typemap(doctype) signature "memoryview";
130-
%typemap(out, fragment="memoryview_funcs") (signature) %{
128+
%typemap(out, fragment="memoryview_funcs") (signature) {
131129
$result = PyMemoryView_FromMemory((char*)$1, size_func, flags);
132130
if (!$result)
133131
SWIG_fail;
134132
// Store a weak ref to the new memoryview
135-
if (store_view(self, $result, callback))
133+
if (store_view(self, $result))
136134
SWIG_fail;
137-
%}
135+
}
138136
#if #doc_method != ""
139137
%feature("docstring") doc_method
140138
"Returns a temporary Python memoryview of the object's data.
@@ -143,9 +141,6 @@ WARNING: do not resize or delete the object while using the view.
143141
144142
:rtype: memoryview"
145143
#endif
146-
%enddef // RETURN_VIEW_CB
147-
%define RETURN_VIEW(signature, size_func, flags, doc_method)
148-
RETURN_VIEW_CB(signature, size_func, flags, NULL, doc_method)
149144
%enddef // RETURN_VIEW
150145

151146

src/interface/types.i

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ RETURN_VIEW(Exiv2::byte* pData_, arg1->DATABUF_SIZE, PyBUF_WRITE,
244244
Exiv2::DataBuf::pData_)
245245
RETURN_VIEW(Exiv2::byte* data, arg1->DATABUF_SIZE, PyBUF_WRITE,
246246
Exiv2::DataBuf::data)
247+
%noexception Exiv2::DataBuf::_view_deleted_cb;
248+
%extend Exiv2::DataBuf {
249+
void _view_deleted_cb(PyObject* ref) {};
250+
}
247251

248252
// Release memoryview when other functions are called
249253
%typemap(ret, fragment="memoryview_funcs")

src/interface/utilities.i

Lines changed: 0 additions & 77 deletions
This file was deleted.

0 commit comments

Comments
 (0)