How to properly share polymorphic data created at runtime between different cores? Embedded c++

Viewed 133

I am using a dual-core device and have a requirement for core A to create a structure of data that contains arguments for a list of functions running on core B, periodically updating it and informing core B. The number of arguments and types can change during run time.

My plan was as follows..

Create a ParamsInterface base class and each function would have it's own parameter class that derives from the base class. Using polymorphism, this would allow me to create a vector of ParamsInterface* in shared memory, and populate it from core A.

Core A populates the vector by newing whichever child class it needs, and pushing the returned pointer to the vector.

Example core A:

volatile std::vector<ParamsInterface*> sharedParams __attribute__((section(".shared_memory")))

class ParamsInterface
{
public:
    virtual ~ParamsInterface(){};
};

class Function1Params: public ParamsInterface
{
public:
    float mParam1;
    bool  mParam2;
};


void populate() 
{
    Function1Params *params = new Function1Params ;
    params->mParam1= 1.1;
    params->mParam2 = true;
    sharedParams.push_back(params);
}

Core B should then be able to statically cast the pointer to the child type, since it knows which type each member of the vector will be. Of course access is controlled by hardware semaphore, and all pointers are deleted before refreshing the list.

Example core B:

void process(ParamsInterface* params)
{
    Function1Params paramsCasted = static_cast<Function1Params *>(params);
    processFunc1(paramsCasted->mParam1, paramsCasted->mParam2);
}

The problem is that when core B reads this shared, uncached data, the address pointed to by ParamsInterface* is not correct, and I get a hard fault when trying to cast. I have checked the pointer created by Core A, and it points to a memory region accessible to both cores.

My question is two fold, firstly, am I doing something incorrect here - perhaps expecting a method to work that can never work? And secondly, am I missing a much better way to do this? I've left out the information about how the list of functions work to try and keep the question concise.

Many thanks for any help.

EDIT:

So it seems that the issue is to do with where the ParamsInterface objects are being created. Core A creates them and they are being placed in heap, which unfortunately happens to be in an area that isn't in a managed, and core B apparently cannot access.

If I use placement new to place them in a managed section of memory, I have to specify a precise address, but this would get very complicated since all of the ParamsInterface objects could be a different size.

So is there a way to new() 'polymorphic' objects into a container in a specific memory section? Or do I need to write some sort of custom memory pool manager?

Thanks

1 Answers

You appear to be approaching C++ code from a very C-like perspective. They are very different beast in more than syntax.

C++ code is interpreted in isolation, free from hardware context, and compilers have no obligation to respect what you write beyond ensuring that the side-effects produced by your code are maintained. This is known as the as-if rule.

This extends to memory access in multi-threaded and multi-process environments. Unless told otherwise, compilers are allowed to assume that memory will only be altered and read by the code they are compiling, and that no external factors are involved.

Here's a concrete example:

int* sharedParams[12] __attribute__((section(".shared_memory")));

void populate() 
{
    auto params = new int{12};
    sharedParams[4] = params;
}

int main() {
    populate();

    while(1) {}
}

If I compile this with gcc -O3 -flto, I get the following program:

 jmp    401020 <main>
 cs nop WORD PTR [rax+rax*1+0x0]
 nop    DWORD PTR [rax+0x0]

The sharedParams array does not even exist anymore! see on godbolt

Keeping that in mind, the following:

Of course access is controlled by hardware semaphore.

is not enough. C++, the language, does not know about these, and the synchronization between objects across threads/cores must be done at the language level as well as at the hardware level. If you don't, the compiler is allowed to make various assumptions about how the memory is used, and will optimize a lot of stuff away.

There are two ways you can address this:

  1. Use language-level constructs like the various utilities present in the standard library. std::atomic_thread_fence is probably what you need here. See on godbolt
void populate() 
{    
    int* params = new int{12};
    sharedParams[4] = params;
    std::atomic_thread_fence( std::memory_order_release ); // std::memory_order_acquire when reading
}

N.B. std::atomic_thread_fence is not really a great fit for this use-case, but as you are operating outside of the bounds of the language already. It should fit the bill here, but there will be no formal guarantee.

  1. Mark the contentious memory as volatile so that the compiler stops making assumptions about when and how it can change. See on godbolt
volatile int* sharedParams[12] __attribute__((section(".shared_memory")));
void populate() 
{
    int volatile * params = new volatile int{12};
    sharedParams[4] = params;
}

The first option is vastly preferable. While it's not as brutal as a full memory barrier on every access, the later will severely tie the hands of the compiler. volatile also does not provide guarantees that things will happen in the correct order.

further reading at: https://en.cppreference.com/w/cpp/atomic/memory_order

Related