Skip to content
Merged
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
60 changes: 54 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ A Python library for defining and manipulating C-like packed structures with sup
- **Bit-Level Precision**: Full support for bit-fields and non-byte-aligned data
- **Nested Structures**: Create complex hierarchical data structures
- **Endianness Control**: Support for big-endian, little-endian, and native byte order
- **Array Support**: Built-in support for fixed-size arrays
- **Array Support**: Fixed-size arrays of primitives, of structs, and arbitrarily-nested arrays of arrays (multi-dimensional)
- **Numeric Operators**: Arithmetic and ordering operators are available only on numeric types (`c_unsigned_int`, `c_signed_int`, `c_float`); using them on non-numeric types (`c_bool`, `c_char`, `c_raw_bytes`, `c_padding`, `c_array`) raises `TypeError`
- **C Compatibility**: Generate structures that are binary-compatible with C's `__attribute__((packed))` structs
- **Zero Dependencies**: Only requires `bitstruct` package

Expand Down Expand Up @@ -119,14 +120,29 @@ All data types inherit from the base `Type` class and support bit-level precisio

#### Arrays

- **`c_array(type, type_size_bits, array_size)`**: Fixed-size array
- `type_size_bits` must be multiple of 8
- **`c_array(template, array_size)`**: Fixed-size array.
- `template` is a *template instance* of any `Type` or `Struct`; it is deep-copied for each element.
- Arrays can be nested (array of arrays) to build multi-dimensional arrays of arbitrary rank.

```python
# Array of 5 unsigned 8-bit integers
values = c_array(c_unsigned_int, 8, 5)
values = c_array(c_unsigned_int(8), 5)
values.set_value([10, 20, 30, 40, 50])

# Array of 3 structs
point = Struct({"x": c_signed_int(16), "y": c_signed_int(16)})
points = c_array(point, 3)
points[0].set_data(x=1, y=2)

# 2-D array (array of arrays of arrays of structs is supported too)
matrix = c_array(c_array(c_unsigned_int(8), 4), 3) # 3x4 matrix
matrix.set_value([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
```

Indexing returns the wrapped element (a `Type` or `Struct`), so `arr[i].value`, `arr[i].set_data(...)` and `arr[r][c]` all work as expected. The element still compares equal to its scalar value (`arr[i] == 10`).

### Struct Class

The `Struct` class represents a collection of typed fields:
Expand Down Expand Up @@ -287,7 +303,7 @@ from packed_struct import Struct, c_array, c_unsigned_int
# Array of integers
data = Struct({
"header": c_unsigned_int(16),
"values": c_array(c_unsigned_int, 8, 10) # 10 bytes
"values": c_array(c_unsigned_int(8), 10) # 10 bytes
})

data.set_data(header=0xFFFF)
Expand All @@ -298,6 +314,36 @@ for i, element in enumerate(data["values"]):
print(f"values[{i}] = {element.value}")
```

### Multi-dimensional arrays of nested structs

For a more complex case combining nested structs, arrays inside structs and arrays of arrays of structs, see
[`examples/complex/warehouse_grid.py`](https://github.com/lu-maca/py-packed-struct/blob/main/examples/complex/warehouse_grid.py).

```python
from packed_struct import Struct, c_array, c_char, c_unsigned_int

location = Struct({"lat": c_unsigned_int(16), "lon": c_unsigned_int(16)})

bin_tmpl = Struct({
"id": c_unsigned_int(8),
"loc": location, # nested struct
"label": c_char(8 * 8),
"samples": c_array(c_unsigned_int(16), 4), # array inside struct
})

warehouse = Struct({
"version": c_unsigned_int(8),
"grid": c_array(c_array(bin_tmpl, 3), 2), # 2x3 grid of Bin structs
})

warehouse.set_data(version=1)
cell = warehouse["grid"][0][1] # walk the matrix
cell.set_data(id=7, label="B0-1", samples=[10, 11, 12, 13])
cell.get_data()["loc"].set_data(lat=45000, lon=9001)

blob = warehouse.pack()
```

## Use Cases

- **Embedded Systems**: Communicate with hardware devices using binary protocols
Expand All @@ -313,7 +359,9 @@ for i, element in enumerate(data["values"]):
| C-like structures | ✅ Supported |
| Bit-fields | ✅ Supported |
| Nested structures | ✅ Supported |
| Arrays | ✅ Supported |
| Arrays of primitives and structs | ✅ Supported |
| Multi-dimensional arrays (array of arrays) | ✅ Supported |
| Arithmetic operators on numeric types only | ✅ Supported |
| Byte endianness | ✅ Supported (big, little, native) |
| Bit endianness | 🚧 Planned |
| Dynamic arrays | 🚧 Planned |
Expand Down
45 changes: 45 additions & 0 deletions examples/array/array_of_ints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Example: array of integers

Shows how to use c_array with a primitive type to represent a fixed-size
sequence of values — equivalent to the following C struct:

struct {
uint8_t samples[4];
uint16_t checksum;
}
"""

from packed_struct import *

msg = Struct(
{
"samples": c_array(c_unsigned_int(8), 4),
"checksum": c_unsigned_int(16),
}
)

# Set the array elements individually …
msg["samples"][0].set_value(10)
msg["samples"][1].set_value(20)
msg["samples"][2].set_value(30)
msg["samples"][3].set_value(40)

# … or all at once via set_data()
msg.set_data(samples=[10, 20, 30, 40], checksum=0xABCD)

packed = msg.pack()
print("packed :", packed.hex()) # 0a141e28abcd

# Unpack back into a fresh struct
msg2 = Struct(
{
"samples": c_array(c_unsigned_int(8), 4),
"checksum": c_unsigned_int(16),
}
)
msg2.unpack(packed)

print("samples:", [msg2["samples"][i] for i in range(4)]) # [10, 20, 30, 40]
print("checksum:", hex(msg2["checksum"])) # 0xabcd
print(msg2["checksum"] == 0xABCD)
print(msg2["samples"][2] == 30)
58 changes: 58 additions & 0 deletions examples/array/array_of_structs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Example: array of structs

Shows how to use c_array with a Struct template to represent a fixed-size
array of composite records — equivalent to the following C struct:

typedef struct {
int16_t x;
int16_t y;
} Point;

struct {
uint8_t count;
Point waypoints[3];
}
"""

from packed_struct import *

point = Struct({"x": c_signed_int(16), "y": c_signed_int(16)})

route = Struct(
{
"count": c_unsigned_int(8),
"waypoints": c_array(point, 3),
}
)

# Set each waypoint separately
route["waypoints"][0].set_data(x=0, y=0)
route["waypoints"][1].set_data(x=100, y=-50)
route["waypoints"][2].set_data(x=200, y=75)
route.set_data(count=3)

# Or set all waypoints at once via set_data() with a list of dicts
route.set_data(waypoints=[
{"x": 0, "y": 0},
{"x": 100, "y": -50},
{"x": 200, "y": 75},
])

packed = route.pack()
print("packed :", packed.hex())
print("size :", route.size, "bits /", len(packed), "bytes")

# Unpack back
point2 = Struct({"x": c_signed_int(16), "y": c_signed_int(16)})
route2 = Struct(
{
"count": c_unsigned_int(8),
"waypoints": c_array(point2, 3),
}
)
route2.unpack(packed)

print("count:", route2["count"])
for i in range(3):
wp = route2["waypoints"][i]
print(f" waypoint[{i}]: x={wp['x']}, y={wp['y']}")
110 changes: 110 additions & 0 deletions examples/complex/warehouse_grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Example: complex composition.

Demonstrates the full expressive power of the library by combining:
* nested ``Struct``s,
* ``c_array`` *inside* a ``Struct``,
* ``c_array`` of ``c_array`` (a 2D matrix),
* the matrix elements themselves being ``Struct``s.

Equivalent C declarations::

typedef struct {
uint16_t lat; // latitude (encoded)
uint16_t lon; // longitude (encoded)
} Location;

typedef struct {
uint8_t id;
Location loc; // <-- nested struct
char label[8];
uint16_t samples[4]; // <-- array inside struct
} Bin;

struct Warehouse {
uint8_t version;
Bin grid[2][3]; // <-- array of array of struct
};
"""

from packed_struct import (
Struct,
c_array,
c_char,
c_unsigned_int,
)

ROWS = 2
COLS = 3
SAMPLES_PER_BIN = 4
LABEL_BYTES = 8

# ---------------------------------------------------------------------------
# Templates
# ---------------------------------------------------------------------------
location_tmpl = Struct({
"lat": c_unsigned_int(16),
"lon": c_unsigned_int(16),
})

bin_tmpl = Struct({
"id": c_unsigned_int(8),
"loc": location_tmpl, # nested struct
"label": c_char(LABEL_BYTES * 8),
"samples": c_array(c_unsigned_int(16), SAMPLES_PER_BIN), # array inside struct
})


def make_warehouse() -> Struct:
"""Build a fresh Warehouse struct (templates are deep-copied internally)."""
return Struct({
"version": c_unsigned_int(8),
# array of array of Bin
"grid": c_array(c_array(bin_tmpl, COLS), ROWS),
})


# ---------------------------------------------------------------------------
# Populate
# ---------------------------------------------------------------------------
warehouse = make_warehouse()
warehouse.set_data(version=1)

for r in range(ROWS):
for c in range(COLS):
bin_ = warehouse["grid"][r][c] # the actual Bin Struct (deep-copied template)
bin_data = bin_.get_data() # dict {field_name: Type/Struct/c_array}
bin_data["loc"].set_data(lat=45000 + r, lon=9000 + c) # nested struct
bin_.set_data(
id=r * COLS + c,
label=f"B{r}-{c}",
samples=[r * 100 + c * 10 + k for k in range(SAMPLES_PER_BIN)],
)

# ---------------------------------------------------------------------------
# Pack
# ---------------------------------------------------------------------------
packed = warehouse.pack()
print("packed :", packed.hex())
print("size :", warehouse.size, "bits /", len(packed), "bytes")

# ---------------------------------------------------------------------------
# Unpack into a fresh instance and verify
# ---------------------------------------------------------------------------
restored = make_warehouse()
restored.unpack(packed)

print("\nrestored.version =", restored["version"])
for r in range(ROWS):
for c in range(COLS):
b = restored["grid"][r][c]
loc = b.get_data()["loc"]
print(
f" grid[{r}][{c}]: id={b['id']:>2} "
f"loc=({loc['lat']}, {loc['lon']}) "
f"label={b['label']!r:<10} "
f"samples={[b['samples'][k].value for k in range(SAMPLES_PER_BIN)]}"
)

# Round-trip sanity check
assert restored.pack() == packed, "round-trip mismatch"
print("\nround-trip OK")
61 changes: 61 additions & 0 deletions examples/nested-struct/nested_struct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Example: pack / unpack round-trip with nested structs

Shows how to serialize a struct to bytes and deserialize it back —
equivalent to the following C structs:

typedef struct {
char brand[10];
uint8_t size;
} Shoe;

typedef struct {
char tshirt[10];
char shorts[10];
Shoe shoes;
} Dresses;

typedef struct {
char name[10];
uint8_t age;
float weight;
Dresses dresses;
} Person;
"""

from packed_struct import *


def make_person_struct():
shoes = Struct({"brand": c_char(10 * 8), "size": c_unsigned_int(8)})
dresses = Struct({"tshirt": c_char(10 * 8), "shorts": c_char(10 * 8), "shoes": shoes})
person = Struct({"name": c_char(10 * 8), "age": c_unsigned_int(8), "weight": c_float(32), "dresses": dresses})
return person, dresses, shoes


# --- Sender side ---
sender, sender_dresses, sender_shoes = make_person_struct()

sender.set_data(name="Alice", age=30, weight=58.5)
sender_dresses.set_data(tshirt="white", shorts="blue")
sender_shoes.set_data(brand="Nike", size=38)

payload = sender.pack(byte_endianness="big")
print("payload :", payload.hex())
print("size :", sender.size, "bits /", len(payload), "bytes")

# --- Receiver side ---
receiver, receiver_dresses, receiver_shoes = make_person_struct()
receiver.unpack(payload, byte_endianness="big")

# get_data() returns the internal dict of Type/Struct objects
top = receiver.get_data()
dr = receiver_dresses.get_data()
sh = receiver_shoes.get_data()

print("\nReceived:")
print(" name :", top["name"].value)
print(" age :", top["age"].value)
print(" weight :", top["weight"].value)
print(" tshirt :", dr["tshirt"].value)
print(" shorts :", dr["shorts"].value)
print(" brand :", sh["brand"].value, " size:", sh["size"].value)
Loading
Loading