Forum: PC-Programmierung Queue für verschiedene Events in C


von Haudi (Gast)


Lesenswert?

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:
1
void myService_1(uint8 param1, uint16 param2);
2
void myService_2(uint32 param1);
3
void myService_3(uint168 param1, uint16 param2, uint32 param3);

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

von Nop (Gast)


Lesenswert?

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.

von Manfred M. (bittbeisser)


Lesenswert?

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.

Beitrag #5129972 wurde von einem Moderator gelöscht.
von zer0 (Gast)


Lesenswert?

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
struct QueueEntry {
2
  void (*func)(void* args);
3
  void *args;
4
};
5
6
void processEntry(struct QueueEntry* 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
struct QueueEntry {
2
  void (*func)(void* args);
3
  char args[1];
4
};
5
6
struct service1_args {
7
  uint32_t x;
8
  uint8_t y;
9
};
10
extern void service1(struct service1_args*);
11
12
QueueEntry* makeService1Call(uint32_t x, uint8_t y) {
13
  struct special {
14
    void (*func)(struct service1_args*);
15
    struct service1_args args;
16
  } *result = (struct special*)malloc(sizeof(special));
17
  special->func = &service1;
18
  special->args = (struct service1_args){x,y};
19
  return (QueueEntry*)special;
20
}
makeService*Call lassen sich mit ein bisschen #define-Magie mit relativ 
wenig Aufwand implementieren.

von c-hater (Gast)


Lesenswert?

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.

von Haudi (Gast)


Lesenswert?

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.

von c-hater (Gast)


Lesenswert?

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...

von Rolf M. (rmagnus)


Lesenswert?

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.

von Eddy C. (chrisi)


Lesenswert?

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.

von Haudi (Gast)


Lesenswert?

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 ;-)

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.