firstclass 0.0.1__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {firstclass-0.0.1 → firstclass-0.0.3}/PKG-INFO +146 -104
- {firstclass-0.0.1 → firstclass-0.0.3}/README.md +139 -97
- {firstclass-0.0.1 → firstclass-0.0.3}/firstclass/__init__.py +7 -2
- {firstclass-0.0.1 → firstclass-0.0.3}/firstclass/_core.py +326 -251
- firstclass-0.0.3/firstclass/_header.py +1078 -0
- {firstclass-0.0.1 → firstclass-0.0.3}/firstclass/_specs.py +163 -1
- {firstclass-0.0.1 → firstclass-0.0.3}/firstclass.egg-info/PKG-INFO +146 -104
- {firstclass-0.0.1 → firstclass-0.0.3}/firstclass.egg-info/SOURCES.txt +6 -1
- {firstclass-0.0.1 → firstclass-0.0.3}/pyproject.toml +14 -14
- {firstclass-0.0.1 → firstclass-0.0.3}/setup.cfg +4 -4
- {firstclass-0.0.1 → firstclass-0.0.3}/tests/test_ctypes_types.py +182 -176
- firstclass-0.0.3/tests/test_edge_cases.py +559 -0
- firstclass-0.0.3/tests/test_enum_bitfield.py +233 -0
- {firstclass-0.0.1 → firstclass-0.0.3}/tests/test_example.py +4 -0
- {firstclass-0.0.1 → firstclass-0.0.3}/tests/test_firstclass.py +39 -0
- firstclass-0.0.3/tests/test_header.py +582 -0
- firstclass-0.0.3/tests/test_header_parser.py +532 -0
- {firstclass-0.0.1 → firstclass-0.0.3}/firstclass.egg-info/dependency_links.txt +0 -0
- {firstclass-0.0.1 → firstclass-0.0.3}/firstclass.egg-info/top_level.txt +0 -0
|
@@ -1,104 +1,146 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: firstclass
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: dataclass with bytes, ctypes, C struct, JSON and XML serialization
|
|
5
|
-
Requires-Python: >=3.10
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
|
|
8
|
-
# firstclass
|
|
9
|
-
|
|
10
|
-
`@firstclass` works like `@dataclass` (same keyword args) and adds conversion to and from
|
|
11
|
-
packed bytes, `ctypes`, a C struct header, JSON and XML. Pure Python 3.10+, no dependencies.
|
|
12
|
-
|
|
13
|
-
```python
|
|
14
|
-
from firstclass import firstclass, uint8_t, uint16_t, int32_t, float32_t, bool_t, char_array, bytes_t, array_t
|
|
15
|
-
|
|
16
|
-
@firstclass
|
|
17
|
-
class Point:
|
|
18
|
-
x: int32_t
|
|
19
|
-
y: int32_t
|
|
20
|
-
|
|
21
|
-
@firstclass
|
|
22
|
-
class Packet:
|
|
23
|
-
kind: uint8_t
|
|
24
|
-
ok: bool_t
|
|
25
|
-
temp: float32_t
|
|
26
|
-
name: char_array[8] # NUL-padded UTF-8
|
|
27
|
-
mac: bytes_t[6] # exactly 6 bytes
|
|
28
|
-
samples: array_t[uint16_t, 4]
|
|
29
|
-
origin: Point # nested
|
|
30
|
-
|
|
31
|
-
p = Packet(1, True, 21.5, "sensor", b"\x00\x11\x22\x33\x44\x55", [1, 2, 3, 4], Point(10, -20))
|
|
32
|
-
|
|
33
|
-
raw = p.to_bytes() # packed, little-endian
|
|
34
|
-
assert Packet.from_bytes(raw) == p
|
|
35
|
-
Packet.sizeof() # 36
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Types
|
|
39
|
-
|
|
40
|
-
| Marker | C | Bytes |
|
|
41
|
-
|---|---|---|
|
|
42
|
-
| `uint8_t` `uint16_t` `uint32_t` `uint64_t` | `uintN_t` | 1/2/4/8 |
|
|
43
|
-
| `int8_t` `int16_t` `int32_t` `int64_t` | `intN_t` | 1/2/4/8 |
|
|
44
|
-
| `float32_t` / `float64_t` | `float` / `double` | 4 / 8 |
|
|
45
|
-
| `bool_t` | `bool` | 1 |
|
|
46
|
-
| `char_t` | `char` | 1 |
|
|
47
|
-
| `char_array[N]` | `char x[N]` | N (UTF-8, NUL-padded) |
|
|
48
|
-
| `bytes_t[N]` | `uint8_t x[N]` | N |
|
|
49
|
-
| `array_t[T, N]` | `T x[N]` | N * size(T) |
|
|
50
|
-
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
(`
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: firstclass
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: dataclass with bytes, ctypes, C struct, JSON and XML serialization
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# firstclass
|
|
9
|
+
|
|
10
|
+
`@firstclass` works like `@dataclass` (same keyword args) and adds conversion to and from
|
|
11
|
+
packed bytes, `ctypes`, a C struct header, JSON and XML. Pure Python 3.10+, no dependencies.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from firstclass import firstclass, uint8_t, uint16_t, int32_t, float32_t, bool_t, char_array, bytes_t, array_t
|
|
15
|
+
|
|
16
|
+
@firstclass
|
|
17
|
+
class Point:
|
|
18
|
+
x: int32_t
|
|
19
|
+
y: int32_t
|
|
20
|
+
|
|
21
|
+
@firstclass
|
|
22
|
+
class Packet:
|
|
23
|
+
kind: uint8_t
|
|
24
|
+
ok: bool_t
|
|
25
|
+
temp: float32_t
|
|
26
|
+
name: char_array[8] # NUL-padded UTF-8
|
|
27
|
+
mac: bytes_t[6] # exactly 6 bytes
|
|
28
|
+
samples: array_t[uint16_t, 4]
|
|
29
|
+
origin: Point # nested
|
|
30
|
+
|
|
31
|
+
p = Packet(1, True, 21.5, "sensor", b"\x00\x11\x22\x33\x44\x55", [1, 2, 3, 4], Point(10, -20))
|
|
32
|
+
|
|
33
|
+
raw = p.to_bytes() # packed, little-endian
|
|
34
|
+
assert Packet.from_bytes(raw) == p
|
|
35
|
+
Packet.sizeof() # 36
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Types
|
|
39
|
+
|
|
40
|
+
| Marker | C | Bytes |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `uint8_t` `uint16_t` `uint32_t` `uint64_t` | `uintN_t` | 1/2/4/8 |
|
|
43
|
+
| `int8_t` `int16_t` `int32_t` `int64_t` | `intN_t` | 1/2/4/8 |
|
|
44
|
+
| `float32_t` / `float64_t` | `float` / `double` | 4 / 8 |
|
|
45
|
+
| `bool_t` | `bool` | 1 |
|
|
46
|
+
| `char_t` | `char` | 1 |
|
|
47
|
+
| `char_array[N]` | `char x[N]` | N (UTF-8, NUL-padded) |
|
|
48
|
+
| `bytes_t[N]` | `uint8_t x[N]` | N |
|
|
49
|
+
| `array_t[T, N]` | `T x[N]` | N * size(T) |
|
|
50
|
+
| `enum_t[Color, uint8_t]` | `uint8_t x` (+ `enum Color {..}` in header) | storage size |
|
|
51
|
+
| `bitfield[uint8_t, 3]` | `uint8_t x : 3` | shares a storage unit |
|
|
52
|
+
| another `@firstclass` class | `struct Name` | its size |
|
|
53
|
+
|
|
54
|
+
Values are checked: out of range ints, over-long strings and wrong lengths raise
|
|
55
|
+
`ValueError`; wrong Python types raise `TypeError`. Nothing is truncated or coerced silently.
|
|
56
|
+
|
|
57
|
+
**Enums.** Int-valued `Enum`/`IntEnum`/`IntFlag` classes. `enum_t[Color, uint8_t]` picks the width;
|
|
58
|
+
a bare `Color` annotation is 32-bit like a C `int` (unsigned unless a value is negative). Values
|
|
59
|
+
are stored, and written to JSON/XML, as ints; reading an unknown value raises `ValueError`.
|
|
60
|
+
`c_header()` emits `enum Color { Color_RED = 1, ... };`.
|
|
61
|
+
|
|
62
|
+
**Bitfields.** `bitfield[uint8_t, 3]` is 3 bits of a `uint8_t` unit, like `uint8_t x : 3`.
|
|
63
|
+
Neighbouring bitfields with the same storage type share a unit while they fit; otherwise a new
|
|
64
|
+
unit starts. Little-endian fills from the lowest bit, big-endian from the highest (what ctypes
|
|
65
|
+
does). Signed storage (`int8_t`) gives signed fields. Mixing storage types in one run is not
|
|
66
|
+
portable across C compilers; keep a run on one type. No arrays of bitfields.
|
|
67
|
+
|
|
68
|
+
Raw `ctypes` types work as annotations too (packed, same layout as a `_pack_ = 1` Structure):
|
|
69
|
+
`ctypes.c_int`, `c_uint64`, `c_size_t`, `c_bool`, `c_float`, `c_double`, `c_char`, `c_uint16 * 4`,
|
|
70
|
+
`c_char * 8`. Pointers (`c_void_p`, `POINTER(T)`, `c_char_p`) are stored as the address (an `int`,
|
|
71
|
+
0 = NULL) at pointer size; `to_ctypes()` gives a real pointer field you can dereference. The
|
|
72
|
+
address only means something inside the same process. Sizes follow the platform
|
|
73
|
+
(`c_long` is 4 bytes on Windows, 8 on Linux).
|
|
74
|
+
|
|
75
|
+
## Methods added to every class
|
|
76
|
+
|
|
77
|
+
| Method | |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `obj.to_bytes()` / `Cls.from_bytes(data)` | binary; `from_bytes` needs exactly `sizeof()` bytes |
|
|
80
|
+
| `Cls.unpack_from(buffer, offset=0)` | like `struct.unpack_from`: read one object at `offset`, trailing bytes are fine; any buffer type |
|
|
81
|
+
| `Cls.sizeof()` | size in bytes |
|
|
82
|
+
| `obj.to_dict()` / `Cls.from_dict(d)` | JSON-compatible dict (bytes as hex) |
|
|
83
|
+
| `obj.to_json(**kw)` / `Cls.from_json(s)` | `kw` goes to `json.dumps` |
|
|
84
|
+
| `obj.to_xml(indent=None)` / `Cls.from_xml(s)` | `<Packet><kind>1</kind>...</Packet>`, arrays as `<item>` |
|
|
85
|
+
| `obj.to_ctypes()` / `Cls.from_ctypes(c)` / `Cls.ctypes_type()` | `ctypes.Structure` with matching layout |
|
|
86
|
+
| `Cls.c_header()` | C source for the struct(s), nested ones first |
|
|
87
|
+
|
|
88
|
+
## Import C/C++ headers
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from firstclass import from_header # also: firstclass.from_header
|
|
92
|
+
|
|
93
|
+
t = from_header("protocol.h") # or from_header(text="struct P { ... };")
|
|
94
|
+
pkt = t.Packet(1, True, ...) # structs -> @firstclass classes
|
|
95
|
+
t.Color.RED # enums / enum class -> IntEnum
|
|
96
|
+
t.constants["MAX_LEN"] # integer #defines, consts, enum members
|
|
97
|
+
t.skipped # {"name": "reason"} for anything not convertible
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Understood: `struct`/`class`/`typedef struct`, nested and anonymous-member structs, `enum` and
|
|
101
|
+
`enum class : type` (explicit values, implicit increments, constant expressions), bitfields, arrays
|
|
102
|
+
(multi-dimensional, sizes from `#define`/enum/`const`/`sizeof`), `char[N]` -> `char_array`,
|
|
103
|
+
`uint8_t[N]` -> `bytes_t`, pointers and function pointers (stored as addresses), `typedef`/`using`,
|
|
104
|
+
`#define`/`#if`/`#ifdef`/`#elif` (preset macros with `defines={...}`), `namespace`, `extern "C"`,
|
|
105
|
+
`#pragma pack(push/pop/N)` and `__attribute__((packed))`. Layout follows C: natural alignment
|
|
106
|
+
padding unless the struct is packed; force it with `align=True/False`. Options: `byteorder`,
|
|
107
|
+
`long_size` (size of C `long`; default is this machine's).
|
|
108
|
+
|
|
109
|
+
Skipped with a reason instead of failing: unions, flexible array members, unnamed padding
|
|
110
|
+
bitfields, inheritance, virtual members, unknown types (`#include`d headers are not read), `pack(2)`
|
|
111
|
+
/`pack(4)`, `aligned(N)`, `long double`, empty structs, anything depending on a skipped struct.
|
|
112
|
+
Plain C enums are 32-bit like C unless `: type` is given. C field names that are Python keywords
|
|
113
|
+
(`class`, `from`, ...) get a trailing underscore (`class_`).
|
|
114
|
+
|
|
115
|
+
## Layout options
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
@firstclass(byteorder="big") # default "little"
|
|
119
|
+
@firstclass(align=True) # default False = packed, no padding (like __attribute__((packed)))
|
|
120
|
+
@firstclass(frozen=True, slots=True) # any dataclass option
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`align=True` inserts natural-alignment padding like a normal C struct (assumes the x86-64 style
|
|
124
|
+
rule: alignment equals size). Padding bytes are written as zero and ignored on read.
|
|
125
|
+
C headers describe the struct only; C compilers use native byte order, so `byteorder` is noted
|
|
126
|
+
in a comment.
|
|
127
|
+
|
|
128
|
+
## Notes
|
|
129
|
+
|
|
130
|
+
- Fields with plain types (`str`, `int`) work for JSON/XML only; `to_bytes()` raises
|
|
131
|
+
`TypeError` naming the field.
|
|
132
|
+
- Use `from __future__ import annotations` freely; annotations are resolved lazily. Classes
|
|
133
|
+
defined inside functions that refer to local names are not resolvable.
|
|
134
|
+
- Don't parse untrusted XML with `from_xml` without extra care (stdlib `xml.etree`).
|
|
135
|
+
- Field names that collide with the generated methods (`to_bytes`, ...) raise `TypeError`.
|
|
136
|
+
|
|
137
|
+
## Example
|
|
138
|
+
|
|
139
|
+
`python examples/example.py` defines a class in Python, then imports `examples/protocol.h` (enums, `enum class`,
|
|
140
|
+
bitfields, `#pragma pack` structs) and uses the generated classes.
|
|
141
|
+
|
|
142
|
+
## Develop
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
python -m unittest discover -s tests
|
|
146
|
+
```
|
|
@@ -1,97 +1,139 @@
|
|
|
1
|
-
# firstclass
|
|
2
|
-
|
|
3
|
-
`@firstclass` works like `@dataclass` (same keyword args) and adds conversion to and from
|
|
4
|
-
packed bytes, `ctypes`, a C struct header, JSON and XML. Pure Python 3.10+, no dependencies.
|
|
5
|
-
|
|
6
|
-
```python
|
|
7
|
-
from firstclass import firstclass, uint8_t, uint16_t, int32_t, float32_t, bool_t, char_array, bytes_t, array_t
|
|
8
|
-
|
|
9
|
-
@firstclass
|
|
10
|
-
class Point:
|
|
11
|
-
x: int32_t
|
|
12
|
-
y: int32_t
|
|
13
|
-
|
|
14
|
-
@firstclass
|
|
15
|
-
class Packet:
|
|
16
|
-
kind: uint8_t
|
|
17
|
-
ok: bool_t
|
|
18
|
-
temp: float32_t
|
|
19
|
-
name: char_array[8] # NUL-padded UTF-8
|
|
20
|
-
mac: bytes_t[6] # exactly 6 bytes
|
|
21
|
-
samples: array_t[uint16_t, 4]
|
|
22
|
-
origin: Point # nested
|
|
23
|
-
|
|
24
|
-
p = Packet(1, True, 21.5, "sensor", b"\x00\x11\x22\x33\x44\x55", [1, 2, 3, 4], Point(10, -20))
|
|
25
|
-
|
|
26
|
-
raw = p.to_bytes() # packed, little-endian
|
|
27
|
-
assert Packet.from_bytes(raw) == p
|
|
28
|
-
Packet.sizeof() # 36
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Types
|
|
32
|
-
|
|
33
|
-
| Marker | C | Bytes |
|
|
34
|
-
|---|---|---|
|
|
35
|
-
| `uint8_t` `uint16_t` `uint32_t` `uint64_t` | `uintN_t` | 1/2/4/8 |
|
|
36
|
-
| `int8_t` `int16_t` `int32_t` `int64_t` | `intN_t` | 1/2/4/8 |
|
|
37
|
-
| `float32_t` / `float64_t` | `float` / `double` | 4 / 8 |
|
|
38
|
-
| `bool_t` | `bool` | 1 |
|
|
39
|
-
| `char_t` | `char` | 1 |
|
|
40
|
-
| `char_array[N]` | `char x[N]` | N (UTF-8, NUL-padded) |
|
|
41
|
-
| `bytes_t[N]` | `uint8_t x[N]` | N |
|
|
42
|
-
| `array_t[T, N]` | `T x[N]` | N * size(T) |
|
|
43
|
-
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
(`
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
# firstclass
|
|
2
|
+
|
|
3
|
+
`@firstclass` works like `@dataclass` (same keyword args) and adds conversion to and from
|
|
4
|
+
packed bytes, `ctypes`, a C struct header, JSON and XML. Pure Python 3.10+, no dependencies.
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from firstclass import firstclass, uint8_t, uint16_t, int32_t, float32_t, bool_t, char_array, bytes_t, array_t
|
|
8
|
+
|
|
9
|
+
@firstclass
|
|
10
|
+
class Point:
|
|
11
|
+
x: int32_t
|
|
12
|
+
y: int32_t
|
|
13
|
+
|
|
14
|
+
@firstclass
|
|
15
|
+
class Packet:
|
|
16
|
+
kind: uint8_t
|
|
17
|
+
ok: bool_t
|
|
18
|
+
temp: float32_t
|
|
19
|
+
name: char_array[8] # NUL-padded UTF-8
|
|
20
|
+
mac: bytes_t[6] # exactly 6 bytes
|
|
21
|
+
samples: array_t[uint16_t, 4]
|
|
22
|
+
origin: Point # nested
|
|
23
|
+
|
|
24
|
+
p = Packet(1, True, 21.5, "sensor", b"\x00\x11\x22\x33\x44\x55", [1, 2, 3, 4], Point(10, -20))
|
|
25
|
+
|
|
26
|
+
raw = p.to_bytes() # packed, little-endian
|
|
27
|
+
assert Packet.from_bytes(raw) == p
|
|
28
|
+
Packet.sizeof() # 36
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Types
|
|
32
|
+
|
|
33
|
+
| Marker | C | Bytes |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `uint8_t` `uint16_t` `uint32_t` `uint64_t` | `uintN_t` | 1/2/4/8 |
|
|
36
|
+
| `int8_t` `int16_t` `int32_t` `int64_t` | `intN_t` | 1/2/4/8 |
|
|
37
|
+
| `float32_t` / `float64_t` | `float` / `double` | 4 / 8 |
|
|
38
|
+
| `bool_t` | `bool` | 1 |
|
|
39
|
+
| `char_t` | `char` | 1 |
|
|
40
|
+
| `char_array[N]` | `char x[N]` | N (UTF-8, NUL-padded) |
|
|
41
|
+
| `bytes_t[N]` | `uint8_t x[N]` | N |
|
|
42
|
+
| `array_t[T, N]` | `T x[N]` | N * size(T) |
|
|
43
|
+
| `enum_t[Color, uint8_t]` | `uint8_t x` (+ `enum Color {..}` in header) | storage size |
|
|
44
|
+
| `bitfield[uint8_t, 3]` | `uint8_t x : 3` | shares a storage unit |
|
|
45
|
+
| another `@firstclass` class | `struct Name` | its size |
|
|
46
|
+
|
|
47
|
+
Values are checked: out of range ints, over-long strings and wrong lengths raise
|
|
48
|
+
`ValueError`; wrong Python types raise `TypeError`. Nothing is truncated or coerced silently.
|
|
49
|
+
|
|
50
|
+
**Enums.** Int-valued `Enum`/`IntEnum`/`IntFlag` classes. `enum_t[Color, uint8_t]` picks the width;
|
|
51
|
+
a bare `Color` annotation is 32-bit like a C `int` (unsigned unless a value is negative). Values
|
|
52
|
+
are stored, and written to JSON/XML, as ints; reading an unknown value raises `ValueError`.
|
|
53
|
+
`c_header()` emits `enum Color { Color_RED = 1, ... };`.
|
|
54
|
+
|
|
55
|
+
**Bitfields.** `bitfield[uint8_t, 3]` is 3 bits of a `uint8_t` unit, like `uint8_t x : 3`.
|
|
56
|
+
Neighbouring bitfields with the same storage type share a unit while they fit; otherwise a new
|
|
57
|
+
unit starts. Little-endian fills from the lowest bit, big-endian from the highest (what ctypes
|
|
58
|
+
does). Signed storage (`int8_t`) gives signed fields. Mixing storage types in one run is not
|
|
59
|
+
portable across C compilers; keep a run on one type. No arrays of bitfields.
|
|
60
|
+
|
|
61
|
+
Raw `ctypes` types work as annotations too (packed, same layout as a `_pack_ = 1` Structure):
|
|
62
|
+
`ctypes.c_int`, `c_uint64`, `c_size_t`, `c_bool`, `c_float`, `c_double`, `c_char`, `c_uint16 * 4`,
|
|
63
|
+
`c_char * 8`. Pointers (`c_void_p`, `POINTER(T)`, `c_char_p`) are stored as the address (an `int`,
|
|
64
|
+
0 = NULL) at pointer size; `to_ctypes()` gives a real pointer field you can dereference. The
|
|
65
|
+
address only means something inside the same process. Sizes follow the platform
|
|
66
|
+
(`c_long` is 4 bytes on Windows, 8 on Linux).
|
|
67
|
+
|
|
68
|
+
## Methods added to every class
|
|
69
|
+
|
|
70
|
+
| Method | |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `obj.to_bytes()` / `Cls.from_bytes(data)` | binary; `from_bytes` needs exactly `sizeof()` bytes |
|
|
73
|
+
| `Cls.unpack_from(buffer, offset=0)` | like `struct.unpack_from`: read one object at `offset`, trailing bytes are fine; any buffer type |
|
|
74
|
+
| `Cls.sizeof()` | size in bytes |
|
|
75
|
+
| `obj.to_dict()` / `Cls.from_dict(d)` | JSON-compatible dict (bytes as hex) |
|
|
76
|
+
| `obj.to_json(**kw)` / `Cls.from_json(s)` | `kw` goes to `json.dumps` |
|
|
77
|
+
| `obj.to_xml(indent=None)` / `Cls.from_xml(s)` | `<Packet><kind>1</kind>...</Packet>`, arrays as `<item>` |
|
|
78
|
+
| `obj.to_ctypes()` / `Cls.from_ctypes(c)` / `Cls.ctypes_type()` | `ctypes.Structure` with matching layout |
|
|
79
|
+
| `Cls.c_header()` | C source for the struct(s), nested ones first |
|
|
80
|
+
|
|
81
|
+
## Import C/C++ headers
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from firstclass import from_header # also: firstclass.from_header
|
|
85
|
+
|
|
86
|
+
t = from_header("protocol.h") # or from_header(text="struct P { ... };")
|
|
87
|
+
pkt = t.Packet(1, True, ...) # structs -> @firstclass classes
|
|
88
|
+
t.Color.RED # enums / enum class -> IntEnum
|
|
89
|
+
t.constants["MAX_LEN"] # integer #defines, consts, enum members
|
|
90
|
+
t.skipped # {"name": "reason"} for anything not convertible
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Understood: `struct`/`class`/`typedef struct`, nested and anonymous-member structs, `enum` and
|
|
94
|
+
`enum class : type` (explicit values, implicit increments, constant expressions), bitfields, arrays
|
|
95
|
+
(multi-dimensional, sizes from `#define`/enum/`const`/`sizeof`), `char[N]` -> `char_array`,
|
|
96
|
+
`uint8_t[N]` -> `bytes_t`, pointers and function pointers (stored as addresses), `typedef`/`using`,
|
|
97
|
+
`#define`/`#if`/`#ifdef`/`#elif` (preset macros with `defines={...}`), `namespace`, `extern "C"`,
|
|
98
|
+
`#pragma pack(push/pop/N)` and `__attribute__((packed))`. Layout follows C: natural alignment
|
|
99
|
+
padding unless the struct is packed; force it with `align=True/False`. Options: `byteorder`,
|
|
100
|
+
`long_size` (size of C `long`; default is this machine's).
|
|
101
|
+
|
|
102
|
+
Skipped with a reason instead of failing: unions, flexible array members, unnamed padding
|
|
103
|
+
bitfields, inheritance, virtual members, unknown types (`#include`d headers are not read), `pack(2)`
|
|
104
|
+
/`pack(4)`, `aligned(N)`, `long double`, empty structs, anything depending on a skipped struct.
|
|
105
|
+
Plain C enums are 32-bit like C unless `: type` is given. C field names that are Python keywords
|
|
106
|
+
(`class`, `from`, ...) get a trailing underscore (`class_`).
|
|
107
|
+
|
|
108
|
+
## Layout options
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
@firstclass(byteorder="big") # default "little"
|
|
112
|
+
@firstclass(align=True) # default False = packed, no padding (like __attribute__((packed)))
|
|
113
|
+
@firstclass(frozen=True, slots=True) # any dataclass option
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`align=True` inserts natural-alignment padding like a normal C struct (assumes the x86-64 style
|
|
117
|
+
rule: alignment equals size). Padding bytes are written as zero and ignored on read.
|
|
118
|
+
C headers describe the struct only; C compilers use native byte order, so `byteorder` is noted
|
|
119
|
+
in a comment.
|
|
120
|
+
|
|
121
|
+
## Notes
|
|
122
|
+
|
|
123
|
+
- Fields with plain types (`str`, `int`) work for JSON/XML only; `to_bytes()` raises
|
|
124
|
+
`TypeError` naming the field.
|
|
125
|
+
- Use `from __future__ import annotations` freely; annotations are resolved lazily. Classes
|
|
126
|
+
defined inside functions that refer to local names are not resolvable.
|
|
127
|
+
- Don't parse untrusted XML with `from_xml` without extra care (stdlib `xml.etree`).
|
|
128
|
+
- Field names that collide with the generated methods (`to_bytes`, ...) raise `TypeError`.
|
|
129
|
+
|
|
130
|
+
## Example
|
|
131
|
+
|
|
132
|
+
`python examples/example.py` defines a class in Python, then imports `examples/protocol.h` (enums, `enum class`,
|
|
133
|
+
bitfields, `#pragma pack` structs) and uses the generated classes.
|
|
134
|
+
|
|
135
|
+
## Develop
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
python -m unittest discover -s tests
|
|
139
|
+
```
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from ._core import firstclass
|
|
2
|
+
from ._header import from_header
|
|
2
3
|
from ._specs import (
|
|
3
4
|
array_t,
|
|
5
|
+
bitfield,
|
|
4
6
|
bool_t,
|
|
5
7
|
bytes_t,
|
|
6
8
|
char_array,
|
|
7
9
|
char_t,
|
|
10
|
+
enum_t,
|
|
8
11
|
float32_t,
|
|
9
12
|
float64_t,
|
|
10
13
|
int8_t,
|
|
@@ -18,9 +21,11 @@ from ._specs import (
|
|
|
18
21
|
)
|
|
19
22
|
|
|
20
23
|
__all__ = [
|
|
21
|
-
"firstclass",
|
|
24
|
+
"firstclass", "from_header",
|
|
22
25
|
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
|
|
23
26
|
"int8_t", "int16_t", "int32_t", "int64_t",
|
|
24
27
|
"float32_t", "float64_t", "bool_t", "char_t",
|
|
25
|
-
"char_array", "bytes_t", "array_t",
|
|
28
|
+
"char_array", "bytes_t", "array_t", "bitfield", "enum_t",
|
|
26
29
|
]
|
|
30
|
+
|
|
31
|
+
firstclass.from_header = from_header # also reachable as the decorator's attribute
|