Hallo,
unser C-Code für mehrere µC enthält viele structs. Exemplarisch:
1
typedefstruct
2
{
3
uint8u8;
4
uint16cnt;
5
uint32u32;
6
}tStatus47;
7
8
tStatus47Status47;
Wir stellen sicher, dass die Strukturen auf allen Plattformen gleich
gepackt sind (padding-byte zwischen U8 und cnt).
Im Code haben wir dann tausende Zeilen a la "Status47.u32 = xy;".
Die meisten Datenfelder werden zwischen verschiedenen Geräte oder PC
ausgetauscht, in denen der selbe Source-Code läuft. Wir senden quasi per
(47, &Status47, sizeof(Status47)) problemlos, da Typ, struct, Padding,
Endian gleich sind.
Jetzt haben wir erstmals ein *Big-Endian*-Gerät.
Es wäre unschön, künftig vor jedem send/receive sowas einzufügen:
1
switch(typ)
2
{
3
case47:SendStatus47.u32=htonl(Status47.u32);
4
SendStatus47.cnt=htons(Status47.cnt);
5
SendStatus47.u8=Status47.u8;
6
break;
7
}
Es geht nicht um Performance, nur um Wartbarkeit. Wo sonst die
Strukturdefinition ausreicht (+ein paar Compile-Time-Asserts), muss ich
nun im Code Annahmen über die Größe von cnt machen. Und kein Element
vergessen etc.
Kennst Du eine Möglichkeit, die manuelle Auscodierung im Code zu
vermeiden?
Gibt es eine Art Metacompiler, der bei Angabe des Typs die
Byte-Strukturen analysiert und aus den Original-Headern z.B. solche
Funktionen erzeugt:
tStatus47* hton_Status47(tStatus47 *src);
tStatus47* ntoh_Status47(tStatus47 *src);
BTW.: Es geht auch nicht um Versions-Kompatibilität. Wenn ein
(projektspezifische) Typ geändert wird, werden alle Plattformen dafür
neu gebuildet.
Verstehe das Problem nicht: das un/marshalling in network-byte-order
macht man doch nur einmal in den send()/receive() Funktionen. Also an
einer Stelle.
Ja. Aber wenn ich z.B. 20 Stati habe, die jeweils 100 Elemente habe, die
zudem noch projektspezifisch (per #define) Ein- oder Ausgeblendet
werden, dann muss ich manuell dafür sorgen, dass dies in den
Sende-Empfangsroutinen völlig synchron ebenso geschieht. Sowohl die
#define-Bedingungen, also auch die Elementgröße (2 oder 4 Byte) müssen
synchron nachgezogen werden.
Das Ändern von einer Information an zwei (oder mehr) Stellen
reduziert die Wartbarkeit.
Zudem kommen bei uns noch einige hundert Kommandos mit variablen
Parametern hinzu (die ein kleineres Problem sind) und teilweise
geschachtelte Strukturen. Reine gemischte Folgen von 16 und
32-Bit-Werten würde ich per Script automatisieren können.
In C++ gäbe es diverse Möglichkeiten via templates und Compile Time
Algorithmen, ggf. unter Verwendung von Boost Fusion, aber ich vermute
das ist außer Frage?
Achim S. schrieb:> Wir senden quasi per (47, &Status47, sizeof(Status47)) problemlos, da> Typ, struct, Padding, Endian gleich sind.
Diese Herangehensweise rächt sich hier, deswegen macht man das direkt
richtig...
Dr. Sommer schrieb:> In C++ gäbe es diverse Möglichkeiten via templates und Compile Time> Algorithmen, ggf. unter Verwendung von Boost Fusion, aber ich vermute> das ist außer Frage?
Das denke ich wohl. Es ist eigentlich wie immer: man verteufelt C++
wegen der angeblich undurchsichtigen Abstraktion ....
>> Achim S. schrieb:>> Wir senden quasi per (47, &Status47, sizeof(Status47)) problemlos, da>> Typ, struct, Padding, Endian gleich sind.> Diese Herangehensweise rächt sich hier, deswegen macht man das direkt> richtig...
... und wenn man diese Abtraktionsmöglichkeit mal braucht ...
Eine simple Überladung würde schon mal helfen, geht ja aber nicht!
Schade eben.
Für derartige Fragestellungen gibt es spezielle Beschreibungssprachen
für Datenstrukturen, aus denen sich die entsprechenden Wrapper und
Encoder/Decoder automatisch generieren lassen. Ein typischer,
mittlerweile reichlich angestaubter Vertreter ist z.B. ASN.1:
https://de.wikipedia.org/wiki/Abstract_Syntax_Notation_One
Heutzutage werden z.B. SNMP, LDAP oder Teile von GSM und UMTS in ASN.1
definiert. Und z.B. auch X.509-Zertifikate für TLS/HTTPS.
Wilhelm M. schrieb:> Eine simple Überladung würde schon mal helfen, geht ja aber nicht!
oh, C++ wäre möglich, wenn Du eine gute Idee für obige Beispiel hast.
Dr. Sommer schrieb:> deswegen macht man das direkt richtig...
wie wäre es denn richtig?
Das Problem ist ja, dass ich keine wartbare Lösung kenne. Manuelles
Marshalling ist doch nur eine fragile Krücke, von der man, solange es
ohne geht, die Finger lassen sollte.
Achim S. schrieb:> wie wäre es denn richtig?
Indem man nicht die Rohdaten einfach auf C-Structs umcastet, sondern die
Bytes einzeln auseinandernimmt und bitshifts verwendet.
Achim S. schrieb:> oh, C++ wäre möglich, wenn Du eine gute Idee für obige Beispiel hast.
Im Anhang wäre ein Ansatz dazu. Der ist noch relativ simpel und kann nur
structs und unsigned integer deserialisieren. Die fehlenden Funktionen
kann man relativ leicht ergänzen.
Es wird Boost.Fusion zur Definition und Iteration der structs genutzt.
Hier werden statt pointer-casts die Bytes manuell extrahiert und zu
Integern zusammen gesetzt, allerdings mithilfe von Meta-Algorithmen was
Tipp-Arbeit erspart. Ab Zeile 143 kommt der eigentliche Code, davor sind
Hilfsfunktionen: Man definiert die structs mit etwas abgewandelter
Syntax als Boost-Fusion-structs:
1
BOOST_FUSION_DEFINE_STRUCT(
2
(),tStatus47,
3
(uint8_t,u8)
4
(uint16_t,cnt)
5
(uint32_t,u32)
6
(uint8_t,u8_2)
7
)
8
9
BOOST_FUSION_DEFINE_STRUCT(
10
(),tStatus48,
11
(tStatus47,s47)
12
(uint16_t,x)
13
)
Davon kann man dann beliebig viele beliebig verschachtelt deklarieren.
Diese sind dann simpel durch einen Aufruf an "deserialize" aus einem
Byte-Array zu parsen. Der Vorteil dieses Ansatzes besteht darin, dass
man das Binär-Format explizit, aber zentral für alle structs, im Code
angibt:
In Zeilen 109-111 wird das gewünschte Padding berechnet. Das kann man
beliebig einstellen, indem man immer 0 zurückgibt wird kein Padding
genutzt. Aufgrund des byte-Zugriffs können niemals Fehler bezüglich
Alignment auftreten, der Compiler kopiert das automatisch richtig.
In Zeile 93 werden Integer aus Bytes zusammengesetzt. Hierbei wird
angenommen, die Binärdaten seien immer little endian. Das kann man hier
ebenfalls zentral ändern. Das funktioniert unabhängig von der Byte-Order
der Plattform - egal ob der Computer LE oder BE ist, werden die Daten
immer korrekt eingelesen. Dank Bitshifts kann auf htons & co. verzichtet
werden.
Somit verhält sich der Code auf allen Plattformen garantiert gleich,
sofern die gewünschten Datentypen (uintX_t) auf diesen Plattformen
vorhanden sind.
Der wesentliche Vorteil in der Nutzung der Meta-Algorithmen besteht
darin, dass man keinerlei externe Tools/Sprachen benötigt, sondern nur
den C++ Compiler, und es sich dennoch immer korrekt verhält. Die structs
definiert man - mit durch die Boost-Makros abgewandelte Syntax - normal
im Code.
Achim S. schrieb:> Es geht nicht um Performance, nur um Wartbarkeit.
Dann laß das direkte Senden von internen Daten komplett bleiben!
Was du brauchst, ist ein geräteunabhängiges Protokoll bzw.
Übertragungsformat. Das sollte doch klar sein.
Damit hat jedes beteiligte sendende Gerät seine internen Daten in das
definierte Austauschformat zu bringen und jedes empfangende Gerät muß
selbige in sein internes Format umwandeln. Und kein Gerät braucht sich
drum zu scheren, wie die internen Darstellungen in anderen Geräten sind.
So und nur so erreicht man sowas wie Funktionalität und Wartbarkeit.
Wenn du hingegen Interna eines Gerätes allen anderen Geräten auf's Auge
drücken willst, verstrickst du dich nur in Inkompatibilitäten.
W.S.
W.S. schrieb:> Was du brauchst, ist ein geräteunabhängiges Protokoll bzw.> Übertragungsformat. Das sollte doch klar sein.
Hat er doch.
> Wenn du hingegen Interna eines Gerätes allen anderen Geräten auf's Auge> drücken willst, verstrickst du dich nur in Inkompatibilitäten.
Will er doch gar nicht.
Wie in einigen Beiträgen bereits bemerkt wurde, ist die sauberste Lösung
meiner Meinung nach die Nutzung von C++. Egal ob durch virtuelle
Funktionen, Type Traits oder simple Overloads... da wäre viel möglich
und die Anbindung an den bestehenden C Code wäre problemlos.
Will man das nicht, dann würde ich einen Hook vor die Sende Routine
schieben, die die Endianess zur Laufzeit prüft.
Irgendwie sowas in die Richtung ->
union ist aber unschoen, weil der Standard (so mein wissensstand) sagt,
dass du nur das Element lesen darfst, welches als letztes geschrieben
wurde.
Siehe auch diese Beitraege:
Beitrag "Re: zu Zugriff auf union"
1
Ich vermute, dass du die union mit dem einen Member setzen und durch den
2
anderen lesen willst.
3
...
4
Deine Vermutung stimmt auch, geschrieben werden die Bits im Programm und
5
später im eeprom gesichert. Ich verstehe jetzt nicht, warum das in
"Jo eh."
Tatsächlich findet man solche Konstrukte aber sehr häufig. Selbst im
CMSIS Code (z.B. bei den DSP Sachen) werden unions auf diese Art und
Weise genutzt.
Und grad zur Endian-Prüfung fällt mir akut kein anderer Weg ein. Auf
Präprozessor-Macros wie etwa _LITTLE_ENDIAN_ würde ich mich persönlich
noch weniger verlassen.
Achim S. schrieb:> Kennst Du eine Möglichkeit, die manuelle Auscodierung im Code zu> vermeiden?>> Gibt es eine Art Metacompiler, der bei Angabe des Typs die> Byte-Strukturen analysiert und aus den Original-Headern z.B. solche> Funktionen erzeugt:
Habe ich selbst noch nicht verwendet, aber evtl. könnte das hier was für
Dich sein: https://developers.google.com/protocol-buffers/
mfg Torsten
Vincent H. schrieb:> Wie in einigen Beiträgen bereits bemerkt wurde, ist die sauberste Lösung> meiner Meinung nach die Nutzung von C++. Egal ob durch virtuelle> Funktionen, Type Traits oder simple Overloads... da wäre viel möglich> und die Anbindung an den bestehenden C Code wäre problemlos.
Egal ob C / C++ / younameit als Sprache benutzt wird: wenn lose
gekoppelte Systeme miteinander kommunizieren können sollen, dann sollte
man eine Interfacedefinition haben. Mit einem entsprechenden
Interface-Compiler kann man sich dann die stubs ohne Aufwand generieren
lassen (oder man schreibt sich die paar Klassen / Funktionen selbst).
Verwendet man dann ein gängiges Netzwerkprotokoll wie etwa JSON, dann
kann man das ganze auch mit fremden Tools analysieren (und das ist ein
riesen Vorteil).
Bspw: https://github.com/nlohmann/json
Endianness lässt sich nicht (einfach) standard-konform detektieren (und
das ist auch gut so). Man sollte davon unäbhängig sein.
Es gibt ein paar Compiler-abhängige Wege:
https://sourceforge.net/p/predef/wiki/Endianness
Wilhelm M. schrieb:> Verstehe das Problem nicht: das un/marshalling in network-byte-order> macht man doch nur einmal in den send()/receive() Funktionen. Also an> einer Stelle.
Sehe ich genauso.
Achim S. schrieb:> Ja. Aber wenn ich z.B. 20 Stati habe, die jeweils 100 Elemente habe, die> zudem noch projektspezifisch (per #define) Ein- oder Ausgeblendet> werden,
Das ist kein C-Problem. Wenn du den Präprozessor für Features nutzt,
dann musst du auch mit den "Konsequenzen" leben. Also bspw. das
un/marshalling ebenfalls mit entsprechenden Präprozessor-Direktiven
versehen.
> Zudem kommen bei uns noch einige hundert Kommandos mit variablen> Parametern hinzu (die ein kleineres Problem sind) und teilweise> geschachtelte Strukturen. Reine gemischte Folgen von 16 und> 32-Bit-Werten würde ich per Script automatisieren können.
Häufig ist man tatsächlich mit einen "Präprozessor" besser bedient (aber
nicht unbedingt cpp), der entsprechenden C-Code erzeugen kann. Die
Schnittstelle muss also "formal" definiert werden (bspw. wie oben
gennannt mit ASN.1/BER).
Wilhelm M. schrieb:> Vincent H. schrieb:>> Wie in einigen Beiträgen bereits bemerkt wurde, ist die sauberste Lösung>> meiner Meinung nach die Nutzung von C++. Egal ob durch virtuelle>> Funktionen, Type Traits oder simple Overloads... da wäre viel möglich>> und die Anbindung an den bestehenden C Code wäre problemlos.>> Egal ob C / C++ / younameit als Sprache benutzt wird: wenn lose> gekoppelte Systeme miteinander kommunizieren können sollen, dann sollte> man eine Interfacedefinition haben. Mit einem entsprechenden> Interface-Compiler kann man sich dann die stubs ohne Aufwand generieren> lassen (oder man schreibt sich die paar Klassen / Funktionen selbst).
Wenn hier Legacy Code bereits auf mehreren Systemen läuft, dann wird man
im Nachhinein vermutlich nicht mehr serialisieren wollen.
Jan K. schrieb:> Funktioniert sowas auch auf (32 Bit arm) Mikrocontrollern?
Also mein Code sollte auf allen Plattformen, egal ob 8/16/32/64 Bit oder
LE/BE funktionieren. Interessant wird es bei Plattformen wo ein Byte
nicht 8 Bits sind, aber auch da könnte man es mit etwas komplizierterem
Shiften hinbekommen.
Wilhelm M. schrieb:> Verwendet man dann ein gängiges Netzwerkprotokoll wie etwa JSON, dann> kann man das ganze auch mit fremden Tools analysieren (und das ist ein> riesen Vorteil).
Dafür ist JSON wesentlich ineffizienter (De/Kodierung, Speicherverbrauch
durch ASCII-Darstellung).
Vincent H. schrieb:> Irgendwie sowas in die Richtung ->
Das ist ja schlimm, zur Laufzeit überprüfen ob es ein LE/BE System ist,
und dann falls nötig, tauschen? Wenn man mit Bitshifts arbeitet, braucht
man grundsätzlich keine Fallunterscheidung zwischen LE/BE, weil der
Compiler das automatisch richtig macht.
Vincent H. schrieb:> Wenn hier Legacy Code bereits auf mehreren Systemen läuft, dann wird man> im Nachhinein vermutlich nicht mehr serialisieren wollen.
Mein Ansatz wäre abwärtskompatibel, da man ihn so einstellen kann, dass
er zum bestehenden Protokoll passt.
Niklas G. schrieb:> Vincent H. schrieb:>> Irgendwie sowas in die Richtung ->> Das ist ja schlimm, zur Laufzeit überprüfen ob es ein LE/BE System ist,> und dann falls nötig, tauschen? Wenn man mit Bitshifts arbeitet, braucht> man grundsätzlich keine Fallunterscheidung zwischen LE/BE, weil der> Compiler das automatisch richtig macht.
Natürlich ist das schlimm, deshalb hab ich deinen Beitrag auch als
lesenswert markiert...
Aber den C-Compiler kann man schlecht mit constexpr und std::enable_if
füttern.
Vincent H. schrieb:> Natürlich ist das schlimm, deshalb hab ich deinen Beitrag auch als> lesenswert markiert...
Na danke ;-)
Vincent H. schrieb:> Aber den C-Compiler kann man schlecht mit constexpr und std::enable_if> füttern.
<< und >> versteht er aber:
1
uint16_tdecodeUint16(constuint8_t*raw/* 2 bytes, LE */){
2
return((uint16_t)raw[0])
3
|(((uint16_t)raw[1])<<8);
4
}
Es wird also ein Little-Endian-16bit-UInt aus einem array, welches zB.
per Netzwerk empfangen wurde, in ein uint16_t umgewandelt, welches dann
automatisch in der korrekten Byte-Order für die jeweilige Plattform ist.
Keine Fallunterscheidung, kein union, kein ntohs, kein undefiniertes
Verhalten, funktioniert immer solange der Prozessor 8-Byte-Chars hat.
Und zumindest der OP lehnt C++ wohl nicht ab:
Achim S. schrieb:> oh, C++ wäre möglich, wenn Du eine gute Idee für obige Beispiel hast.
Wilhelm M. schrieb:> Verwendet man dann ein gängiges Netzwerkprotokoll wie etwa JSON,
Irgendwie kann ich bei JSON, XML und allen anderen textbasierenden
Protokollen kein Problem mit Endianess erkennen. Das mag auch einer der
Gründe für ihre Existenz sein.
Achim S. schrieb:> Wir stellen sicher, dass die Strukturen auf allen Plattformen gleich> gepackt sind (padding-byte zwischen U8 und cnt).>> Im Code haben wir dann tausende Zeilen a la "Status47.u32 = xy;".>> Die meisten Datenfelder werden zwischen verschiedenen Geräte oder PC> ausgetauscht, in denen der selbe Source-Code läuft. Wir senden quasi per> (47, &Status47, sizeof(Status47)) problemlos, da Typ, struct, Padding,> Endian gleich sind.>> Jetzt haben wir erstmals ein *Big-Endian*-Gerät.
Solche Fehlentscheidungen beim Design eines Datenübertragungsprotokolls
haben schon einige Firmen in den Ruin getrieben.
MfG Klaus
Niklas G. schrieb:> Es wird also ein Little-Endian-16bit-UInt aus einem array, welches zB.> per Netzwerk empfangen wurde, in ein uint16_t umgewandelt, welches dann> automatisch in der korrekten Byte-Order für die jeweilige Plattform ist.
Und wenn das zufällig sogar die korrekt Darstellung für die
Zielplattform ist, dann wird der Compiler das sicherlich erkennen und
die beiden Bytes einfach rüber kopieren.
Klaus schrieb:> Wilhelm M. schrieb:>> Verwendet man dann ein gängiges Netzwerkprotokoll wie etwa JSON,>> Irgendwie kann ich bei JSON, XML und allen anderen textbasierenden> Protokollen kein Problem mit Endianess erkennen.
Hab ich doch auch nicht geschrieben, sondern das Gegenteil ...
> Das mag auch einer der> Gründe für ihre Existenz sein.
Genau!
Niklas G. schrieb:> Also mein Code sollte auf allen Plattformen, egal ob 8/16/32/64 Bit oder> LE/BE funktionieren.
Neben BE und LE gibt es auch noch Plattformen mit gemischter Endianess,
d.h. statt 3210 und 0123 gibt es dort 1032. Ein typischer Vertreter ist
z.B. PDP-11, aber wenn man bei einem klasssischen ARM-Prozessor (z.B.
ARM7/9) einen 32-Bit-Schreibzugriff an einer um zwei Byte versetzten
Adresse durchführt, erhält man ebenfalls solch einen Mid-Endian-Wert.
Letzteres ist aber ausdrücklich nicht zulässig, aber üblicherweise genau
so implementiert. Einige ältere ARM-Prozessoren (z.B. von Oki)
unterstützen überhaupt keine 8-Bit- oder 16-Bit-Schreibzugriffe, so dass
man hier auch gar nicht mit gepackten Strukturen arbeiten kann, außer
indem man sie vorher im Register aus Einzelbytes zusammensetzt.
Weiterhin solltem an auch beachten, dass es Prozessoren mit
umschaltbarer Endianess gibt. Bei ARM wird die Endianess entweder bei
der Synthese eingestellt oder per Konfigurationspin beim Systemstart.
Eine Umschaltung zur Laufzeit ist nicht vorgesehen. Einige
MIPS-Prozessoren kann man aber zur Laufzeit umschalten. Dies ist
insbesondere dann interessant, wenn man z.B. einen TCP/IP-Stack mit
Network Byte Order (also BE) einsetzen will, aber der Rest mit LE läuft,
da die meisten anderen binären Protokolle und Dateiformate auf LE
basieren.
Problematisch ist bei der ganzen Angelegenheit natürlich, dass der
Compiler auch in der Lage sein muss, die Endianess korrekt zu behandeln.
> Dafür ist JSON wesentlich ineffizienter (De/Kodierung, Speicherverbrauch> durch ASCII-Darstellung).
Deswegen ja auch ASN.1 o.ä..
Andreas S. schrieb:> Neben BE und LE gibt es auch noch Plattformen mit gemischter Endianess,> d.h. statt 3210 und 0123 gibt es dort 1032.
Okay, auch auf denen sollte der Code funktionieren.
Andreas S. schrieb:> Problematisch ist bei der ganzen Angelegenheit natürlich, dass der> Compiler auch in der Lage sein muss, die Endianess korrekt zu behandeln.
Richtig, darauf verlässt sich der Code. Das muss der Compiler aber laut
Standard können, sonst darf er sich nicht "C-Compiler" nennen. Der Code
shiftet die Bits an die richtige (logische!) Stelle im Integer, der
Compiler/Prozessor kümmert sich darum, dass das die richtige Stelle im
Speicher ist.
Klaus schrieb:> Solche Fehlentscheidungen beim Design eines Datenübertragungsprotokolls> haben schon einige Firmen in den Ruin getrieben.
Eine der schönsten Nachrichten aller Zeiten findet sich hier:
http://www.speicherguide.de/news/wd-stellt-arkeia-backup-produktlinie-ein-20935.aspx
Solch eine Scheißsoftware wie Arkeia Backup habe ich bislang selten
gesehen. Abgesehen davon, dass Arkeia ca. ein Promille der Dateien nicht
mitsichert, macht es auch Probleme bei Installationen mit gemischter
Endianess. Wir hatten früher(TM) Arkeia mit x86-basiertem Linux und
SPARC-basiertem Solaris eingesetzt. Magnetbänder wurden dann in der
Endianess des Backup-Hosts geschrieben und waren auf Systemen mit
anderer Endianess nicht lesbar.
Das allergrößte Knaller waren aber die Konfigurationsdateien. Nicht alle
Features konnte man per GUI konfigurieren, sondern musste die Dateien
dann händisch nachbearbeiten. Da die Entwicklung von Arkeia an zwei
Standorten (Frankreich, USA) stattfand, besaßen einige englischsprachige
Parameter die Auswahlmöglichkeiten YES/NO und andere OUI/NON. In der
Dokumentation fand man aber keinen Hinweis darauf, welche Sprache
jeweils erwartet wurde, und es gab auch keine Fehlermeldung bei falschen
Parameterwerten.
Wilhelm M. schrieb:> Hab ich doch auch nicht geschrieben, sondern das Gegenteil ...
Hab irgendwie die "fremden Tools" in den falschen Hals bekommen, sorry.
Niklas G. schrieb:> Dafür ist JSON wesentlich ineffizienter (De/Kodierung, Speicherverbrauch> durch ASCII-Darstellung).
Dafür ist JSON wesentlich effizienter was Portierung, Wartung
Zukunftssicherheit (auch des eigenen Arbeitsplatzes) angeht. Und ich
halte es auch nicht für effizient, einen 64 Bit Prozessor zu zwingen mit
8 Bit Integern zu rechnen, nur weil man "früher" mal ein Byte sparen
wollte. Da es z.B. JSON serializer/deserializer eigentlich für jede
höhere Sprache gibt, ist es schneller implementiert.
MfG Klaus
Niklas G. schrieb:> Richtig, darauf verlässt sich der Code. Das muss der Compiler aber laut> Standard können, sonst darf er sich nicht "C-Compiler" nennen.
Der Compiler kann/darf überhaupt nicht erkennen, wenn im laufenden
Programm die Endianess umgeschaltet wird bzw. ein Betriebssystem so
konfiguriert ist, dass bestimmte Prozesse mit BE und andere mit LE
ausgeführt werden. Folglich ist es die Aufgabe des Programmierers,
darauf zu achten, dass die entsprechenden Komponenten mit der richtigen
Endianess kompiliert werden. Und der Linker, der ja normalerweise streng
darauf achtet, dass nur Objektdateien mit identischer Endianess gelinkt
werden, muss auch dazu überredet werden, gemischte Binaries
zusammenzubauen.
Aber ähnliche Aufgabenstellungen hat man natürlich auch auf Systemen,
die z.B. die Umschaltung des Befehlssatzes zur Laufzeit unterstützen,
wie z.B. bei einigen Großrechern.
uint16_tdecodeUint16(constuint8_t*raw/* 2 bytes, LE */){
2
>return((uint16_t)raw[0])
3
>|(((uint16_t)raw[1])<<8);
4
>}
> Es wird also ein Little-Endian-16bit-UInt aus einem array, welches zB.> per Netzwerk empfangen wurde, in ein uint16_t umgewandelt, welches dann> automatisch in der korrekten Byte-Order für die jeweilige Plattform ist.> Keine Fallunterscheidung, kein union, kein ntohs, kein undefiniertes> Verhalten, funktioniert immer solange der Prozessor 8-Byte-Chars hat.
So wie ich das verstanden hab soll auf dem Big-Endian Gerät gelesen und
empfangen werden. Ich versteh nicht wie das ohne Fallunterscheidung mit
dem selben Code möglich sein soll?
Klaus schrieb:> Dafür ist JSON wesentlich effizienter was Portierung, Wartung> Zukunftssicherheit (auch des eigenen Arbeitsplatzes) angeht.
Jetzt muss man aber auch nicht zu viel Fantasie besitzen, wenn man in
einem Microcontroller Forum schreibt, um darauf zu kommen, dass evtl.
Bandbreiten (CAN, BLE, etc.) und Code-Größen ein Problem sein könnten.
Das es sich um ein lose gekoppeltes System handelt, habt ihr dem OP bis
jetzt einfach nur unterstellt. Wenn ich 2,3,4 Controller auf dem selben
Board habe, dann kann ich auch einfach annehmen, dass alle Controller
mit zu einander passender Firmware bestückt sind.
uint16_tdecodeUint16(constuint8_t*raw/* 2 bytes, LE */){
2
>>return((uint16_t)raw[0])
3
>>|(((uint16_t)raw[1])<<8);
4
>>}
>> Es wird also ein Little-Endian-16bit-UInt aus einem array, welches zB.>> per Netzwerk empfangen wurde, in ein uint16_t umgewandelt, welches dann>> automatisch in der korrekten Byte-Order für die jeweilige Plattform ist.>> Keine Fallunterscheidung, kein union, kein ntohs, kein undefiniertes>> Verhalten, funktioniert immer solange der Prozessor 8-Byte-Chars hat.>> So wie ich das verstanden hab soll auf dem Big-Endian Gerät gelesen und> empfangen werden. Ich versteh nicht wie das ohne Fallunterscheidung mit> dem selben Code möglich sein soll?
Die Protokoll-Ebenen ergibt sich aus dem Zugriff auf raw (raw[0] ist das
LSB, raw[1] das MSB; damit ist das little endian). Die Plattform-Ebene
ergibt sich aus dem schiften, dass auf jeder Plattform die Bits da hin
schiebt, wo sie hin gehören (das LSB nach unten, das MSB nach oben;
unabhängig davon, wo oben und unten im Speicher liegt).
Vincent H. schrieb:> Ich versteh nicht wie das ohne Fallunterscheidung mit> dem selben Code möglich sein soll?
Nicht? Ist doch ganz einfach... Wenn du in C "a * b" schreibst, dann
brauchst du doch auch keine Fallunterscheidung, ob die Plattform LE oder
BE ist - die Multiplikation funktioniert immer korrekt, dafür sorgt der
Compiler/Prozessor. Genau so ist das mit Bitshifts - die funktionieren
auch immer korrekt, denn letztendlich sind das ja auch nur
Multiplikationen mit 2er-Potenzen.
Das wird hier so genutzt:
Es werden mit raw[0] und raw[1] die beiden Bytes aus der Quelle gelesen.
raw[0] sind die unteren 8 Bits (da wir ja Little Endian übertragen
wollen), und raw[1] die oberen. Möchte man BE auf der Leitung verwenden,
tauscht man die 1 und 0.
Dann werden die beiden nach uint16_t gecastet. Das obere byte (raw[1])
wird um 8 bits nach links geshiftet. Links bedeutet, in Richtung der
höherwertigen Bits. Ob die höheren Bits jetzt letztendlich an einer
höheren (LE) oder niedrigen (BE) Adresse im Speicher stehen, ist für
diesen Shift vollkommen egal. Hauptsache die höherwertigen Bits aus
raw[1] landen an den höherwertigen Bits des uint16_t. Beide werden dann
noch verodert, um sie zu einem einzelnen uint16_t zusammenzusetzen. Das
Ergebnis wird zurückgegeben.
Vincent H. schrieb:> Gott sei Dank gibts C++. :)
Hm? Das ist alles C. Kennst du eine bessere Möglichkeit dafür in C++
(die nicht genau darauf basiert und es einfach nur besser kapselt, wie
meine obige Lösung)?
Torsten R. schrieb:> Jetzt muss man aber auch nicht zu viel Fantasie besitzen, wenn man in> einem Microcontroller Forum schreibt, um darauf zu kommen, dass evtl.> Bandbreiten (CAN, BLE, etc.) und Code-Größen ein Problem sein könnten.
Gründe für Fehlentscheidungen kann man immer finden. Sie ändern aber
nichts daran, daß die Entscheidung falsch war.
Für den TO: es gibt zwei Möglichkeiten, die Datenübertragung komplett
neu machen. Das heißt Überstunden und Urlaubssperre (manchmal auch
Todesmarsch genannt). Ist das geschafft, wird die Releasenummer
zweistellig und das Produkt hat Zukunft.
Oder man verkauft die Bestände ab und stellt das Produkt ein.
Alles andere verschiebt das Problem nur in die Zukunft. Die nächste
Prozessor/Compilergeneration läßt es wieder aus dem Keller
hervorkriechen.
MfG Klaus
@Achim S.
Um erst einmal die Frage zu beantworten, nein, ist mir leider nicht
bekannt.
Ich habe mich bei gleicher Problemstellung für den Einsatz eines Streams
entschieden, in welchen Daten nur in network byte order gespeichert oder
gelesen werden. Die Datenübertragung erfolgt dann nur mit den Daten des
Streams (eigentlich nur ein Puffer). Alle Strukturen bekommen dann einen
Aus- und Eingabegabeoperator (in der Strukturdefinition), was zumindest
die Möglichkeit, das man bei Änderungen etwas vergisst zu übertragen,
einschränkt. Ja, ich habe dann die Daten doppelt im Speicher und ich
muss c++ benutzen, aber ich muss mir über die Architektur keine Gedanken
mehr machen.
Ich verwende für sowas seit über 10 Jahren das ACE-Framework in C++. Das
hat tolle IO-Klassen für CDR (common data representation).
In dem Fall hier reicht aber hton und ntoh vollkommen. Einfach alles vor
dem Übertragen auf Networkorder und beim Empfangen wieder auf Host.
Klaus schrieb:> Oder man verkauft die Bestände ab und stellt das Produkt ein.
Meinst Du nicht, dass Du etwas übertreibst? Der OP erweitert ein Produkt
und stößt dabei auf eine lösbare Aufgabe. Das ist kein Grund gleich
aufzugeben und das normalste von der Welt.
Natürlich kann man vorausschauend zwischen benachbarten Microcontrollern
Glasfasern einplanen und darüber TCP/IP/SOAP/REST sprechen. Aber: dann
wird es halt teurer und später fertig (vielleicht sogar zu spät).
Niklas G. schrieb:> Vincent H. schrieb:>> Gott sei Dank gibts C++. :)> Hm? Das ist alles C.
Ich mein ja, die C++ Lösung ist viel schöner.
Niklas G. schrieb:> Kennst du eine bessere Möglichkeit dafür in C++> (die nicht genau darauf basiert und es einfach nur besser kapselt, wie> meine obige Lösung)?
Ich hätte einen Type Trait auf Basis meiner obigen "Hack" Funktion via
union eingeführt. Jedoch hab ich grad bemerkt, dass es für den
Lesezugriff zur Compilezeit auf ein union member, dass zuvor nicht
explizit geschrieben wurde, mittlerweile sogar eine eigene Fehlermeldung
gibt! (GCC 6.3.1)
Also nein, ich kenne keine bessere Möglichkeit.
Vincent H. schrieb:> Niklas G. schrieb:>> Vincent H. schrieb:>>> Gott sei Dank gibts C++. :)>> Hm? Das ist alles C.>> Ich mein ja, die C++ Lösung ist viel schöner.>>> Niklas G. schrieb:>> Kennst du eine bessere Möglichkeit dafür in C++>> (die nicht genau darauf basiert und es einfach nur besser kapselt, wie>> meine obige Lösung)?>> Ich hätte einen Type Trait auf Basis meiner obigen "Hack" Funktion via> union eingeführt. Jedoch hab ich grad bemerkt, dass es für den> Lesezugriff zur Compilezeit auf ein union member, dass zuvor nicht> explizit geschrieben wurde, mittlerweile sogar eine eigene Fehlermeldung> gibt! (GCC 6.3.1)
Wegen des Zugriffs auf nicht-aktive Elemente einer union gibt es
"gewisse" Unterschiede zwischen C und C++:
http://stackoverflow.com/questions/11373203/accessing-inactive-union-member-and-undefined-behavior/11996970
hth
Die nächstbeste Lösung wäre vielleicht in einer high level Scriptsprache
einen eigenen kleinen Codegenerator zu schreiben der mit einer möglichst
kompakten Definition des Binärprotokolls gefüttert wird und dann C-Code
ausspuckt mit structs und gettern und settern, dem union und was sonst
noch nötig ist.
Dann kannst Du an einer zentralen Stelle Deine Datenstrukturen
definieren und der ganze Boilerplate wird passend dazu ohne
Flüchtigkeitsfehler automatisch erzeugt.
Liebe Leser, vielen Dank für Eure Mühen. Ich möchte gerne als TO
exemplarisch auf einige Vorschläge und Anregungen eingehen:
Torsten R. schrieb:> Das es sich um ein lose gekoppeltes System handelt, habt ihr dem OP bis> jetzt einfach nur unterstellt.
Es ist sogar so, dass auf allen (relevanten) Teilnehmern der selbe
Quelltext läuft (vom HAL abgesehen). 200 gleichartige Geräte bilden
einen Gsamtprozess. Für jedes Projekt (und jede Version) wird der Code
verschieden zugeschnitten, aber dann ist er auf allen Geräten identisch.
Keine Fremdgeräte, kein Projektmix, kein Versionsmix. Nur 3 (oder mehr)
Plattformen und nun erstmals eine mit BigEndian.
Die Schlussfolgerungen "Wenn lose gekoppelt, dann ..." sind damit (m.E.)
gegenstandslos.
Welcher Overhead auch immer künftig notwendig sein wird, er wäre nicht
kleiner, wenn wir ihn (unnötig) schon all die Jahre mitgeschleppt
hätten.
Niklas G. schrieb:> Es wird Boost.Fusion zur Definition und Iteration der structs genutzt.
und
Andreas S. schrieb:> Ein typischer, mittlerweile reichlich angestaubter Vertreter ist z.B. ASN.1:
Vielen Dank für Deine Mühe. Uns ist es jedoch wichtig, dass die
Definitionen am Ende irgendwie in C-Syntax lesaber im Quelltext
erscheinen. Damit IDE (und ein paar andere Tools) ohne Verrenkung damit
klarkommen. Auch muss der Präprozessor zwingend eingebunden werden (zum
Tailoring)
Ein paar Zusätze, in Kommentar oder in der Nähe der Definition sind OK.
Also dann eher das wie
Bernd K. schrieb:> Die nächstbeste Lösung wäre vielleicht in einer high level Scriptsprache> einen eigenen kleinen Codegenerator zu schreiben der mit einer möglichst> kompakten Definition des Binärprotokolls gefüttert wird
oder
Torsten R. schrieb:> Habe ich selbst noch nicht verwendet, aber evtl. könnte das hier was für> Dich sein: https://developers.google.com/protocol-buffers/
Vielen Dank auch für die Ausführungen zur Konvertierung selber, die ist
für uns aber bisher unproblematisch. Eine native C/C++ - Lösung habe ich
bisher nur von Niklas G. gesehen. Vielleicht kannst Du, Jan, Deinen
Ansatz noch mal näher beschreiben?
Jan A. schrieb:> Alle Strukturen bekommen dann einen> Aus- und Eingabegabeoperator (in der Strukturdefinition), was zumindest> die Möglichkeit, das man bei Änderungen etwas vergisst zu übertragen,> einschränkt.
Herzliche Grüße und Dank an Alle für Eure Zeit.
Achim S. schrieb:> Vielen Dank für Deine Mühe. Uns ist es jedoch wichtig, dass die> Definitionen am Ende irgendwie in C-Syntax lesaber im Quelltext> erscheinen.
Das BOOST_FUSION_DEFINE_STRUCT-Makro produziert im Endeffekt ein simples
struct. Gute IDE's, wie eclipse, erkennen das und ermöglichen die
Auto-Completion darauf. Alternativ kann man auch die klassische
Lang-Form verwenden:
Kommt das selbe bei raus, ist aber etwas mehr Tipp-Arbeit. Beide
BOOST_FUSION-Makros produzieren allerdings C++ Code. Wenn du die Header,
welche die structs definieren, aber von C-Code aus inkludieren möchtest,
könntest du dir so in der Art behelfen:
Und oben MY_ADAPT_STRUCT statt BOOST_FUSION_ADAPT_STRUCT nutzen.
Somit "sieht" der C-Compiler nur das klassische struct. Aus C++
Quellcode heraus sind aber die Meta-Informationen sichtbar, sodass du
o.g. "deserialize" -Funktion nutzen kannst.
Achim S. schrieb:> Auch muss der Präprozessor zwingend eingebunden werden (zum> Tailoring)
Du meinst, du schaltest per #ifdef einzelne Member im struct ein/aus?
Eine einfache Lösung wäre, einfach mehrere struct-Varianten zu
definieren, und per #ifdef und typedef je zur gewünschten Variante ein
alias setzen. Es gibt vermutlich auch bessere Lösungen mit templates,
aber dafür muss ich mehr über die genaue Aufgabe wissen.