Search code examples
c++serializationboostboost-asio

Trying to send an derived class through a boos::asio socket using boost::serialization


I'm trying to send an object that is an instance of a derived class through a boost::asio socket using UDP.

Let's say the child class is PacketA with the base class being the Packet.

I'm able to serialize PacketA in the client program but whenever I try to deserialize it in the server it throws the following error:

terminate called after throwing an instance of 'boost::archive::archive_exception' what(): unregistered class

To try and fix this I added the macros BOOST_CLASS_EXPORT_IMPLEMENT in the PacketA cpp file and the BOOST_CLASS_EXPORT_KEY in the header file while in the Packet class I didn't add any macro, but it still doesn't work. I added these macros because of this section of the boost docs. I also tried to use the register_type() function to register the child classes but I wasn't successful either and the solutions seems to be worse than the macros.

Is there any obvious mistake that I'm making or am I using the API wrongly?

Code:

Deserialize:

        udp::endpoint senderEndPoint;
        char buffer[MAX_PACKET_SIZE] = {"\n"};
        int bytes = socket->receive_from(boost::asio::buffer(buffer, MAX_PACKET_SIZE), senderEndPoint, 0,error);
   
        std::stringstream stringStream(buffer);
        boost::archive::text_iarchive ia{stringStream};
        Packet *packet; //<-It throws the exception in this line but If I switch this pointer to 
                        //PacketA it works fine but the idea is to deserialize multiple child 
                        //packets that came from the sockets.
        ia & packet; 
        packet->bytes = 0;
        packet->senderEndPoint = senderEndPoint;

Packet.cpp:

#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include "Packet.hpp"
template<class Archive>
void Packet::serialize(Archive &ar, unsigned int version) {
  //I didnt add any code in here since I don't really need to serialize any information just the child packets
}

template void Packet::serialize(boost::archive::text_iarchive &arch, const unsigned int version);

template void Packet::serialize(boost::archive::text_oarchive &arch, const unsigned int version);

Packet.hpp:

#include <boost/serialization/access.hpp>
#include <boost/serialization/export.hpp>
#include <boost/asio/ip/udp.hpp>

using PacketType = std::string;

class Packet {
public:
    friend class boost::serialization::access;

    /*Some variables and functions from packet*/

    template<class Archive>
    void serialize(Archive &, unsigned int version);

};

PacketA.cpp:

#include "PacketA.hpp"
#include <boost/archive/text_oarchive.hpp>
#include <boost/serialization/base_object.hpp>

/*Some other functions*/

template<class Archive>
void PacketA::serialize(Archive &ar, unsigned int version) {
    ar & boost::serialization::base_object<Packet>(*this);
    ar & boost::serialization::make_nvp("PacketType", packetType);
}

BOOST_CLASS_EXPORT_IMPLEMENT(PacketA)

PacketA.hpp:

#include <boost/serialization/export.hpp>
#include "../Packet.hpp"

class PacketA : public Packet {
public:
    PacketType packetType = "PacketA";

    friend class boost::serialization::access;

    /*Some functions*/

    template<class Archive>
    void serialize(Archive &ar, unsigned int version);
};

BOOST_CLASS_EXPORT_KEY(PacketA)

To serialize all the packets I'm using this function:

std::stringstream foo::serializePacket(Packet *packet) { //<-Here the *packet could be any 
                                                         //packet child
    std::stringstream ss;
    boost::archive::text_oarchive oa{ss};
    oa & packet;
    return ss;
}

Solution

  • You registration implementation cannot see the input archive definition because PacketA.cpp fails to include:

    #include <boost/archive/text_iarchive.hpp>
    

    This requirement is explained in the docs:

    BOOST_CLASS_EXPORT in the same source module that includes any of the archive class headers will instantiate code required to serialize polymorphic pointers of the indicated type to the all those archive classes. If no archive class headers are included, then no code will be instantiated.

    Note that the implemenation of this functionality requires that the BOOST_CLASS_EXPORT macro appear after the inclusion of any archive class headers for which code is to be instantiated.

    Additional Notes

    • The class hierarchy needs to be virtual to polymorphically (de)serialize through a pointer. Simplest way is to make sure of it is adding the virtual destructor.

    • Note that you fail to NUL-terminate the receive buffer, which means you will invoke UB unless the sent packet includes it (and it fits in the buffer size). So the following would be a start of what safer deserialization looks like:

      std::array<char, MAX_PACKET_SIZE> buffer {'\0'}; // fill with NULs
      boost::system::error_code error;
      int bytes = socket->receive_from(boost::asio::buffer(buffer, MAX_PACKET_SIZE), senderEndPoint, 0,error);
      
      if (!error) {
          std::stringstream stringStream(std::string(buffer.data(), bytes));
          boost::archive::text_iarchive ia{stringStream};
          Packet* packet = nullptr;
          ia & packet;
          packet->bytes = 0;
          packet->senderEndPoint = senderEndPoint;
      }
      

    Full Test Demo

    • File Packet.hpp

       #include <boost/serialization/access.hpp>
       #include <boost/serialization/export.hpp>
       #include <boost/asio/ip/udp.hpp>
       #include <string>
      
       using PacketType = std::string;
      
       class Packet {
       public:
           virtual ~Packet() = default;
           friend class boost::serialization::access;
      
           /*Some variables and functions from packet*/
           int bytes = 0;
           boost::asio::ip::udp::endpoint senderEndPoint;
      
           template<class Archive>
           void serialize(Archive & /*ar*/, unsigned version);
       };
      
    • File Packet.cpp

       #include <boost/archive/text_iarchive.hpp>
       #include <boost/archive/text_oarchive.hpp>
       #include <boost/serialization/string.hpp>
       #include "Packet.hpp"
       template <class Archive> void Packet::serialize(Archive& /*ar*/, unsigned /*version*/)
       {
           // I didnt add any code in here since I don't really need to serialize any
           // information just the child packets
       }
      
       template void Packet::serialize(
           boost::archive::text_iarchive& arch, const unsigned int version);
      
       template void Packet::serialize(
           boost::archive::text_oarchive& arch, const unsigned int version);
      
    • File PacketA.hpp

       #include <boost/serialization/export.hpp>
       #include "Packet.hpp"
      
       #define DECLARE_PACKET(Name)                                                   \
           struct Name : Packet {                                                     \
               PacketType packetType = #Name;                                         \
               /*Some functions*/                                                     \
                                                                                      \
             private:                                                                 \
               friend class boost::serialization::access;                             \
               template <class Archive>                                               \
               void serialize(Archive& ar, unsigned int version);                     \
           };                                                                         \
                                                                                      \
           BOOST_CLASS_EXPORT_KEY(Name)
      
       DECLARE_PACKET(PacketA)
       DECLARE_PACKET(PacketB)
       DECLARE_PACKET(PacketC)
       DECLARE_PACKET(PacketD)
       DECLARE_PACKET(PacketE)
      
    • File PacketA.cpp

       #include "PacketA.hpp"
       #include <boost/archive/text_oarchive.hpp>
       #include <boost/archive/text_iarchive.hpp>
       #include <boost/serialization/base_object.hpp>
      
       #define IMPLEMENT_PACKET(Name)                                                 \
           /*Some other functions*/                                                   \
                                                                                      \
           template <class Archive>                                                   \
           void Name::serialize(Archive& ar, unsigned /*version*/)                    \
           {                                                                          \
               ar& boost::serialization::base_object<Packet>(*this);                  \
               ar& boost::serialization::make_nvp("PacketType", packetType);          \
           }                                                                          \
                                                                                      \
           BOOST_CLASS_EXPORT_IMPLEMENT(Name)
      
      
       IMPLEMENT_PACKET(PacketA)
       IMPLEMENT_PACKET(PacketB)
       IMPLEMENT_PACKET(PacketC)
       IMPLEMENT_PACKET(PacketD)
       IMPLEMENT_PACKET(PacketE)
      
    • File test.cpp

       #include <boost/asio.hpp>
       #include <iostream>
       #include <iomanip>
       using boost::asio::ip::udp;
      
       #include "PacketA.hpp"
       #include <boost/archive/text_iarchive.hpp>
       #include <boost/archive/text_oarchive.hpp>
       #include <boost/core/demangle.hpp> // for test output
      
       static constexpr size_t MAX_PACKET_SIZE = 1024;
      
       std::unique_ptr<Packet> receive_packet(uint16_t port) {
           boost::asio::io_context io;
           udp::endpoint senderEndPoint;
           auto socket = std::make_unique<udp::socket>(io, udp::endpoint { {}, port });
      
           std::array<char, MAX_PACKET_SIZE> buffer {'\0'}; // fill with NULs
           boost::system::error_code error;
           int bytes = 0 = socket->receive_from(boost::asio::buffer(buffer, MAX_PACKET_SIZE), senderEndPoint, 0,error);
      
           Packet* packet = nullptr;
      
           if (!error) {
               {
                   std::stringstream stringStream(std::string(buffer.data(), bytes));
                   boost::archive::text_iarchive ia{stringStream};
                   ia & packet;
               }
      
               packet->bytes = 0;
               packet->senderEndPoint = senderEndPoint;
           }
      
           return std::unique_ptr<Packet>(packet); // take ownership
       }
      
       struct foo {
           static std::stringstream serializePacket(Packet* packet);
       };
      
       std::stringstream foo::serializePacket(Packet* packet)
       { //<-Here the *packet could be any packet child
           std::stringstream ss;
           boost::archive::text_oarchive oa { ss };
           oa& packet;
           return ss;
       }
      
       template <typename Type>
       void send() {
           auto request = std::make_unique<Type>();
           auto msg = foo::serializePacket(request.get()).str();
      
           boost::asio::system_executor ex;
           udp::socket s { ex };
           s.open(udp::v4());
           s.send_to(boost::asio::buffer(msg), { {}, 9977 });
       }
      
       template <typename Type>
       void test_roundtrip() {
           auto fut = std::async(std::launch::async, receive_packet, 9977);
      
           std::this_thread::yield(); // be reasonably sure the read started
           send<Type>();
      
           if (auto p = fut.get()) {
               std::cout << "Deserialized a "
                         << boost::core::demangle(typeid(*p).name()) << " packet"
                         << std::endl;
           }
       }
      
       int main() {
           test_roundtrip<PacketA>();
           test_roundtrip<PacketB>();
           test_roundtrip<PacketC>();
           test_roundtrip<PacketD>();
           test_roundtrip<PacketE>();
       }
      

    Prints

    Deserialized a PacketA packet
    Deserialized a PacketB packet
    Deserialized a PacketC packet
    Deserialized a PacketD packet
    Deserialized a PacketE packet