Hi zusammen,
also, ich hock gerade an einer Lib für ein Kommunikationsprotokoll,
diese soll PC-seitig und auf einem Mikrocontroller eingesetzt werden. C
ist als Sprache gesetzt, C++ scheidet definitiv aus, sonst würd ich das
wohl benutzen.
Meine Lib bietet verschiedene Services an. Höhere Softwareschichten
können diese Services bei Bedarf in Anspruch nehmen.
Je nach Service sind aber komplett unterschiedliche Parameter
erforderlich und auch die Schritte zur Abarbeitung sind unterschiedlich,
sieht ungefähr so aus:
Weil die Services asynchron abgearbeitet werden (da ist Kommunikation im
Spiel) werden die Aufträge nur in einer Queue geparkt.
Meine Servicefunktionen blockieren also nicht und returnen sofort. Die
Queue wird nach und nach abgearbeitet (ein Überlauf ist hier erstmal
kein Problem).
Nun sind wir aber in C unterwegs.
Ich bin nun dazu übergegangen die Parameter zu Strukturen
zusammenzufassen, ungefähr so:
1
void myService_1(service_1_data_t *serviceData);
2
void myService_1(service_2_data_t *serviceData);
3
void myService_1(service_3_data_t *serviceData);
Dadurch kann ich die Services sauber in einer Queue parken, die Queue
beinhaltet einfach Voidzeiger. Der Voidzeiger ist ja generisch und darf
auf alles zeigen.
Allerdings muss ich die Services ja irgendwann abarbeiten.
Dazu muss ich die Voidzeiger dereferenzieren.
Polymorphie existiert nicht. Also muss ich vorm Dereferenzieren wissen
welcher Zeigertyp das ursprünglich mal war.
Ich muss mir in meiner Queue also zu jedem Voidzeiger noch eine Info
ablegen um welchen Service es sich handelt. Z.B. mittels einer Enum oder
so.
Nun, funktionieren wird dies sicherlich:
Mich interessiert eher was ihr dazu meint.
Ist das so in Ordnung?
Verrenn ich mich hier in etwas?
Soll ich das ganz anders angehen?
Irgendwelcher sontiger konstruktiver Input?
Schönes WE
Haudi schrieb:> Ich muss mir in meiner Queue also zu jedem Voidzeiger noch eine Info> ablegen um welchen Service es sich handelt.
Du könntest auch das erste Byte in JEDEM Service als ID benutzen, was
für ein Service es ist. Damit kannst Du den Zeiger erstmal nach uint8_t
casten, dereferenzieren, gucken, welche ID es ist, dann weitermachen.
Das kann man elegent als struct machen, das aus der ID besteht, gefolgt
von einer Union, in der die jeweiligen verschiedenen structs für die
verschiedenen Services stecken. Dann hat ein Element in der Queue
unabhängig vom Service auch immer dieselbe Größe.
Ein Problem wird der Fall, daß Du nennenswert Daten übergeben willst.
Hierzu kann man stattdessen nur einen Zeiger übergeben. Dann aber muß
der Absender den Speicher ja bereitstellen und eine Info kriegen, wann
der wieder frei wird. Das könnte man mit ner Callbackfunktion lösen.
Oder man versieht jede Message mit einer eindeutigen ID, und in einer
Queue in Gegenrichtung kriegt man dann irgendwas wie eine Bestätigung,
womöglich auch mit Fehlercode.
Ich mache etwas ähnliches. Ursprünglich in C und jetzt auch in C++. In
beiden Fällen verwende ich einen Ringpuffer. In C++ könnte ich natürlich
auch die STL verwenden, aber warum, wenn ich funktionierenden Code habe.
Aber in beiden Fällen läuft das abnehmende Programm in einem anderen
Thread, was aber nicht in jeder Anwendung erforderlich ist.
Die C Löung war für einen Morsezeichengenerator. da wollte ich die
Tastatureingaben von der Tonausgabe entkoppeln.
Im aktuellen Fall ist das für eine Grafik Ausgabe, ein Oszilloskop mit 1
bis 8 Kanälen. Da laufen dann aber auch andere Kommandos über den
Ringpuffer, woraus sich unterschiedliche Längen ergeben.
In Deinem Fall würde ich mit einer kleinen Struktur arbeiten, die
mindestens aus einer ID-Nummer und dem void* besteht. Für das Interface
sehe ich da 2 Möglichkeiten:
queue_in()
queue_out()
queue_delete()
Bei dieser Variante müsst der Abnehmer die Daten aber kopieren.
Die einfachere, aber von OOP Hardlinern verteufelte, ist wenn
queue_out() nur die Datenstruktur aus der Queue in den Abnehmerbereich
kopiert und danach aus der Queue entfernt. Das bedeutet dann aber auch,
das der Abnehmer den void* nach Gebrauch freigeben muss. Dann wüder
queue_delete() nicht gebracht.
Nop schrieb:> Du könntest auch das erste Byte in JEDEM Service als ID benutzen, was> für ein Service es ist.
Würde ich nicht tun, wenn Speicher nicht extrem knapp ist.
Besser den Zeiger auf die Service-Funktion mit in die Queue hängen.
1
structQueueEntry{
2
void(*func)(void*args);
3
void*args;
4
};
5
6
voidprocessEntry(structQueueEntry*work){
7
work->func(work->args);
8
}
Der abarbeitende Teil kann dann zwar immer noch nur mit void* arbeiten,
man kann aber wenigstens die addToQueue()-Funktion type-safe definieren,
so dass immer nur valide Paare in der Queue landen.
Mit ein bisschen C-style Quasi-Polymorphie lässt sich ein bisschen
Speicher auch wieder einsparen, indem man casts ausnutzt.
1
structQueueEntry{
2
void(*func)(void*args);
3
charargs[1];
4
};
5
6
structservice1_args{
7
uint32_tx;
8
uint8_ty;
9
};
10
externvoidservice1(structservice1_args*);
11
12
QueueEntry*makeService1Call(uint32_tx,uint8_ty){
13
structspecial{
14
void(*func)(structservice1_args*);
15
structservice1_argsargs;
16
}*result=(structspecial*)malloc(sizeof(special));
17
special->func=&service1;
18
special->args=(structservice1_args){x,y};
19
return(QueueEntry*)special;
20
}
makeService*Call lassen sich mit ein bisschen #define-Magie mit relativ
wenig Aufwand implementieren.
Haudi schrieb:> Allerdings muss ich die Services ja irgendwann abarbeiten.> Dazu muss ich die Voidzeiger dereferenzieren.> Polymorphie existiert nicht. Also muss ich vorm Dereferenzieren wissen> welcher Zeigertyp das ursprünglich mal war.
Nö.
Du musst das so umstricken, dass jeder Service einfach nur einen
void-Zeiger erwartet (auf eine Datenstruktur, die die für ihn passenden
Parameter enthält).
Beim Eingang der Daten muss entschieden werden, für welchen Service sie
sind, und in der Queue werden dann nur zwei Zeiger gespeichert, nämlich
der auf die zuständige Servicefunktion und der auf seine Parameterdaten.
Den cast auf die korrekte Struktur wird dann in der Servicefunktion
selber erledigt.
Vorteile: erheblich höhere Effizienz, weil:
- nur einmal irgendwelche Entscheidungen zu fällen sind
- die Queue-Elemente immer die Große einer 2er-Potenz haben
Nachteil: der existiert nur für den Fall, dass die Servicefunktionen
auch von anderer Stelle aus aufgerufen werden sollen und nicht nur vom
Queue-Poller aus. Das kann man dann aber leicht umgehen, indem man für
jeden Service eine Wrapperfunktion implementiert, die halt eine passende
Struktur mit den Parametern füllt und dann den eigentlichen Service
aufruft.
Hi zusammen, danke für den Input.
Die Antworten gehen ja grundsätzlich in die gleiche Richtung.
irgendeine Info muss ich, zusätzlich zum Parameterpaket, mitspeichern.
Das ist soweit ja logisch, immerhin kann ich in C zur Laufzeit keine
Typeninformationen gewinnen.
Die Idee mit dem Void-Zeiger scheint aber grundsätzlich in Ordnung zu
sein.
Was das speichern betrifft: am Besten gefällt mir bisher die Idee vom
c-hater. Durch mitabspeichern der jeweiligen Servicefunktion als
Callback spar ich mir im Eventhandler eine dicke if-else-Orgie.
Im Prinzip hab ich also für jeden Service eine nach außen Sichtbare API
die der User aufruft und einen internen Handler der die eigentliche
Abarbeitung macht.
Nop schrieb:> Ein Problem wird der Fall, daß Du nennenswert Daten übergeben willst.> Hierzu kann man stattdessen nur einen Zeiger übergeben. Dann aber muß> der Absender den Speicher ja bereitstellen und eine Info kriegen, wann> der wieder frei wird. Das könnte man mit ner Callbackfunktion lösen.
Das passt schon so. Ich gehe davon aus dass das Parameterobjekt vom
Aufrufer erstellt wird und als Zeiger übergeben wird.
Dafür kann der Aufrufer etwaige Response-Nachrichten direkt aus diesem
Objekt auslesen und seinen Receive-Puffer komplett selbst verwalten.
Im Parameterobjekt könnte die Lib auch die Info ablegen wie der Status
des Service ist. Der Aufrufer kann dann den Status pollen oder sich per
Callback benachrichtigen lassen.
Glaub der lwip-Stack macht das auch ähnlich.
Haudi schrieb:> Was das speichern betrifft: am Besten gefällt mir bisher die Idee vom> c-hater. Durch mitabspeichern der jeweiligen Servicefunktion als> Callback spar ich mir im Eventhandler eine dicke if-else-Orgie.
Andere sind seeehr lange vor mir auf exakt die gleiche Idee gekommen.
Schon zu Zeiten, als es noch nicht einmal C gab...
Haudi schrieb:> Was das speichern betrifft: am Besten gefällt mir bisher die Idee vom> c-hater. Durch mitabspeichern der jeweiligen Servicefunktion als> Callback spar ich mir im Eventhandler eine dicke if-else-Orgie.> Im Prinzip hab ich also für jeden Service eine nach außen Sichtbare API> die der User aufruft und einen internen Handler der die eigentliche> Abarbeitung macht.
Ja, das ist eine gute Lösung. Das ist im Prinzip genau die Art, wie auch
in C++ virtuelle Memberfunktionen üblicherweise umgesetzt werden (bzw.
ein kleiner Teil davon), nur eben zu Fuß implementiert.
Im Prinzip benötigen die Low-Level-Funktionen doch nur eine einzige
zusätzliche Information, um arbeiten zu können:
Wie groß ist ein Datenelement? Und genau diese Information packt man mit
in die Datenstruktur und übergibt sie bei der Initialisierung per
sizeof.
Der Pufferspeicher wird mit einem 8-Bit-Datentyp implementiert, die
Grösse der Element sind immer Vielfache davon.
Ich wüsste nicht, wozu man da noch einen Callback benötigt.
Eddy C. schrieb:> Ich wüsste nicht, wozu man da noch einen Callback benötigt.
Du gehst davon aus dass dem Service nur rohe Datenframes übergeben
werden die dann bei Gelegenheit versendet werden.
Dann hättest du recht, aber diese Annahme ist falsch.
Ich schrieb deswegen bewusst von "Services".
Diese erwarten komplett unterschiedliche Parameter und auch die
auszuführenden Aktionen unterscheiden sich.
Da brauchts schon jeweils nen Handler, ne generische Transmit-Funktion
reicht da nicht.
Insgesamt bin ich mit meiner Lösung zufrieden und da keiner Veto
eingelegt hat ist es wohl auch nicht die dreckigste Methode ;-)