The FreeRTOS queue is a convenient way for intertask communication on FreeRTOS. But it is written in pain C and therefore it feels a little uncomfortable for C++ programmers. But the main problem is, that is can only handle POD objects.
I found some C++ wrappers for the queue but they all only work with POD classes. So I tried to create my own wrapper. It creates a deep copy of the object and then deallocates it without calling the destructor. This leaves all the heap data intact and when I pop the object it can continue using it.
Anyway, this works fine now but I'm not that confident with it. So i wonder if this works as I expect or just by accident. Is this still defined behavior? What could possibly go wrong?
To make things clearer, here an example: std::string usually has a sizeof 32 byte. These 32 bytes will be copied by xQueueSend, this works fine. But std::string holds a pointer a char* which contains the string data. This data will not be copied. When I give a pointer to the string to xQueueSend, the 32 byte will be copied but not the char array. Would the original std::string get deleted, it would also delete the char array and the data pointer of the copy in the Queue would be dangling. So I create a copy of the std::string with new but then don't delete it. This way the char array will not be deleted. To clean up the 32 bytes the new call has created, I call deallocate. This only deletes the 32 bytes, but not the char array. Reading from the Queue works the other way around. I first create a std::string, but without calling the constructor. This way, I can be sure the it doesn't allocate any additional heap memory. Now I call xQueueReceive with the new generated uninitialized memory. This copies the 32 byte from the string into the new string pointer. Now I have the fully initialized std::string with valid pointer to the char array.
/* This is a C++ Queue wrapper for FreeRTOS queues.
* It uses std::allocator to allocate memory for the queue items.
* The stack data of the objects is copied to the queue but the
* heap data will remain and will be reused when the object is
* popped from the queue.
*/
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <memory>
namespace {
TickType_t ticksToMs(int ms) {
return ms >= 0 ? ms / portTICK_PERIOD_MS : portMAX_DELAY;;
}
}
template <typename T>
class Queue {
public:
Queue(int size) :
_queue{xQueueCreate(size, sizeof(T))} {
assert(_queue);
}
~Queue() {
vQueueDelete(_queue);
}
bool push(const T &data, int timeout = -1) const {
T *copy = new T{data};
const BaseType_t ret = xQueueSend(_queue, static_cast<void*>(copy), msToTicks(timeout));
_allocator.deallocate(copy, 1);
return ret == pdTRUE;
}
T pop(int timeout = -1) {
std::unique_ptr<T> data{_allocator.allocate(1)};
const BaseType_t ret = xQueueReceive(_queue, static_cast<void*>(data.get()), msToTicks(timeout));
if (ret == pdTRUE)
return std::move(*data.get());
return T{};
}
private:
struct Deallocator {
void operator()(T* t) {
_allocator.deallocate(t, 1);
}
};
QueueHandle_t _queue;
static std::allocator<T> _allocator;
};
2 Answers 2
Dealing properly with non-POD types
I found some C++ wrappers for the queue but they all only work with POD classes. So I tried to create my own wrapper. It creates a deep copy of the object and then deallocates it without calling the destructor.
As already mentioned, this is wrong. Consider a type whose behavior depends on whether it was moved in memory or not:
class Foo {
Foo* self;
public:
Foo(): self{this} {}
bool check() { return this == self; }
};
Now consider what happens if you push it to and immediately pop it from a queue:
Foo *copy = new Foo;
assert(copy->check() == true);
xQueueSend(_queue, copy, ...);
_allocator.deallocate(copy, 1);
std::unique_ptr<Foo> data{_allocator.allocate(1)};
xQueueReceive(_queue, data.get(), ...);
assert(copy->check() == true); // FAILS!
This looks like a silly example, but there are types that work like this. They are safe to use if you let C++ copy or move them, but not if you use the equivalent of a C mempcy()
.
There are several options to deal with non-POD types properly though:
Allow only trivially copyable types
You can check if a type is trivially copyable (ie, with something like memcpy()
) by using std::is_trivially_copyable
in a static_assert()
or requires
clause.
Store pointers in the queue
Since you are already allocating memory for copy
and data
, why not just keep that memory around and store a pointer to that in the queue?
template <typename T>
class Queue {
public:
Queue(int size) :
_queue{xQueueCreate(size, sizeof(T*))} {
assert(_queue);
}
bool push(const T &data, int timeout = -1) const {
T *copy = new T{data};
const BaseType_t ret = xQueueSend(_queue, ©, msToTicks(timeout));
if (ret == pdFALSE) {
delete copy;
}
return ret == pdTRUE;
}
T pop(int timeout = -1) {
T* copy;
const BaseType_t ret = xQueueReceive(_queue, ©, msToTicks(timeout));
if (ret == pdTRUE) {
T data = std::move(*data);
delete copy;
return data;
}
return T{};
}
...
}
The above example copies data in push()
and moves it in pop()
, so it only works if T
is both copyable and movable. You could add a std::move()
to push()
so it no longer copies. But you could also consider having push()
take a T*
and pop()
return a T*
, so your Queue
no longer has to worry about memory management, and since it then doesn't copy or move anything, it will work with types that are both non-copyable and non-movable.
Unnecessary use of static_cast<void*>
C++ allows casting pointers to void*
without needing a static_cast<>
. The reverse is not true.
-
\$\begingroup\$ Thank you. Now my only remaining question is why I have implemented such a cumbersome solution. \$\endgroup\$Mr. Clear– Mr. Clear2025年05月22日 09:20:46 +00:00Commented May 22 at 9:20
-
\$\begingroup\$ There is still a problem. In push, the copy itself will not be deleted. In pop, copy is uninitialized, but xQueueReceive will try to write to the location it points on. \$\endgroup\$Mr. Clear– Mr. Clear2025年05月22日 11:41:41 +00:00Commented May 22 at 11:41
-
\$\begingroup\$ @Mr.Clear Copying a pointer and sending/receiving a pointer to/from the queue is safe. That's what my example code does:
copy
is aT*
, and by passing©
toxQueueSend()
andxQueueReceive()
, it just stores the copy, not theT
it points to. \$\endgroup\$G. Sliepen– G. Sliepen2025年05月22日 17:48:52 +00:00Commented May 22 at 17:48 -
\$\begingroup\$ I meant, it just stores the pointer, not the data it points to. \$\endgroup\$G. Sliepen– G. Sliepen2025年05月22日 18:28:52 +00:00Commented May 22 at 18:28
FreeRTOS Queues handle memory management for you. The xQueueSend
function stores objects by copy not not reference. Hence, you do not need manual memory management via new
or std::allocate
calls. This would mean something like this is sufficient.
bool push(const T &data, int timeout = -1) const {
return xQueueSend(_queue, static_cast<void*>(copy), msToTicks(timeout)) == pdTRUE;
}
std::optional<T> pop(int timeout = -1) {
T t;
const BaseType_t ret = xQueueReceive(_queue, static_cast<void*>(data.get()), msToTicks(timeout));
if (ret == pdTRUE)
return t;
return {};
}
However, this also means that non-plain old datatypes
classes are not compatible as you do not invoke the copy/move constructors. C++ does its dynamic memory management via calls to new
/delete
or std::allocate
/std::deallocate
. FreeRTOS does not. So, there is no way to wrap the freertos queue in C++ to support all C++ types without invoking UB. So, I would suggest a check via type_traits
to constraint the T
to pod
s. Something like this.
static_assert(std::is_pod_v<T>, "Queue only accepts plain old data_type");
I will further add move
constructor and assignment operator to the queue
. Adding copy
constructor is impossible here without manually adding sync
. But at the point, we pretty much lose main benifit of using FreeRTOS
queue. So, I would explicitly delete those operations.
-
1\$\begingroup\$ Note that
std::is_pod
is deprecated since C++20, and you should usestd::is_trivially_copyable
in this case. Your code also doesn't compile; there's nocopy
inpush()
and nodata
inpop()
. \$\endgroup\$G. Sliepen– G. Sliepen2025年05月21日 15:02:46 +00:00Commented May 21 at 15:02 -
\$\begingroup\$ There is also no
std::allocate
orstd::deallocate
. 🤷🏼 \$\endgroup\$indi– indi2025年05月23日 20:03:31 +00:00Commented May 23 at 20:03
TickType_t ticksToMs(int ms)
I'm really confused about what goes in, and what comes out of this function. Am I having a stroke??!! \$\endgroup\$empty()
andfreeSpace()
are ambiguous function names. Are they queries or are they actions with consequences? Using the handle 'Mr. Clear', you're not living up to it (imho)... \$\endgroup\$