MemoryBlock and void*

Hi everyone,

I tested the following code:

example class:

class TestClass {

public:

int iVar;
std::string strVar;
AudioChannelSet cs;
bool boolVar;
float floatVar;

TestClass(int i, std::string s, AudioChannelSet a, bool b, float f) {
    
    iVar = i;
    strVar = s;
    cs = a;
    boolVar = b;
    floatVar = f;
 }
};

creating a MemoryBlock and sending it:

TestClass testClass(42, "Some Text", AudioChannelSet::stereo(), true, 0.154);
void* vPtr = &testClass;
        
MemoryBlock memBlock;
memBlock.replaceWith(vPtr, sizeof(TestClass));

activeConnection->sendMessage (memBlock);

receiving the MemoryBlock:

void messageReceived(const MemoryBlock& message)
{

const TestClass* tcPtr { static_cast<const TestClass*>(message.getData()) };
        
        int newInt = tcPtr->iVar;
        std::string newStr = tcPtr->strVar;
        AudioChannelSet newCs = tcPtr->cs;
        bool newBool = tcPtr->boolVar;
        float newFloat = tcPtr->floatVar;
}

This is working so far but it seems to be quite dangerous because there is no type safety and if I would send something else, the app would crash. I know there are a lot of workarounds with primitive types, but I need to send dataStructures like the above AudioChannelSet without deconstructing it.

Maybe someone has a better idea?

Thanks,
Stefan

Surprised to read that this works at all. The biggest problem in your example is with std::string, as the object itself isn’t guranteed to contain the whole string. While doing so for short strings, for longer strings it is completely allowed to only manage some heap memory to hold the string. Now doing a copy like that would lead to a string object created at the receiver side, that contains a pointer to the string on the sender side – I think you can imagine that this is likely to crash in any real-world context. The same goes for your AudioChannelSet, it contains a BigInteger internally which can do the same as the std::string. What you are doing can and should only be done with trivially copyable objects, which gurantee that this kind of trivial copy can be done with them.

A more stable way to do what you are doing could be serializing the content to a nested var which can be written to a MemoryOutputStream, which can wrap a MemoryBlock to write to. De-serializing then would be the other way round. However you’d still need to implement some var <-> AudioChannelSet conversion yourself.

Oh yeah, the var class! Haven’t thought about that and never used it so far.

Okay, a new attempt:

class TestClass: public ReferenceCountedObject {
public:

int iVar;
std::string strVar;
AudioChannelSet cs;
bool boolVar;
float floatVar;

using Ptr = ReferenceCountedObjectPtr<TestClass>;

TestClass(int i, std::string s, AudioChannelSet a, bool b, float f) {
    
    iVar = i;
    strVar = s;
    cs = a;
    boolVar = b;
    floatVar = f;
 }
};

Writing:

 
            TestClass* tcPtrOut = new TestClass(42, "Some Text", AudioChannelSet::stereo(), true, 0.154);

            var varWrapper(tcPtrOut);
            
            MemoryBlock memBlock;
            MemoryOutputStream mos(memBlock, false);
            varWrapper.writeToStream(mos);

Reading:

void messageReceived (const MemoryBlock& message)
        {
            
            MemoryInputStream mis(message, false);
            var v = var::readFromStream(mis);
            
            TestClass* tcPtrIn = dynamic_cast<TestClass*>(v.getObject());
            if (tcPtrIn) {

                int newInt = tcPtrIn->iVar;
                std::string newStr = tcPtrIn->strVar;
                AudioChannelSet newCs = tcPtrIn->cs;
                bool newBool = tcPtrIn->boolVar;
                float newFloat = tcPtrIn->floatVar; 
            }
}

It compiles but varWrapper.writeToStream(mos); will trigger a JUCE Assertion failure in juce_Variant.cpp:301
jassertfalse; // Can't write an object to a stream!

If var is holding an ìnt instead of the TestClass object for example, the code is working as expected.

I’ve also seen that var can hold binary data as a MemoryBlock. But that would lead to the same problems like the above, right?

In the meantime, thank you so much for pointing me to the right direction!!
Stefan

Maybe think about using ValueTree? Write all needed data from your class to a ValueTree, send it as a MemoryBlock and recreate safely your class on the other side. I use it all the time for sending data forth and back.

2 Likes

@StefanS Well I meant something completely different. My point was that most of your member variables are more or less directly convertible to var so you should create an Array<var> to store them all as vars in an ordner known to you and then restore them back again from such an array. This should be a lot safer. Example class layout:

class TestClass
{
public:

    int iVar;
    std::string strVar;
    AudioChannelSet cs;
    bool boolVar;
    float floatVar;


    TestClass (int i, const std::string& s, AudioChannelSet a, bool b, float f)
      : iVar     (i),
        strVar   (s),
        cs       (a),
        boolVar  (b),
        floatVar (f)
    {}

    /** Serializes this class into a var */
    var toVar()
    {
        // To be able to safely serialize/deserialize our content, each element needs to
        // be stored to a separate var. Therefore, we create an array of vars with one array
        // field for each value
        Array<var> valuesArray;
        valuesArray.resize (numElements);

        // var has a constructor allowing implicit int to var conversion
        valuesArray.set (iVarIdx, iVar);

        // var has a constructor that takes a JUCE string. We can convert a std::string to a
        // JUCE string. Other, probaly better option: Use a JUCE string directly
        valuesArray.set (strVarIdx, String (strVar));

        // The AudioChannelSet needs some custom serialization/deserialization logic. How
        // straightforward/complicated this logic is will depend on what kind of sets you expect
        valuesArray.set (csIdx, channelSetToVar (cs));

        // var has a constructor allowing implicit bool to var conversion
        valuesArray.set (boolVarIdx, boolVar);

        // var has a constructor allowing implicit double to var conversion
        valuesArray.set (floatVarIdx, double (floatVar));

        // After all the nested values have been set, we wrap the array in a single var
        var valuesWrapper (valuesArray);

        return valuesWrapper;
    }

    /** This constructor lets us (re) construct a TestClass instance previously serialized by toVar */
    TestClass (const var& source)
    {
        // We stored an array in our outer wrapper var below – so let's ensure if it is an array
        // in the first place and if the size matches
        jassert (source.isArray());
        auto& valuesArray = *source.getArray();
        jassert (valuesArray.size() == numElements);

        // No we do the whole stuff from above backwards.
        iVar     = valuesArray[iVarIdx];
        strVar   = valuesArray[iVarIdx].toString().toStdString();
        cs       = varToChannelSet (valuesArray[csIdx]);
        boolVar  = valuesArray[boolVarIdx];
        floatVar = float (double (valuesArray[floatVarIdx]));
    }

private:

    // Each element should correspond to a certain element in the inner var array, this enum holds the indices
    enum ValueIdx
    {
        iVarIdx = 0,
        strVarIdx = 1,
        csIdx = 2,
        boolVarIdx = 3,
        floatVarIdx = 4,

        numElements
    };

    static var channelSetToVar (const AudioChannelSet& setToConvert)
    {
        // A suitable way to do the conversion implemented here!

        return var();
    }

    static AudioChannelSet varToChannelSet (const var& varToConvert)
    {
        // A suitable backwards conversion implemented here

        return AudioChannelSet();
    }
};

Then writing would look like

TestClass tc (42, "Some Text", AudioChannelSet::stereo(), true, 0.154);

MemoryBlock memBlock;
MemoryOutputStream mos (memBlock, false);

auto tcVar = tc.toVar();
tcVar.writeToStream (mos);

Reading:

void messageReceived (const MemoryBlock& message)
{
    MemoryInputStream mis (message, false);
    var v = var::readFromStream (mis);

    TestClass tc (v);
}

Note, that in your version you are still stupidly copying a non trivial copyable object (because of the nested std::string and audio channel set) there in a trivial way, which will lead to crashes.

Ok, it took a bit but now I get it.
jassertfalse; // Can't write an object to a stream!
means that that no object can be written into streams! I thought it was an error with my particular TestClass.
Well, before making things more complicated, I will put my variables out of the class into a ValueTree as MBO suggested and try to get away with primitive types. I can pass the AudioChannelSet information as an int and recreate it on the other side. And the std::String can also be transmitted as a juce::String.

Thanks PluginPenguin for taking the time.

Best regards,
Stefan

FWIW, if your class has arrays or vectors as member variables, you can convert those containers to memory blocks (since they are contiguous), then convert those memory blocks to Base64 strings, which are easily added as a property to a ValueTree.

1 Like