Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3161,6 +3161,72 @@ fields, or any other data types containing pointer type fields.
that should be merged into a containing structure or union.


.. decorator:: struct(*, align=None, layout, endian='native', pack=None)
:module: ctypes.util

A :term:`decorator` that allows generating structure types using an
annotation-based syntax, similar to the :mod:`dataclasses` module.

For example:

.. code-block:: python

from ctypes.util import struct
from ctypes import c_int

@struct
class Point:
x: c_int
y: c_int

point = Point(1, 2)

*align*, *layout*, and *pack* supply the value for the :attr:`~ctypes.Structure._align_`,
:attr:`~ctypes.Structure._layout_`, and :attr:`~ctypes.Structure._pack_`
attributes, respectively.

*endian* controls which structure class will be used as the base.

- If *endian* is ``'native'``, :class:`~ctypes.Structure` will be used.
- If *endian* is ``'big'``, :class:`~ctypes.BigEndianStructure` will be used.
- If *endian* is ``'little'``, :class:`~ctypes.LittleEndianStructure` will be used.

Any other value will raise a :class:`ValueError`.

For controlling field-specific data, wrap the annotation in :class:`typing.Annotated`
with :class:`CFieldInfo` as the second argument, like so:

.. code-block:: python

@struct
class PyObject:
ob_refcnt: c_ssize_t
ob_type: c_void_p

@struct
class PyHovercraftObject:
ob_base: Annotated[PyObject, CFieldInfo(anonymous=True)]

.. versionadded:: next


.. class:: CFieldInfo(anonymous=False, bit_width=None)
:module: ctypes.util

Information regarding a structure field defined by the :func:`struct`
decorator. This should be used in the second argument of a
:class:`typing.Annotated` wrapping a ctypes type.

*anonymous* specifies whether the field will be present in the
:attr:`~ctypes.Structure._anonymous_` attribute of the generated class.

If *bit_width* is non-``None``, the annotated field will be *bit_width*
number of bits in the generated structure. This is equivalent to passing
a third item in :attr:`~ctypes.Structure._fields_`.

.. versionadded:: next


.. _ctypes-arrays-pointers:

Arrays and pointers
Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,16 @@ curses
wide-character support.
(Contributed by Serhiy Storchaka in :gh:`133031`.)


ctypes
------

* Add :func:`ctypes.util.struct` for generating :class:`~ctypes.Structure` types
from an annotation-based syntax, similar to how the :mod:`dataclasses` module
is used.
(Contributed by Peter Bierma in :gh:`104533`.)


encodings
---------

Expand Down
84 changes: 84 additions & 0 deletions Lib/ctypes/util.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import os
import sys

from dataclasses import dataclass

lazy import functools
lazy import shutil
lazy import subprocess

lazy import annotationlib
lazy from typing import Annotated, get_args, ClassVar, get_origin
lazy from ctypes import Structure, BigEndianStructure, LittleEndianStructure

# find_library(name) returns the pathname of a library, or None.
if os.name == "nt":

Expand Down Expand Up @@ -491,6 +498,83 @@ def dllist():
ctypes.byref(ctypes.py_object(libraries)))
return libraries


@dataclass(slots=True, frozen=True)
class CFieldInfo:
anonymous: bool = False
bit_width: int | None = None


def _process_struct(klass, /, *, align, layout, endian, pack):
fields = []
anonymous = []
if issubclass(klass, Structure):
fields.extend(klass._fields_)
anonymous.extend(klass._anonymous_)

for name, hint in annotationlib.get_annotations(klass).items():
if get_origin(hint) is ClassVar:
continue

field = [name]
if get_origin(hint) is Annotated:
args = get_args(hint)
field.append(args[0])
field_info = args[1]
if not isinstance(field_info, CFieldInfo):
raise TypeError(f"expected CFieldInfo in Annotated, got {field_info!r}")

if field_info.bit_width is not None:
field.append(field_info.bit_width)

if field_info.anonymous is True:
anonymous.append(name)
else:
field.append(hint)

fields.append(field)

if endian == 'big':
endian_class = BigEndianStructure
elif endian == 'little':
endian_class = LittleEndianStructure
elif endian == 'native':
endian_class = Structure
else:
raise ValueError(f"expected 'big', 'little', or 'native', but got {endian!r}")

@functools.wraps(klass, updated=())
class _Struct(endian_class):
vars().update(vars(klass))
if align is not None:
_align_ = align
if layout is not None:
_layout_ = layout
if pack is not None:
_pack_ = pack
_fields_ = fields
_anonymous_ = anonymous

return _Struct


def struct(class_or_none=None, /, *, align=None, layout=None, endian='native', pack=None):
process_the_struct = functools.partial(
_process_struct,
align=align,
layout=layout,
endian=endian,
pack=pack
)

if class_or_none is None:
def inner(klass):
return process_the_struct(klass)

return inner
Comment thread
ZeroIntensity marked this conversation as resolved.

return process_the_struct(class_or_none)

################################################################
# test code

Expand Down
22 changes: 16 additions & 6 deletions Lib/test/test_ctypes/test_anon.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
import unittest
import test.support
from ctypes import c_int, Union, Structure, sizeof
from ctypes.util import CFieldInfo, struct
from typing import Annotated
from ._support import StructCheckMixin


class AnonTest(unittest.TestCase, StructCheckMixin):

def test_anon(self):
@test.support.subTests("use_struct_util", [False, True])
def test_anon(self, use_struct_util):
class ANON(Union):
_fields_ = [("a", c_int),
("b", c_int)]
self.check_union(ANON)

class Y(Structure):
_fields_ = [("x", c_int),
("_", ANON),
("y", c_int)]
_anonymous_ = ["_"]
if use_struct_util:
@struct
class Y:
x: c_int
_: Annotated[ANON, CFieldInfo(anonymous=True)]
y: c_int
else:
class Y(Structure):
_fields_ = [("x", c_int),
("_", ANON),
("y", c_int)]
_anonymous_ = ["_"]
self.check_struct(Y)

self.assertEqual(Y.a.offset, sizeof(c_int))
Expand Down
22 changes: 16 additions & 6 deletions Lib/test/test_ctypes/test_bitfields.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
from typing import Annotated
import unittest
from ctypes import (CDLL, Structure, sizeof, POINTER, byref, alignment,
LittleEndianStructure, BigEndianStructure,
Expand All @@ -8,8 +9,9 @@
c_short, c_ushort, c_int, c_uint, c_long, c_ulong,
c_longlong, c_ulonglong,
Union)
from ctypes.util import struct, CFieldInfo
from test import support
from test.support import import_helper
from test.support import import_helper, subTests
from ._support import StructCheckMixin
_ctypes_test = import_helper.import_module("_ctypes_test")

Expand Down Expand Up @@ -126,11 +128,19 @@ def test_generic_checks(self):
self.check_struct(BITS_msvc)
self.check_struct(BITS_gcc)

def test_longlong(self):
class X(Structure):
_fields_ = [("a", c_longlong, 1),
("b", c_longlong, 62),
("c", c_longlong, 1)]
@subTests("use_struct_util", [False, True])
def test_longlong(self, use_struct_util):
if use_struct_util:
@struct
class X:
a: Annotated[c_longlong, CFieldInfo(bit_width=1)]
b: Annotated[c_longlong, CFieldInfo(bit_width=62)]
c: Annotated[c_longlong, CFieldInfo(bit_width=1)]
else:
class X(Structure):
_fields_ = [("a", c_longlong, 1),
("b", c_longlong, 62),
("c", c_longlong, 1)]
self.check_struct(X)

self.assertEqual(sizeof(X), sizeof(c_longlong))
Expand Down
Loading
Loading