Grief
Grief

Reputation: 2040

ctypes: structure, size_t field

I am trying to implement python-to-c binding via ctypes for libnfc. I have a structure, here is a wrong variant:

class nfc_iso14443a_info(Structure):
    _fields_ = [
        ('abtAtqa',  c_uint8 * 2),
        ('btSak',    c_uint8),
        ('szUidLen', c_uint8 * 8),
        ('abtUid',   c_uint8 * 10),
        ...

At some moment during debug session it looks like that: enter image description here

The problem here is that I want szUidLen to be 64-bit unsigned integer equal to 7. More precisely, it must match size_t szUidLen; from nfc-types.h. So i tried an obvious variant and changed c_uint8 * 8 to c_size_t and it doesn't work:

class nfc_iso14443a_info(Structure):
    _fields_ = [
        ('abtAtqa',  c_uint8 * 2),
        ('btSak',    c_uint8),
        ('szUidLen', c_size_t),
        ('abtUid',   c_uint8 * 10),
        ...

enter image description here

What am I missing here?

Upvotes: 1

Views: 3424

Answers (2)

jno
jno

Reputation: 1027

from ctypes import *
c_size_t = c_unit64

and go ahead You may need to specify ._pack_=1 too (if your compiler generates code that way) before defining _fields_.

Update: There is ready made c_size_t (and c_ssize_t) type in ctypes.

Note: (c_char * 8) is not equal to c_int64 or c_long because of possible alignment issues (c_char fields are not aligned). ctypes.alignment(c_type) may give you a hint on how c_type is aligned:

In [7]: c.alignment(c.c_char * 8), c.alignment(c.c_size_t)
Out[7]: (1, 8)

Upvotes: -2

abarnert
abarnert

Reputation: 365617

The issue here is that the C struct you're trying to map is packed, as (tersely) explained in the Structure/union alignment and byte order section of the docs:

By default, Structure and Union fields are aligned in the same way the C compiler does it. It is possible to override this behavior be specifying a _pack_ class attribute in the subclass definition. This must be set to a positive integer and specifies the maximum alignment for the fields. This is what #pragma pack(n) also does in MSVC.

That only makes sense if you already know about packing and alignment in C, but it's not that complicated.

By default, C structure elements are aligned to start on nice boundaries. For example, a 32-bit int following an 8-bit int doesn't run from bytes 1-4, it runs from bytes 4-7 (and bytes 1-3 are unused padding). So, ctypes follows the same rules.

That means that, while szUidLen runs from bytes 3-10 when it's defined as an array of 8-bit ints, it gets aligned to bytes 8-15 (or 4-11, depending on your compiler) when it's defined as a 64-bit int. You can see this by printing out nfc_iso14443a_info.szUidLen.offset.

So, the first one gets the bytes 7, 0, 0, 0, 0, 0, 0, 0, which is little-endian int64 for 7, while the second one gets the bytes 0, 0, 0, a, b, c, d, e, where abcde are the first 5 bytes of the next field, which is little-endian int64 for some huge number (unless the next field happens to be 0).

Of course you don't want to just guess that this is the problem. If you based your Structure on a struct from a C header, this can only be true if the header or the compile flags specify some non-default packing, like the #pragma pack(1) used by MSVC. If you based your Structure on something like an RFC packet description, the alignment is not even according to C rules, but is defined somewhere in the documentation you're reading (although protocol RFCs almost always use 1-byte alignment).

Anyway, the docs don't explain the problem very well, but they explain the solution:

class nfc_iso14443a_info(Structure):
    _pack_ = 1
    _fields_ = [
        ('abtAtqa',  c_uint8 * 2),
        ('btSak',    c_uint8),
        ('szUidLen', c_size_t),
        ('abtUid',   c_uint8 * 10),
        ...

Now szUidLen runs from bytes 3-10, but it's interpreted as a 64-bit int instead of an array of 8-bit ints.

Upvotes: 3

Related Questions