Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-128715: Expose ctypes.CField, with info attributes #128950

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9d4be49
Restore max field size to sys.maxsize, as in Python 3.13 & below
encukou Dec 18, 2024
09c81a8
PyCField: Split out bit/byte sizes/offsets.
encukou Jan 9, 2025
c397cf4
Expose CField
encukou Jan 10, 2025
20ecd84
Add generic checks for all the test structs/unions
encukou Jan 17, 2025
60e7b32
More testing
encukou Jan 17, 2025
18334d8
Tests: import CField from ctypes
encukou Jan 17, 2025
294b1b8
Clarify bit_offset
encukou Jan 17, 2025
52114fa
Add a blurb
encukou Jan 17, 2025
32d41f3
Regen
encukou Jan 17, 2025
f6f596f
Merge in the main branch
encukou Jan 17, 2025
9b0e7eb
include <stdbool.h> in the common header
encukou Jan 17, 2025
d7bd835
Explicit casts
encukou Jan 17, 2025
06458f6
Remove problematic assert
encukou Jan 17, 2025
bb41481
Merge in the main branch
encukou Jan 24, 2025
91365b0
Add a test for the new info, and fix 'name' for nested anonymous structs
encukou Jan 24, 2025
b6d0510
Use subTest
encukou Jan 24, 2025
6e279e2
Use PyUnicode_FromObject to get an exact PyUnicode
encukou Jan 27, 2025
176de87
Normalize exception message
encukou Jan 31, 2025
085720e
Fix refcounting
encukou Jan 31, 2025
05c9591
Add pretty spaces
encukou Jan 31, 2025
b77074c
Merge in the main branch
encukou Jan 31, 2025
4e95755
Fix bit-packed size test for big-endian machines
encukou Jan 31, 2025
9cde20e
Remove `size` from _layout.py
encukou Jan 31, 2025
34865e8
Fix bit_offset for big-endian structs (where bitfields are laid out f…
encukou Feb 7, 2025
89fc44d
Merge in the main branch
encukou Feb 7, 2025
f327ffb
Name the magic constant
encukou Feb 7, 2025
14270ad
Remove unused variable
encukou Feb 7, 2025
7ce3cb9
Remove an unacceptable blank line
encukou Feb 7, 2025
d9d593c
Update documentation for tp_basicsize & tp_itemsize
encukou Feb 8, 2025
d52b8c9
Merge in the main branch
encukou Feb 21, 2025
2cdcb5b
Skip "is in" test for bitfields of underaligned types (bug filed)
encukou Feb 21, 2025
753090a
Merge in the main branch
encukou Mar 14, 2025
fde4204
Address review
encukou Mar 14, 2025
b2fddd8
One more alignment
encukou Mar 14, 2025
5ce595c
Don't use `self` while it's NULL
encukou Mar 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 98 additions & 5 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -657,12 +657,13 @@ Nested structures can also be initialized in the constructor in several ways::
>>> r = RECT((1, 2), (3, 4))

Field :term:`descriptor`\s can be retrieved from the *class*, they are useful
for debugging because they can provide useful information::
for debugging because they can provide useful information.
See :class:`CField`::

>>> print(POINT.x)
<Field type=c_long, ofs=0, size=4>
>>> print(POINT.y)
<Field type=c_long, ofs=4, size=4>
>>> POINT.x
<ctypes.CField 'x' type=c_int, ofs=0, size=4>
>>> POINT.y
<ctypes.CField 'y' type=c_int, ofs=4, size=4>
>>>


Expand Down Expand Up @@ -2812,6 +2813,98 @@ fields, or any other data types containing pointer type fields.
present in :attr:`_fields_`.


.. class:: CField(*args, **kw)

Descriptor for fields of a :class:`Structure` and :class:`Union`.
For example::

>>> class Color(Structure):
... _fields_ = (
... ('red', c_uint8),
... ('green', c_uint8),
... ('blue', c_uint8),
... ('intense', c_bool, 1),
... ('blinking', c_bool, 1),
... )
...
>>> Color.red
<ctypes.CField 'red' type=c_ubyte, ofs=0, size=1>
>>> Color.green.type
<class 'ctypes.c_ubyte'>
>>> Color.blue.byte_offset
2
>>> Color.intense
<ctypes.CField 'intense' type=c_bool, ofs=3, bit_size=1, bit_offset=0>
>>> Color.blinking.bit_offset
1

All attributes are read-only.

:class:`!CField` objects are created via :attr:`~Structure._fields_`;
do not instantiate the class directly.

.. versionadded:: next

Previously, descriptors only had ``offset`` and ``size`` attributes
and a readable string representation; the :class:`!CField` class was not
available directly.

.. attribute:: name

Name of the field, as a string.

.. attribute:: type

Type of the field, as a :ref:`ctypes class <ctypes-data-types>`.

.. attribute:: offset
byte_offset

Offset of the field, in bytes.

For bitfields, this is the offset of the underlying byte-aligned
*storage unit*; see :attr:`~CField.bit_offset`.

.. attribute:: byte_size

Size of the field, in bytes.

For bitfields, this is the size of the underlying *storage unit*.
Typically, it has the same size as the bitfield's type.

.. attribute:: size

For non-bitfields, equivalent to :attr:`~CField.byte_size`.

For bitfields, this contains a backwards-compatible bit-packed
value that combines :attr:`~CField.bit_size` and
:attr:`~CField.bit_offset`.
Prefer using the explicit attributes instead.

.. attribute:: is_bitfield

True if this is a bitfield.

.. attribute:: bit_offset
bit_size

The location of a bitfield within its *storage unit*, that is, within
:attr:`~CField.byte_size` bytes of memory starting at
:attr:`~CField.byte_offset`.

To get the field's value, read the storage unit as an integer,
:ref:`shift left <shifting>` by :attr:`!bit_offset` and
take the :attr:`!bit_size` least significant bits.

For non-bitfields, :attr:`!bit_offset` is zero
and :attr:`!bit_size` is equal to ``byte_size * 8``.

.. attribute:: is_anonymous

True if this field is anonymous, that is, it contains nested sub-fields
that should be be merged into a containing structure or union.


.. _ctypes-arrays-pointers:

Arrays and pointers
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,11 @@ ctypes
to help match a non-default ABI.
(Contributed by Petr Viktorin in :gh:`97702`.)

* The class of :class:`~ctypes.Structure`/:class:`~ctypes.Union`
field descriptors is now available as :class:`~ctypes.CField`,
and has new attributes to aid debugging and introspection.
(Contributed by Petr Viktorin in :gh:`128715`.)

* On Windows, the :exc:`~ctypes.COMError` exception is now public.
(Contributed by Jun Komoda in :gh:`126686`.)

Expand Down
4 changes: 4 additions & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(_get_sourcefile)
STRUCT_FOR_ID(_handle_fromlist)
STRUCT_FOR_ID(_initializing)
STRUCT_FOR_ID(_internal_use)
STRUCT_FOR_ID(_io)
STRUCT_FOR_ID(_is_text_encoding)
STRUCT_FOR_ID(_isatty_open_only)
Expand Down Expand Up @@ -295,6 +296,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(before)
STRUCT_FOR_ID(big)
STRUCT_FOR_ID(binary_form)
STRUCT_FOR_ID(bit_offset)
STRUCT_FOR_ID(bit_size)
STRUCT_FOR_ID(block)
STRUCT_FOR_ID(bound)
Expand All @@ -305,6 +307,8 @@ struct _Py_global_strings {
STRUCT_FOR_ID(buffers)
STRUCT_FOR_ID(bufsize)
STRUCT_FOR_ID(builtins)
STRUCT_FOR_ID(byte_offset)
STRUCT_FOR_ID(byte_size)
STRUCT_FOR_ID(byteorder)
STRUCT_FOR_ID(bytes)
STRUCT_FOR_ID(bytes_per_sep)
Expand Down
4 changes: 4 additions & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Lib/ctypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from _ctypes import RTLD_LOCAL, RTLD_GLOBAL
from _ctypes import ArgumentError
from _ctypes import SIZEOF_TIME_T
from _ctypes import CField

from struct import calcsize as _calcsize

Expand Down
52 changes: 15 additions & 37 deletions Lib/ctypes/_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,6 @@ def round_up(n, multiple):
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


Expand Down Expand Up @@ -213,13 +190,10 @@ def get_layout(cls, input_fields, is_struct, base):

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
bit_offset = next_bit_offset - 8 * offset
assert 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
Expand Down Expand Up @@ -253,18 +227,17 @@ def get_layout(cls, input_fields, is_struct, base):
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
bit_offset = last_field_bit_size + next_bit_offset
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)
if is_bitfield and big_endian:
# On big-endian architectures, bit fields are also laid out
# starting with the big end.
bit_offset = type_bit_size - bit_size - bit_offset

# Add the format spec parts
if is_struct:
Expand All @@ -286,16 +259,21 @@ def get_layout(cls, input_fields, is_struct, base):
# 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")
f"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,
byte_size=type_size,
byte_offset=offset,
bit_size=bit_size if is_bitfield else None,
bit_offset=bit_offset if is_bitfield else None,
index=i,

# Do not use CField outside ctypes, yet.
# The constructor is internal API and may change without warning.
_internal_use=True,
))
if is_bitfield and not gcc_layout:
assert type_bit_size > 0
Expand Down
Loading
Loading