io::VariantQueue
Description
The idea of a “variant queue” here is to encode multiple frame types with attached
dynamically-sized payloads and transmit them via io::MemoryQueue. This allows
more flexibility and better memory utilization than typed spsc::Queue
+ fixed-size frames.
VariantQueue
only implements serialization/deserialization, not a different queue type.
Underneath, io::MemoryQueue is used.
Encoding looks like this:
uint8 |
N bytes |
M bytes |
type id |
frame/header |
payload |
Note
Structs used as headers need to be POD. Unless unaligned read/write is acceptable on the
target platform, they should also have 1-byte alignment requirement. Helper structs:
estd::big_endian<T>
, estd::little_endian<T>
can be used for serializing integers.
Other types can be wrapped with estd::unaligned<T>
(defined in estd/memory.h
).
Example
This is how you declare a VariantQueue
which can encode types A
and B
:
struct A
{
uint8_t some_bytes[10];
};
struct __attribute__((packed)) B
{
::estd::be_uint16_t x;
::estd::be_uint32_t y;
};
constexpr size_t MAX_B_PAYLOAD_SIZE = 20;
using MyVariantQTypeList = ::io::make_variant_queue<
::io::VariantQueueType<A>, // struct A doesn't need payload
::io::VariantQueueType<B, MAX_B_PAYLOAD_SIZE>>;
using MyTypes = MyVariantQTypeList::type_list;
static constexpr size_t TOTAL_QUEUE_CAPACITY = 512;
using MyQueue = ::io::VariantQueue<MyVariantQTypeList, TOTAL_QUEUE_CAPACITY>;
To write to the queue, you have several options (with or without payload, passing the payload
directly or filling it in when memory is already allocated from the queue) - see different overloads
of the ::io::variant_q::write()
function.
When the queue is full and an element cannot be written, write()
will return false.
MyQueue queue;
MyQueue::Writer writer(queue);
// write struct A without payload:
A const a = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
::io::variant_q<MyTypes>::write(writer, a);
// write struct B with payload:
B const b{::estd::be_uint16_t::make(42), ::estd::be_uint32_t::make(123456)};
uint8_t const payload[] = {0xAA, 0xBB, 0xCC};
::io::variant_q<MyTypes>::write(writer, b, payload);
// write struct B with payload, but fill it with content in memory allocated from the queue
// (useful when the payload is too big to make a full copy)
size_t const big_payload_size = 80U;
::io::variant_q<MyTypes>::write(
writer,
b,
big_payload_size,
[](::estd::slice<uint8_t> const& buffer) { std::fill(buffer.begin(), buffer.end(), 42); });
To read from a queue and dispatch elements of different types you need to implement a visitor, similar to when you use estd::variant. You can choose to read header structs only:
struct Visit
{
void operator()(A const& a) { printf("received A"); }
void operator()(B const& b) { printf("received B"); }
};
MyQueue queue;
MyQueue::Reader reader(queue);
Visit v;
while (!reader.empty())
{
::io::variant_q<MyTypes>::read(v, reader.peek());
reader.release();
}
Or read them with payload:
struct VisitWithPayload
{
void operator()(A const& a, ::estd::slice<uint8_t const> payload) { printf("received A"); }
void operator()(B const& b, ::estd::slice<uint8_t const> payload) { printf("received B"); }
};
MyQueue queue;
MyQueue::Reader reader(queue);
VisitWithPayload vp;
while (!reader.empty())
{
::io::variant_q<MyTypes>::read_with_payload(vp, reader.peek());
reader.release();
}