Forum: Mikrocontroller und Digitale Elektronik placement new c++ & FreeRTOS


von A. B. (funky)


Lesenswert?

Hi,

ich probiere mich an C++ auf Mikrocontroller mit FreeRTOS und einem 
dynamischen Nachrichtenhandling und ich möchte Speicherfragmentierung 
verhindern

Header
1
class MessageFabric
2
{
3
public:
4
  static Message* createMessage(const Event& event);
5
6
  virtual ~MessageFabric() {}
7
8
private:
9
  MessageFabric();
10
  static uint8_t memory_[2048];
11
};

Implementierung
1
uint8_t MessageFabric::memory_[2048];
2
 
3
Message* MessageFabric::createMessage(const Event& event)
4
{
5
  Message* result(nullptr);
6
7
  switch (event.Type()) {
8
    case config:  
9
     result = new (memory_) MessageConfig();  
10
    break;
11
  }
12
  return result;
13
 }

Aufruf in einem FreeRtos-Thread (data bekomme ich aus einer FreeRTOS 
Queue)
1
 Event msg(data);
2
 Message* message = MessageFabric::createMessage(msg);
3
 if (message) {
4
    message->process();
5
    message->~Message();  // manually call destructor because of placment new!!!!
6
 }
Prinzipiell funktioniert das. Schau ich mir im Debugger die Adresse von 
memory_ und vom Message Objekt an so sind die gleich.

Was ich mich jetzt frage:

Müßte ich das ganze nicht noch Threadsafe machen? Ich habe bei Freertos 
heap4.c eingebunden, und dort gibts auch ein pvMalloc was Threads 
blockiert usw.
Wenn ich bei mir durchsteppe, scheine ich bei meinem new aber keine der 
Freertos Funktionen aufzurufen. Ich frage mich jetzt also ob ich das new 
noch überladen muss bzw. was genau passiert da überhaupt?

Und mein Memory Block ist jetzt fix auf 2kB Größe festgelegt. Wie kann 
ich denn sicherstellen, das meine abgeleiteten MessageObjekte da 
aufjedenfall reinpassen? Ich habe eine Abstrakte BasisMessage Klasse und 
die process() Methode wird dann immer überladen.

Ist der Ansatz so überhaupt valide oder ist das totaler Schrott auf 
einem Mikrocontroller?

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Sofern du MessageFabric::createMessage nur von 1 Thread aus aufrufst 
passt das schon. Placement New allokiert ja nicht wirklich Speicher, 
sondern ruft im Wesentlichen einfach nur den Konstruktor auf.

A. B. schrieb:
> Und mein Memory Block ist jetzt fix auf 2kB Größe festgelegt. Wie kann
> ich denn sicherstellen, das meine abgeleiteten MessageObjekte da
> aufjedenfall reinpassen?

Vielleicht wäre es cleverer einfach std::variant<Message1, Message2, 
Message3> zu verwenden. Das hat dann genau die richtige Größe.

A. B. schrieb:
> Ich habe bei Freertos
> heap4.c eingebunden, und dort gibts auch ein pvMalloc was Threads
> blockiert usw.

Ja, malloc() arbeitet auf einer Datenstruktur um Speicherblöcke zu 
finden und zu reservieren, auf welche auch andere Threads zugreifen. 
Placement new hingegen nutzt einfach den einen Block der da ist.

: Bearbeitet durch User
von A. B. (funky)


Lesenswert?

Ja, ich rufe das nur aus einem Thread auf.
Danke für die Erläuterungen.

std::variant würde sicher funktionieren und wäre Speicherschonender als 
mein Ansatz. Muss man dann nur drauf achtgeben wenn man eine neue 
Message anlegt, dass dort auch einzutragen

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

A. B. schrieb:
> Muss man dann nur drauf achtgeben wenn man eine neue
> Message anlegt, dass dort auch einzutragen

Ja genau das ist doch der Vorteil, dass so die Speichergröße immer 
garantiert ist und man es nicht vergessen kann. Wenn du bei deiner 
Variante eine neue größere Nachrichtenklasse implementierst aber die 
Speichergröße nicht erhöhst, geht es schief.

von A. B. (funky)


Lesenswert?

Hab es mal probiert, leider ist variant c++17 und ich hänge bei 11.
Es gibt aber paar Implementierungen fpr 11...werde das mal probieren ob 
ich damit zum Ziel komme

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Ein paar Gedanken von mir:

* Fabric ist das englische Wort für Stoff, wahrscheinlich suchst Du 
Factory ;-)

* Wenn der c'tor von MessageFabric privat ist, kann keine Instanz davon 
angelegt werden, ergo braucht es auch keinen d'tor.

* Virtueller d'tor zeigt in der Regel an, dass die Klasse als Basis 
dienen soll. Ergibt hier dann so keinen Sinn.

* So wie es jetzt aussieht, hast Du immer maximal eine Instanz einer 
Message und rufst genau eine Funktion darauf auf. Wolltest Du mehr als 
eine Message z.Z. haben, müsstest Du memory_ anders dimensionieren und 
vor allem, auch anders verwalten. Dann würde es auch nicht mehr 
ausreichen, den d'tor explizit aufzurufen, sondern Du müsstest es dann 
der factory überlassen, das Objekt wieder abzuräumen.

* Wenn die Anforderungen mit dem, was Du uns bis jetzt gezeigt hast, 
abgedeckt sind, würde es absolut ausreichen, unterschiedliche 
`process()` Funktionen anhand des Event.types aufzurufen (ggf. diese 
Funktion auch direkt in `Event` zu implementieren). Anhand des 
Event.types temporär eine korrespondierende Message zu erzeugen, nur um 
eine Funktion auf der Message aufzurufen, ist maximal verwirrend.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Torsten R. schrieb:
> * Wenn der c'tor von MessageFabric privat ist, kann keine Instanz davon
> angelegt werden, ergo braucht es auch keinen d'tor.

Das würde ich nicht so sehen. Es ist ein gängiges Pattern manche Klassen 
nur per Factory(-Funktion) erstellen zu können, aber normal zerstören zu 
können.

Torsten R. schrieb:
> Wenn die Anforderungen mit dem, was Du uns bis jetzt gezeigt hast,
> abgedeckt sind, würde es absolut ausreichen, unterschiedliche
> `process()` Funktionen anhand des Event.types aufzurufen

Stimmt!

A. B. schrieb:
> Hab es mal probiert, leider ist variant c++17 und ich hänge bei
> 11.
> Es gibt aber paar Implementierungen fpr 11...werde das mal probieren ob
> ich damit zum Ziel komme

Eine union geht auch, std::variant ist im Endeffekt nichts anders, man 
muss halt genau aufpassen wann man welche Instanz darin verwendet.

von A. B. (funky)


Lesenswert?

Danke für die Anmerkungen.
Mit der Fabric das ist ein wenig peinlich...

Eigentlich wollte ich weg von riesigen switch cases in denen alle 
MessageIDs gehandelt&bearbeitet werden.

Jetzt hab ich immer noch ein großes switch case (etwas übersichtlicher 
da dort nur die Message erzeugt wird) aber irgendwie ist das immer noch 
Murks, viel Schreibarbeit und Boilerplate und der Aufwand mit Objekt 
erzeugen, process() aufrufen und Objekt löschen ist auch nicht geil.

Also irgendwo werde ich immer einnen großen switch case haben um jede 
Message zu erzeugen/zu handeln :/

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

A. B. schrieb:
> Eigentlich wollte ich weg von riesigen switch cases in denen alle
> MessageIDs gehandelt&bearbeitet werden.

Wenn in jedem "case" nur ein normaler Funktionsaufruf zu einer Funktion 
für den jeweiligen Typ ist, passt das schon. Lange Funktionen möchte man 
ja normalerweise deswegen vermeiden, weil sie schwer verständlich und 
wartbar sind, aber ein langes switch-case ist ja nicht schwer zu 
begreifen.

Alternativ kannst du auch std::map<int,MsgFun> o.ä. arbeiten, um von 
Nachrichten-Typ auf einen Funktionsaufruf abzubilden. MsgFun wäre 
einfach ein Funktionszeiger; alternativ ging auch eine Klasse mit einer 
virtuellen Funktion oder std::function.

von name (Gast)


Lesenswert?

In deiner ursprünglichen Lösung könntest du mit
1
static_assert()
 zur Compilezeit prüfen, ob dein Array groß genug ist.

Aber union/variant ist natürlich immer noch besser.

von Rolf M. (rmagnus)


Lesenswert?

Torsten R. schrieb:
> * Wenn der c'tor von MessageFabric privat ist, kann keine Instanz davon
> angelegt werden, ergo braucht es auch keinen d'tor.

Ich würde die Klasse ganz rauswerfen. Für mich machen Klassen, von denen 
es weder direkt noch über abgeleitete Klassen je eine Instanz gibt, 
irgendwie wenig Sinn. Wenn es in der Klasse nur Dinge gibt, die nicht zu 
einer Instanz gehören (also alles static ist), dann kann man das auch 
ohne Klasse als freie Funktion definieren, ggf. in einem namespace.

> * Virtueller d'tor zeigt in der Regel an, dass die Klasse als Basis
> dienen soll. Ergibt hier dann so keinen Sinn.

Und es führt normalerweise dazu, dass er Compiler dafür extra eine 
vtable anlegen muss, die dann nie benutzt wird.

Niklas G. schrieb:
> Torsten R. schrieb:
>> * Wenn der c'tor von MessageFabric privat ist, kann keine Instanz davon
>> angelegt werden, ergo braucht es auch keinen d'tor.
>
> Das würde ich nicht so sehen. Es ist ein gängiges Pattern manche Klassen
> nur per Factory(-Funktion) erstellen zu können, aber normal zerstören zu
> können.

Hier geht's aber nicht um das per Factory erzeugte Objekt, sondern um 
die Factory selbst, die im Prinzip nur aus einer static-Memberfunktion 
einer Klasse besteht. Eine Instanz dieser Factory-Klasse gibt es also 
nicht.


Es gibt mit dem Code noch ein Problem:
1
static uint8_t memory_[2048];
Dieses Array muss nicht unbedingt ein passendes Alignment für ein 
Message-Objekt haben.

: Bearbeitet durch User
von A. B. (funky)


Lesenswert?

Mir reichts :( Ich stell das ganze wieder um und verabschiede mich von 
diesem ich kreiere mir mein Temporäres Message Object Ansatz.

Aber kennt jemand ein größeres Projekt für einen Mikrocontroller mit c++ 
was man sich mal auf github oder so ansehen kann umd mal zu sehen wie 
sowas da umgesetzt ist? ich finde immer nur sehr abstrakte 
Codeschnippsel für embedded und im Nebensatz wird dann gesagt: auf 
Mikrocontrollern mit den ganzen Einschränkungen geht es so dann doch 
nicht und man muss es anders lösen. Wie ist mir aber immer noch nicht 
ganz klar, ausser ich programmiere quasi wieder C-Style :(


Es gibt mit dem Code noch ein Problem:
static uint8_t memory_[2048];
Würde __aligned() reichen oder was muss man da noch beachten?
Erstmal hatte es so funktioniert, aber evtl war das auch Glück

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

A. B. schrieb:
> Aber kennt jemand ein größeres Projekt für einen Mikrocontroller mit c++
> was man sich mal auf github oder so ansehen kann umd mal zu sehen wie
> sowas da umgesetzt ist?

https://github.com/TorstenRobitzki/bluetoe

Ist halt eine Library die mehr als einen Anwendungsfall abdecken muss 
und damit etwas komplexer wirkt. Kommt ohne heap aus und resultiert in 
sehr kleinen binaries.

Als ich damit vor langer Zeit angefangen hatte, war C++11 noch "neu". 
Jetzt könnte es langsam mal einen update auf z.B. C++20 gebrauchen.

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

A. B. schrieb:

> Es gibt mit dem Code noch ein Problem:
> static uint8_t memory_[2048];
> Würde __aligned() reichen oder was muss man da noch beachten?
> Erstmal hatte es so funktioniert, aber evtl war das auch Glück
1
static alignas(alignof(MessageConfig)) std::uint8_t memory_[2048];

https://en.cppreference.com/w/cpp/language/alignas

von db8fs (Gast)


Lesenswert?

Die Idee mit dem union find ich aber eigentlich gar nicht übel, kannst 
ja auch nen union wie ne Klasse anlegen:
1
union Message
2
{
3
   char text[4];
4
   struct 
5
   { 
6
     int a : 2;
7
     int b : 30;
8
   } data;
9
10
   Message() { this->data.a = 2; } 
11
}

Wenn du dir davon nen Array anlegst, haste auch deinen MemoryPool. 
Kannste glaube auch ableiten davon.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.