Skip to content

Commit 40ec6b3

Browse files
committed
feat: publicly exposed size_of method
1 parent 5e8dd5a commit 40ec6b3

3 files changed

Lines changed: 70 additions & 18 deletions

File tree

libdestruct/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from libdestruct.common.enum import enum, enum_of
2020
from libdestruct.common.ptr.ptr import ptr
2121
from libdestruct.common.struct import ptr_to, ptr_to_self, struct
22+
from libdestruct.common.utils import size_of
2223
from libdestruct.libdestruct import inflate, inflater
2324

2425
__all__ = [
@@ -41,5 +42,6 @@
4142
"ptr",
4243
"ptr_to",
4344
"ptr_to_self",
45+
"size_of",
4446
"struct",
4547
]

libdestruct/common/utils.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,33 @@ def is_field_bound_method(item: obj) -> bool:
2525

2626

2727
def size_of(item_or_inflater: obj | callable[[Resolver], obj]) -> int:
28-
"""Return the size of an object, from an obj or it's inflater."""
29-
if hasattr(item_or_inflater.__class__, "size"):
30-
# This has the priority over the size of the object itself
31-
# as we might be dealing with a struct object
32-
# that defines an attribute named "size"
33-
return item_or_inflater.__class__.size
34-
if hasattr(item_or_inflater, "size"):
35-
return item_or_inflater.size
36-
37-
# Check if item is the bound method of a Field
38-
if is_field_bound_method(item_or_inflater):
39-
field_object = item_or_inflater.__self__
40-
return field_object.get_size()
41-
42-
# Check if item is directly a Field instance
28+
"""Return the size in bytes of a type, instance, or field descriptor."""
29+
# Field instances (e.g. array_of, ptr_to) — must come before .size check
4330
if isinstance(item_or_inflater, Field):
4431
return item_or_inflater.get_size()
32+
if is_field_bound_method(item_or_inflater):
33+
return item_or_inflater.__self__.get_size()
34+
35+
# Struct types: size is on the inflated _type_impl class
36+
if isinstance(item_or_inflater, type) and hasattr(item_or_inflater, "_type_impl"):
37+
return item_or_inflater._type_impl.size
38+
39+
# Struct types not yet inflated: trigger inflation to compute size
40+
if isinstance(item_or_inflater, type) and not hasattr(item_or_inflater, "size"):
41+
from libdestruct.common.type_registry import TypeRegistry
42+
43+
impl = TypeRegistry().inflater_for(item_or_inflater)
44+
if hasattr(impl, "size") and isinstance(impl.size, int):
45+
return impl.size
46+
47+
# Check class-level size (works for both types and instances)
48+
if isinstance(item_or_inflater, type):
49+
if hasattr(item_or_inflater, "size") and isinstance(item_or_inflater.size, int):
50+
return item_or_inflater.size
51+
elif hasattr(item_or_inflater.__class__, "size"):
52+
return item_or_inflater.__class__.size
53+
elif hasattr(item_or_inflater, "size"):
54+
return item_or_inflater.size
4555

4656
raise ValueError(f"Cannot determine the size of {item_or_inflater}")
4757

test/scripts/types_unit_test.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import struct as pystruct
1010
import unittest
1111

12-
from libdestruct import c_int, c_long, c_str, c_uint, c_float, c_double, inflater, struct, ptr, ptr_to_self
12+
from libdestruct import c_int, c_long, c_str, c_uint, c_float, c_double, inflater, struct, ptr, ptr_to_self, size_of, array_of
1313
from libdestruct.backing.memory_resolver import MemoryResolver
1414

1515

@@ -107,7 +107,7 @@ class test_t(struct):
107107
self.assertIn("0x0", s)
108108

109109
def test_ptr_add(self):
110-
"""ptr + 1 returns new ptr at addr + sizeof(target)."""
110+
"""ptr + 1 returns new ptr at addr + size_of(target)."""
111111
# Array of 3 c_int values: [10, 20, 30]
112112
memory = bytearray(8 + 12)
113113
memory[0:8] = (8).to_bytes(8, "little") # pointer to offset 8
@@ -124,7 +124,7 @@ def test_ptr_add(self):
124124
self.assertEqual(p3.unwrap().value, 30)
125125

126126
def test_ptr_sub(self):
127-
"""ptr - 1 returns new ptr at addr - sizeof(target)."""
127+
"""ptr - 1 returns new ptr at addr - size_of(target)."""
128128
memory = bytearray(8 + 12)
129129
memory[0:8] = (12).to_bytes(8, "little") # pointer to second element
130130
memory[8:12] = (10).to_bytes(4, "little")
@@ -313,5 +313,45 @@ def test_setitem_middle(self):
313313
self.assertEqual(s.get(1), b"a")
314314

315315

316+
class SizeofTest(unittest.TestCase):
317+
"""size_of() function."""
318+
319+
def test_size_of_c_int(self):
320+
self.assertEqual(size_of(c_int), 4)
321+
322+
def test_size_of_c_long(self):
323+
self.assertEqual(size_of(c_long), 8)
324+
325+
def test_size_of_c_float(self):
326+
self.assertEqual(size_of(c_float), 4)
327+
328+
def test_size_of_ptr(self):
329+
self.assertEqual(size_of(ptr), 8)
330+
331+
def test_size_of_struct(self):
332+
class two_ints(struct):
333+
a: c_int
334+
b: c_int
335+
336+
self.assertEqual(size_of(two_ints), 8)
337+
338+
def test_size_of_instance(self):
339+
obj = c_int.from_bytes((42).to_bytes(4, "little"))
340+
self.assertEqual(size_of(obj), 4)
341+
342+
def test_size_of_array_field(self):
343+
self.assertEqual(size_of(array_of(c_int, 10)), 40)
344+
345+
def test_size_of_nested_struct(self):
346+
class inner(struct):
347+
x: c_int
348+
349+
class outer(struct):
350+
a: inner
351+
b: c_int
352+
353+
self.assertEqual(size_of(outer), 8)
354+
355+
316356
if __name__ == "__main__":
317357
unittest.main()

0 commit comments

Comments
 (0)