io::IWriter

The interface IWriter is an abstraction to write chunks of bytes with variable size to a communication channel.

Public API

/**
 * Returns the maximum number of bytes that can be allocated in one allocation
 */
virtual size_t maxSize() const = 0;

/**
 * Allocates a slice of a given number of bytes.
 * \return - Slice of size bytes if underlying queue was not full and size <= maxSize()
 *         - Empty slice otherwise
 *
 * Calling allocate multiple times before calling commit will re-allocate, i.e. a new slice of
 * bytes will be returned and the previous allocation is lost and will not be automatically
 * committed. This can also be used to trim the allocated slice to the right size before
 * committing, thus calling allocate will NOT modify the memory (e.g. zero it).
 */
virtual ::estd::slice<uint8_t> allocate(size_t size) = 0;

/**
 * Makes the previously allocated slice available for the reader.
 *
 * Calling commit() makes a new allocation mandatory! The previously allocated slice becomes
 * invalid and must not be used anymore. Calling commit() without a previous successful
 * allocation has no effect.
 */
virtual void commit() = 0;

/**
 * Flushes any buffered data which has not yet been sent.
 *
 * flush() is part of the interface to make subclassing, that implements buffering
 * to e.g. improve bandwidth usage possible. It might have an empty implementation.
 */
virtual void flush() = 0;

Usage of API

The interface IWriter provides a two step API. The user first needs to allocate a slice of bytes. After a successful allocation, the slice can be filled with the data to be transferred to the reader side of the stream. Calling commit makes the data available to the io::IReader connected to the same channel. It also makes the allocated data invalid - it must not be modified after committing!

actor SW
SW -> IWriter : auto d = allocate(size)
alt d.size() > 0 case
    SW -> SW  : fill d
    SW -> IWriter : commit()
end

Example

The following example shows, how the allocated memory can be used to directly emplace the data which shall be transferred into it.


/**
 * Returns the number of bytes available in the CAN hardware buffer, zero if no frame is available.
 * The maximum rx size is 8 bytes.
 */
size_t getCanRxSize();

/**
 * Reads an available CAN frame into a provided object of type CanFrame.
 * \return true if frame was successfully read from hardware, false otherwise.
 */
bool readCanFrame(CanFrame& frame);

/**
 * Tries to read a CAN frame from the hardware and forwards it to the channel represented by a given
 * IWriter.
 * \param writer IWriter to use for sending the frame.
 * \return true if frame has successfully been forwarded, false otherwise.
 */
bool forwardCanFrame(::io::IWriter& writer)
{
    // Check if a frame is available.
    auto const size = getCanRxSize();
    if (size == 0)
    {
        // No frame available.
        return false;
    }
    // Allocate data from channel. We need room for a uint32_t id + data.
    auto data = writer.allocate(sizeof(::estd::be_uint32_t) + size);
    if (data.size() == 0)
    {
        // Couldn't allocate data.
        return false;
    }
    // The big endian 32bit id comes first in the serialization.
    auto& id = ::estd::memory::take<estd::be_uint32_t>(data);
    // We pass the slice data to the frame so that readCanFrame can read the data directly from
    // the hardware to the allocated memory.
    CanFrame frame;
    frame.data = data;

    if (!readCanFrame(frame))
    {
        // Reading the frame from hardware failed.
        return false;
    }
    // We need to update the previously allocated id.
    id = frame.id;
    // Commit data to channel.
    writer.commit();
    return true;
}

Multiple Producers

The two step nature of the interface makes it by default only be usable for a single producer. Multiple producers using the same writer in parallel lead to race conditions because calling allocate() would return the same memory to multiple users. A wrapper implementation which turns the two step API into a single call API makes multiple producers possible, however with slightly different semantics because data you want to write needs to be present before the call and cannot be put into the allocated memory on the fly.

Example

The following example shows, how with the help of a locking mechanism, the IWriter interface can be used to work with multiple producers.

bool forwardCanFrame(::io::IWriter& writer)
{
    // Check if a frame is available.
    auto const size = getCanRxSize();
    if (size == 0)
    {
        // No frame available.
        return false;
    }
    // Allocate temporary CanFrame on the stack.
    uint8_t rxData[8];
    CanFrame rxFrame;
    rxFrame.data = rxData;

    // Reading takes too long to surround with lock.
    if (!readCanFrame(rxFrame))
    {
        // Reading the frame from hardware failed.
        return false;
    }
    // RAII lock to block concurrent access to writer.
    Lock lock;
    (void)lock;
    // Allocate data from channel. We need room for a uint32_t id + data.
    auto data = writer.allocate(sizeof(::estd::be_uint32_t) + size);
    if (data.size() == 0)
    {
        // Couldn't allocate data.
        return false;
    }
    // The big endian 32bit id comes first in the serialization.
    ::estd::memory::take<estd::be_uint32_t>(data) = rxFrame.id;
    // Copy payload to allocated data.
    ::estd::memory::copy(data, rxFrame.data);
    // Commit data to channel.
    writer.commit();
    return true;
}