From 5bca4a1f756d9421fdb017d7fd8ffc0399c07c64 Mon Sep 17 00:00:00 2001 From: Andrea Bonomi Date: Wed, 15 Jan 2025 22:42:54 +0100 Subject: [PATCH] Access to Python class attributes in struct definition --- .readthedocs.yaml | 9 ++++ README.md | 27 ++++++++++ changelog.txt | 10 ++++ cstruct/__init__.py | 4 +- cstruct/abstract.py | 20 +++++-- cstruct/base.py | 2 +- cstruct/c_expr.py | 50 ++++++++++++++++-- cstruct/c_parser.py | 45 +++++++++++----- cstruct/cstruct.py | 2 +- cstruct/exceptions.py | 7 ++- cstruct/field.py | 63 +++++++++++++++++----- cstruct/mem_cstruct.py | 6 +-- cstruct/native_types.py | 17 +++--- setup.cfg | 1 + tests/test_cstruct_var.py | 107 ++++++++++++++++++++++++++++++++++++++ 15 files changed, 321 insertions(+), 49 deletions(-) create mode 100644 tests/test_cstruct_var.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8774cd0..b7168c0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,16 @@ # .readthedocs.yaml version: 2 + +# Set the OS and Python version +build: + os: ubuntu-22.04 + tools: + python: "3.12" + mkdocs: configuration: mkdocs.yml + +# Declare the Python requirements required to build the documentation python: install: - requirements: requirements-dev.txt diff --git a/README.md b/README.md index de0a08d..771adbd 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,33 @@ pkg.length = 4 pkg.data = [10, 20, 30, 40] ``` +### Python object attributes + +In struct definition, you can access Python object attributes using `self`. +The value of expression accessing class attributes is evaluated at runtime. + +```python +class RT11DirectoryEntry(cstruct.CStruct): + + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct RT11DirectoryEntry { + uint8_t type; + uint8_t clazz; + uint16_t raw_filename1; + uint16_t raw_filename2; + uint16_t raw_extension; + uint16_t length; + uint8_t job; + uint8_t channel; + uint16_t raw_creation_date; + uint16_t extra_bytes[self.extra_bytes_len]; /* The size of the array is determined at runtime */ + }; + """ + + extra_bytes_len: int = 0 +``` + ### Pack and Unpack A code example illustrating how to use diff --git a/changelog.txt b/changelog.txt index 8a7f723..e0ff73f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -182,3 +182,13 @@ ### Improved - Python 3.12 support + +## [6.0] - 2025-01-16 + +### Added + +- access to Python class attributes in struct definition + +### Improved + +- Python 3.13 support diff --git a/cstruct/__init__.py b/cstruct/__init__.py index 580b400..c86d726 100644 --- a/cstruct/__init__.py +++ b/cstruct/__init__.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -24,7 +24,7 @@ __author__ = "Andrea Bonomi " __license__ = "MIT" -__version__ = "5.3" +__version__ = "6.0" __date__ = "15 August 2013" from typing import Any, Dict, Optional, Type, Union diff --git a/cstruct/abstract.py b/cstruct/abstract.py index 31a9ffb..378d3ce 100644 --- a/cstruct/abstract.py +++ b/cstruct/abstract.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -163,7 +161,7 @@ def set_flexible_array_length(self, flexible_array_length: Optional[int]) -> Non flexible_array: Optional[FieldType] = [x for x in self.__fields_types__.values() if x.flexible_array][0] if flexible_array is None: raise CStructException("Flexible array not found in struct") - flexible_array.vlen = flexible_array_length + flexible_array.vlen_ex = flexible_array_length def unpack(self, buffer: Optional[Union[bytes, BinaryIO]], flexible_array_length: Optional[int] = None) -> bool: """ @@ -202,6 +200,17 @@ def pack(self) -> bytes: # pragma: no cover """ raise NotImplementedError + def pack_into(self, buffer: bytearray, offset: int = 0) -> None: + """ + Pack the structure data into a buffer + + Args: + buffer: target buffer (must be large enough to contain the packed structure) + offset: optional buffer offset + """ + tmp = self.pack() + buffer[offset : offset + len(tmp)] = tmp + def clear(self) -> None: self.unpack(None) @@ -300,6 +309,9 @@ def __setstate__(self, state: bytes) -> bool: class CEnumMeta(EnumMeta): + __size__: int + __native_format__: str + class WrapperDict(_EnumDict): def __setitem__(self, key: str, value: Any) -> None: env = None diff --git a/cstruct/base.py b/cstruct/base.py index fc3a29c..ea5c223 100644 --- a/cstruct/base.py +++ b/cstruct/base.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # diff --git a/cstruct/c_expr.py b/cstruct/c_expr.py index b2d16c6..70edf46 100644 --- a/cstruct/c_expr.py +++ b/cstruct/c_expr.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -23,11 +23,12 @@ # import ast +import inspect import operator -from typing import TYPE_CHECKING, Any, Callable, Dict, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, Union from .base import DEFINES, STRUCTS -from .exceptions import EvalError +from .exceptions import ContextNotFound, EvalError if TYPE_CHECKING: from .abstract import AbstractCStruct @@ -65,7 +66,36 @@ def c_eval(expr: str) -> Union[int, float]: raise EvalError +def eval_attribute_node(node: ast.Attribute) -> Union[int, float]: + """ + Evaluate node attribute, e.g. 'self.x' + Only 'self' is allowed. The attribute must be a number. + + Args: + node: attribute node + + Returns: + result: the attribute value + + Raises: + EvalError: expression result is not a number, or not self attribute + ContextNotFound: context is not defined + """ + if not node.value or node.value.id != "self": # type: ignore + raise EvalError("only self is allowed") + context = get_cstruct_context() + if context is None: + raise ContextNotFound("context is not defined") + result = getattr(context, node.attr) + if not isinstance(result, (int, float)): + raise EvalError("expression result is not a number") + return result + + def eval_node(node: ast.stmt) -> Union[int, float]: + if isinstance(node, ast.Attribute): + return eval_attribute_node(node) + handler = OPS[type(node)] result = handler(node) if isinstance(result, bool): # convert bool to int @@ -116,6 +146,20 @@ def eval_call(node) -> Union[int, float]: raise KeyError(node.func.id) +def get_cstruct_context() -> Optional["AbstractCStruct"]: + """ + Get the calling CStruct instance from the stack (if any) + """ + from .abstract import AbstractCStruct + + stack = inspect.stack() + for frame in stack: + caller_self = frame.frame.f_locals.get("self") + if isinstance(caller_self, AbstractCStruct): + return caller_self + return None + + try: Constant = ast.Constant except AttributeError: # python < 3.8 diff --git a/cstruct/c_parser.py b/cstruct/c_parser.py index 8c24e56..2a95705 100644 --- a/cstruct/c_parser.py +++ b/cstruct/c_parser.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -24,11 +24,21 @@ import re from collections import OrderedDict -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Tuple, + Type, + Union, +) from .base import DEFINES, ENUMS, STRUCTS, TYPEDEFS from .c_expr import c_eval -from .exceptions import CStructException, ParserError +from .exceptions import CStructException, EvalError, ParserError from .field import FieldType, Kind, calculate_padding from .native_types import get_native_type @@ -41,7 +51,7 @@ SPACES = [" ", "\t", "\n"] -class Tokens(object): +class Tokens: def __init__(self, text: str) -> None: # remove the comments text = re.sub(r"//.*?$|/\*.*?\*/", "", text, flags=re.S | re.MULTILINE) @@ -59,7 +69,7 @@ def __init__(self, text: str) -> None: text = "\n".join(lines) self.tokens = self.tokenize(text) - def tokenize(self, text) -> List[str]: + def tokenize(self, text: str) -> List[str]: tokens: List[str] = [] t: List[str] = [] for c in text: @@ -72,7 +82,7 @@ def tokenize(self, text) -> List[str]: else: t.append(c) if t: - tokens.append(t.getvalue()) + tokens.append("".join(t)) return tokens def pop(self) -> str: @@ -101,7 +111,8 @@ def __str__(self) -> str: return str(self.tokens) -def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: bool) -> Tuple[str, int, bool]: +def parse_length(tokens: Tokens, next_token: str, flexible_array: bool) -> Tuple[str, Union[int, Callable[[], int]], bool]: + # Extract t_vlen t = next_token.split("[") if len(t) != 2: raise ParserError(f"Error parsing: `{next_token}`") @@ -114,14 +125,19 @@ def parse_length(tokens: Tokens, next_token: str, vlen: int, flexible_array: boo t_vlen = vlen_part.split("]")[0].strip() vlen_expr.append(vlen_part.split("]")[0].strip()) t_vlen = " ".join(vlen_expr) + # Evaluate t_vlen + vlen: Union[int, Callable[[], int]] if not t_vlen: + # If the length expression is empty, this is a flex array flexible_array = True vlen = 0 else: + # Evaluate the length expression + # If the length expression is not a constant, it is evaluated at runtime try: - vlen = c_eval(t_vlen) - except (ValueError, TypeError): - vlen = int(t_vlen) + vlen = int(c_eval(t_vlen)) + except EvalError: + vlen = lambda: int(c_eval(t_vlen)) return next_token, vlen, flexible_array @@ -133,7 +149,7 @@ def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Opt if c_type in ["signed", "unsigned", "struct", "union", "enum"] and len(tokens) > 1: c_type = c_type + " " + tokens.pop() - vlen = 1 + vlen: Union[int, Callable[[], int]] = 1 flexible_array = False if not c_type.endswith("{"): @@ -148,20 +164,21 @@ def parse_type(tokens: Tokens, __cls__: Type["AbstractCStruct"], byte_order: Opt c_type = "void *" # parse length if "[" in next_token: - next_token, vlen, flexible_array = parse_length(tokens, next_token, vlen, flexible_array) + next_token, vlen, flexible_array = parse_length(tokens, next_token, flexible_array) tokens.push(next_token) # resolve typedefs while c_type in TYPEDEFS: c_type = TYPEDEFS[c_type] # calculate fmt + ref: Union[None, Type[AbstractCEnum], Type[AbstractCStruct]] if c_type.startswith("struct ") or c_type.startswith("union "): # struct/union c_type, tail = c_type.split(" ", 1) kind = Kind.STRUCT if c_type == "struct" else Kind.UNION if tokens.get() == "{": # Named nested struct tokens.push(tail) tokens.push(c_type) - ref: Union[Type[AbstractCEnum], Type[AbstractCStruct]] = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) + ref = __cls__.parse(tokens, __name__=tail, __byte_order__=byte_order) elif tail == "{": # Unnamed nested struct tokens.push(tail) tokens.push(c_type) @@ -428,7 +445,7 @@ def parse_struct( raise ParserError(f"Invalid reserved member name `{vname}`") # parse length if "[" in vname: - vname, field_type.vlen, field_type.flexible_array = parse_length(tokens, vname, 1, flexible_array) + vname, field_type.vlen_ex, field_type.flexible_array = parse_length(tokens, vname, flexible_array) flexible_array = flexible_array or field_type.flexible_array # anonymous nested union if vname == ";" and field_type.ref is not None and (__is_union__ or field_type.ref.__is_union__): diff --git a/cstruct/cstruct.py b/cstruct/cstruct.py index 9e899d9..66be23f 100644 --- a/cstruct/cstruct.py +++ b/cstruct/cstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # diff --git a/cstruct/exceptions.py b/cstruct/exceptions.py index b5546b4..cf496ae 100644 --- a/cstruct/exceptions.py +++ b/cstruct/exceptions.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -27,6 +27,7 @@ "CStructException", "ParserError", "EvalError", + "ContextNotFound", ] @@ -44,3 +45,7 @@ class ParserError(CStructException): class EvalError(CStructException): pass + + +class ContextNotFound(EvalError): + pass diff --git a/cstruct/field.py b/cstruct/field.py index 3568b36..2d0bb16 100644 --- a/cstruct/field.py +++ b/cstruct/field.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -23,12 +23,13 @@ # import copy +import inspect import struct from enum import Enum -from typing import TYPE_CHECKING, Any, List, Optional, Type +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, Union from .base import NATIVE_ORDER -from .exceptions import ParserError +from .exceptions import ContextNotFound, ParserError from .native_types import get_native_type if TYPE_CHECKING: @@ -49,6 +50,20 @@ def calculate_padding(byte_order: Optional[str], alignment: int, pos: int) -> in return 0 +def get_cstruct_context() -> Optional["AbstractCStruct"]: + """ + Get the current CStruct context (instance) from the stack + """ + from .abstract import AbstractCStruct + + stack = inspect.stack() + for frame in stack: + caller_self = frame.frame.f_locals.get("self") + if isinstance(caller_self, AbstractCStruct): + return caller_self + return None + + class Kind(Enum): """ Field type @@ -64,7 +79,7 @@ class Kind(Enum): "Enum type" -class FieldType(object): +class FieldType: """ Struct/Union field @@ -72,18 +87,27 @@ class FieldType(object): kind (Kind): struct/union/native c_type (str): field type ref (AbstractCStruct): struct/union class ref - vlen (int): number of elements + vlen_ex (int|callable int): number of elements flexible_array (bool): True for flexible arrays offset (int): relative memory position of the field (relative to the struct) padding (int): padding """ + kind: Kind + c_type: str + ref: Optional[Type["AbstractCStruct"]] + vlen_ex: Union[int, Callable[[], int]] + flexible_array: bool + byte_order: Optional[str] + offset: int + padding: int + def __init__( self, kind: Kind, c_type: str, ref: Optional[Type["AbstractCStruct"]], - vlen: int, + vlen_ex: Union[int, Callable[[], int]], flexible_array: bool, byte_order: Optional[str], offset: int, @@ -95,26 +119,27 @@ def __init__( kind: struct/union/native c_type: field type ref: struct/union class ref - vlen: number of elements + vlen_ex: number of elements flexible_array: True for flexible arrays offset: relative memory position of the field (relative to the struct) """ self.kind = kind self.c_type = c_type self.ref = ref - self.vlen = vlen + self.vlen_ex = vlen_ex self.flexible_array = flexible_array self.byte_order = byte_order self.offset = self.base_offset = offset self.padding = 0 - def unpack_from(self, buffer: bytes, offset: int = 0) -> Any: + def unpack_from(self, buffer: bytes, offset: int = 0, context: Optional["AbstractCStruct"] = None) -> Any: """ Unpack bytes containing packed C structure data Args: buffer: bytes to be unpacked offset: optional buffer offset + context: context (cstruct instance) Returns: data: The unpacked data @@ -153,13 +178,24 @@ def pack(self, data: Any) -> bytes: bytes: The packed structure """ if self.flexible_array: - self.vlen = len(data) # set flexible array size + self.vlen_ex = len(data) # set flexible array size return struct.pack(self.fmt, *data) elif self.is_array: - return struct.pack(self.fmt, *data) + if self.vlen == 0: # empty array + return bytes() + else: + return struct.pack(self.fmt, *data) else: return struct.pack(self.fmt, data) + @property + def vlen(self) -> int: + "Number of elements" + try: + return self.vlen_ex() if callable(self.vlen_ex) else self.vlen_ex + except ContextNotFound: + return 0 + @property def is_array(self) -> bool: "True if field is an array/flexible array" @@ -202,7 +238,10 @@ def native_format(self) -> str: def fmt(self) -> str: "Field format prefixed by byte order (struct library format)" if self.is_native or self.is_enum: - fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else "") + self.native_format + if self.vlen == 0: + fmt = "" + else: + fmt = (str(self.vlen) if self.vlen > 1 or self.flexible_array else "") + self.native_format else: # Struct/Union fmt = str(self.vlen * self.ref.sizeof()) + self.native_format if self.byte_order: diff --git a/cstruct/mem_cstruct.py b/cstruct/mem_cstruct.py index a0aaf1d..e457de6 100644 --- a/cstruct/mem_cstruct.py +++ b/cstruct/mem_cstruct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -35,7 +35,7 @@ def __init__(self, values: List[Any], name: str, parent: Optional["MemCStruct"] self.name = name self.parent = parent - def __setitem__(self, key: int, value: List[Any]) -> None: # noqa: F811 + def __setitem__(self, key: int, value: List[Any]) -> None: # type: ignore super().__setitem__(key, value) # Notify the parent when a value is changed if self.parent is not None: @@ -136,7 +136,7 @@ def __setattr__(self, attr: str, value: Any) -> None: else: # native if field_type.flexible_array and len(value) != field_type.vlen: # flexible array size changed, resize the buffer - field_type.vlen = len(value) + field_type.vlen_ex = len(value) ctypes.resize(self.__mem__, self.size + 1) addr = field_type.offset + self.__base__ self.memcpy(addr, field_type.pack(value), field_type.vsize) diff --git a/cstruct/native_types.py b/cstruct/native_types.py index f1e205c..fd20f47 100644 --- a/cstruct/native_types.py +++ b/cstruct/native_types.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (c) 2013-2019 Andrea Bonomi +# Copyright (c) 2013-2025 Andrea Bonomi # # Published under the terms of the MIT license. # @@ -54,10 +54,10 @@ ] -NATIVE_TYPES: Dict[str, "AbstractNativeType"] = {} +NATIVE_TYPES: Dict[str, Type["AbstractNativeType"]] = {} -def get_native_type(type_: str) -> "AbstractNativeType": +def get_native_type(type_: str) -> Type["AbstractNativeType"]: """ Get a base data type by name @@ -79,10 +79,6 @@ def get_native_type(type_: str) -> "AbstractNativeType": class NativeTypeMeta(ABCMeta): __size__: int = 0 " Size in bytes " - type_name: str = "" - " Type name " - native_format: str = "" - " Type format " def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[str, Any]) -> Type[Any]: if namespace.get("native_format"): @@ -92,7 +88,7 @@ def __new__(metacls: Type[type], name: str, bases: Tuple[str], namespace: Dict[s native_format = None namespace["native_format"] = None namespace["__size__"] = None - new_class: Type[Any] = super().__new__(metacls, name, bases, namespace) + new_class: Type[AbstractNativeType] = super().__new__(metacls, name, bases, namespace) # type: ignore if namespace.get("type_name"): NATIVE_TYPES[namespace["type_name"]] = new_class return new_class @@ -108,6 +104,11 @@ def size(cls) -> int: class AbstractNativeType(metaclass=NativeTypeMeta): + type_name: str = "" + " Type name " + native_format: str = "" + " Type format " + def __str__(self) -> str: return self.type_name diff --git a/setup.cfg b/setup.cfg index 59d4b3f..f737c08 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,6 +26,7 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 project_urls = Bug Tracker = http://github.com/andreax79/python-cstruct/issues Documentation = https://python-cstruct.readthedocs.io/en/latest/ diff --git a/tests/test_cstruct_var.py b/tests/test_cstruct_var.py new file mode 100644 index 0000000..4ad9c07 --- /dev/null +++ b/tests/test_cstruct_var.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# ***************************************************************************** +# +# Copyright (c) 2013-2025 Andrea Bonomi +# +# Published under the terms of the MIT license. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +# +# ***************************************************************************** + +import cstruct +from cstruct import sizeof +from cstruct.c_expr import c_eval + + +class Struct0(cstruct.CStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t a; + uint16_t b; + char c[6]; + } + """ + + +class Struct1(cstruct.CStruct): + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + #define X1 6 + struct { + uint16_t a; + uint16_t b; + char c[X1]; + } + """ + + +class Struct2(cstruct.CStruct): + c_len: int = 6 + + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t a; + uint16_t b; + char c[self.c_len]; + } + """ + + +class Struct3(cstruct.MemCStruct): + c_len: int = 0 + + __byte_order__ = cstruct.LITTLE_ENDIAN + __def__ = """ + struct { + uint16_t a; + uint16_t b; + uint16_t c[self.c_len]; + } + """ + + +def test_v(): + assert c_eval('10') == 10 + + s0 = Struct0() + assert len(s0) == 10 + s1 = Struct1() + assert len(s1) == 10 + + assert sizeof(Struct2) == 4 + + s2 = Struct2() + assert len(s2) == 10 + s2.c_len = 10 + assert len(s2) == 14 + + for i in range(10): + s2.c_len = i + assert len(s2) == 4 + i + + assert sizeof(Struct3) == 4 + s3 = Struct3() + assert len(s3) == 4 + + for i in range(10): + s3.c_len = i + assert len(s3) == 4 + i * 2