I’m writing an InterprocessConnection Plug-In/App and am a bit struggling with the MemoryBlock handling. So far I have this code for primitive types like bool, int, float, etc
// sending process: preparing the memory block for sending
int someInt = 46;
MemoryBlock mBlock;
MemoryOutputStream mos(mBlock, false);
mos.writeInt(someInt);
interprocessSender.sendMessage(mBlock);
// receiving process: callback for reading the received memory block
void messageReceived (const MemoryBlock& message)
{
MemoryInputStream mis(message, false);
int result = mis.readInt();
}
This is working so far. I also read (and understand) the InterprocessConnection example code for sending and receiving Strings. But I wonder how I can transmit other types like custom structs. Will I have to read the memory block byte per byte and writing my own code to reconstruct it? Or is there an easier and safer way to do this?
Since C++ has no reflection (you can’t query what member variables classes have), you will have to do it manually. If your class is trivial to construct and destroy (including all its member variables), you may be able to get away by doing a byte level copy into and back from the MemoryBlock.
This isn’t very flexible and safe, though. All the member variables of the class that is serialized this way need to be trivial types like ints and doubles.
With a bit of templating, you can use the same function to read/write to a stream, which makes round-tripping a lot more robust (there’s no danger that you edit the ‘write’ function and forget to update the ‘read’ function). This is also a lot safer than just writing the raw bits that make up the object into a stream.
// Wrappers around the juce streams...
struct OutputArchive final {
OutputArchive(OutputStream& stream) : stream_{stream} {}
void operator()(bool x) { stream_.writeBool(x); }
void operator()(int x) { stream_.writeInt(x); }
void operator()(float x) { stream_.writeFloat(x); }
void operator()(const std::string& x) { stream_.writeString(x); }
OutputStream& stream_;
};
struct InputArchive final {
InputArchive(InputStream& stream) : stream_{stream} {}
void operator()(bool& x) { x = stream_.readBool(); }
void operator()(int& x) { x = stream_.readInt(); }
void operator()(float& x) { x = stream_.readFloat(); }
void operator()(std::string& x) { x = stream_.readString(); }
InputStream& stream_;
};
struct MyComplexStruct final {
bool bool_;
int int_;
float float_;
std::string string_;
// This function is used to both read and write this object!
template <typename Archive>
void serialize(Archive& archive) {
archive(string_);
archive(bool_);
archive(int_);
archive(float_);
}
};
int main() {
MyComplexStruct str{true, 543, 0.12, "hello world"};
MemoryOutputStream outputStream;
OutputArchive outputArchive{outputStream};
// When we call serialize with an OutputArchive, the object writes its
// contents into an output stream
str.serialize(outputArchive);
MemoryInputStream inputStream{outputStream.getData(),
outputStream.getDataSize(), false};
InputArchive inputArchive{inputStream};
MyComplexStruct copy;
// When we call serialize with an InputArchive, the object updates its data
// members using the next items in the input stream
copy.serialize(inputArchive);
std::cout << copy.bool_ << '\n';
std::cout << copy.int_ << '\n';
std::cout << copy.float_ << '\n';
std::cout << copy.string_ << '\n';
}