0

A frame read out from external device is stored in shared memory (in a struct), to be used both by main (C++) application, and an ANSI C library.

For reasons a bit too broad to explain here the library must remain pure ANSI C, and must retain access to the structure in its "pure ANSI C" form. But the main application uses the data in a lot of places, and it's a nuisance to do this the "ANSI C" way, treating it as a dumb data container. It would be much nicer if it was a class - if I could add constructors, copy constructor, comparison operators, a neater 'is valid' method (currently checked as absence of a magic number in one of the struct fields), generally a lot of stuff that a C++ class can do, but an ANSI C struct can't. Except if I replace the struct with the class in the shared memory, it will break compatibility with the library.

What's a neat way to achieve this? Create a class that inherits from the struct? Inheritance through composition? A separate class with a set of conversion methods? Something else I didn't think about? Some transparent way to keep the data visible to C unchanged, but enhanced with class features for C++?

Note: both C++ and C operate on the same instance of the structure. Main app reads out the frame out of the device, writes to the structure in shared memory, then calls the library functions to do their magic on the frame (possibly, but not necessarily modifying it; business logic), then performs own operations on it (display, logging, re-broadcasting on other media etc.

I have full control over the C++ code, but C code is mostly out of my control; I can create a local copy of the structure, if that's beneficial, but my copy and the 'business logic instance' should remain in sync, or at least be synced before and after each library function call (operation under my control, but timing dictated by system requirements.)

edit: some extra details, as requested:

  • the "business logic" is implemented in the C library, customized C code generated by an external application (for PC) from input from user (graphical interface for drafting the logic; think "block diagram"), varies per device (many users, even more devices). The device requires cross-compiling; only an ANSI C cross-compiler is available in a form that can be easily bundled with the PC application; C++ cross-compiler is available only on the system developer's (my) PCs ; its installation process and license make it impossible to bundle with the (sold) generator app.

  • the library and the C++ application on the device use shared memory as storage of about all input and output data, for two reasons:

    • primarily because the volume and variety of that data would make it extremely difficult to provide it as parameters in function calls (over 20 wildly varying external systems the device can cooperate with, each with own communication protocol, each providing inputs and/or accepting outputs which may be utilized in business logic). C++ app handles all communication and converts the data back and forth between the various interfaces and an "easily digestible" data format stored in the shared memory for consumption by the library as required (by the particular instance of business logic).
    • But there are other applications running on the device - a WWW server, a debug app, etc - which can peer into the shared memory too, to display current state, allow real-time parameter tweaks etc. While such "centralized storage"/"superglobal" may be considered an anti-pattern, considering the wild variety of interactions between the systems (internal, and external, where the C++ serves as a central hub for connecting them) and makes for a much clearer structure than what kind of byzantine web would arise if I tried to connect every data provider with every data consumer directly.
  • The main app handles synchronization (timing, locking) reads from shared memory for all interfaces where it matters; others can just peer into the shared memory and pick what they need whenever they want (read-only); resulting race condition errors will result in a perfectly acceptable momentary glitch that will be corrected come next 'tick'. All writes are synchronized.
  • because the C library is the primary, most important consumer and provider for the shared memory, the structure in shared memory must remain C compatible.
SF.
  • 13,549
  • 14
  • 71
  • 107
  • What's "ANSI-C"? C90 or standard C? – Lundin Apr 12 '17 at 10:43
  • @Lundin: In this case, [Tiny C Compiler](http://www.bellard.org/tcc/tcc-doc.html#SEC7), call parameters under my control, so C99 and GNU C extensions are available although not used currently; otherwise, "TCC implements all the ANSI C standard, including structure bit fields and floating point numbers (long double, double, and float fully supported)." – SF. Apr 12 '17 at 10:48
  • I'm in a similar situation with the GSL library. Would RAII (resource allocation is initialization) work? This might be the cleanest way to wrap your code. However, it depends on how exactly your C structs are allocated and how much control you have over them (e.g. the shared memory constraint you briefly mention), and maybe you've ruled this out. Your C++ class could always just contain a pointer to the C struct in shared memory. Can you provide more details? – jwimberley Apr 12 '17 at 10:58
  • The question is what is the benefit of C++ here? Is the aim to port to C++ in the long term? Are there other libraries or GUI that need C++? If not, then simply stick to C. Otherwise, if there are real reasons to switch to C++, probably just write a wrapper class around the struct, and keep the struct as a public, exposed member. – Lundin Apr 12 '17 at 11:13
  • @Lundin: `frame.isValid()` vs `frame.type != VDV_FRAME_INVALID_TOKEN`, `if(frame1 == frame2)` vs `if(memcmp(&frame1, &frame2, sizeof(frame1) == 0)`, `frame.cmd = x; frame.adr = y; frame.send()` vs `frame.id = framehandler::convert_to_id(x,y); interface::Instance()->send(frame);` and many such. Being a smart object instead of a (complex but dumb) data structure is a big benefit. – SF. Apr 12 '17 at 11:52
  • @SF. Function pointers have been in the C language since the beginning. No need to call your struct dumb just because you don't know of them. Nothing in C would prevent you to encapsulate the original struct then use function pointers to get identical syntax. If you want polymorphism then function pointers are preferred. Though traditional C would probably rather use "ADT member functions": `frame_is_valid(frame)`, `frame_is_equal(frame1, frame2)` and so on. What you can't get in C is RAII behavior out of constructors/destructors, otherwise it is quite similar to C++. – Lundin Apr 12 '17 at 12:06
  • @Lundin: Ever used C function pointers through a shared memory? – SF. Apr 12 '17 at 12:11
  • @SF. As I said, you'd write a wrapper. – Lundin Apr 12 '17 at 12:24

4 Answers4

2

Create a class that is derived from the C-struct but make sure that the memory layout stays untouched, i.e. do not use virtual methods (these would add a vtable) or add member variables. In C++11 terms this would be called a standard-layout class. For more information see here:

What are Aggregates and PODs and how/why are they special?

Obeying this rule you can safely cast between the C struct and C++ class and use the appropriate member functions.

Note: Regarding allocating the data structure you need to use the same set of functions for release as for allocation, i.e. if it has been allocated using malloc(), it must be released using free() and if it has been allocated using new it must be released using delete. Therefore, if you want to allocate objects both from within C and C++ code, you are limited to malloc/free since new/delete is not available from within C.

Meixner
  • 615
  • 5
  • 8
  • This would work. How should I access the instance of the "C-struct - as - Class" though? Create pointer to Class, reinterpret_cast<> the pointer to struct? – SF. Apr 12 '17 at 12:24
  • 2
    Using a pointer would be one option. The other would be to use a reference, e.g. YourClass &cxx=reinterpret_cast(c). – Meixner Apr 12 '17 at 12:32
  • It seems like a very nice option but I'll wait for input of others... May be just me, but reinterpretation of the underlying object like that feels a little... *precarious*. Also, if I do need some more member variables after all (say, to hold some file handles or pointers, e.g. to reader/writer), is there some trick to get them? – SF. Apr 12 '17 at 12:41
  • @SF. If you do need extra member variables, you can create a struct `struct containing_struct { // extra data members };` and another `struct isomorphic_struct : containing_struct { // identical to C struct }`. Then given the containing struct, you can `static_cast` it to the `isomorphic_struct` and then `reinterpret_cast` it as your C struct, if need be (or some combination of these techniques). This keeps the struct in standard layout. – jwimberley Apr 12 '17 at 12:46
  • @SF. Or, you can just put the extra members at the end, and `reinterpret_cast` the address of the struct. – jwimberley Apr 12 '17 at 12:50
  • You can add member variables in the derived class but then you are no longer able to allocate this struct/class from within C code, since the allocation would lack the added member variables. – Meixner Apr 12 '17 at 12:52
  • @Meixner: Allocation is in C++, but if it's a member of a different struct (it is), the consecutive fields of the parent on the C side will be mismatched. OTOH I suspect I could (ab)use static variables in methods as static members, say, a method containing a (local, static) file handle and returning a reference to it. (I will hardly ever need custom per-instance members.) – SF. Apr 12 '17 at 13:07
  • @SF: If the C++ class has been derived from the C struct, the consecutive fields match. BTW there is no need to cast to the base class (the C struct), i.e. explicit cast operations like static_cast<> or the like are not required. This is handled by C++ automatically. An explicit cast is only required for the opposite direction and since these are related types static_cast<> is sufficient and reinterpret_cast<> is not required. – Meixner Apr 12 '17 at 15:09
  • @Meixner: Not sure if we're talking about the same thing. C++ instantiates `super_struct { smart_class X; int Y }`. C acquires access to the instance (in shm) as `super_struct { dumb_struct X; int Y }`. If `smart_class` derived from `dumb_struct` is larger (has extra member variables at the end), Y will be misaligned. – SF. Apr 12 '17 at 15:44
  • @SF: In this case we are no longer talking about inheritance only. This is a different situation and has nothing to do with compatibility between C and C++. – Meixner Apr 13 '17 at 06:28
  • @Meixner: it has everything to do with compatibility... just fails at it. My current situation is `super_struct { dumb_struct X; int Y }` on both sides, and if I understand correctly, casting reference to the `dumb_struct` member into reference to `smart_class` solves my problem - as long as all the data members match 1:1, no extras at the end. – SF. Apr 13 '17 at 07:28
1

If you somehow subclass this, the base structure would need a vtable. Can you change the struct to include these elements, and then cast it to/from the class/struct. You'd need to use reinterpret_cast<>().

However that is a terrible idea. Please do not do it.

Instead, implement your business logic in a class, that contains the struct. This way you won't need to support marshalling the structure between the two codebases, you can keep one copy of the struct.

However remember to mark the struct as volatile, if it needs to be altered by any form of background thread that the C++ code is not aware of.

Paul Bentley
  • 344
  • 1
  • 9
  • I would add that this class that contains the struct could employ RAII (resource allocation is initialization), if that's possible for the OP's work case. I was in a similar situation recently, wrapping GSL's structs in C++ ... but unfortunately because of other constraints I've had to use the `reinterpret_cast` trick as well, and it's been such a pain. – jwimberley Apr 12 '17 at 10:59
  • Business logic is in the C library, custom, per device - generated code; the user uses a friendly graphical application to create the business logic, which is then compiled to ANSI C, then that C is (cross-)compiled into the library using TCC and deployed on the device. We can't require the user to install a C++ cross-compiler (installing it and getting it to run is an hour for a skilled IT person, not a task for a common user), we're stuck with TCC which is easily bundled with the generator app, supports cross-compile to the target device but provides ANSI C only. – SF. Apr 12 '17 at 11:04
  • @SF. What's the exact relationship between the C and C++ code? Are you saying that these are separate programs, compiled separately, accessing the same shared memory? In that case, how does the C++ get the the pointer to the shared memory - through a C API? – jwimberley Apr 12 '17 at 11:08
  • C++ generates the shared memory space, initializes it, and serves as interface between the whole world and the shared memory. It also performs cyclic calls of the C code library functions (which has no own thread/engine; just logic). The C code performs all the calculations/decisions basing on the (very broad) input, and produces (equally broad) output (stored in shared memory) which then the C++ app "delivers to the world". – SF. Apr 12 '17 at 11:41
0

simple, move your c struct to c++ PIMPL

// A.h
class A
{
 //...
private:
 struct impl;
 static std::unique_ptr<impl> p_impl;
}

// A.cpp
std::unique_ptr<A::impl>  A::p_impl;

struct A::impl
{
  // c code here
}
sailfish009
  • 2,561
  • 1
  • 24
  • 31
0

My understanding is that the C++ managing code allocates a block of shared memory, and the C and C++ code coordinate what type of information will be written where. The C++ manager periodically examines locations in memory, knowing what type of data it expects to find. I imagine then that the C++ code has a table of void* pointers to your_c_struct* pointers to examine. In the former case, these can always be converted as necessary via reinterpret_cast<your_c_struct*>(void_ptr). So, I'll assume that the C++ code has a table of pointers to C structs in shared memory. In that case I think a solution is to create a pseudo-RAII class, which can either point to un-owned locations in shared memory or own and allocate/deallocate memory on the heap. This would look something like:

class Wrapper {
public:
    Wrapper() : owned(true), data(new your_c_struct{}) {}
    Wrapper(your_c_struct* _data) : owned(false), data(_data) {}
    ~Wrapper() {
        if (owned)
            delete data;
    }

    // copy constructors
    // overloaded comparison operators

private:
    bool owned;
    your_c_struct* data;
};

There are two main ways to construct this class: either making a new object on the heap or passing it a pointer to shared memory that it does not own. I've used this technique for the GSL library successfully, where the equivalent of C structs in shared memory are C structs allocated within and returned by GSL numerical algorithms. I went a further step of making the second constructor provide and providing a named constructor Wrapper Wrapper::SoftWrap(your_c_struct* _data).

jwimberley
  • 1,696
  • 11
  • 24
  • It's much simpler: the shared memory doesn't ever store any pointers, just values. It's just an enormous, very complex `struct`. C++ holds it as a plain variable (which happens to be located over a shared memory fragment), and performs all operations as on normal variable. C holds it as a pointer to a variable of the same type, performs all operations just the same. There are no dynamic allocations (the hardware is always the same and must work in most pessimistic configuration variant, so there's no point in savings through dynamic allocations; shm is just the max size it can ever be.) – SF. Apr 12 '17 at 12:18
  • @SF. So it's one giant struct with dozens/hundreds/thousands of fields? Ok... but there's just one? You mentioned you wanted copy constructors and comparison operators, so I presumed you had multiple objects of the same type. – jwimberley Apr 12 '17 at 12:23
  • One enormous struct, with about 5 fields for structs of varied classes of subsystems, each with a dozen or so structs for subsystems, these with static arrays of structs for input/output channels, and variables for configurations, and so on. This is the shared memory, only one instance of it ever, and with all writes and most reads synchronized. But outside it (in the C++ app) there are converters, parsers, queues, loggers, and so on and so on, which operate on copies of structures picked from the shared memory struct tree or create them (to prepare a write), – SF. Apr 12 '17 at 12:33
  • ...and I need these functions primarily for these - but maintaining two versions of every data type (or even some of them), one smart, and one dumb, would bothersome, so it would be great if the same 'smart' type of data was a member of the shm super-struct and usable as independent instance. – SF. Apr 12 '17 at 12:38