Skip to content

Commit f0db59d

Browse files
committed
Implement a dataclass-like struct decorator for ctypes.
1 parent d807210 commit f0db59d

2 files changed

Lines changed: 440 additions & 125 deletions

File tree

Lib/ctypes/util.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import os
22
import sys
33

4+
from dataclasses import dataclass
5+
46
lazy import shutil
57
lazy import subprocess
68

9+
lazy import annotationlib
10+
lazy from typing import Annotated, get_args, ClassVar, get_origin
11+
lazy from ctypes import Structure, BigEndianStructure, LittleEndianStructure
12+
713
# find_library(name) returns the pathname of a library, or None.
814
if os.name == "nt":
915

@@ -491,6 +497,86 @@ def dllist():
491497
ctypes.byref(ctypes.py_object(libraries)))
492498
return libraries
493499

500+
501+
@dataclass(slots=True, frozen=True)
502+
class CFieldInfo:
503+
anonymous: bool = False
504+
bit_width: int | None = None
505+
506+
507+
def _process_struct(klass, /, *, align, layout, endian, pack):
508+
fields = []
509+
anonymous = []
510+
if issubclass(klass, (Structure, LittleEndianStructure, BigEndianStructure)):
511+
fields.extend(klass._fields_)
512+
anonymous.extend(klass._anonymous_)
513+
514+
for name, hint in annotationlib.get_annotations(klass).items():
515+
if get_origin(hint) is ClassVar:
516+
continue
517+
518+
field = [name, hint]
519+
if get_origin(hint) is Annotated:
520+
field_info = get_args(hint)[1]
521+
if not isinstance(field_info, CFieldInfo):
522+
raise TypeError(f"expected CFieldInfo in Annotated, got {field_info!r}")
523+
524+
if field_info.bit_width is not None:
525+
field.append(field_info.bit_width)
526+
527+
if field_info.anonymous is True:
528+
anonymous.append(name)
529+
530+
continue
531+
532+
fields.append(field)
533+
534+
if endian == 'big':
535+
endian_class = BigEndianStructure
536+
elif endian == 'little':
537+
endian_class = LittleEndianStructure
538+
elif endian == 'native':
539+
endian_class = Structure
540+
else:
541+
raise ValueError(f"expected 'big', 'little', or 'native', but got {endian!r}")
542+
543+
# These fields don't apply correctly when set later.
544+
# As a workaround, we have this weird _StructData thing to set the attributes
545+
# in advance.
546+
class _StructData:
547+
pass
548+
549+
if align is not None:
550+
_StructData._align_ = align
551+
552+
if layout is not None:
553+
_StructData._layout_ = layout
554+
555+
if pack is not None:
556+
_StructData._pack_ = pack
557+
558+
for attr, value in klass.__dict__.items():
559+
if attr != "__dict__":
560+
setattr(_StructData, attr, value)
561+
562+
class _Struct(_StructData, endian_class):
563+
_fields_ = fields
564+
_anonymous_ = anonymous
565+
566+
_Struct.__name__ = klass.__name__
567+
_Struct.__qualname__ = klass.__qualname__
568+
return _Struct
569+
570+
571+
def struct(class_or_none=None, /, *, align=None, layout=None, endian='native', pack=None):
572+
if class_or_none is None:
573+
def inner(klass):
574+
return _process_struct(klass, align=align, layout=layout, endian=endian, pack=pack)
575+
576+
return inner
577+
578+
return _process_struct(class_or_none, align=align, layout=layout, endian=endian, pack=pack)
579+
494580
################################################################
495581
# test code
496582

0 commit comments

Comments
 (0)