Protokoll: Pakete mit automatischer Serialisierung
-
@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.
-
Die Serialisierungsfunktion kann dann _1, _2 usw. verwenden.
Ja, da hat camper einen Volltreffer erzielt...**
unions
**. Bastard. Mir fällt nie was tolles ein, stattdessen mache ich die DrecksarbeitFunktioniert nicht mit Arrays.
Funktioniert auch nicht ohne weiteres mit UDTs.
Hie ist die Lösung:
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; #include <type_traits> template<typename T> using noref = typename std::remove_reference<T>::type; template< typename T, typename = typename std::enable_if<std::is_class<noref<T>>::value>::type > void destroy( T&& t ) { t.~noref<T>(); } template< typename T, typename = typename std::enable_if<not std::is_class<typename std::remove_reference<T>::type>::value>::type, typename=void > void destroy( T&& ) {} /// __________________________________________________________________________ #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/size.hpp> #include <boost/preprocessor/tuple/elem.hpp> #include <boost/preprocessor/repetition/repeat.hpp> #include <boost/preprocessor/repetition/enum.hpp> #include <boost/preprocessor/variadic/elem.hpp> #define GET_NTH_TYPE( n, tuple ) \ fun_arg<void( BOOST_PP_TUPLE_ELEM( BOOST_PP_TUPLE_SIZE(tuple), n, tuple ) )> #define DECLARE_ELEMS( z, n, t ) \ GET_NTH_TYPE(n, t) _##n #define DEFINE_UNION( z, number, declarations ) \ union \ { \ BOOST_PP_TUPLE_ELEM( BOOST_PP_TUPLE_SIZE(declarations), number, declarations ) ; \ DECLARE_ELEMS(, number, declarations) ; \ static_assert( not std::is_array< GET_NTH_TYPE(number, declarations) >::value, "Array type not supported!" ); \ }; #define DUMP_FUNCTION( z, i, t ) std::cout << _##i << '\n'; #define CTOR_INITIALIZER_LIST( z, n, t ) \ _##n( _##n ) #define DESTROY_ALL( z, i, t ) \ destroy( _##i ); #define DEFINE_PACKET( name, dir, id, tuple ) \ struct name##Packet : Packet<dir, id> \ { \ BOOST_PP_REPEAT( BOOST_PP_TUPLE_SIZE(tuple), DEFINE_UNION, tuple ) \ \ name##Packet ( BOOST_PP_ENUM(BOOST_PP_TUPLE_SIZE(tuple), DECLARE_ELEMS, tuple) ) : \ BOOST_PP_ENUM( BOOST_PP_TUPLE_SIZE(tuple), CTOR_INITIALIZER_LIST, ) \ {} \ \ ~ name##Packet() \ { \ BOOST_PP_REPEAT( BOOST_PP_TUPLE_SIZE(tuple), DESTROY_ALL, ) \ } \ \ void dump() \ { \ BOOST_PP_REPEAT( BOOST_PP_TUPLE_SIZE(tuple), DUMP_FUNCTION, ) \ } \ }; #include <string> #include <iostream> struct A { A( int ) { std::cout << "A: constructor\n"; } A( A const& ) { std::cout << "A: copy constructor\n"; } ~A() { std::cout << "A: destructor\n"; } friend std::ostream& operator<<( std::ostream& os, A const& ) { return os; } }; DEFINE_PACKET( Foo, TwoWayTag, 0x20, ( int x, std::string s, double d, A a ) ) int main() { FooPacket p{4, "Hi", 54.87, 4}; p.dump(); static_assert( std::is_same<decltype(p._1), std::string>::value, "" ); }
Noch sehr unschön, funktioniert an sich aber und hat AFAICS wohldefiniertes Verhalten.
~Edit: Nein, hat schwerwiegende Bugs. Wird gefixt.~
Edit²: Bug gefixt.Arrays funktionieren immer noch nicht... ein weiterer Faktor, der einem die Wahl von
std::array
erleichtertEine dummy-Serialisierungs-Funktion ist vorimplementiert. Wobei ich nicht genau weiß, was "Serialisierung" hier eigentlich heißt, einfach alles in einen Stream schreiben?
-
Erklaer mal lieber, wie das ganze ueberhaupt funktioniert.
-
Sone schrieb:
Arrays funktionieren immer noch nicht...
Dafür gibt es wahrscheinlich auch keine allgemeingültige Lösung. Ursprünglich hatte ich das [1] angefügt, am den Decay zu vermeiden, dann ist mir aufgefallen, dass das ja am falschen Ende angehängt würde. Probleme mit UDTs sehe ich nicht, sofern diese nicht inplace-definiert werden sollen. fun_arg funktioniert nicht mit abstrakten Klassen, aber die können bei dieser Anwendung sowieso nicht auftreten.
-
Kellerautomat schrieb:
Erklaer mal lieber, wie das ganze ueberhaupt funktioniert.
Das ist doch total einfach! Was gibt es da zu erklären?
Was soll's:
Das Grundprinzip hinter diese Variante ist die, dass man innerhalb der Paketklasse für jeden Member eine anonyme union mit zwei Membern definiert:
union { int i; // Der eigentliche, durch das Makro vorgegebene Member. fun_arg< void(int i) > _1; // Der zweite Member, dessen Namen 'wir' wissen };
Da beide Typen exakt gleich sind, können wir problemlos auf _1 oder i zugreifen. Das funktioniert für alle Typen, auch non-PODs, usw.
Das geniale hier ist zudem, dass wir den Namen mit schreiben können, da in der Parameterliste eines Funktionstyp Namen zugelassen sind. Der Nachteil ist, dass Arrays nicht erlaubt sind (siehe auch das gerade hinzugefügtestatic_assert
).Die dump-Funktion nutzt nun _1, _2, und so weiter, während du einfach entweder die von dir festgelegten Identifier nimmst, oder auch _x.
Das kann sogar soweit getrieben werden, dass du sowas schreiben kannst:
DEFINE_PACKET( Foo, TwoWayTag, 0x20, ( int x, std::string s, double d ) ) ( std::ostream& os ) { os << x << ' ' << s.size() << ' ' << s << ' ' << d; }
Und jetzt sag mir, das ist nicht wunderschön.
-
Probleme mit UDTs sehe ich nicht, sofern diese nicht inplace-definiert werden sollen
Dann guck mal, was ich in meinen Destruktor schreiben musste. (UDTs gehen nicht ohne weiteres)
Und die anderen speziellen Memberfunktionen funktionieren auch nicht ohne weiteres; siehe Standard:§9.5/3 schrieb:
[ Example: Consider the following union:
union U { int i; float f; std::string s; };
Since std::string (21.3) declares non-trivial versions of all of the special member functions, U will have an implicitly deleted default constructor, copy/move constructor, copy/move assignment operator, and destructor. To use U, some or all of these member functions must be user-provided.—end example ]
-
Sone schrieb:
(UDTs gehen nicht ohne weiteres)
Das hatte ich überlesen. UDT is ja auch ein bisschen unpräzise.
-
Ich sehe nicht, wie
static_assert( not std::is_array< GET_NTH_TYPE(number, declarations) >::value, "Array type not supported!" );
jemals erfüllt sein könnte.
Sofern für ein array ein typedef existiert und verwendet wird, kann man das Ganze zum funktionieren bringen:
typedef int foo[42]; // als Makroargument: foo x; // ==> using type1 = fun_arg<void(foo x)>; union dummy { foo x, _0; }; // wenn std::is_same<type1, std::decay<decltype(dummy::_0)>::type>::value // so enthält die Deklaration von x keine weiteren Deklaratoren und decltype(dummy::_0) ist der gesuchte Typ // andernfalls verwenden wir type1 // können aber nicht zwischen // int x[42] und int* x unterscheiden
sofern man ein bisschen unportabel wird, könnte man noch Arrays zulassen, deren Größe nicht mit der Größe eines Zeigers übereinstimmt, dann ist die gesuchte Dimension sizeof(dummy) / sizeof(dummy::_0)
statt
DEFINE_PACKET( Foo, TwoWayTag, 0x20, ( int x, std::string s[42], double d ) )
schreibt man dann eben
template <typename T> using simple_typeid = T; DEFINE_PACKET( Foo, TwoWayTag, 0x20, ( int x, simple_typeid<std::string[42]> s, double d ) )
wobei das nicht mehr ganz so schön ist.
-
Ich sehe nicht, wie [...] jemals erfüllt sein könnte.
Ich schon. In dem der entsprechende Typ kein Array ist.
Schon wieder irgend etwas überlesen?
UDT is ja auch ein bisschen unpräzise.
Was soll ich schon schreiben, nicht-triviale Klassen? :p
wobei das nicht mehr ganz so schön ist.
Doch, durchaus verkraftbar. Eine simple Lösung. Leider natürlich nicht so perfekt wie die ideale, aber anwendbar und relativ kurz.
-
Sone schrieb:
Ich sehe nicht, wie [...] jemals erfüllt sein könnte.
Ich schon. In dem der entsprechende Typ kein Array ist.
Schon wieder irgend etwas überlesen?
umgekhhrt nat.:
not std::is_array< GET_NTH_TYPE(number, declarations) >::value
kann niemals false sein, fun_arg<...> ist unter keinen Umständen ein Array.
Sone schrieb:
Aber das ist doch auch trivial. Dass man einfach den Array-Typ in einem Wrapper verpackt, der selbst natürlich kein Array-Typ mehr ist. Dann testet man einfach nur noch irgendwo intern, ob
fun_arg<...>
eine Spezialisierung von diesem Wrapper-Template ist, und nimmt dann entsprechend das Argument des Templates als Typ. Und dann kommt man noch drauf, dass es eine alias-Deklaration sein muss, weil sonst der Typ des ersten Union-Members der Typ des Wrappers und nicht des gewrappten Typs ist. Und dann kommt man drauf, dass man dann auch nicht mehr testen kann oder braucht.???
-
kann niemals false sein, fun_arg<...> ist unter keinen Umständen ein Array
Arghh, verdammt! Ich übersehe immer solche Kleinigkeiten... ja, das ist natürlich eine sinnfreie Abfrage. Dann wird der User wohl oder übel sorgfältig die Dokumentation lesen müssen.
Mich wurmt es immer noch, dass man den Typ nicht so leicht bekommen kann... das muss doch einfacher gehen...!
???
Ach, natürlich, das muss alias-Template heißen....
Damit wollte ich nur darlegen, dass der Schreiber dieses Makros selbst nichts machen muss, da der User da von selbst drauf kommt, wenn er sich das Prinzip ansieht. Zugegeben ein wenig kontext-frei... daher raus editiert...
-
Wusste gar nicht, dass man Makro-Parameter einfach leer lassen kann. Gibts dazu nicht BOOST_PP_EMPTY?
Ausserdem hat C++ meines Wissens nach keine anonymen Unions.Anonsten finde ich hustbaers Ansatz immer noch schoener, sorry
-
Kellerautomat schrieb:
Wusste gar nicht, dass man Makro-Parameter einfach leer lassen kann. Gibts dazu nicht BOOST_PP_EMPTY?
Nein, kann man lehr lassen.
BOOST_PP_EMPTY
ist eine Makro-Funktion, die zu nichts evaluiert, die nutzt man also dort wo man den Namen einer Makro-Funktion uebergeben will.Ausserdem hat C++ meines Wissens nach keine anonymen Unions.
Jetzt weisst du es besser. Siehe auch Standard 9.5.5.
Anonsten finde ich hustbaers Ansatz immer noch schoener, sorry
Meiner ist genauso flexibel. Du musst das Serialisierungs- und Deserialisierungs-Makro genauso definieren... dazu musst du nichts mehrfach einbinden... und es ist kuerzer ein Paket zu definieren... was genau ist bei hustbaer schoener? AFAICS nichts.
-
Du Frage muss lauten: Was ist haesslicher? Schoen sind beide nicht.