emb
Declarative library for for describing embedded C data types in python
Motivation
emb is a wrapper above the ctypes library providing a more simple and declarative syntax,
what I find more easy to use.
The motivation behind creating this library is to be able to write down a c structure/union very similarly as its written down in a c file, and then to be able to get the memory contents of such a data structure, that can be transferred to an embedded device with a direct memory access method (like XCP) and in the other way as well, and also to be able to generate the actual c code that describes the same data structure, from the python description.
I have used several similar libraries, such as construct,
structures, or even Protocol Buffers, however one was too slow,
the other could not generate code, the third is waaay too big, and actually what I needed was very
very limited, and that's exactly the scope of this library. Also, it was fun to write, and beware,
I use exec under the hood as well! Yes, I know.
Examples
Declaring structures / unions
structures and unions can be declared in a similar way as with dataclasses:
from emb import emb_struct, emb_union
from ctypes import *
@emb_struct
class Inner:
a: c_uint8
b: c_uint8
@emb_struct
class Outer:
first: Inner
second: c_uint8
third: c_uint8
@emb_union
class MyUnion:
as_struct: Outer
as_int: c_uint32
Instantiating
The instances of the above declared classes can be created as shown below.
# empty constructor
inner = Inner()
print(inner)
>>> Inner(a:u8=0x0, b:u8=0x0)
# constructor with default values
inner = Inner(a=1, b=2)
print(inner)
>>> Inner(a:u8=0x1, b:u8=0x2)
# value checking
inner = Inner(a=256, b=300)
>>> ValueError: 256 cannot be set for c_ubyte (error('ubyte format requires 0 <= number <= 255'))!
# embedded structures
outer = Outer()
>>> Outer(first=Inner(a:u8=0x0, b:u8=0x0), second:u8=0x0, third:u8=0x0)
outer = Outer(Inner(42, 43), 1, 2)
>>> Outer(first=Inner(a:u8=0x2A, b:u8=0x2B), second:u8=0x1, third:u8=0x2)
# creating a union instance
my_union = MyUnion()
>>> MyUnion(as_struct=Outer(first=Inner(a:u8=0x0, b:u8=0x0), second:u8=0x0, third:u8=0x0), as_int:u32=0x0)
# with defaults
my_union = MyUnion(as_struct=Outer(Inner(1,2), 3,4))
>>> MyUnion(as_struct=Outer(first=Inner(a:u8=0x1, b:u8=0x2), second:u8=0x3, third:u8=0x4), as_int:u32=0x4030201)
Setting field/member values
Value setting is protected for fields:
outer.second = 0x1234
>>> ValueError: 4660 cannot be set for c_ubyte (error('ubyte format requires 0 <= number <= 255'))!
Parsing from binary data
outer = Outer(first=Inner(a=1, b=2), second=3)
print(outer)
>>> Outer(first=Inner(a:u8=0x1, b:u8=0x2), second:u8=0x3)
outer.parse(b'\x11\x22\x33')
print(outer)
>>> Outer(first=Inner(a:u8=0x11, b:u8=0x22), second:u8=0x33)
Packing of structures
The structures are by default packed to 4 bytes. This means, that empty fill bytes are added between struct members to align them to the 4 byte boundaries. This packing can be modified by the user.
@emb_struct
class Inner:
a: c_uint8
@emb_struct
class Outer:
first: Inner
second: c_uint32
outer = Outer(first=Inner(1), second = 0xFFEEDDCC)
outer.stream()
>>> b'\x01\x00\x00\x00\xcc\xdd\xee\xff'
If we define the outer structure as below, the fill bytes will disappear
@emb_struct(pack=1)
class Outer:
first: Inner
second: c_uint32
outer = Outer(first=Inner(1), second = 0xFFEEDDCC)
outer.stream()
>>> b'\x01\xcc\xdd\xee\xff'
Note: this is true for the parse() method as well!
Endianness
The default endianness / byteorder is the one of the system's. (sys.byteorder).
However, it can be adjusted in the decorators.
@emb_struct(endian="little")
class Little:
a: c_uint16
little = Little(a=0xFF00)
little.stream()
>>> b'\x00\xff'
@emb_struct(endian="big")
class Big:
a: c_uint16
big = Big(a=0xFF00)
big.stream()
>>> b'\xff\x00'
However, for now, unions only support native byteorder, as the ctypes module does not implement the appropriate BigEndianUnion and LittleEndianUnion types. See details:
https://stackoverflow.com/questions/49524952/bigendianunion-is-not-part-of-pythons-ctypes https://bugs.python.org/issue33178
Bitfields
Bitfields can be defined with the following syntax:
@emb_struct
class S:
a: (c_uint8, 2)
b: (c_uint8, 6)
s = S()
len(s)
>>> 1
print(s)
>>> S(a:u8@2=0x0, b:u8@6=0x0)
s.parse(b'\xAA')
print(s)
>>> S(a:u8@2=0x2, b:u8@6=0x2A)
The parsing and streaming works for them just like for normal structures.
Bitfield definition order
Note, that just as in c, the definition order of the bitfields in one byte depends on the byteorder of the containing structure.
This means, that for a little-endian structure, the bitfields inside a byte shall be defined from top-down as LSB to MSB, however, for big-endian structures, the top-down order means MSB to LSB. See the example below, these two bitfield structures describe the same thing, note the change in the order of the bitfields!
@emb_struct(endian="little")
class BF_LE:
# byte 0
a: (c_uint8, 3) # LSB
b: (c_uint8, 5) # MSB
# byte 1
c: c_uint8
@emb_struct(endian="big")
class BF_BE:
# byte 0
b: (c_uint8, 5) # MSB
a: (c_uint8, 3) # LSB
# byte 1
c: c_uint8
Generating c code
The ANSI c representation of a structure/union can be created from the class itself
or from its instance. The ccode() static method returns a list of lines.
print('\n'.join(Outer.ccode()))
# or
print('\n'.join(outer.ccode()))
# or
outer.print_ccode()
typedef struct _tag_Inner {
unsigned char a;
unsigned char b;
} Inner;
typedef struct _tag_Outer {
Inner first;
unsigned char second;
} Outer;
Generating c code for bitfields
BF_LE.print_ccode()
typedef struct _tag_BF_LE {
unsigned char a : 3;
unsigned char b : 5;
unsigned char c;
} BF_LE;
Metadata
Release files for emb 0.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| emb-0.0.2.tar.gz | 13.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| emb-0.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 24.3 kB
Release files / emb-0.0.2.tar.gz
| Download URL | emb-0.0.2.tar.gz |
|---|---|
| Size | 13.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b61e8f5925c4206d782d4412acf8ad566b4945d51612732068fb75a3db9b13be
|
|
BLAKE2b-256 checksum How to use checksums |
77f45e7cde274b4589198d32d5322b94da326324130421d573680ffd3b2a45fc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/3.7.1 importlib_metadata/4.8.1 pkginfo/1.8.2 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.9
|
Release files / emb-0.0.2-py3-none-any.whl
| Download URL | emb-0.0.2-py3-none-any.whl |
|---|---|
| Size | 11.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b665f1112d06863758c7c4e6c35a00d0a61d85323189c07c4993ae8ddea5b15d
|
|
BLAKE2b-256 checksum How to use checksums |
750dbe68a9438216f9469d6af64e43ca36f6b804a512829604685f7011cd5511
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/3.7.1 importlib_metadata/4.8.1 pkginfo/1.8.2 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.9
|
Security for emb 0.0.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.