"""Python implementation of computing the layout of a struct/union
This code is internal and tightly coupled to the C part. The interface
may change at any time.
"""
import sys
import warnings
import struct
from _ctypes import CField, buffer_info
import ctypes
def round_down(n, multiple):
assert n >= 0
assert multiple > 0
return (n // multiple) * multiple
def round_up(n, multiple):
assert n >= 0
assert multiple > 0
return ((n + multiple - 1) // multiple) * multiple
def LOW_BIT(offset):
return offset & 0xFFFF
def NUM_BITS(bitsize):
return bitsize >> 16
def BUILD_SIZE(bitsize, offset):
assert 0 <= offset, offset
assert offset <= 0xFFFF, offset
# We don't support zero length bitfields.
# And GET_BITFIELD uses NUM_BITS(size) == 0,
# to figure out whether we are handling a bitfield.
assert bitsize > 0, bitsize
result = (bitsize << 16) + offset
assert bitsize == NUM_BITS(result), (bitsize, result)
assert offset == LOW_BIT(result), (offset, result)
return result
def build_size(bit_size, bit_offset, big_endian, type_size):
if big_endian:
return BUILD_SIZE(bit_size, 8 * type_size - bit_offset - bit_size)
return BUILD_SIZE(bit_size, bit_offset)
_INT_MAX = (1 << (ctypes.sizeof(ctypes.c_int) * 8) - 1) - 1
class StructUnionLayout:
def __init__(self, fields, size, align, format_spec):
# sequence of CField objects
self.fields = fields
# total size of the aggregate (rounded up to alignment)
self.size = size
# total alignment requirement of the aggregate
self.align = align
# buffer format specification (as a string, UTF-8 but bes
# kept ASCII-only)
self.format_spec = format_spec
def get_layout(cls, input_fields, is_struct, base):
"""Return a StructUnionLayout for the given class.
Called by PyCStructUnionType_update_stginfo when _fields_ is assigned
to a class.
"""
# Currently there are two modes, selectable using the '_layout_' attribute:
#
# 'gcc-sysv' mode places fields one after another, bit by bit.
# But "each bit field must fit within a single object of its specified
# type" (GCC manual, section 15.8 "Bit Field Packing"). When it doesn't,
# we insert a few bits of padding to avoid that.
#
# 'ms' mode works similar except for bitfield packing. Adjacent
# bit-fields are packed into the same 1-, 2-, or 4-byte allocation unit
# if the integral types are the same size and if the next bit-field fits
# into the current allocation unit without crossing the boundary imposed
# by the common alignment requirements of the bit-fields.
#
# See https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html#index-mms-bitfields
# for details.
# We do not support zero length bitfields (we use bitsize != 0
# elsewhere to indicate a bitfield). Here, non-bitfields have bit_size
# set to size*8.
# For clarity, variables that count bits have `bit` in their names.
layout = getattr(cls, '_layout_', None)
if layout is None:
if sys.platform == 'win32' or getattr(cls, '_pack_', None):
gcc_layout = False
else:
gcc_layout = True
elif layout == 'ms':
gcc_layout = False
elif layout == 'gcc-sysv':
gcc_layout = True
else:
raise ValueError(f'unknown _layout_: {layout!r}')
align = getattr(cls, '_align_', 1)
if align < 0:
raise ValueError('_align_ must be a non-negative integer')
elif align == 0:
# Setting `_align_ = 0` amounts to using the default alignment
align == 1
if base:
align = max(ctypes.alignment(base), align)
swapped_bytes = hasattr(cls, '_swappedbytes_')
if swapped_bytes:
big_endian = sys.byteorder == 'little'
else:
big_endian = sys.byteorder == 'big'
pack = getattr(cls, '_pack_', None)
if pack is not None:
try:
pack = int(pack)
except (TypeError, ValueError):
raise ValueError("_pack_ must be an integer")
if pack < 0:
raise ValueError("_pack_ must be a non-negative integer")
if pack > _INT_MAX:
raise ValueError("_pack_ too big")
if gcc_layout:
raise ValueError('_pack_ is not compatible with gcc-sysv layout')
result_fields = []
if is_struct:
format_spec_parts = ["T{"]
else:
format_spec_parts = ["B"]
last_field_bit_size = 0 # used in MS layout only
# `8 * next_byte_offset + next_bit_offset` points to where the
# next field would start.
next_bit_offset = 0
next_byte_offset = 0
# size if this was a struct (sum of field sizes, plus padding)
struct_size = 0
# max of field sizes; only meaningful for unions
union_size = 0
if base:
struct_size = ctypes.sizeof(base)
if gcc_layout:
next_bit_offset = struct_size * 8
else:
next_byte_offset = struct_size
last_size = struct_size
for i, field in enumerate(input_fields):
if not is_struct:
# Unions start fresh each time
last_field_bit_size = 0
next_bit_offset = 0
next_byte_offset = 0
# Unpack the field
field = tuple(field)
try:
name, ctype = field
except (ValueError, TypeError):
try:
name, ctype, bit_size = field
except (ValueError, TypeError) as exc:
raise ValueError(
'_fields_ must be a sequence of (name, C type) pairs '
+ 'or (name, C type, bit size) triples') from exc
is_bitfield = True
if bit_size <= 0:
raise ValueError(
f'number of bits invalid for bit field {name!r}')
type_size = ctypes.sizeof(ctype)
if bit_size > type_size * 8:
raise ValueError(
f'number of bits invalid for bit field {name!r}')
else:
is_bitfield = False
type_size = ctypes.sizeof(ctype)
bit_size = type_size * 8
type_bit_size = type_size * 8
type_align = ctypes.alignment(ctype) or 1
type_bit_align = type_align * 8
if gcc_layout:
# We don't use next_byte_offset here
assert pack is None
assert next_byte_offset == 0
# Determine whether the bit field, if placed at the next
# free bit, fits within a single object of its specified type.
# That is: determine a "slot", sized & aligned for the
# specified type, which contains the bitfield's beginning:
slot_start_bit = round_down(next_bit_offset, type_bit_align)
slot_end_bit = slot_start_bit + type_bit_size
# And see if it also contains the bitfield's last bit:
field_end_bit = next_bit_offset + bit_size
if field_end_bit > slot_end_bit:
# It doesn't: add padding (bump up to the next
# alignment boundary)
next_bit_offset = round_up(next_bit_offset, type_bit_align)
offset = round_down(next_bit_offset, type_bit_align) // 8
if is_bitfield:
effective_bit_offset = next_bit_offset - 8 * offset
size = build_size(bit_size, effective_bit_offset,
big_endian, type_size)
assert effective_bit_offset <= type_bit_size
else:
assert offset == next_bit_offset / 8
size = type_size
next_bit_offset += bit_size
struct_size = round_up(next_bit_offset, 8) // 8
else:
if pack:
type_align = min(pack, type_align)
# next_byte_offset points to end of current bitfield.
# next_bit_offset is generally non-positive,
# and 8 * next_byte_offset + next_bit_offset points just behind
# the end of the last field we placed.
if (
(0 < next_bit_offset + bit_size)
or (type_bit_size != last_field_bit_size)
):
# Close the previous bitfield (if any)
# and start a new bitfield
next_byte_offset = round_up(next_byte_offset, type_align)
next_byte_offset += type_size
last_field_bit_size = type_bit_size
# Reminder: 8 * (next_byte_offset) + next_bit_offset
# points to where we would start a new field, namely
# just behind where we placed the last field plus an
# allowance for alignment.
next_bit_offset = -last_field_bit_size
assert type_bit_size == last_field_bit_size
offset = next_byte_offset - last_field_bit_size // 8
if is_bitfield:
assert 0 <= (last_field_bit_size + next_bit_offset)
size = build_size(bit_size,
last_field_bit_size + next_bit_offset,
big_endian, type_size)
else:
size = type_size
if type_bit_size:
assert (last_field_bit_size + next_bit_offset) < type_bit_size
next_bit_offset += bit_size
struct_size = next_byte_offset
assert (not is_bitfield) or (LOW_BIT(size) <= size * 8)
# Add the format spec parts
if is_struct:
padding = offset - last_size
format_spec_parts.append(padding_spec(padding))
fieldfmt, bf_ndim, bf_shape = buffer_info(ctype)
if bf_shape:
format_spec_parts.extend((
"(",
','.join(str(n) for n in bf_shape),
")",
))
if fieldfmt is None:
fieldfmt = "B"
if isinstance(name, bytes):
# a bytes name would be rejected later, but we check early
# to avoid a BytesWarning with `python -bb`
raise TypeError(
"field {name!r}: name must be a string, not bytes")
format_spec_parts.append(f"{fieldfmt}:{name}:")
result_fields.append(CField(
name=name,
type=ctype,
size=size,
offset=offset,
bit_size=bit_size if is_bitfield else None,
index=i,
))
if is_bitfield and not gcc_layout:
assert type_bit_size > 0
align = max(align, type_align)
last_size = struct_size
if not is_struct:
union_size = max(struct_size, union_size)
if is_struct:
total_size = struct_size
else:
total_size = union_size
# Adjust the size according to the alignment requirements
aligned_size = round_up(total_size, align)
# Finish up the format spec
if is_struct:
padding = aligned_size - total_size
format_spec_parts.append(padding_spec(padding))
format_spec_parts.append("}")
return StructUnionLayout(
fields=result_fields,
size=aligned_size,
align=align,
format_spec="".join(format_spec_parts),
)
def padding_spec(padding):
if padding <= 0:
return ""
if padding == 1:
return "x"
return f"{padding}x"