Building Self-Referencing Tuples
AFAICT, the reason you are seeing problems is because PyTuple_SetItem
fails if the refcount of the tuple isn't exactly one. This is to prevent the function from being used if the tuple has already been used elsewhere. I'm not sure why you get an access violation from that, but it may be because the exception thrown by PyTuple_SetItem
isn't properly dealt with. Furthermore, the reason why the array seems to mutate to some other object is because PyTuple_SetItem
DECREF's the tuple on each failure; after two failures, the refcount is zero so the object is freed (and some other object apparently ends up in the same memory location).
Using the pythonapi
object in ctypes is the preferred way to get access to the Python DLL, as it handles Python exceptions properly and is guaranteed to use the correct calling convention.
I don't have a Windows machine handy to test this out, but the following works fine on Mac OS X (both Python 2.7.3 and 3.2.2):
import ctypes
def self_reference(array, index):
# Sanity check. We can't let PyTuple_SetItem fail, or it will Py_DECREF
# the object and destroy it.
if not isinstance(array, tuple):
raise TypeError("array must be a tuple")
if not 0 <= index < len(array):
raise IndexError("tuple assignment index out of range")
arrayobj = ctypes.py_object(array)
# Need to drop the refcount to 1 in order to use PyTuple_SetItem.
# Needless to say, this is incredibly dangerous.
refcnt = ctypes.pythonapi.Py_DecRef(arrayobj)
for i in range(refcnt-1):
ctypes.pythonapi.Py_DecRef(arrayobj)
try:
ret = ctypes.pythonapi.PyTuple_SetItem(arrayobj, ctypes.c_ssize_t(index), arrayobj)
if ret != 0:
raise RuntimeError("PyTuple_SetItem failed")
except:
raise SystemError("FATAL: PyTuple_SetItem failed: tuple probably unusable")
# Restore refcount and add one more for the new self-reference
for i in range(refcnt+1):
ctypes.pythonapi.Py_IncRef(arrayobj)
Result:
>>> x = (1,2,3,4,5)
>>> self_reference(x, 1)
>>> import pprint
>>> pprint.pprint(x)
(1, <Recursion on tuple with id=4299516720>, 3, 4, 5)
Thanks to nneonneo's help, I settled on the following implementation of the self_reference
method.
import ctypes
ob_refcnt_p = ctypes.POINTER(ctypes.c_ssize_t)
class GIL:
acquire = staticmethod(ctypes.pythonapi.PyGILState_Ensure)
release = staticmethod(ctypes.pythonapi.PyGILState_Release)
class Ref:
dec = staticmethod(ctypes.pythonapi.Py_DecRef)
inc = staticmethod(ctypes.pythonapi.Py_IncRef)
class Tuple:
setitem = staticmethod(ctypes.pythonapi.PyTuple_SetItem)
@classmethod
def self_reference(cls, array, index):
if not isinstance(array, tuple):
raise TypeError('array must be a tuple')
if not isinstance(index, int):
raise TypeError('index must be an int')
if not 0 <= index < len(array):
raise ValueError('index is out of range')
GIL.acquire()
try:
obj = ctypes.py_object(array)
ob_refcnt = ctypes.cast(id(array), ob_refcnt_p).contents.value
for _ in range(ob_refcnt - 1):
Ref.dec(obj)
if cls.setitem(obj, ctypes.c_ssize_t(index), obj):
raise SystemError('PyTuple_SetItem was not successful')
for _ in range(ob_refcnt):
Ref.inc(obj)
finally:
GIL.release()
To use the method, follow the example shown down below for creating your own self-referencing tuples.
>>> array = tuple(range(5))
>>> Tuple.self_reference(array, 1)
>>> array
(0, (...), 2, 3, 4)
>>> Tuple.self_reference(array, 3)
>>> array
(0, (...), 2, (...), 4)
>>>