Skip to content

Commit 570dea3

Browse files
committed
feat: implement bitfield support
1 parent 3778fda commit 570dea3

8 files changed

Lines changed: 421 additions & 46 deletions

File tree

libdestruct/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@
1515
from libdestruct.c import c_int, c_long, c_str, c_uint, c_ulong
1616
from libdestruct.common.array import array, array_of
1717
from libdestruct.common.attributes import offset
18+
from libdestruct.common.bitfield import bitfield_of
1819
from libdestruct.common.enum import enum, enum_of
1920
from libdestruct.common.ptr.ptr import ptr
2021
from libdestruct.common.struct import ptr_to, ptr_to_self, struct
2122
from libdestruct.libdestruct import inflate, inflater
2223

2324
__all__ = [
25+
"Resolver",
2426
"array",
2527
"array_of",
28+
"bitfield_of",
2629
"c_int",
2730
"c_long",
2831
"c_str",
@@ -36,6 +39,5 @@
3639
"ptr",
3740
"ptr_to",
3841
"ptr_to_self",
39-
"Resolver",
4042
"struct",
4143
]

libdestruct/c/struct_parser.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,37 @@
1414

1515
from pycparser import c_ast, c_parser
1616

17+
from libdestruct.c.c_integer_types import c_char, c_int, c_long, c_short, c_uchar, c_uint, c_ulong, c_ushort
1718
from libdestruct.common.array.array_of import array_of
19+
from libdestruct.common.bitfield.bitfield_of import bitfield_of
1820
from libdestruct.common.ptr.ptr_factory import ptr_to, ptr_to_self
1921
from libdestruct.common.struct import struct
2022

23+
# Mapping from ctypes types to libdestruct native integer types (needed for bitfields)
24+
_CTYPES_TO_NATIVE = {
25+
ctypes.c_byte: c_char,
26+
ctypes.c_char: c_char,
27+
ctypes.c_ubyte: c_uchar,
28+
ctypes.c_short: c_short,
29+
ctypes.c_ushort: c_ushort,
30+
ctypes.c_int: c_int,
31+
ctypes.c_uint: c_uint,
32+
ctypes.c_long: c_long,
33+
ctypes.c_ulong: c_ulong,
34+
ctypes.c_longlong: c_long,
35+
ctypes.c_ulonglong: c_ulong,
36+
ctypes.c_int8: c_char,
37+
ctypes.c_int16: c_short,
38+
ctypes.c_int32: c_int,
39+
ctypes.c_int64: c_long,
40+
ctypes.c_uint8: c_uchar,
41+
ctypes.c_uint16: c_ushort,
42+
ctypes.c_uint32: c_uint,
43+
ctypes.c_uint64: c_ulong,
44+
ctypes.c_size_t: c_ulong,
45+
ctypes.c_ssize_t: c_long,
46+
}
47+
2148
if TYPE_CHECKING:
2249
from libdestruct.common.obj import obj
2350

@@ -81,14 +108,25 @@ def struct_to_type(struct_node: c_ast.Struct) -> type[struct]:
81108
elif not struct_node.decls:
82109
raise ValueError("Struct must have fields.")
83110

111+
class_dict = {}
112+
84113
for decl in struct_node.decls:
85114
name = decl.name
86115
typ = type_decl_to_type(decl.type, struct_node)
87116
fields[name] = typ
88117

118+
# Handle bitfields: decl.bitsize is set when the declaration has ": N"
119+
if decl.bitsize is not None:
120+
bit_width = int(decl.bitsize.value)
121+
# Convert ctypes types to native libdestruct types for bitfield backing
122+
native_type = _CTYPES_TO_NATIVE.get(typ, typ)
123+
class_dict[name] = bitfield_of(native_type, bit_width)
124+
fields[name] = native_type
125+
89126
type_name = struct_node.name if struct_node.name else "anon_struct"
90127

91-
return type(type_name, (struct,), {"__annotations__": fields})
128+
class_dict["__annotations__"] = fields
129+
return type(type_name, (struct,), class_dict)
92130

93131

94132
def ptr_to_type(ptr: c_ast.PtrDecl, parent: c_ast.Struct | None = None) -> type[obj]:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2026 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from libdestruct.common.bitfield.bitfield import bitfield
8+
from libdestruct.common.bitfield.bitfield_field import BitfieldField
9+
from libdestruct.common.bitfield.bitfield_of import bitfield_of
10+
11+
__all__ = ["BitfieldField", "bitfield", "bitfield_of"]
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2026 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from __future__ import annotations
8+
9+
from typing import TYPE_CHECKING
10+
11+
from libdestruct.common.obj import obj
12+
13+
if TYPE_CHECKING: # pragma: no cover
14+
from libdestruct.backing.resolver import Resolver
15+
16+
17+
class bitfield(obj):
18+
"""A bitfield within a backing integer type."""
19+
20+
_backing_instance: obj
21+
"""The inflated backing integer instance (shared with sibling bitfields)."""
22+
23+
_bit_offset: int
24+
"""The starting bit position within the backing integer."""
25+
26+
_bit_width: int
27+
"""The number of bits this field occupies."""
28+
29+
_signed: bool
30+
"""Whether to sign-extend when reading."""
31+
32+
_is_group_owner: bool
33+
"""Whether this bitfield owns the backing bytes (first in its group)."""
34+
35+
def __init__(
36+
self: bitfield,
37+
resolver: Resolver,
38+
backing_instance: obj,
39+
bit_offset: int,
40+
bit_width: int,
41+
signed: bool,
42+
is_group_owner: bool,
43+
) -> None:
44+
"""Initialize the bitfield.
45+
46+
Args:
47+
resolver: The backing resolver.
48+
backing_instance: The already-inflated backing integer (shared across bitfields in the same group).
49+
bit_offset: The starting bit position within the backing integer.
50+
bit_width: The number of bits this field occupies.
51+
signed: Whether to sign-extend when reading.
52+
is_group_owner: Whether this bitfield is the first in its group (owns the backing bytes).
53+
"""
54+
super().__init__(resolver)
55+
self._backing_instance = backing_instance
56+
self._bit_offset = bit_offset
57+
self._bit_width = bit_width
58+
self._signed = signed
59+
self._mask = (1 << bit_width) - 1
60+
self._is_group_owner = is_group_owner
61+
# Owner reports the full backing size; non-owners report 0
62+
self.size = backing_instance.size if is_group_owner else 0
63+
64+
def get(self: bitfield) -> int:
65+
"""Return the value of the bitfield."""
66+
raw = self._backing_instance.get()
67+
# For signed backing types, raw may be negative. Work with unsigned representation.
68+
if raw < 0:
69+
raw += 1 << (self._backing_instance.size * 8)
70+
value = (raw >> self._bit_offset) & self._mask
71+
if self._signed and (value >> (self._bit_width - 1)) & 1:
72+
value -= 1 << self._bit_width
73+
return value
74+
75+
def _set(self: bitfield, value: int) -> None:
76+
"""Set the value of the bitfield."""
77+
masked_value = value & self._mask
78+
raw = self._backing_instance.get()
79+
if raw < 0:
80+
raw += 1 << (self._backing_instance.size * 8)
81+
raw = (raw & ~(self._mask << self._bit_offset)) | (masked_value << self._bit_offset)
82+
total_bits = self._backing_instance.size * 8
83+
is_signed = hasattr(self._backing_instance, "signed") and self._backing_instance.signed
84+
if is_signed and raw >= (1 << (total_bits - 1)):
85+
raw -= 1 << total_bits
86+
self._backing_instance._set(raw)
87+
88+
def to_bytes(self: bitfield) -> bytes:
89+
"""Return the serialized representation of the backing type.
90+
91+
Only the group owner emits bytes; non-owners return empty bytes
92+
to avoid duplication when the struct serializes all members.
93+
"""
94+
if self._is_group_owner:
95+
return self._backing_instance.to_bytes()
96+
return b""
97+
98+
def to_str(self: bitfield, _: int = 0) -> str:
99+
"""Return a string representation of the bitfield."""
100+
return f"{self.get()}"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2026 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from __future__ import annotations
8+
9+
from typing import TYPE_CHECKING
10+
11+
from libdestruct.common.field import Field
12+
13+
if TYPE_CHECKING: # pragma: no cover
14+
from libdestruct.backing.resolver import Resolver
15+
from libdestruct.common.obj import obj
16+
17+
18+
class BitfieldField(Field):
19+
"""A generator for a bitfield within a struct."""
20+
21+
base_type: type[obj]
22+
23+
def __init__(self: BitfieldField, backing_type: type, bit_width: int) -> None:
24+
"""Initialize the bitfield field.
25+
26+
Args:
27+
backing_type: The backing integer type (e.g., c_int, c_uint).
28+
bit_width: The number of bits this field occupies.
29+
"""
30+
self.backing_type = backing_type
31+
self.bit_width = bit_width
32+
self.base_type = backing_type
33+
34+
def inflate(self: BitfieldField, resolver: Resolver) -> obj:
35+
"""Inflate the field. Not used directly — struct_impl handles bitfield inflation."""
36+
raise NotImplementedError("BitfieldField inflation is handled by struct_impl.")
37+
38+
def get_size(self: BitfieldField) -> int:
39+
"""Returns 0 — bitfields do not independently advance the struct offset."""
40+
return 0
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2026 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from __future__ import annotations
8+
9+
from typing import TYPE_CHECKING
10+
11+
from libdestruct.common.bitfield.bitfield_field import BitfieldField
12+
13+
if TYPE_CHECKING: # pragma: no cover
14+
from libdestruct.common.obj import obj
15+
16+
17+
def bitfield_of(backing_type: type[obj], bit_width: int) -> BitfieldField:
18+
"""Create a bitfield descriptor for use in struct annotations.
19+
20+
Args:
21+
backing_type: The backing integer type (e.g., c_int, c_uint).
22+
bit_width: The number of bits this field occupies.
23+
"""
24+
if bit_width <= 0:
25+
raise ValueError("Bit width must be positive.")
26+
27+
if hasattr(backing_type, "size") and bit_width > backing_type.size * 8:
28+
raise ValueError(f"Bit width {bit_width} exceeds backing type size ({backing_type.size * 8} bits).")
29+
30+
return BitfieldField(backing_type, bit_width)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#
2+
# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct).
3+
# Copyright (c) 2026 Roberto Alessandro Bertolini. All rights reserved.
4+
# Licensed under the MIT license. See LICENSE file in the project root for details.
5+
#
6+
7+
from __future__ import annotations
8+
9+
from typing import TYPE_CHECKING
10+
11+
from libdestruct.common.bitfield.bitfield import bitfield
12+
13+
if TYPE_CHECKING: # pragma: no cover
14+
from libdestruct.backing.resolver import Resolver
15+
from libdestruct.common.bitfield.bitfield_field import BitfieldField
16+
from libdestruct.common.obj import obj
17+
from libdestruct.common.type_registry import TypeRegistry
18+
19+
20+
class BitfieldTracker:
21+
"""Tracks bitfield group state during struct field inflation.
22+
23+
Consecutive bitfields with the same backing type are packed into a shared
24+
backing integer instance. This class manages the grouping, bit offset
25+
tracking, and byte offset advancement.
26+
"""
27+
28+
def __init__(self: BitfieldTracker) -> None:
29+
"""Initialize the tracker with no active group."""
30+
self._bit_offset: int = 0
31+
self._backing_type: type | None = None
32+
self._backing_instance: obj | None = None
33+
34+
@property
35+
def active(self: BitfieldTracker) -> bool:
36+
"""Return whether a bitfield group is currently active."""
37+
return self._backing_type is not None
38+
39+
def flush(self: BitfieldTracker) -> int:
40+
"""Close the current bitfield group and return the byte size to advance.
41+
42+
Returns:
43+
The backing type's byte size if a group was active, 0 otherwise.
44+
"""
45+
if self._backing_type is not None:
46+
size = self._backing_type.size
47+
self._backing_type = None
48+
self._backing_instance = None
49+
self._bit_offset = 0
50+
return size
51+
return 0
52+
53+
def create_bitfield(
54+
self: BitfieldTracker,
55+
field: BitfieldField,
56+
inflater: TypeRegistry,
57+
resolver: Resolver,
58+
current_offset: int,
59+
) -> tuple[bitfield, int]:
60+
"""Create a bitfield instance, managing group transitions.
61+
62+
Args:
63+
field: The BitfieldField descriptor.
64+
inflater: The type registry for inflating the backing type.
65+
resolver: The struct's resolver.
66+
current_offset: The current byte offset in the struct.
67+
68+
Returns:
69+
A tuple of (bitfield_instance, byte_offset_delta).
70+
The delta is nonzero only when a new group starts (flushing the old one).
71+
"""
72+
backing_type = field.backing_type
73+
bit_width = field.bit_width
74+
backing_size_bits = backing_type.size * 8
75+
offset_delta = 0
76+
77+
# Start a new group if the backing type changed or bits would overflow
78+
if self._backing_type is not backing_type or self._bit_offset + bit_width > backing_size_bits:
79+
offset_delta = self.flush()
80+
self._backing_type = backing_type
81+
self._backing_instance = inflater.inflater_for(backing_type)(
82+
resolver.relative_from_own(current_offset + offset_delta, 0),
83+
)
84+
85+
is_owner = self._bit_offset == 0
86+
signed = getattr(backing_type, "signed", False)
87+
88+
result = bitfield(
89+
resolver.relative_from_own(current_offset + offset_delta, 0),
90+
self._backing_instance,
91+
self._bit_offset,
92+
bit_width,
93+
signed,
94+
is_owner,
95+
)
96+
self._bit_offset += bit_width
97+
return result, offset_delta
98+
99+
def compute_size(self: BitfieldTracker, field: BitfieldField) -> int:
100+
"""Account for a bitfield during size computation, without inflating.
101+
102+
Args:
103+
field: The BitfieldField descriptor.
104+
105+
Returns:
106+
The byte size delta (nonzero only when a new group starts).
107+
"""
108+
backing_type = field.backing_type
109+
bit_width = field.bit_width
110+
backing_size_bits = backing_type.size * 8
111+
size_delta = 0
112+
113+
if self._backing_type is not backing_type or self._bit_offset + bit_width > backing_size_bits:
114+
size_delta = self.flush()
115+
self._backing_type = backing_type
116+
117+
self._bit_offset += bit_width
118+
return size_delta

0 commit comments

Comments
 (0)