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.
Default queue implementation
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.
On embedded platforms that have support for std::thread
and std::mutex
, the default queue is thread-safe by using mutexes.
To use the default queue implementation, add platform\common\baremetal\doublequeue.cpp
to your project.
Note: If you want to use the default queues, or your OS does not offer OS-specific ones, you can skip this implementation and immediately continue with the next topic: Handling touch input
OS queue implementation
Most operating systems 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 OS queues enable EventQueue to be used in cases where DoubleQueue
's limitations would normally prevent the usage.
This topic covers how to adapt MessageQueueInterface API to use queues provided by operating systems, 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 be 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
orfalse
.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 andfalse
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:
- MessageQueueInterface::enqueueOrDiscardFromInterrupt() - This function is the interrupt-safe version of MessageQueueInterface::enqueueOrDiscard().
The following is an example implementation of it:
MessageQueueStatus enqueueOrDiscardFromInterrupt(const void *message) QUL_DECL_OVERRIDE { // disableInterrupts(); MessageQueueStatus state = enqueueOrDiscard(message); // enableInterrupts(); return state; }
- MessageQueueInterface::enqueueOrOverwriteFromInterrupt() - This function is the interrupt-safe version of MessageQueueInterface::enqueueOrOverwrite().
- MessageQueueInterface::receiveFromInterrupt() - This function is the interrupt-safe version of MessageQueueInterface::receive().
Note: Although MessageQueueInterface::receiveFromInterrupt() takes the timeout argument, most interrupt calls must avoid waiting if possible.
The following is an example implementation of this function:
MessageQueueStatus receiveFromInterrupt(void *message, int32_t timeout = 0) QUL_DECL_OVERRIDE { (void) timeout; // This example does not implement timeout handling. // disableInterrupts(); MessageQueueStatus state = receive(message); // enableInterrupts(); return state; }
Note: The
timeout
argument is not used in this example implementation. - MessageQueueInterface::isEmptyFromInterrupt() - This function is the interrupt-safe version of MessageQueueInterface::isEmpty().
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.