如何在C++中将字节数组正确反序列化为对象?

J K*_*fer 0 c++ arrays embedded deserialization

我的团队现在已经有几个星期这个问题了,我们有点难过.善意和知识将优雅地收到!

使用嵌入式系统,我们试图序列化一个对象,通过Linux套接字发送它,在另一个进程中接收它,然后将它反序列化回原始对象.我们有以下反序列化功能:

 /*! 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));
}
Run Code Online (Sandbox Code Playgroud)

该对象已成功反序列化,可以从中读取.但是,当std::shared_ptr<Foo>调用返回的析构函数时,程序会出现段错误.Valgrind给出以下输出:

==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)
Run Code Online (Sandbox Code Playgroud)

我们对任何建议都持开放态度!感谢您的时间 :)

Jer*_*ner 5

一般来说,这不起作用:

auto msg = reinterpret_cast<Foo *>(serialized);
Run Code Online (Sandbox Code Playgroud)

你不能只取一个任意数组的字节并假装它是一个有效的C++对象(即使reinterpret_cast <>允许你编译试图这样做的代码).首先,任何包含至少一个虚方法的C++对象都将包含一个vtable指针,该指针指向该对象类的虚方法表,并在调用虚方法时使用.但是如果你在计算机A上序列化该指针,然后通过网络发送它并反序列化然后尝试使用计算机B上的重构对象,你将调用未定义的行为,因为不能保证该类的vtable将同时存在它在计算机A上执行的计算机B上的内存位置.此外,任何进行任何动态内存分配的类(例如,

但是,假设您已将序列化限制为仅POD(普通旧数据)不包含指针的对象.它会起作用吗?答案是:可能在非常具体的情况下,但它会非常脆弱.原因是编译器可以以不同的方式自由地在内存中布置类的成员变量,并且它将在不同的硬件上以不同的方式插入填充(有时甚至使用不同的优化设置),从而导致字节的情况表示计算机A上的特定Foo对象与表示计算机B上的同一对象的字节不同.最重要的是,您可能不得不担心不同计算机上的不同字长(例如,长度为32位)一些架构和64位其他架构)和不同的端点(例如 Intel CPU以小端形式表示值,而PowerPC CPU通常以big-endian表示它们.这些差异中的任何一个都会导致您的接收计算机误解其收到的字节,从而严重损坏您的数据.

所以问题的其余部分是,序列化/反序列化C++对象的正确方法是什么?答案是:你必须以艰难的方式去做,为每个通过成员变量执行序列化成员变量的类编写例程,并考虑类的特定语义.例如,以下是您可以对可序列化类进行定义的一些方法:

// 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);
Run Code Online (Sandbox Code Playgroud)

...这是一个实现方法的简单x/y点类的示例:

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;
 };
Run Code Online (Sandbox Code Playgroud)

...那么你的unmarshal函数可能看起来像这样(注意我已经模仿它,以便它适用于任何实现上述方法的类):

/*! 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
    [...]
}
Run Code Online (Sandbox Code Playgroud)

如果这看起来像很多工作,只需抓住你的类对象的原始内存字节并逐字逐句地发送它们,你就是对的 - 它是.但是,如果您希望序列化可靠地工作并且每次升级编译器时都不会中断,或者更改优化标志,或者希望在具有不同CPU架构的计算机之间进行通信,那么这就是您必须要做的.如果您不想手动执行此类操作,则可以使用预先打包的库来帮助(部分)自动化该过程,例如Google的Protocol Buffers库,甚至是旧的XML.

  • @Klaus你通常会看到什么? (2认同)