0

I'm trying to write (in Linux), a "Sender" and "Receiver" programs that can send/receive a few types of messages between them, using shared memory, and message_queue. I decided to use boost/interprocess/ipc/message_queue as it looks like the best solution.

The problem is that since there are a few types of messages with different structures and sizes, the receiver cannot know how many bytes to receive() and how they are structured.

I can solve this by placing the "type enumerator" as the first field, and upon reading the type, the receiver knows what is the size and how to parse the message to be read.

mq.receive(&type, sizeof(type), recvd_size, priority);
switch (type) {
   case typeA:
      mq.receive(&VarOfStructA, sizeof(struct A), recvd_size, priority);
      break;
   case typeB:
      mq.receive(&VarOfStructB, sizeof(struct B), recvd_size, priority);
      break;
   default:
      break;
}

I tried to search for solutions, as I assume this is a common problem. However, I couldn't find any helpful idea.

Yaniv G
  • 307
  • 4
  • 11
  • 2
    The only suggestion I'd make is that rather than 'rolling your own' is to use one of the various serialization libraries that exist (including from boost). – john Aug 27 '22 at 15:21
  • You are inventing [Tagged Union](http://go/wpda/Tagged_union) for determining the type, and if there really can be different types in your setup, you will need something to distinguish them from each other. If it's just the length that varies, you can send a `std::uint32_t` at the front of your serialized data to indicate the length. – Ryan Haining Aug 27 '22 at 16:47

1 Answers1

0

Since c++17 you can use std::variant for a tagged union and if c++17 is not an option for you, then pick boost::variant as you are already using boost.

Here is an example how to use a variant to send and receive messages between two processes or even over network. I used concepts which is available since c++20 to produce better error messages but you can do without them.

#include <variant>
#include <cassert> // for test

class BufferWriter;
class BufferReader;

// concepts for better error messages
template<class T>
concept Writable = requires(BufferWriter& writer, const T& msg) {
    writer << msg;
};
template<class T>
concept Readable = requires(BufferReader& reader, T& msg) {
    reader >> msg;
};

class BufferWriter {
    // your impl here
public:
    template<Writable Msg>
    void write(const Msg& msg) {
        *this << msg;
    }
};

class BufferReader {
    // your impl here
public:
    template<Readable Msg>
    void read(Msg& msg) {
        *this >> msg;
    }

    template<Readable Msg>
    Msg read() {
        Msg msg;
        *this >> msg;
        return msg;
    }
};

// for integral as the index is of integer type
template<std::integral I>
BufferWriter& operator<<(BufferWriter& writer, I msg);
template<std::integral I>
BufferReader& operator>>(BufferReader& reader, I msg);

template<Writable ... Msgs>
BufferWriter& operator<<(BufferWriter& writer, const std::variant<Msgs...>& msgs) {
    writer << msgs.index();
    std::visit([&writer](const auto& msg) { writer << msg; }, msgs);
    return writer;
}

template<std::size_t I, class Var>
bool try_read_one_var_msg(BufferReader& reader, Var& var, std::size_t i) {
    if (i == I) {
        using msg_type = std::variant_alternative_t<I, Var>;
        var.template emplace<I>(reader.read<msg_type>());
        return true;
    }
    return false;
}

template<class Var, std::size_t ... IndexSeq>
void read_msg_of_variant(BufferReader& reader, Var& var, std::index_sequence<IndexSeq...>) {
    auto index = reader.read<std::size_t>();
    bool success = (try_read_one_var_msg<IndexSeq>(reader, var, index) || ...);
    if (!success) // the index didn't match any variant index
        throw std::bad_variant_access{};
}

template<Readable ... Msgs>
BufferReader& operator>>(BufferReader& reader, std::variant<Msgs...>& msgs) {
    read_msg_of_variant(reader, msgs, std::index_sequence_for<Msgs...>{});
    return reader;
}

struct Msg1 { 
    // your msg impl
};
struct Msg2 {
    // your msg impl
};

// pack a message into the writer using a serialization protocol
BufferWriter& operator<<(BufferWriter& writer, const Msg1& msg);
BufferWriter& operator<<(BufferWriter& writer, const Msg2& msg);
// extract a message from the writer using a serialization protocol
BufferReader& operator>>(BufferReader& reader, const Msg1& msg);
BufferReader& operator>>(BufferReader& reader, const Msg2& msg);

int main() {

    std::variant<Msg1, Msg2> msgs;
    msgs.emplace<Msg2>();

    BufferWriter writer;
    BufferReader reader;
    writer << msgs;

    std::variant<Msg1, Msg2> msgs2;
    reader >> msgs2;
    assert(msgs2.index() == 1);
    
    return 0;
}

I didn't include the serialization part because this is per application requirement. Also the compiler compiles this better than a hand written switch statement in some cases and I think at worst it will be identical to a switch or if. In the write method an exception will be thrown in std::visit if the variant is valueless_by_exception and in the read method an exception will be thrown if the index is not contained in the variant.

code and asm result on godbolt: https://godbolt.org/z/f4bM51x8E

dev65
  • 1,440
  • 10
  • 25