Protokoll: Pakete mit automatischer Serialisierung
-
Hallo Leute!
Ich implementiere gerade ein auf TCP aufbauendes Protokoll. Dieses Protokoll definiert einige Pakete, die jeweils einen Namen, eine Richtung (Client->Server, Server->Client, Two-Way), eine Nummer (z.B. 0x25) und ein paar Attribute haben.
Ich moechte nun mittels Makros das definieren dieser Pakete so erleichtern, dass man einerseits automatische Serialisierung bekommt, andererseits moechte ich auf die Attribute per Name zugreifen koennen.
Fuer die automatische Serialisierung habe ich bereits eine Loesung. Die Grundidee ist, dass man das Paket als std::tuple repraesentiert (Code nur schematisch, habe den richtigen gerade nicht zur Hand):
template <typename DirectionTag, UInt8 Id, typename... Attributes> struct Packet { static constexpr auto ID = Id; std::tuple<Attributes...> attributes; }; struct ClientTag; struct ServerTag; struct TwoWayTag; #define EXPAND(...) __VA_ARGS__ #define DEFINE_PACKET(dir, name, id, attr) typedef Packet<dir##Tag, id, EXPAND attr> name##Packet;
Eine Paketdefinition koennte dann wie folgt aussehen:
DEFINE_PACKET(TwoWay, Foo, 0x20, (int, double, std::string))
Will ich nun aber auf diese Attribute zugreifen, so muss ich das ueber einen Index tun. Das fuehrt aber dazu, dass man schnell mal den falschen Index erwischt und der Code u.U. trotzdem kompiliert.
Idealerweise haette ich gerne, dass die Benutzung des Makros wie folgt aussieht:
DEFINE_PACKET(TwoWay, Foo, 0x20, (int x, double y, std::string s))
Das Paket soll einfach diese Member haben und gleichzeitig will ich irgendeine Form von Typinformation haben, mit der ich serialisieren kann, z.B. eine Liste an Member-Pointern. Leider habe ich keine Ahnung, ob und wie ich das mit Makros hinbekommen koennte.
Hat jemand eine Idee? Boost.PP waere auch okay, denn Boost verwende ich sowieso schon.
Gruesse,
Der Kellerautomat
-
Disclaimer: Dieser Beitrag enthaelt nicht das gesuchte Makro.
Allgemein: Schon mal Anregungen bei proto buffer geholt?
Aber normalerweise ist es so, dass bei einem benutzerdefiniertem Typ auch die Serialisierungsroutine benutzerdefiniert ist. Mit Reflektions waere es einfacher.
Der naechste Punkt: Warum sollen die Attributtypen nach aussen hin sichtbar sein. Ich kann Pakete mit unterschiedlichem Inhalt in keinen Container packen. Deinem Paket zu hause siehst du auch nicht an, was drin ist. Die Kennzeichnung uebernimmt dene ID.
-
Kellerautomat schrieb:
Idealerweise haette ich gerne, dass die Benutzung des Makros wie folgt aussieht:
DEFINE_PACKET(TwoWay, Foo, 0x20, (int x, double y, std::string s))
Mit der Syntax wird das vermutlich schwierig.
Leicht modifiziert könnte man aber vermutlich mit Boost.PP was hintricksen:// v------ Anzahl der Member DEFINE_PACKET(TwoWay, Foo, 0x20, 3, ((int, x), (double, y), (std::string, s)))
Alternative Präprozessor-Variante: eigene .h Files für die Structs machen, und mehrfach inkludieren, mit unterschiedlich definierten Makros. Dann könnte man es ca. so schreiben:
BEGIN_PACKET(TwoWay, Foo, 0x20) PACKET_MEMBER(int, x) PACKET_MEMBER(double, y) PACKET_MEMBER(std::string, s) END_PACKET()
Oder du machst das was man üblicherweise macht, und schreibst halt eine(De-)Serialisierungs Funktion pro
struct
.
Das ist natürlich mehr Wartungsaufwand wenn man öfter was an den Paketen ändert. Und auch genau die Art von Wartungsaufwand wo man leicht vergessen kann es zu machen, und der Fehler dann erst beim Testen auftritt, weil der Compiler es nicht finden kann....Andrerseits ist die Frage wie oft man wirklich Änderungen an diesen Strukturen macht. Bei Netzwerkprotokollen von z.B. Spielen könnte ich mir vorstellen dass da sehr oft rumgeschraubt wird, da könnte es sich vielleicht wirklich auszahlen so üble Präprozessor-Geschichten zu verwenden.
Oder du bastelst dir ein kleines Programm das aus einem Datenfile (XML, JSON, ...) die passenden Source-Files erstellt, und bindest dieses kleine Programm als Code-Generator in deinen Build-Prozess ein. Sowas ist ziemlich easy in Java, JavaScript, C#, PHP, Perl, ... zu schreiben. Bzw. generell in allen Sprachen die einen einfach und angenehm zu verwendenden XML/JSON/... Parser mitbringen.
Solche Code-Generator sind auch nicht ganz unüblich. Quazal hat z.B. so einen Code-Generator/Precompiler für die Entities. Dort kann man dann auch angeben ob und in welcher Weise Dead-Reckoning gemacht werden soll und noch einige andere Dinge.
Je nachdem an was für einem Projekt du arbeitest, und was das System dann noch alles können soll, könnte das ein grosser Vorteil sein.Mit C++ Boardmitteln stösst man da schnell an recht harte Grenzen wenn man halbwegs leserlichen Code haben möchte der nicht voll mit Redundanzen ist.
Seinen eigenen Code-Generator um das eine oder andere Feature zu erweitern ist dagegen relativ einfach.
-
hustbaer schrieb:
Kellerautomat schrieb:
Idealerweise haette ich gerne, dass die Benutzung des Makros wie folgt aussieht:
DEFINE_PACKET(TwoWay, Foo, 0x20, (int x, double y, std::string s))
Mit der Syntax wird das vermutlich schwierig.
Schwierigkeiten macht dabei der Name. Es ist allerdings relativ leciht, an den Typen heranzukommen. Wenn man bestimmte plausible Annahmen über das Layout (ABI) machen kann (Annahme: Layout hängt nur von Größe und Alignment der Elemente ab), wäre eine Lösung denkbar.
-
Das Paket soll einfach diese Member haben und gleichzeitig will ich irgendeine Form von Typinformation haben, mit der ich serialisieren kann, z.B. eine Liste an Member-Pointern. Leider habe ich keine Ahnung, ob und wie ich das mit Makros hinbekommen koennte.
Ich glaube, ich missverstehe dich... aber trifft das hier es nicht:
#include <cstdint> template< typename DirectionTag, uint8_t Id> struct Packet { static constexpr auto ID = Id; using Tag = DirectionTag; }; struct ClientTag; struct ServerTag; struct TwoWayTag; #include <boost/preprocessor/tuple/elem.hpp> // Edit: Habe die Header angepasst. #include <boost/preprocessor/tuple/size.hpp> #include <boost/preprocessor/repetition/repeat.hpp> #define MEMBER_DEFINER(z, n, attr) BOOST_PP_TUPLE_ELEM( BOOST_PP_TUPLE_SIZE(attr), n, attr ) ; #define DEFINE_PACKET( dir, name, id, tuple ) \ struct name##Packet : Packet<dir, id> \ { \ BOOST_PP_REPEAT( BOOST_PP_TUPLE_SIZE(tuple), \ MEMBER_DEFINER, \ tuple ); \ } #include <string> DEFINE_PACKET(TwoWayTag, Foo, 0x20, (int x, double y, std::string s)); int main() { }
eine Liste an Member-Pointern
Was genau soll den die Liste so halten?
void
-Pointer? Mit Type-Erasure ginge was.Schwierigkeiten macht dabei der Name.
Ich glaube, ich verstehe nicht, worum es gerade geht... wohl kaum darum, dass der Name der Membervariablen bei den Makros Schwierigkeiten macht...
Wenn man bestimmte plausible Annahmen über das Layout (ABI) machen kann (Annahme: Layout hängt nur von Größe und Alignment der Elemente ab), wäre eine Lösung denkbar.
Wovon sprichst du gerade?
-
@camper
Hmmmmmmm...
Man könnte vielleicht nen parallelen, Layout-kompatible Typen basteln, der fixe Namen für die Member verwendet, ala so:struct Foo { int i; string s; }; struct FooCompatible { int member_1; string member_2; };
Und mit
offsetof(FooCompatible, member_{N})
kommt man dann an den Offset.Dann könnte man mit Foo-Basisadresse + FooCompatible-Offset einen Zeiger "in" das Foo Objekt erstellen, und damit die ABI-Abhängigkeit vermeiden.
Nur: darf man das? Fertiges Makro zur "Umkehrung" von
offsetof
gibt's ja nicht, oder? Oder darf man hier tatsächlich mit*reinterpret_cast<MemberType*>(static_cast<char*>(&foo) + MemberOffset)
arbeiten?ps: Aaaaah, ich glaube ich hab dein "leicht, an den Typen heranzukommen" falsch verstanden. Du meintest vermutlich nicht über Präprozessor-Magie sondern einfach über
decltype
bzw. nen Aufruf einer Template-Funktion. Dann scheidet meineLösungIdee natürlich aus, da hier ja alle Member-Typen im Präprozessor-Makro ein zweites Mal benötigt würden -- ohne angehängten Namen. Da manFooCompatible
im Makro sonst ja nicht definieren kann.
-
dass man einerseits automatische Serialisierung bekommt, andererseits moechte ich auf die Attribute per Name zugreifen koennen.
Jetzt verstehe ich, warum der Name schwierigkeiten macht....
Und worüber camper und hustbaer reden.
Ist eine Alternative Syntax a laDEFINE_PACKET(TwoWayTag, Foo, 0x20, ((int, x), (double, d), (std::string, s)) );
Verkraftbar? Das ginge dann. Sonst, nur mit Leerzeichen dazwischen, wird's schwer.
-
Ich baue etwas Ähnliches.
Ich habe hierbei nicht die C++-Typen in den Mittelpunkt gestellt, sondern portable Formate. So ein Format ist beispielsweise "Big Endian 32-Bit", kurz
be32
. Ein einzelner Integer wird beispielsweise mitbe32().serialize(sink, 123)
serialisiert. Für die andere Richtung hat jedes Format eine Methodedeserialize
. Die meisten Formate unterstützen eine Vielzahl von C++-Typen.bytes
kann mitvector<char>
,string
etc umgehen. Der Template-Parameter vonbytes
gibt das Format für die Länge an, die vor den Bytes steht. Beivector
ist der erste Parameter die Länge und der zweite das Element.
Weitere bisher realisierte Formate sindle
(8 bis 64),bool
,array
,map
,pair
,pod
,optional
undunique_ptr
. Polymorphe Objekte werden noch nicht unterstützt.Eine Struktur wird als Folge von Elementen dieser Formate definiert. Ein geeigneter Typ für die Darstellung in C++ wird automatisch gewählt oder kann vom Benutzer überschrieben werden.
struct simple_struct { RXN_REFLECT((SZN_AUTO_MEMBERS) (SZN_ITERATE), (a, be16) (int), //C++-Typ kann angegeben werden, ansonsten wird einer automatisch gewählt (b, be32), (c, bytes<be8>), (d, bytes<be8>) (std::string), (v, vector<be8, be16>) ) }; //nach Präprozessor: struct simple_struct { int a; std::uint32_t b; std::vector<char> c; std::string d; std::vector<std::uint16_t> v; template <class Visitor> void iterate(Visitor &visitor) { visitor.template accept<be16>(a); visitor.template accept<be32>(b); visitor.template accept<bytes<be8>>(c); visitor.template accept<bytes<be8>>(d); visitor.template accept<vector<be8, be16>>(v); } //.. noch einmal für const };
Es wird eine Methode generiert, mit der man über die Elemente iterieren kann. Ein Besucher, der die Elemente serialisiert, sieht ungefähr so aus:
struct serializing_visitor { szn::sink &sink; template <class Format, class Element> void accept(Element &element) const { Format().serialize(sink, element); } };
RXN_REFLECT
ist eher allgemein gehalten und mit Boost Preprocessor realisiert. Es ruft jeden der übergebenen Generatoren mit allen Feldern auf. Zuerst definiertSZN_AUTO_MEMBERS
die Elemente und dannSZN_ITERATE
dieiterate
-Methoden.
Die umschließende Struktur definiert man selbst für eine größere Flexibilität (typedef
, Konstruktoren, andere Methoden usw).
-
@Sone
Er will über das Makro ja nicht nur die struct definieren, sondern auch eine dazupassende serialize/deserialize Funktion.
Und dazu musst du die Member-Liste nochmal wiederholen, aber halt nur die Namen.Oder eine Möglichkeit finden den Namen zu extrahieren.
Oder Typ und Name im Makro-Aufruf trennen, so wie in meinem Vorschlag.
EDIT: OK, hat sich erledigt während ich gelesen/getippt habe
-
TyRoXx schrieb:
struct simple_struct { RXN_REFLECT((SZN_AUTO_MEMBERS) (SZN_ITERATE), (a, be16) (int), //C++-Typ kann angegeben werden, ansonsten wird einer automatisch gewählt (b, be32), (c, bytes<be8>), (d, bytes<be8>) (std::string), (v, vector<be8, be16>) ) };
Bekommst du bei
(v, vector<be8, be16>)
kein Problem mit dem Komma zwischenbe8
undbe16
? Falls nicht, wie löst man das?Und verträgt sich diese Lösung mit MSVC?
EDIT: Huch! Ich sehe gerade MSVC kann ab 2005 variadic Makros. Wusste ich gar nicht. Damit erübrigt sich die Frage wohlEDIT 2: Mann, so viel sinnvoll gemeinte aber bei näherer Betrachtung dann doch sinnlose Beiträge wie hier schreib ich sonst nicht. Hoffe ich zumindest. Erst denken, dann posten. Man vergebe mir bitte.
-
Wow, danke fuer die vielen Antworten!
knivil schrieb:
Aber normalerweise ist es so, dass bei einem benutzerdefiniertem Typ auch die Serialisierungsroutine benutzerdefiniert ist. Mit Reflektions waere es einfacher.
Fuer benutzerdefinierte Typen muss man das auch. Pakete sind aber nunmal fest vorgegeben und es gib zig verschiedene, dafuer will ich keine Serialisierungsroutinen schreiben muessen.
knivil schrieb:
Der naechste Punkt: Warum sollen die Attributtypen nach aussen hin sichtbar sein. Ich kann Pakete mit unterschiedlichem Inhalt in keinen Container packen. Deinem Paket zu hause siehst du auch nicht an, was drin ist. Die Kennzeichnung uebernimmt dene ID.
Das brauche ich nicht. Sobald das Paket hereinkommt, kenne ich den Typen und kann es direkt bearbeiten. Die Paketklassen dienen einfach nur als Daten-structs.
@hustbaer: Den Ansatz mit 1 File pro Paket hatte ich schon, finde ich nicht gut. Fuer jedes Paket ein File includen zu muessen ist der reinste Horror.
Es handelt sich um das Protokoll von Minecraft, die Pakete werden sich also stetig aendern. Wenn moeglich, moechte ich bei C++ bleiben.Sone schrieb:
Das Paket soll einfach diese Member haben und gleichzeitig will ich irgendeine Form von Typinformation haben, mit der ich serialisieren kann, z.B. eine Liste an Member-Pointern. Leider habe ich keine Ahnung, ob und wie ich das mit Makros hinbekommen koennte.
Ich glaube, ich missverstehe dich... aber trifft das hier es nicht:
...
Nein, das trifft es eigentlich sehr gut. Nur brauche ich eben noch Serialisierung dazu.
Sone schrieb:
eine Liste an Member-Pointern
Was genau soll den die Liste so halten?
void
-Pointer? Mit Type-Erasure ginge was.Na Member-Pointer, also die Dinger mit ::*. Ich haette mir vorgestellt, jedem Paket eine statische Liste an Member-Pointern zu verpassen.
Sone schrieb:
dass man einerseits automatische Serialisierung bekommt, andererseits moechte ich auf die Attribute per Name zugreifen koennen.
Jetzt verstehe ich, warum der Name schwierigkeiten macht....
Und worüber camper und hustbaer reden.
Ist eine Alternative Syntax a laDEFINE_PACKET(TwoWayTag, Foo, 0x20, ((int, x), (double, d), (std::string, s)) );
Verkraftbar? Das ginge dann. Sonst, nur mit Leerzeichen dazwischen, wird's schwer.
Wenn nicht anders geht. Schoener waere es natuerlich wie von mir gezeigt. Ich dachte, du liebst Challenges
@TyRoXx: Sieht interessant aus, danke! Wenn ich etwas mehr Zeit habe, werde ich mir deine Lib genauer ansehen.
Insebesondere auf einen Visitor bin ich noch gar nicht gekommen.
-
Ich dachte, du liebst Challenges
Das tue ich! Ich habe schon viel darüber nachgedacht. Die beste Variante wäre, einen durch ein Leerzeichen getrennten Makro-Parameter P Irgendwie zu konkatenieren:
#define GetSecond(N) ... ## N ## ... GetSecond(int a) // wird zu a
Nur ist die entscheidende Frage wie...
-
EIgentlich brauchen wir den Namen gar nicht.
Wenn das Ergebnis so aussehen kann
DEFINE_PACKET((int x, double y, std::string s)) =>template <typename T> struct fun_arg_; template <typename T, typename A> struct fun_arg_<T(A)> { using type = A; }; template <typename T> using fun_arg = typename fun_arg<T>::type; struct PacketXYZ { union { int x; std::remove_pointer<fun_arg<decltype(void(int x[1]))>>::type _1; }; union { double y; std::remove_pointer<fun_arg<decltype(void(double y[1]))>>::type _2; }; union { std::string s; std::remove_pointer<fun_arg<decltype(void(std::string s[1]))>>::type _3; }; };
Die Serialisierungsfunktion kann dann _1, _2 usw. verwenden.
Das Makroschreiben überlasse ich anderen. Funktioniert nicht mit Arrays.
-
@TyRoXx: MessagePack
@Kellerautomat: proto buffer wenn Pakete auch als Typ vorliegen, ansonsten MessagePack.Es sieht so in etwa in Code aus:
packer p( Byte blob als paket) p.pack(irgend ne id).pack(123.f).pack("jeha").pack(128). ....
Kann gerne mit Streamoperatoren verfeinert werden. Dazu bedarf es auch keines extra Typs um Pakete zu spezifizieren, sondern nur, wie sie gepackt und entpackt werden, d.h. welche Member und in welcher Reihenfolge.
Ich bin meist nie fuer Selbstbauen wenn die Bibliothek alzuviel mehr anbietet. Da man Spezifikation und Dokumentation auch machen muss. Obengenannte waren Kandidaten fuer Serialisierung von RPC-Aufrufen mittels ZeroMQ. Habe mich fuer MessagePack mit eigener Implementation entschieden (keine externen Tools, kein Framework).
-
knivil schrieb:
@TyRoXx: MessagePack
Damit hat man wieder die Duplizierung beim Lesen und Schreiben. Meine Strukturen sollen unter anderem genau das verhindern. Außerdem wird Platz verschwendet, weil die Datentypen mit gespeichert werden.
Man könnte meine Bibliothek allerdings auch so umbauen, dass alternative Backends möglich sind, zum Beispiel MessagePack. Oder JSON. Oder ganz andere Einsatzgebiete wie SQL.
-
Kellerautomat schrieb:
@hustbaer: Den Ansatz mit 1 File pro Paket hatte ich schon, finde ich nicht gut. Fuer jedes Paket ein File includen zu muessen ist der reinste Horror.
Du kannst ja trotzdem viele structs in ein File packen.
Nur musst du das Ding irgendwo mehrfach includen. Lässt sich aber auch relativ elegant lösen:// Packets1Def.h: BEGIN_PACKET(SomePacket) PACKET_MEMBER(...) PACKET_MEMBER(...) PACKET_MEMBER(...) END_PACKET() BEGIN_PACKET(OtherPacket) PACKET_MEMBER(...) PACKET_MEMBER(...) PACKET_MEMBER(...) END_PACKET() BEGIN_PACKET(YetAnotherPacket) PACKET_MEMBER(...) PACKET_MEMBER(...) PACKET_MEMBER(...) END_PACKET() // ... // Packets2Def.h: // s.o. // IncludePacketDefinition.h: #define BEGIN_PACKET ... Passend zum Erstellen der Klassendeklaration ... #define PACKET_MEMBER ... Passend zum Erstellen der Klassendeklaration ... #define END_PACKET ... Passend zum Erstellen der Klassendeklaration ... #include PACKET_DEFINITION #undef BEGIN_PACKET #undef PACKET_MEMBER #undef END_PACKET #define BEGIN_PACKET ... Passend zum Erstellen der Serialisierungs-Funktion ... #define PACKET_MEMBER ... Passend zum Erstellen der Serialisierungs-Funktion ... #define END_PACKET ... Passend zum Erstellen der Serialisierungs-Funktion ... #include PACKET_DEFINITION #undef BEGIN_PACKET #undef PACKET_MEMBER #undef END_PACKET #define BEGIN_PACKET ... Passend zum Erstellen der Deserialisierungs-Funktion ... #define PACKET_MEMBER ... Passend zum Erstellen der Deserialisierungs-Funktion ... #define END_PACKET ... Passend zum Erstellen der Deserialisierungs-Funktion ... #include PACKET_DEFINITION #undef BEGIN_PACKET #undef PACKET_MEMBER #undef END_PACKET #undef PACKET_DEFINITION // Packets.h: #pragma once #define PACKET_DEFINITION "Packets1Def.h" #include "IncludePacketDefinition.h" #define PACKET_DEFINITION "Packets2Def.h" #include "IncludePacketDefinition.h" // Foo.cpp: #include "Packets.h"
So in der Richtung.
Und falls in den Packet-Definition-Files noch andere Dinge stehen sollen die nicht wiederholt werden können, dann lässt sich das ja relativ einfach über ein weiteres #define lösen:
// Packets1Def.h: #ifdef NORMAL_STUFF // nur im 1. Durchgang definiert struct SomeNormalStruct {...}; #endif BEGIN_PACKET(SomePacket) PACKET_MEMBER(...) PACKET_MEMBER(...) PACKET_MEMBER(...) END_PACKET() //...
-
Nun, es gibt Vor- und Nachteile fuer alle Varianten. Einige Kritikpunkte, die mich als Entwickler davon abhalten wuerde es zu benutzen als auch es zu entwickeln. Reflektions bzw. deren Nachbau ist in deiner Variante sehr intrusiv. Durch das Makro ist nicht nur Serialisierung sondern auch die ganze Klassendefinition betroffen. Auch scheint das Framework sehr umfangreich zu sein, eine Entwicklerdoku fuer interne Zwecke als auch die Qualitaetssicherung/Tests scheint sehr aufwendig zu werden.
Aus deinem Beispiel sehe ich kaum Vorteile bei der Benutzung, ich habe Format, Senke (was bei mir der Byteblob ist) und den Wert. Eine kurze Gegenueberstellung:
szn::be32().serialize(sink2, s); fprintf(fd, "%d", s)
Beide Zeilen enthalten die genannten Elemente. Nun kann wenig weggelassen werden. Ich wuerde am Format sparen und die Umwandlung der Endianess oder Strings mit UTF-Codierung einer Policy ueberlassen wenn noetig.
Außerdem wird Platz verschwendet, weil die Datentypen mit gespeichert werden.
Aus gegebenen Anlass habe ich viel drueber nachgedacht. Natuerlich wird Platz verschwendet. Die "Reflektion" steckt in der Serialisierung und nicht in der Klasse, dafuer auf Primitive beschraenkt. Weiterhin ist die Serialisierung wertorientiert, in deinem Beispiel werden immer 4 Byte fuer die Codierung benutzt, auch wenn es sich um kleine Zahlen handelt. Kommt halt auf Narichtengroesse und Einsatzzweck an. Ich habe mich fuer groessere Nachrichten entschieden. Komprimierung mit lz4 hat sich nicht ausgezahlt.
Damit hat man wieder die Duplizierung beim Lesen und Schreiben.
Ich weiss nicht was du damit meinst, aber ich habe deine Implementation nur ueberflogen.
-
@hustbaer: Den Ansatz finde ich super. Man muss zwar bei den Definitionen ein wenig mehr tippen, dafuer ist alles um einiges flexibler.
-
knivil schrieb:
Aus deinem Beispiel sehe ich kaum Vorteile bei der Benutzung, ich habe Format, Senke (was bei mir der Byteblob ist) und den Wert. Eine kurze Gegenueberstellung:
szn::be32().serialize(sink2, s); fprintf(fd, "%d", s)
Beide Zeilen enthalten die genannten Elemente. Nun kann wenig weggelassen werden. Ich wuerde am Format sparen und die Umwandlung der Endianess oder Strings mit UTF-Codierung einer Policy ueberlassen wenn noetig.
Hast du dazu mal ein Beispiel? Um das Format geht es doch gerade. Das kann man nicht weglassen.
knivil schrieb:
Weiterhin ist die Serialisierung wertorientiert, in deinem Beispiel werden immer 4 Byte fuer die Codierung benutzt, auch wenn es sich um kleine Zahlen handelt.
Dass ein 4-Byte-Integer 4 Byte lang ist, ist by design so. Selbstverständlich kann man ganz leicht komprimierende Formate hinzufügen. Meine Bibliothek ist für benutzerdefinierte Formate offen.
knivil schrieb:
Damit hat man wieder die Duplizierung beim Lesen und Schreiben.
Ich weiss nicht was du damit meinst, aber ich habe deine Implementation nur ueberflogen.
Es ist redundant, wenn sowohl beim Lesen als auch beim Schreiben Reihenfolge und Typen vom Benutzer einhalten werden müssen:
int main(void) { // This is target object. std::vector<std::string> target; target.push_back("Hello,"); target.push_back("World!"); // Serialize it. msgpack::sbuffer sbuf; // simple buffer msgpack::pack(&sbuf, target); // Deserialize the serialized data. msgpack::unpacked msg; // includes memory pool and deserialized object msgpack::unpack(&msg, sbuf.data(), sbuf.size()); msgpack::object obj = msg.get(); // Print the deserialized object to stdout. std::cout << obj << std::endl; // ["Hello," "World!"] // Convert the deserialized object to staticaly typed object. std::vector<std::string> result; obj.convert(&result); // If the type is mismatched, it throws msgpack::type_error. obj.as<int>(); // type is mismatched, msgpack::type_error is thrown }
Ein Typfehler kann zur Laufzeit noch festgestellt werden. Ein einfaches Verwechseln zweier Typ-gleicher Elemente kann MessagePack nicht erkennen.
Mit meiner Bibliothek muss man die Struktur nur an einer Stelle definieren. Außerdem kann man sofort mit C++-struct
s arbeiten und kommt nicht mit Zwischenformen oder -schritten in Berührung. Fehler werden oft schon vom Compiler erkannt. Die schlanke Notation ist auch leicht lesbar.
Wer will, kann sich das noch weiter verkürzen.MESSAGE (example, (a, be16) (int), (b, be32), (c, bytes<be8>), (d, bytes<be8>) (std::string), (v, vector<be8, be16>) )
EDIT: MessagePack benutzt sich in D anscheinend wie ich mir das vorstelle. Die Sprache hat wohl brauchbare Reflection.
-
Hast du dazu mal ein Beispiel? Um das Format geht es doch gerade. Das kann man nicht weglassen.
Okay, zu be32: Format ist Big endian 32 bit. Der Part fuer 32 Bit wird schon durch bspw. uint32_t uebernommen, d.h. eine passende Ueberladung/Template reicht aus. Falls 32 Bit Integer in 64 Bit serialisiert werden soll, kann entprechend vom Nutzer gecastet werden, um die richtige Ueberladung auszuwaehlen. Big endian wird wahrscheinlich innerhalb eines Pakets sich wohl nicht aendern und kann anderweitig gesetzt werden. Deswegen schlage ich eine Klasse Packer vor, die wie folgt benutzt werden kann:
Packer p(sink, policy1, policy2, ...); // gern auch als template parameter uint32_t value1; float value2; p.pack(value1); // ruft Packroutine fuer uint32_t mit Byte order policy p.pack(value2); // ruft das Ueberladung fuer float auf
Im Vergleich dazu
szn::be32().serialize(sink2, s);
hat s einen Typ, der nochmals durch be32 wiederholt wird. D.h. 32 kann weggelassen werden. Nun werde ich die endianess nur selten im Paket selbst als auch zwischen Paketen aendern. Es ist Muehsam es explizit hinzuschreiben. Deswegen wuerde ich "ausklammern" (k.A. finde kein besseres Wort fuer eng. to factor out).
Wie das mit deiner Bibliothek in Einklang zu bringen ist, weiss ich nicht. Es sind nur einige Anregungen.
Ein einfaches Verwechseln zweier Typ-gleicher Elemente kann MessagePack nicht erkennen.
Ich benutze MessagePack nicht als Bibliothek, sondern nur als Formatspezifikation. Weiterhin habe ich keine Erfahrung in D.