Difference between ctypes.c_int and ctypes.py_object of an int

53 Views Asked by At

I'm on Python 3.9.6 on CPython.

I am trying to understand the difference in the internal representation of PyObject and c_int for a number.

>>> x = 1234
>>> c1 = c_int(x)
>>> id(c1)
4339302720
>>> type(c1)
<class 'ctypes.c_int'>
>>> c1.value
1234
>>> cast(addressof(c1),POINTER(c_int)).contents.value
1234
>>>

Here, c1 is a PyObject itself and has it's address as 4339302720. It represents a ctypes.c_int object, and this addressof(c1) should point to the memory location where the value of the int is stored. This is confirmed when that address is interpreted as an int and it's value dumped. I assume c1.value is doing the same internally.

2.


>>> c2 = cast(id(x),py_object)
>>> id(c2)
4337584448
>>> type(c2)
<class 'ctypes.py_object'>
>>> c2.value
1234
>>> cast(addressof(c2),POINTER(c_int)).contents.value
43544656

Here, c2 is also a PyObject and has it's address as 4337584448. Does it represent another ctypes.py_object object?

Since this a ctypes object, it supports addressof(c2).

What does addressof(c2) point to, since it does not point to the integer 1234? Could you also supplement your answer with a pointer to a good refernce for this so I could understand from "first principles"?

Also, how does c2.value internally evaluate to 1234?

1

There are 1 best solutions below

0
Mark Tolonen On

The ctypes.addressof of a ctypes.c_int is the address of the 4 bytes that store the C integer.

>>> from ctypes import *
>>> x = 1234
>>> a = c_uint(x)
>>> hex(addressof(a))  # int*
'0x231c2135498'
>>> string_at(addressof(a),4).hex()  # little-endian 4-byte integer
'd2040000'
>>> 0x04d2  # byte-swapped and converted to int
1234

The ctypes.addressof of a ctypes.py_object is the address of the 8 bytes that store the PyObject* the object refers to. That's why in the second case you get a strange number.

>>> from ctypes import *
>>> x = 1234
>>> y = py_object(x)
>>> hex(addressof(y))  # PyObject*
'0x231c2135498'
>>> string_at(addressof(y),8).hex()  # little-endian 8-byte pointer (64-bit OS)
'300713c231020000'  # note lower bytes of this matches the "bad" value of cast below.
>>> hex(id(x))  # In CPython id(x) is the PyObject* address of x.
'0x231c2130730'

If that PyObject* is treated as a C int* instead:

>>> cast(addressof(y), POINTER(c_int)).contents
c_long(-1038940368)
>>> import struct
>>> struct.pack('<l',-1038940368).hex() # convert to little-endian 4-byte integer
'300713c2'  # See that it the lower 4 bytes of the little-endian 8-byte PyObject*.

The value of a py_object(1234) will never match the c_int value because it was never a c_int-wrapped value to begin with. but you can get it out as Python object:

>>> x=1234
>>> y=cast(id(x),py_object)  # better, just y=py_object(x)
>>> y
py_object(1234)
>>> y.value
1234