C

Implementing custom queues

This topic explains how to implement a custom queue for Qt Quick Ultralite.

Overview

Queues are an important part of Qt Quick Ultralite when handling events. One of the most prominent examples of this is the usage of queues in input handling. By using EventQueue, events can be propagated to Qt Quick Ultralite so that they are handled at an appropriate time during Qt Quick Ultralite's execution cycle.

The default queue implementation for EventQueue is DoubleQueue, which is found in platform\common\baremetal\doublequeue.cpp. It is interrupt-safe as long as there is only a single writer. In case there are multiple writers, EventQueue::postEvent() must not be interrupted by another EventQueue::postEvent(). Moreover, thread-safety with DoubleQueue cannot be guaranteed, which also limits its usage.

Most RTOSes provide their own queues which may provide additional benefits, such as interrupt-safety and thread-safety. They also enable proper inter-task communication needed in applications using multiple tasks. Using RTOS queues enable EventQueue to be used in cases where DoubleQueue's limitations would normally prevent the usage.

On embedded platforms that have support for std::thread and std::mutex, the default queue is thread-safe by using mutexes.

This topic covers how to adapt MessageQueueInterface API to use queues provided by RTOSes for example. EventQueue's platform implementation enables the usage of custom queues in Qt Quick Ultralite.

Adapting custom queues

The queue abstraction used to implement custom queues is called MessageQueueInterface API. It can accessed by including the platform\messagequeue.h header. The API provides abstract functions and functions that come with default implementation. These functions must be reimplemented. See the MessageQueueInterface class documentation, for more information.

Note: The code snippets shown here are from an example implementation, which can be found under your Qt Quick Ultralite install directory (platform\boards\qt\example-baremetal\examplequeue.cpp). For demonstration purposes it uses a simple circular buffer as queue backend.

The following functions from the MessageQueueInterface API must be implemented to get a working implementation:

  • Constructor - The constructor of the implementation must take at least an integer representing the maximum number of items the queue can hold. It must also call MessageQueueInterface::MessageQueueInterface().

    The following is an example constructor of the implementation:

    MyMessageQueue(const uint32_t &capacity, const uint32_t &messageSize)
        : MessageQueueInterface()
        , mQueue(NULL)
        , mOverrunFlag(false)
    {
        void *memory = qul_malloc(sizeof(Private::CircularBuffer));
        mQueue = new (memory) Private::CircularBuffer(capacity, messageSize);
    }
  • MessageQueueInterface::discardSupported() and MessageQueueInterface::overwriteSupported() - These functions indicate whether the queue implementation supports discarding, overwriting, or both. They must return either true or false.

    Note: One of these functions must return true for the EventQueue to work.

  • MessageQueueInterface::enqueueOrDiscard() - This function pushes the given message to the end of the queue. If the queue is full, the message must be discarded and the overrun state be set. It must return MessageQueueStatus::Success if the message was pushed to the queue, or return MessageQueueStatus::MessageDiscarded if the message was discarded.

    Note: The contents of message argument should be copied to the queue as the original may get deleted.

    If the implementation does not support discarding, the function must return MessageQueueStatus::DiscardingNotSupported, even though the function should never get called.

    The following is an example implementation of the function:

    MessageQueueStatus enqueueOrDiscard(const void *message) QUL_DECL_OVERRIDE
    {
        if (mQueue->isFull()) {
            // Discard message
            mOverrunFlag = true;
            return MessageQueueStatus::MessageDiscarded;
        }
    
        mQueue->pushBack(message);
        return MessageQueueStatus::Success;
    }
  • MessageQueueInterface::enqueueOrOverwrite() - This function pushes the given message to the end of the queue. If the queue is full, the oldest message in the queue must be overwritten with the given message and overrun state must be set. The function must return MessageQueueStatus::Success if the message was pushed successfully, or return MessageQueueStatus::MessageOverwritten if an older message was overwritten with the given message.

    Note: As with enqueueOrDiscard(), the contents of message should be copied to the queue as the original may get deleted.

    If the implementation does not support overwriting, the function must return MessageQueueStatus::OverwritingNotSupported, even though the function should never get called.

    EventQueue does not support overwriting when the event type is a pointer. If this functionality is desired, the implementation must have appropriate memory handling for pointers that are being overwritten.

    The following is an example implementation of MessageQueueInterface::enqueueOrOverwrite() in implementation where overwriting is not supported:

    MessageQueueStatus enqueueOrOverwrite(const void *message) QUL_DECL_OVERRIDE
    {
        return MessageQueueStatus::OverwriteNotSupported;
    }
  • MessageQueueInterface::receive() - Pops a message from the queue within the given timeout and return it. The timeout is specified in milliseconds. If the timeout value is zero, the function does not wait at all, whereas a negative timeout value makes the function wait for a message indefinitely.

    If a message was successfully retrieved from the queue, message argument must contain the retrieved message (i.e. the contents of retrieved message must be copied to the address pointed by message) and the function must return MessageQueueStatus::Success. If the queue was empty, it must return MessageQueueStatus::EmptyQueue or MessageQueueStatus::Timeout instead.

    The following is an example implementation of MessageQueueInterface::receive():

    MessageQueueStatus receive(void *message, int32_t timeout = 0) QUL_DECL_OVERRIDE
    {
        (void) timeout; // This example does not implement timeout handling.
    
        if (mQueue->isEmpty())
            return MessageQueueStatus::EmptyQueue;
    
        mQueue->popFront(message);
        return MessageQueueStatus::Success;
    }
  • MessageQueueInterface::isEmpty() - Returns true if the queue is empty and false otherwise.
  • MessageQueueInterface::isOverrun() and MessageQueueInterface::clearOverrun() - These functions return and modify the overrun state of the queue.
    bool isOverrun() const QUL_DECL_OVERRIDE { return mOverrunFlag; }
    
    void clearOverrun() QUL_DECL_OVERRIDE { mOverrunFlag = false; }

There are also functions for implementing interrupt-safe versions of enqueuing and receiving messages. These methods have default implementations that call their counterparts provided by Qt Quick Ultralite. However, it is recommended to reimplement the following functions to ensure interrupt-safety:

Now, there should be a working implementation of MessageQueueInterface API using custom queues. EventQueue uses MessageQueue convenience API to interface with the queue implementation. However, by itself the MessageQueue does not know anything about the custom implementation other than it implements MessageQueueInterface. Instead, MessageQueue calls requestQueue() function to get the proper queue for its use. The function takes capacity and messageSize arguments, which indicate the number of items the queue can hold and the size of the messages the queue will be using respectively. This function must either return a pointer to an instance of the queue implementation or a null pointer if the implementation does not support the requested capacity or message size.

Example implementation of requestQueue():

MessageQueueInterface *requestQueue(size_t queueCapacity, size_t messageSize)
{
    void *queue = qul_malloc(sizeof(MyMessageQueue));

    if (queue == NULL) {
        return NULL;
    }

    MessageQueueInterface *interface = new (queue) MyMessageQueue(queueCapacity, messageSize);
    return interface;
}

When the queue is not needed anymore, MessageQueue calls deleteQueue() which handles the deletion and deallocation of queue resources.

Example implementation of deleteQueue():

void deleteQueue(MessageQueueInterface *queue)
{
    MyMessageQueue *mq = static_cast<MyMessageQueue *>(queue);
    mq->~MyMessageQueue();
    qul_free(mq);
}

MessageQueue must know the maximum message size allowed so that it can handle situations where the given message size exceeds the implementation's size limit. This information can be provided by implementing maximumQueueMessageSize(). The function must return the maximum supported size in bytes, or SIZE_MAX if the queue supports arbitrary message sizes. If SIZE_MAX is returned, the implementation is responsible for handling the messages, regardless of their size.

The following example implements maximumQueueMessageSize(), which returns SIZE_MAX.

size_t maximumQueueMessageSize()
{
    return LONG_MAX;
}

Available under certain Qt licenses.
Find out more.