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();
}