Post by Rainer WeikusatPost by Peter J. HolzerPost by Rainer WeikusatPost by Peter J. HolzerPost by Rainer WeikusatPost by Thomas Koenigsagt eine ganze Menge über gegenwärtige Praktiken der Software-
Entwicklung. Für sauber geschriebene Software sollte es keinen
Unterschied machen, ob der Prozessor big- oder little-endian
ist, und Power LE wurde extra eingeführt, damit diese unsaubere
Software leichter portiert werden kann...)
Hmm ... naja ... es macht immer dann einen Unterschied, wenn Binaerdaten
serialisiert oder deserialisiert werden muessen. Natuerlich kann man
fordern, dass in solchen Faellen immer eine explizite Byteanordnung
benutzt werden sollte, aber das 'muss' nur sein, weil Leute sich
historisch nicht auf eine einheitliche einigen konnten[*],
Ich bin mir nicht sicher, was Du hier mit "explizit" meinst. Tatsache
ist, dass die meisten Binär-Protokolle das Format eines Byte-Streams
vorgeben. Wenn man das so programmiert (also einen Byte-Stream liest und
schreibt) erhält man automatisch ein Programm, das von der Endianness
(und anderen Eigenschaften der Implementation, wie Typgrößen, Padding,
etc.) unabhängig ist.
Das Resultat einer Serialisierung ist immer ein Bytestream (eigentlich
ist es meistens ein Bitstream ...). Was hier "automatisch
,----
| Den Wert eines
| Ganzzahlobjektes mit einer Groesse > 1 kann man naiv serialisieren, dh
| die Bytes/ chars von der Anfangsaddresses bis zur Endaddresses. Dann ist
| das Ergebnis endian-abhaengig.
`----
Post by Peter J. HolzerWenn man davon ausgeht, dass man einem Bytestream in einem vorgegebenen
Format erzeugen möchte, und das ganz naiv in Standard-C runterkodiert,
landet man automatisch bei einer endian-unabhängigen Implementierung.
Im wirklichen Universum (von dem vielleicht schon mal jemand gehoert
hat), gibt es Dinge wie "Linux", die sowas allerdings mit structs
machen.
Was ist "sowas"? Wie Du vielleicht gesehen hast, verwendet mein Code
auch eine struct. Allerdings nur im RAM, nicht, um on-disk Strukturen
abzubilden. (Und in echtem Code hätte die struct 3 Felder weniger, weil
ich die im RAM gar nie brauche.)
Und ja, ich weiß, dass der Linux-Kernel-Code voll von structs ist, die
on-disk (oder on-wire) Strukturen abbilden. Das heißt nicht unbedingt,
dass ich das für sinnvoll erachten muss.
Als Applikationsprogrammierer kann es mir aber auch egal sein, wie der
Kernel
Post by Rainer WeikusatOder UNIX-Systemaufrufe, die int * uebernehmen. Hier ergibt sich
nichts "automatisch", es sei denn, man hat ausdruecklich die Absicht,
strikt konformes C zu schreiben. Das mag unter besonderen Umstaenden
sinnvoll sein, aber in vielen Faellen ist es das nicht.
Post by Peter J. Holzervoid writeu4(fp, uint_least32_t u) {
putc((u >> 24) & 0xFF);
putc((u >> 16) & 0xFF);
putc((u >> 8) & 0xFF);
putc((u >> 0) & 0xFF);
}
,----
| Oder man kann den Wert mithilfe von Bitoperationen manuell in Bytes
| zerlegen. Solange Sender und Empfaenger dafuer dieselbe Konvention
| benutzen, spielt die konkrete Host-Zahlenrepraesentation keine Rolle
| _aber_ eine Byte-Anordnung muss man sich dann natuerlich aussuchen.
`----
Das ist dann sowas, wie das da oben, und hier benutzt es big-endian weil
es das tut
Nein, "es" (also dieser Code) benutzt Big-Endian weil, die
File-Format-Spezifikation das so vorgibt.
Auf einer "typischen" Big-Endian-Plattform könnte man das zu
void writeu4(fp, uint_least32_t u) {
fwrite(&u, sizeof(u), 1, fp);
}
vereinfachen.
Und auf einer "typischen" Little-Endian-Plattform zu:
void writeu4(fp, uint_least32_t u) {
uint32_t t = (u & 0xFF) << 24 | (u & 0xFF00) << 8 | (u & 0xFF0000) >> 8 | (u & 0xFF000000 >> 24;
fwrite(&t, sizeof(t), 1, fp);
}
Der "typische" Code (wie ich ihn immer und immer wieder gesehen habe),
wäre dann sowas:
void writeu4(fp, uint_least32_t u) {
#if BIG_ENDIAN
unint32_t t = u;
#elif LITTLE_ENDIAN
uint32_t t = (u & 0xFF) << 24 | (u & 0xFF00) << 8 | (u & 0xFF0000) >> 8 | (u & 0xFF000000 >> 24;
#else
#error "Platform not supported"
#endif
fwrite(&t, sizeof(t), 1, fp);
}
Oder wahrscheinlich eher sowas:
void writeu4(fp, uint_least32_t u) {
#if SUNOS && SUNOS_SPARC || IRIX || AIX || __LINUX && __MIPS || BSD || OBSCURIX
unint32_t t = u;
#elif SUNOS && SUNOS_X86 || ULTRIX || __OSF1 || __LINUX && (__X86 || __MIPSEL) || MS_DOS
uint32_t t = (u & 0xFF) << 24 | (u & 0xFF00) << 8 | (u & 0xFF0000) >> 8 | (u & 0xFF000000 >> 24;
#else
#error "Platform not supported"
#endif
fwrite(&t, sizeof(t), 1, fp);
}
Und das ist der einfache Fall. Für ganze Structs wird es komplizierter
(vor allem, wenn man wie hier Padding loswerden muss, was gar nicht bei
allen Compilern möglich ist) oder wenn Bitfields vorkommen.
Post by Rainer Weikusatund eine Menge ueberfluessiger Operations aufgrund von
unguenstiger Programmierung.
Wie gesagt, die Implementation ist naiv. Ob sie ungünstig ist, kommt
darauf an. Sie ist jedenfalls ohne Kenntnisse des konkreten Compilers
les- und validierbar. Sie ruft für den ganzen Header 25 mal putc auf
statt einmal fwrite. Das braucht (fast) sicher mehr CPU-Zeit. Ob das
relevant ist, müsste man für eine konkrete Applikation messen.
Post by Rainer WeikusatPost by Peter J. HolzerErst wenn ich anfange zu überlegen; "Das RAM besteht ja auch aus Bytes,
kann ich diese Struct nicht mit einem einzigen (f)write rausschreiben,
statt unzählige Male putc aufzurufen?" handle ich mir diese
Plattform-Abhängigkeiten ein.
Das halte ich fuer einen Irrtum: C legt nicht fest, welche Breite ein
unsigned char hat, folglicherweise resultiert write4u in einem bitstream
unbekannter Laenge und der kann auf einer Maschinen, deren char-Breite
eine andere ist, nicht ohne weiteres gelesen werden.
"Bitstreams" sind irrelevant. Die Einheit, auf der heute alle
Fileformate und Protokolle aufbauen ist das Octet. Wenn eine
Univac-1100 (ich glaube, es gibt noch ein paar, die produktiv im Einsatz
sind) ein PNG-File liest, dann werden da ziemlich sicher nicht 9 Octets
des Files in 2 36-Bit-Worte gepackt, sondern jeweils ein Octet in einen
9-Bit unsigned char[1]. Der Code funktioniert also ziemlich sicher so wie
er ist, weil jede andere Implementation unsinnig wäre. Ich habe mit DSPs
gearbeitet, wo CHAR_BIT==32 war - aber das keine "hosted" Implementation
und es gab keine stdio.
Post by Rainer WeikusatIm Gegensatz dazu produziert die uint8_t-Variante eine portable
Serialisierung, deren Resultat man mit geeigneten Klimmzuegen in jeder
anderen Umgebung verarbeiten kann.
uint8_t muss es nicht geben. Dort wo es existiert, ist ist es identisch
mit unsigned char, Du hast also nichts gewonnen.
(Tatsächlich würde ich heute auf Architekturen, wo CHAR_BIT != 8 ist,
auch keine Rücksicht mehr nehmen, aber das ist ein anderes Thema. Worum
es mir hier geht, ist dass Fileformate eben eine Folge von Octets sind
und keine Folge von C-Typen (nicht mal primitive Typen wie short oder
long, geschweige denn komplexe Typen wie struct), und dass man das
berücksichtigen sollte.)
hp
[1] So steht's auch schon in K&R I, wenn auch für eine Honeywell 6000.
--
_ | Peter J. Holzer | Fluch der elektronischen Textverarbeitung:
|_|_) | | Man feilt solange an seinen Text um, bis
| | | ***@hjp.at | die Satzbestandteile des Satzes nicht mehr
__/ | http://www.hjp.at/ | zusammenpaßt. -- Ralph Babel