2017-06-29 1 views
0

Mein Team hat dieses Problem seit ein paar Wochen, und wir sind ein bisschen ratlos. Freundlichkeit und Wissen würde anmutig erhalten werden!Wie wird ein Byte-Array in C++ korrekt in ein Objekt deserialisiert?

Mit einem eingebetteten System versuchen wir, ein Objekt zu serialisieren, es über einen Linux-Socket zu senden, es in einem anderen Prozess zu empfangen und es wieder in das ursprüngliche Objekt zu deserialisieren. Wir haben die folgende Deserialisierungsfunktion:

/*! Takes a byte array and populates the object's data members */ 
std::shared_ptr<Foo> Foo::unmarshal(uint8_t *serialized, uint32_t size) 
{ 
    auto msg = reinterpret_cast<Foo *>(serialized); 
    return std::shared_ptr<ChildOfFoo>(
     reinterpret_cast<ChildOfFoo *>(serialized)); 
} 

Das Objekt wurde erfolgreich deserialzed und kann gelesen werden. Wenn jedoch der Destruktor für das zurückgegebene std::shared_ptr<Foo> aufgerufen wird, wird das Programm segfaults. Valgrind gibt folgende Ausgabe:

==1664== Process terminating with default action of signal 11 (SIGSEGV) 
==1664== Bad permissions for mapped region at address 0xFFFF603800003C88 
==1664== at 0xFFFF603800003C88: ??? 
==1664== by 0x42C7C3: std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() (shared_ptr_base.h:149) 
==1664== by 0x42BC00: std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() (shared_ptr_base.h:666) 
==1664== by 0x435999: std::__shared_ptr<ChildOfFoo, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() (shared_ptr_base.h:914) 
==1664== by 0x4359B3: std::shared_ptr<ChildOfFoo>::~shared_ptr() (shared_ptr.h:93) 

Wir sind offen für alle Vorschläge überhaupt! Vielen Dank für Ihre Zeit :)

+0

Darf der Upvoter bitte ihre Argumentation erklären? –

+0

Der Deleter sollte no-op sein, da shared_ptr standardmäßig auf delete aufruft. – Incomputable

+0

Könnten Sie klarstellen, was Sie unter No-Op verstehen? –

Antwort

4

Im Allgemeinen wird dies nicht funktionieren:

auto msg = reinterpret_cast<Foo *>(serialized); 

Sie können nicht nur eine willkürliche Anordnung von Bytes nehmen und so tun, es ist eine gültige C++ Objekt (auch wenn < reinterpret_cast> Sie Code kompilieren können, die versucht dies zu tun). Zum einen enthält jedes C++ - Objekt, das mindestens eine virtuelle Methode enthält, einen vtable-Zeiger, der auf die Tabelle der virtuellen Methoden für die Klasse dieses Objekts zeigt und immer dann verwendet wird, wenn eine virtuelle Methode aufgerufen wird. Aber wenn Sie diesen Zeiger auf Computer A serialisieren, dann über das Netzwerk senden und deserialisieren und dann versuchen, das wiederhergestellte Objekt auf Computer B zu verwenden, rufen Sie ein undefiniertes Verhalten auf, da es keine Garantie dafür gibt, dass die V-Tabelle dieser Klasse gleichzeitig existiert Speicherstelle auf Computer B, die es auf Computer A getan hat. Auch jede Klasse, die irgendeine Art von dynamischer Speicherzuweisung durchführt (z. B. irgendeine String - oder Containerklasse), wird Zeiger auf andere Objekte enthalten, die sie zugewiesen hat dasselbe Problem mit dem ungültigen Zeiger.

Aber sagen wir, Sie haben Ihre Serialisierungen auf nur POD (einfache alte Daten) Objekte beschränkt, die keine Zeiger enthalten. Wird es dann funktionieren? Die Antwort ist: möglicherweise in sehr spezifischen Fällen, aber es wird sehr fragil sein. Der Grund dafür ist, dass der Compiler die Elementvariablen der Klasse frei im Speicher auf verschiedene Arten auslegen kann und Padding auf unterschiedlicher Hardware (oder manchmal sogar mit anderen Optimierungseinstellungen) unterschiedlich einfügt, was zu einer Situation führt, in der die Bytes vorkommen die ein bestimmtes Foo-Objekt auf Computer A repräsentieren, unterscheiden sich von den Bytes, die dasselbe Objekt auf Computer B repräsentieren würden. Darüber hinaus müssen Sie sich möglicherweise um verschiedene Wortlängen auf verschiedenen Computern kümmern (z. B.lang ist 32-bit bei einigen Architekturen und 64-bit bei anderen) und unterschiedliche endian-ness (z. B. Intel-CPUs stellen Werte in Little-Endian-Form dar, während PowerPC-CPUs sie typischerweise in Big-Endian darstellen). Jeder dieser Unterschiede führt dazu, dass der empfangende Computer die empfangenen Bytes falsch interpretiert und dadurch Ihre Daten schlecht beschädigt.

Der verbleibende Teil der Frage ist, was ist der richtige Weg, um ein C++ Objekt zu serialisieren/deserialisieren? Und die Antwort ist: Sie müssen es auf die harte Tour machen, indem Sie eine Routine für jede Klasse schreiben, die die Membervariable für die Serialisierung nach Membervariable unter Berücksichtigung der Semantik der Klasse ausführt. Zum Beispiel sind hier einige Methoden, die Sie vielleicht Ihre serializable Klassen definieren:

// Serialize this object's state out into (buffer) 
// (buffer) must point to at least FlattenedSize() bytes of writeable space 
void Flatten(uint8 *buffer) const; 

// Return the number of bytes this object will require to serialize 
size_t FlattenedSize() const; 

// Set this object's state from the bytes in (buffer) 
// Returns true on success, or false on failure 
bool Unflatten(const uint8 *buffer, size_t size); 

... und hier ist ein Beispiel für eine einfache x/y-Punkt-Klasse, die die Methoden implementiert:

class Point 
{ 
public: 
    Point() : m_x(0), m_y(0) {/* empty */} 
    Point(int32_t x, int32_t y) : m_x(x), m_y(y) {/* empty */} 

    void Flatten(uint8_t *buffer) const 
    { 
     const int32_t beX = htonl(m_x); 
     memcpy(buffer, &beX, sizeof(beX)); 
     buffer += sizeof(beX); 

     const int32_t beY = htonl(m_y); 
     memcpy(buffer, &beY, sizeof(beY)); 
    } 

    size_t FlattenedSize() const {return sizeof(m_x) + sizeof(m_y);} 

    bool Unflatten(const uint8_t *buffer, size_t size) 
    { 
     if (size < FlattenedSize()) return false; 

     int32_t beX; 
     memcpy(&beX, buffer, sizeof(beX); 
     m_x = ntohl(beX); 

     buffer += sizeof(beX); 
     int32_t beY; 
     memcpy(&beY, buffer, sizeof(beY)); 
     m_y = ntohl(beY); 

     return true; 
    } 

    int32_t m_x; 
    int32_t m_y; 
}; 

... dann könnte Ihre Abstellungs Funktion wie folgt aussehen (beachten sie, ich habe es so Templat gemacht, dass es für jede Klasse arbeiten, die die oben genannten Methoden implementiert):

/*! Takes a byte array and populates the object's data members */ 
template<class T> std::shared_ptr<T> unmarshal(const uint8_t *serialized, size_t size) 
{ 
    auto sp = std::make_shared<T>(); 
    if (sp->Unflatten(serialized, size) == true) return sp; 

    // Oops, Unflatten() failed! handle the error somehow here 
    [...] 
} 

Wenn dies wie eine Menge o scheint f Arbeit im Vergleich zu nur die rohen Speicher Bytes Ihres Klassenobjekts und senden sie wörtlich über den Draht, haben Sie Recht - es ist. Aber das müssen Sie tun, wenn Sie möchten, dass die Serialisierung zuverlässig funktioniert und nicht jedes Mal bricht, wenn Sie den Compiler aktualisieren oder die Optimierungsflags ändern oder zwischen Computern mit unterschiedlichen CPU-Architekturen kommunizieren möchten. Wenn Sie solche Dinge lieber nicht von Hand machen möchten, gibt es vorgefertigte Bibliotheken, die den Prozess (teilweise) automatisieren, wie die Bibliothek Google's Protocol Buffers oder sogar gutes altes XML.

+0

Der Code hat eine Klassenhierarchie, die angeblich virtuelle Funktionen verwendet (oder durch das Design unterbrochen ist). –

+0

Nie gesehen, dass jemand eine serialize/deserialize-Methode für jede Funktion von Hand schreibt ... – Klaus

+2

@Klaus, was sehen Sie normalerweise stattdessen? –

1

Der Segfault während der Zerstörung tritt auf, weil Sie ein shared_ptr Objekt erstellen, indem Sie einen Zeiger auf eine uint8_t uminterpretieren. Während der Zerstörung des zurückgegebenen shared_ptr Objekts wird die uint8_t freigegeben, als wäre es ein Zeiger auf eine Foo* und daher tritt der segfault auf.

Aktualisieren Sie Ihre unmarshal wie unten angegeben und versuchen Sie es.

std::shared_ptr<Foo> Foo::unmarshal(uint8_t *&serialized, uint32_t size) 
{  
    ChildOfFoo* ptrChildOfFoo = new ChildOfFoo(); 
    memcpy(ptrChildOfFoo, serialized, size); 

    return std::shared_ptr<ChildOfFoo>(ptrChildOfFoo); 
} 

Hier wird das Eigentum an dem dem ChildOfFoo Objekt durch die Anweisung erstellt ChildOfFoo* ptrChildOfFoo = new ChildOfFoo(); wird auf das von der unmarshal Funktion zurück shared_ptr Objekt übertragen. Wenn also der zurückgegebene Destruktor des Objekts aufgerufen wird, wird er ordnungsgemäß de-allokiert, und es tritt kein Segmentfehler auf.

Hoffe diese Hilfe!

Verwandte Themen