¿Cómo se deserializa correctamente una matriz de bytes en un objeto en C ++?

Mi equipo ha tenido este problema durante algunas semanas, y estamos un poco perplejos. ¡La amabilidad y el conocimiento serían recibidos con gracia!

Al trabajar con un sistema integrado, intentamos serializar un objeto, enviarlo a través de un socket de Linux, recibirlo en otro proceso y deserializarlo de nuevo en el objeto original. Tenemos la siguiente función de deserialización:

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

El objeto se deserializa satisfactoriamente y se puede leer desde. Sin embargo, cuando se llama al destructor para el std::shared_ptr devuelto, el progtwig segfaults. Valgrind da la siguiente salida:

 ==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::_M_release() (shared_ptr_base.h:149) ==1664== by 0x42BC00: std::__shared_count::~__shared_count() (shared_ptr_base.h:666) ==1664== by 0x435999: std::__shared_ptr::~__shared_ptr() (shared_ptr_base.h:914) ==1664== by 0x4359B3: std::shared_ptr::~shared_ptr() (shared_ptr.h:93) 

¡Estamos abiertos a cualquier sugerencia! Gracias por tu tiempo 🙂

En general, esto no funcionará:

 auto msg = reinterpret_cast(serialized); 

No puede simplemente tomar una matriz arbitraria de bytes y pretender que es un objeto de C ++ válido (incluso si reinterpret_cast <> le permite compilar el código que intenta hacerlo). Por un lado, cualquier objeto C ++ que contenga al menos un método virtual contendrá un puntero vtable, que apunta a la tabla de métodos virtuales para la clase de ese objeto, y se usa cada vez que se llama a un método virtual. Pero si serializa ese puntero en la computadora A, luego lo envía a través de la red, lo deserializa y luego intenta usar el objeto reconstituido en la computadora B, invocará un comportamiento indefinido porque no hay garantía de que la vtable de esa clase exista al mismo ubicación de memoria en la computadora B que hizo en la computadora A. Además, cualquier clase que haga cualquier tipo de asignación de memoria dinámica (por ejemplo, cualquier clase de cadena o clase de contenedor) contendrá punteros a otros objetos que asignó, y eso lo llevará a la mismo problema de puntero no válido

Pero digamos que ha limitado sus serializaciones a solo objetos POD (datos antiguos) que no contienen punteros. ¿Funcionará entonces? La respuesta es: posiblemente, en casos muy específicos, pero será muy frágil. La razón de esto es que el comstackdor es libre de disponer las variables miembros de la clase en la memoria de diferentes maneras, e insertará el relleno de manera diferente en diferentes hardware (o incluso con diferentes configuraciones de optimización, a veces), lo que lleva a una situación en la que los bytes que representan un objeto Foo en particular en la computadora A son diferentes de los bytes que representarían ese mismo objeto en la computadora B. Además, es posible que tenga que preocuparse por la longitud de las palabras en diferentes computadoras (por ejemplo, long es de 32 bits) algunas architectures y 64 bits en otras), y diferentes características de endiancia (por ejemplo, las CPU de Intel representan valores en forma de little-endian, mientras que las CPU de PowerPC normalmente las representan en big-endian). Cualquiera de estas diferencias hará que su computadora receptora malinterprete los bytes que recibió y, por lo tanto, dañe sus datos.

Entonces, la parte restante de la pregunta es, ¿cuál es la forma correcta de serializar / deserializar un objeto C ++? Y la respuesta es: hay que hacerlo de la manera más difícil, escribiendo una rutina para cada clase que realice la serie de miembro-variable por miembro-variable, teniendo en cuenta la semántica particular de la clase. Por ejemplo, aquí hay algunos métodos que podría definir sus clases serializables:

 // 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); 

… y aquí hay un ejemplo de una simple clase de puntos x / y que implementa los métodos:

 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; }; 

... entonces su función unmarshal podría tener este aspecto (tenga en cuenta que la he configurado para que funcione para cualquier clase que implemente los métodos anteriores):

 /*! Takes a byte array and populates the object's data members */ template std::shared_ptr unmarshal(const uint8_t *serialized, size_t size) { auto sp = std::make_shared(); if (sp->Unflatten(serialized, size) == true) return sp; // Oops, Unflatten() failed! handle the error somehow here [...] } 

Si esto parece una gran cantidad de trabajo comparado con solo tomar los bytes de memoria sin procesar de su objeto de clase y enviarlos literalmente a través del cable, tiene razón. Pero esto es lo que debe hacer si desea que la serialización funcione de manera confiable y no se rompa cada vez que actualice su comstackdor, o cambie sus indicadores de optimización, o quiera comunicarse entre computadoras con diferentes architectures de CPU. Si prefiere no hacer este tipo de cosas a mano, hay bibliotecas preempaquetadas para ayudarlo (parcialmente) a automatizar el proceso, como la biblioteca de Protocol Buffers de Google , o incluso el XML antiguo.

El segfault durante la destrucción se produce porque está creando un objeto shared_ptr reinterpretando el shared_ptr de un puntero a un uint8_t . Durante la destrucción del objeto shared_ptr devuelto, el uint8_t se liberará como si fuera un puntero a un Foo* y, por lo tanto, se produce el error de seguridad.

Actualiza tu unmarshal como se indica a continuación y pruébalo.

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

Aquí la propiedad del objeto ChildOfFoo creado por la statement ChildOfFoo* ptrChildOfFoo = new ChildOfFoo(); se transfiere al objeto shared_ptr devuelto por la función unmarshal . Por lo tanto, cuando se llama al destructor del objeto shared_ptr devuelto, se shared_ptr correctamente y no se producirá ningún fallo de seguridad.

¡Espero que esto ayude!