Skip to content

Commit 583ef42

Browse files
committed
WIP on the documentation of the Tuple API testing.
1 parent 8ad05dc commit 583ef42

File tree

8 files changed

+389
-45
lines changed

8 files changed

+389
-45
lines changed

doc/sphinx/source/refcount_and_containers.rst

+183-3
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,15 @@ The Python documentation for the `Tuple API <https://docs.python.org/3/c-api/tup
124124
``PyTuple_SetItem()``
125125
---------------------
126126

127+
`PyTuple_SetItem()`_ inserts an object into a tuple with error checking.
128+
It checks for these errors returning non-zero in that case:
129+
130+
* The container is a tuple.
131+
* The index is in range. Negative indexes are not allowed.
132+
133+
As seen below, the failure of `PyTuple_SetItem()`_ has serious consequences for the value that is intended to be
134+
inserted.
135+
127136
Basic Usage
128137
^^^^^^^^^^^
129138

@@ -159,21 +168,192 @@ What happens when you use `PyTuple_SetItem()`_ to replace an existing element in
159168
PyTuple_SetItem(container, 0, value_a); /* Ref count of value_a will be 1. */
160169
PyObject *value_b = new_unique_string(__FUNCTION__, NULL); /* Ref count will be 1. */
161170
PyTuple_SetItem(container, 0, value_b);
162-
/* Ref count of value_b will be 1, value_a ref count will be decremented. */
171+
/* Ref count of value_b will be 1, value_a ref count will be decremented (possibly free'd). */
163172
164173
For code tests see:
165174

166175
* ``dbg_PyTuple_SetItem_steals_replace`` in ``src/cpy/Containers/DebugContainers.c``.
167176
* ``test_PyTuple_SetItem_steals_replace`` in ``src/cpy/RefCount/cRefCount.c``.
168-
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SetItem_steals_replece``.
177+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SetItem_steals_replace``.
178+
179+
``PyTuple_SetItem()`` Failures
180+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
181+
182+
`PyTuple_SetItem()`_ can fail for these reasons:
183+
184+
* The given container is not a tuple.
185+
* The index is out of range; index < 0 or index >= tuple length. So negative indexes are not allowed.
186+
187+
A consequence of failure is that the value being inserted will be decref'd.
188+
For example this code will segfault:
189+
190+
.. code-block:: c
191+
192+
PyObject *container = PyTuple_New(1); /* Reference count will be 1. */
193+
PyObject *value = new_unique_string(__FUNCTION__, NULL); /* Ref count will be 1. */
194+
assert(Py_REFCNT(value) == 1);
195+
/* Lets increment the reference count so it can be examined after
196+
* PyTuple_SetItem() failure. */
197+
Py_INCREF(value);
198+
assert(Py_REFCNT(value) == 2);
199+
assert(!PyErr_Occurred());
200+
int result = PyTuple_SetItem(container, 1, value); /* Index out of range. */
201+
/* Failure... */
202+
assert(result == -1);
203+
assert(PyErr_Occurred());
204+
/* Yes, has been decremented on failure.
205+
* If we hadn't done Py_INCREF(value); above then value would have gone
206+
* out of scope and lost to us. */
207+
assert(Py_REFCNT(value) == 1);
208+
Py_DECREF(container); /* OK. */
209+
/* This would segfault if we hadn't done Py_INCREF(value); above. */
210+
Py_DECREF(value);
211+
212+
Or to be clear:
213+
214+
.. code-block:: c
215+
216+
PyObject *container = PyTuple_New(1); /* Reference count will be 1. */
217+
PyObject *value = new_unique_string(__FUNCTION__, NULL); /* Ref count will be 1. */
218+
PyTuple_SetItem(container, 1, value); /* Index out of range. */
219+
Py_DECREF(value); /* SIGSEGV */
169220
221+
For code tests see (not a tuple):
222+
223+
* ``dbg_PyTuple_SetItem_fails_not_a_tuple`` in ``src/cpy/Containers/DebugContainers.c``.
224+
* ``test_PyTuple_SetItem_fails_not_a_tuple`` in ``src/cpy/RefCount/cRefCount.c``.
225+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SetItem_fails_not_a_tuple``.
226+
227+
And (index out of range):
228+
229+
* ``dbg_PyTuple_SetItem_fails_out_of_range`` in ``src/cpy/Containers/DebugContainers.c``.
230+
* ``test_PyTuple_SetItem_fails_out_of_range`` in ``src/cpy/RefCount/cRefCount.c``.
231+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SetItem_fails_out_of_range``.
170232

171233

172234
``PyTuple_SET_ITEM()``
173235
----------------------
174236

175-
`PyTuple_SET_ITEM()`_
237+
`PyTuple_SET_ITEM()`_ inserts an object into a tuple without any error checking.
238+
Because of that is slightly faster than `PyTuple_SetItem()`_.
239+
If invoked with these errors the results are likely to be tragic, mostly undefined behaviour and/or memory corruption:
240+
241+
* The container is a not a tuple.
242+
* The index is out of range.
243+
244+
Importantly `PyTuple_SET_ITEM()`_ behaves **differently** to `PyTuple_SetItem()`_ when replacing another object.
245+
246+
Basic Usage
247+
^^^^^^^^^^^
248+
249+
`PyTuple_SET_ITEM()`_ *steals* a reference.
250+
251+
.. code-block:: c
252+
253+
PyObject *container = PyTuple_New(1); /* Reference count will be 1. */
254+
PyObject *value = new_unique_string(__FUNCTION__, NULL); /* Ref count will be 1. */
255+
PyTuple_SET_ITEM(container, 0, value); /* Ref count of value will be 1. */
256+
/* get_item == value and Ref count will be 1. */
257+
PyObject *get_item = PyTuple_GET_ITEM(container, 0);
258+
Py_DECREF(container); /* The contents of the container, value, will be decref'd */
259+
/* Do not do this as the container deals with this. */
260+
/* Py_DECREF(value); */
261+
262+
For code tests see:
263+
264+
* ``dbg_PyTuple_PyTuple_SET_ITEM_steals`` in ``src/cpy/Containers/DebugContainers.c``.
265+
* ``test_PyTuple_PyTuple_SET_ITEM_steals`` in ``src/cpy/RefCount/cRefCount.c``.
266+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_PyTuple_SET_ITEM_steals``.
267+
268+
Replacement
269+
^^^^^^^^^^^
270+
271+
`PyTuple_SET_ITEM()`_ differs from `PyTuple_SetItem()`_ when replacing an existing element in a tuple.
272+
The original reference will be leaked:
273+
274+
.. code-block:: c
275+
276+
PyObject *container = PyTuple_New(1); /* Reference count will be 1. */
277+
PyObject *value_a = new_unique_string(__FUNCTION__, NULL); /* Ref count will be 1. */
278+
PyTuple_SET_ITEM(container, 0, value_a); /* Ref count of value_a will be 1. */
279+
PyObject *value_b = new_unique_string(__FUNCTION__, NULL); /* Ref count will be 1. */
280+
PyTuple_SET_ITEM(container, 0, value_b);
281+
assert(Py_REFCNT(value_a) == 1);
282+
/* Ref count of value_b will be 1, value_a ref count will still be at 1. value_a will be leaked. */
283+
284+
For code tests see:
285+
286+
* ``dbg_PyTuple_SET_ITEM_steals_replace`` in ``src/cpy/Containers/DebugContainers.c``.
287+
* ``test_PyTuple_SetItem_steals_replace`` in ``src/cpy/RefCount/cRefCount.c``.
288+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SetItem_steals_replace``.
289+
290+
291+
``PyTuple_SetItem()`` Failures
292+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
293+
294+
`PyTuple_SetItem()`_ can fail for these reasons:
295+
296+
297+
Setting and Replacing ``NULL``
298+
------------------------------
299+
300+
Both `PyTuple_SetItem()`_ and `PyTuple_SET_ITEM()`_ behave the same way.
301+
302+
Setting a ``NULL`` will not cause an error:
303+
304+
.. code-block:: c
305+
306+
assert(!PyErr_Occurred());
307+
PyTuple_SetItem(container, 0, NULL);
308+
assert(!PyErr_Occurred());
309+
310+
For code tests see:
311+
312+
* ``dbg_PyTuple_SetIem_NULL`` in ``src/cpy/Containers/DebugContainers.c``.
313+
* ``test_PyTuple_SetItem_NULL`` in ``src/cpy/RefCount/cRefCount.c``.
314+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SetItem_NULL``.
315+
316+
And:
317+
318+
.. code-block:: c
319+
320+
assert(!PyErr_Occurred());
321+
PyTuple_SET_ITEM(container, 0, NULL);
322+
assert(!PyErr_Occurred());
323+
324+
For code tests see:
325+
326+
* ``dbg_PyTuple_SET_ITEM_NULL`` in ``src/cpy/Containers/DebugContainers.c``.
327+
* ``test_PyTuple_SET_ITEM_NULL`` in ``src/cpy/RefCount/cRefCount.c``.
328+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SET_ITEM_NULL``.
329+
330+
Replacing a ``NULL`` will not cause an error, the replaced value reference is *stolen*:
331+
332+
.. code-block:: c
333+
334+
PyTuple_SetItem(container, 0, NULL);
335+
PyObject *value = new_unique_string(__FUNCTION__, NULL); /* Ref Count of value is 1. */
336+
PyTuple_SetItem(container, 0, value); /* Ref Count of value is still 1. */
337+
338+
For code tests see:
339+
340+
* ``dbg_PyTuple_SetIem_NULL_SetIem`` in ``src/cpy/Containers/DebugContainers.c``.
341+
* ``test_PyTuple_SetItem_NULL_SetIem`` in ``src/cpy/RefCount/cRefCount.c``.
342+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SetItem_NULL_SetIem``.
343+
344+
And:
345+
346+
.. code-block:: c
347+
348+
PyTuple_SET_ITEM(container, 0, NULL);
349+
PyObject *value = new_unique_string(__FUNCTION__, NULL); /* Ref Count of value is 1. */
350+
PyTuple_SET_ITEM(container, 0, value); /* Ref Count of value is still 1. */
351+
352+
For code tests see:
176353

354+
* ``dbg_PyTuple_SET_ITEM_NULL_SET_ITEM`` in ``src/cpy/Containers/DebugContainers.c``.
355+
* ``test_PyTuple_SET_ITEM_NULL_SET_ITEM`` in ``src/cpy/RefCount/cRefCount.c``.
356+
* ``tests.unit.test_c_ref_count.test_test_PyTuple_SET_ITEM_NULL_SET_ITEM``.
177357

178358
``Py_BuildValue()``
179359
-------------------

src/cpy/Containers/DebugContainers.c

+88
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,94 @@ void dbg_PyTuple_SET_ITEM_NULL_SET_ITEM(void) {
371371
assert(!PyErr_Occurred());
372372
}
373373

374+
/**
375+
* A function that checks PyTuple_SetItem when the container is not a tuple.
376+
* This decrements the value reference count.
377+
*/
378+
void dbg_PyTuple_SetItem_fails_not_a_tuple(void) {
379+
printf("%s():\n", __FUNCTION__);
380+
if (PyErr_Occurred()) {
381+
PyErr_Print();
382+
return;
383+
}
384+
assert(!PyErr_Occurred());
385+
Py_ssize_t ref_count;
386+
387+
PyObject *container = PyList_New(1);
388+
assert(container);
389+
390+
ref_count = Py_REFCNT(container);
391+
assert(ref_count == 1);
392+
393+
PyObject *value = new_unique_string(__FUNCTION__, NULL);
394+
ref_count = Py_REFCNT(value);
395+
assert(ref_count == 1);
396+
397+
/* We want to to hold onto this as PyTuple_SetItem() will decref it. */
398+
Py_INCREF(value);
399+
ref_count = Py_REFCNT(value);
400+
assert(ref_count == 2);
401+
402+
int result = PyTuple_SetItem(container, 0, value);
403+
assert(result == -1);
404+
assert(PyErr_Occurred());
405+
PyErr_PrintEx(0);
406+
PyErr_Clear();
407+
408+
/* Yes, has been decremented on failure. */
409+
ref_count = Py_REFCNT(value);
410+
assert(ref_count == 1);
411+
412+
Py_DECREF(container);
413+
Py_DECREF(value);
414+
415+
assert(!PyErr_Occurred());
416+
}
417+
418+
/**
419+
* A function that checks PyTuple_SetItem when the container is not a tuple.
420+
* This decrements the value reference count.
421+
*/
422+
void dbg_PyTuple_SetItem_fails_out_of_range(void) {
423+
printf("%s():\n", __FUNCTION__);
424+
if (PyErr_Occurred()) {
425+
PyErr_Print();
426+
return;
427+
}
428+
assert(!PyErr_Occurred());
429+
Py_ssize_t ref_count;
430+
431+
PyObject *container = PyTuple_New(1);
432+
assert(container);
433+
434+
ref_count = Py_REFCNT(container);
435+
assert(ref_count == 1);
436+
437+
PyObject *value = new_unique_string(__FUNCTION__, NULL);
438+
ref_count = Py_REFCNT(value);
439+
assert(ref_count == 1);
440+
441+
/* We want to to hold onto this as PyTuple_SetItem() will decref it. */
442+
Py_INCREF(value);
443+
ref_count = Py_REFCNT(value);
444+
assert(ref_count == 2);
445+
446+
int result = PyTuple_SetItem(container, 1, value);
447+
assert(result == -1);
448+
assert(PyErr_Occurred());
449+
PyErr_PrintEx(0);
450+
PyErr_Clear();
451+
452+
/* Yes, has been decremented on failure. */
453+
ref_count = Py_REFCNT(value);
454+
assert(ref_count == 1);
455+
456+
Py_DECREF(container);
457+
Py_DECREF(value);
458+
459+
assert(!PyErr_Occurred());
460+
}
461+
374462
/**
375463
* Function that explores Py_BuildValue("(O)", ...).
376464
*/

src/cpy/Containers/DebugContainers.h

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ void dbg_PyTuple_SetIem_NULL(void);
1515
void dbg_PyTuple_SET_ITEM_NULL(void);
1616
void dbg_PyTuple_SetIem_NULL_SetItem(void);
1717
void dbg_PyTuple_SET_ITEM_NULL_SET_ITEM(void);
18+
void dbg_PyTuple_SetItem_fails_not_a_tuple(void);
19+
void dbg_PyTuple_SetItem_fails_out_of_range(void);
1820
void dbg_PyTuple_Py_BuildValue(void);
1921

2022
#endif //PYTHONEXTENSIONPATTERNS_DEBUGCONTAINERS_H

src/cpy/RefCount/cRefCount.c

+48
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,50 @@ test_PyTuple_SET_ITEM_NULL_SET_ITEM(PyObject *Py_UNUSED(module)) {
11701170
return PyLong_FromLong(return_value);
11711171
}
11721172

1173+
static PyObject *
1174+
test_PyTuple_SetItem_fails_not_a_tuple(PyObject *Py_UNUSED(module)) {
1175+
if (PyErr_Occurred()) {
1176+
PyErr_Print();
1177+
return NULL;
1178+
}
1179+
assert(!PyErr_Occurred());
1180+
1181+
PyObject *container = PyList_New(1);
1182+
if (!container) {
1183+
return NULL;
1184+
}
1185+
PyObject *value = new_unique_string(__FUNCTION__, NULL);
1186+
/* This should fail. */
1187+
if (PyTuple_SetItem(container, 0, value)) {
1188+
assert(PyErr_Occurred());
1189+
return NULL;
1190+
}
1191+
PyErr_Format(PyExc_RuntimeError, "Should have raised an error.");
1192+
return NULL;
1193+
}
1194+
1195+
static PyObject *
1196+
test_PyTuple_SetItem_fails_out_of_range(PyObject *Py_UNUSED(module)) {
1197+
if (PyErr_Occurred()) {
1198+
PyErr_Print();
1199+
return NULL;
1200+
}
1201+
assert(!PyErr_Occurred());
1202+
1203+
PyObject *container = PyTuple_New(1);
1204+
if (!container) {
1205+
return NULL;
1206+
}
1207+
PyObject *value = new_unique_string(__FUNCTION__, NULL);
1208+
/* This should fail. */
1209+
if (PyTuple_SetItem(container, 1, value)) {
1210+
assert(PyErr_Occurred());
1211+
return NULL;
1212+
}
1213+
PyErr_Format(PyExc_RuntimeError, "Should have raised an error.");
1214+
return NULL;
1215+
}
1216+
11731217
static PyObject *
11741218
test_PyTuple_Py_BuildValue(PyObject *Py_UNUSED(module)) {
11751219
if (PyErr_Occurred()) {
@@ -1235,6 +1279,10 @@ static PyMethodDef module_methods[] = {
12351279
"Check that PyTuple_SetItem() with NULL then with an object does not error."),
12361280
MODULE_NOARGS_ENTRY(test_PyTuple_SET_ITEM_NULL_SET_ITEM,
12371281
"Check that PyTuple_SET_ITEM() with NULL then with an object does not error."),
1282+
MODULE_NOARGS_ENTRY(test_PyTuple_SetItem_fails_not_a_tuple,
1283+
"Check that PyTuple_SET_ITEM() fails when not a tuple."),
1284+
MODULE_NOARGS_ENTRY(test_PyTuple_SetItem_fails_out_of_range,
1285+
"Check that PyTuple_SET_ITEM() fails when index out of range."),
12381286
MODULE_NOARGS_ENTRY(test_PyTuple_Py_BuildValue,
12391287
"Check that Py_BuildValue() with an existing object."),
12401288
{NULL, NULL, 0, NULL} /* Sentinel */

src/main.c

+2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ int main(int argc, const char * argv[]) {
148148
dbg_PyTuple_SET_ITEM_NULL();
149149
dbg_PyTuple_SetIem_NULL_SetItem();
150150
dbg_PyTuple_SET_ITEM_NULL_SET_ITEM();
151+
dbg_PyTuple_SetItem_fails_not_a_tuple();
152+
dbg_PyTuple_SetItem_fails_out_of_range();
151153
dbg_PyTuple_Py_BuildValue();
152154

153155
printf("Bye, bye!\n");

0 commit comments

Comments
 (0)