Forum: Compiler & IDEs Endianess beim Kopieren automatisieren


von A. S. (Gast)


Lesenswert?

Hallo,

unser C-Code für mehrere µC enthält viele structs. Exemplarisch:
1
typedef struct
2
{
3
uint8  u8;
4
uint16 cnt;
5
uint32 u32;
6
}tStatus47;
7
8
tStatus47 Status47;

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
case 47: 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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von A. S. (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

Wenn es keine primitiven DT mehr sind, benutzt man eben ein anderes 
Marshalling:

- base64
- reines ascii (etwa json)
- XDR
- ...

von Dr. Sommer (Gast)


Lesenswert?

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

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Andreas S. (Firma: Schweigstill IT) (schweigstill) Benutzerseite


Lesenswert?

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.

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

Wartbar wird es eigentlich nur, wenn Du einen Interface-Compiler (bspw. 
C++ <-> JSON) verwendest.

von Niklas G. (erlkoenig) Benutzerseite


Angehängte Dateien:

Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

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.

von Bernd K. (prof7bit)


Lesenswert?

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.

: Bearbeitet durch User
von Vincent H. (vinci)


Lesenswert?

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 ->
1
bool isLittleEndian()
2
{
3
  union
4
  {
5
    uint16_t i;
6
    uint8_t b[2];
7
  } u { 0x1 };
8
9
  return (u.b[0] == 0x1);
10
}

von Kaj (Gast)


Lesenswert?

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
6
  undefiniertem Verhalten enden soll.
7
8
Weil das so im Standard steht.

Beitrag "Re: Shift vs. Union - Was ist effizienter?"

Beitrag "Re: Union vs. Bit schieben"
1
Bitschieben ist eine wohldefinierte Operation und funktioniert auf allen 
2
Plattformen gleich. Union-Zugriffe über verschiedene Member sind gemäß 
3
C-Standard undefiniertes Verhalten

von Vincent H. (vinci)


Lesenswert?

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

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


Lesenswert?

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

von Wilhelm M. (wimalopaan)


Lesenswert?

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

von Mikro 7. (mikro77)


Lesenswert?

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

von Jan K. (jan_k)


Lesenswert?

Funktioniert sowas auch auf (32 Bit arm) Mikrocontrollern?

von Vincent H. (vinci)


Lesenswert?

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.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

: Bearbeitet durch User
von Vincent H. (vinci)


Lesenswert?

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.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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_t decodeUint16 (const uint8_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.

: Bearbeitet durch User
von Klaus (Gast)


Lesenswert?

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

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


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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!

von Kaj (Gast)


Lesenswert?

Niklas G. schrieb:
> solange der Prozessor 8-Byte-Chars hat.
Sollte wohl Bit heisse :P

von Andreas S. (Firma: Schweigstill IT) (schweigstill) Benutzerseite


Lesenswert?

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

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

von Andreas S. (Firma: Schweigstill IT) (schweigstill) Benutzerseite


Lesenswert?

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.

von Klaus (Gast)


Lesenswert?

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

von Andreas S. (Firma: Schweigstill IT) (schweigstill) Benutzerseite


Lesenswert?

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.

von Vincent H. (vinci)


Lesenswert?

Niklas G. schrieb:
> << und >> versteht er aber:
>
1
uint16_t decodeUint16 (const uint8_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?

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


Lesenswert?

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.

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


Lesenswert?

Vincent H. schrieb:
> Niklas G. schrieb:
>> << und >> versteht er aber:
>>
1
uint16_t decodeUint16 (const uint8_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).

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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.

von Vincent H. (vinci)


Lesenswert?

Und wie soll encodeUint16(const uint8_t* raw) aussehen?

: Bearbeitet durch User
von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Vincent H. schrieb:
> Und wie soll encodeUint16(const uint8_t* raw) aussehen?
1
uint8_t* encodeUint16( uint8_t* raw, uint16_t v )
2
{
3
  raw[ 0 ] = v & 0xff;
4
  raw[ 1 ] = ( v & 0xff00 ) >> 8;
5
6
  return raw + sizeof( v );
7
}

von Vincent H. (vinci)


Lesenswert?

Achja danke, auf die Idee, dass man die Daten dann natürlich auch "roh" 
zurückgeben muss kam ich nicht...

Gott sei Dank gibts C++. :)

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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

von Klaus (Gast)


Lesenswert?

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

von Jan A. (fathi69)


Lesenswert?

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

von Chris F. (chfreund) Benutzerseite


Lesenswert?

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.

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


Lesenswert?

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

von Vincent H. (vinci)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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

von Bernd K. (prof7bit)


Lesenswert?

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.
1
from MyCodegen import *
2
3
Struct("status47",
4
    Field("u8",  typ.U8),
5
    Field("cnt", typ.U16),
6
    Field("u32", typ.U32)
7
)
8
9
Struct("status48",
10
    Field("foo", typ.U16),
11
    Field("bar", typ.S16),
12
    Field("baz", typ.S32)
13
)
14
15
generate("protocol.h")

wird zu
(ungetestet, grad so hingeschrieben)
1
typedef struct {
2
    uint8_t u8;
3
    uint8_t _pad0;  /* warning: shoddy alignment, padding inserted */
4
    uint16_t cnt;
5
    uint32_t u32;
6
} status47_t;
7
8
static inline uint8_t Status74_get_u8(status47_t* this) {
9
    return this->u8;
10
}
11
12
static inline uint16_t Status74_get_cnt(status47_t* this) {
13
    return ntohs(this->cnt);
14
}
15
16
static inline uint32_t Status74_get_u32(status47_t* this) {
17
    return ntohl(this->u32);
18
}
19
20
static inline void Status74_set_u8(status47_t* this, uint8_t value) {
21
    this->u8 = value;
22
}
23
24
static inline void Status74_set_cnt(status47_t* this, uint16_t value) {
25
    this->cnt = htons(value);
26
}
27
28
static inline void Status74_set_u32(status47_t* this, uint32_t value) {
29
    this->u32 = htonl(value);
30
}
31
32
typedef struct {
33
    uint16_t foo;
34
    int16_t bar;
35
    int32_t baz;
36
} status48_t;
37
38
[und so weiter]
39
40
typedef union {
41
    status47_t status47;
42
    status48_t status48;
43
} protocol_buffer_t;

Dann kannst Du an einer zentralen Stelle Deine Datenstrukturen 
definieren und der ganze Boilerplate wird passend dazu ohne 
Flüchtigkeitsfehler automatisch erzeugt.

: Bearbeitet durch User
von A. S. (Gast)


Lesenswert?

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.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

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:
1
struct tStatus47 {
2
  uint8_t u8;
3
  uint16_t cnt;
4
  uint32_t u32;
5
  uint8_t u8_2;
6
}
7
BOOST_FUSION_ADAPT_STRUCT(tStatus47, u8, cnt, u32, u8_2)
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:
1
#ifdef __cplusplus
2
#define MY_ADAPT_STRUCT(name,...) BOOST_FUSION_ADAPT_STRUCT(name,__VA_ARGS__)
3
#else
4
#define MY_ADAPT_STRUCT(name,...) 
5
#endif
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.

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.