views:

177

answers:

6

I have the following struct which will be used to hold plugin information. I am very sure this will change (added to most probably) over time. Is there anything better to do here than what I have done assuming that this file is going to be fixed?

struct PluginInfo
{
    public:
        std::string s_Author;
        std::string s_Process;
        std::string s_ReleaseDate;
        //And so on...

        struct PluginVersion
        {
            public:
                std::string s_MajorVersion;
                std::string s_MinorVersion;
                //And so on...
        };
        PluginVersion o_Version;

        //For things we aren't prepared for yet.
        void* p_Future;
};

Further, is there any precautions I should take when building shared objects for this system. My hunch is I'll run into lots of library incompatibilities. Please help. Thanks

+2  A: 

One hideous idea:

A std::map<std::string, std::string> m_otherKeyValuePairs; would be enough for the next 500 years.

Edit:

On the other hand, this suggestion is so prone to misuse that it may qualify for a TDWTF.

Another equally hideous idea:
a std::string m_everythingInAnXmlBlob;, as seen in real software.

(hideous == not recommended)

Edit 3:

  • Advantage:
    The std::map member is not subject to object slicing. When older source code copies an PluginInfo object that contains new keys in the property bag, the entire property bag is copied.
  • Disadvantage:
    many programmers will start adding unrelated things to the property bag, and even starts writing code that processes the values in the property bag, leading to maintenance nightmare.
rwong
of course any general property bag comes with a drawback: it's not strongly typed...(not my downvote btw)
Mitch Wheat
@Mitch: I edited to make it obvious that it's not recommended. It's as widely used as [big ball of mud](http://www.infoq.com/news/2010/09/big-ball-of-mud), though.
rwong
+2  A: 

what rwong suggest (std::map<std::string, std::string>) is a good direction. This is makes it possible to add deliberate string fields. If you want to have more flexibility you might declare an abstract base class

class AbstractPluginInfoElement { public: virtual std::string toString() = 0;};

and

class StringPluginInfoElement : public AbstractPluginInfoElement 
{ 
  std::string m_value;
  public: 
   StringPluginInfoElement (std::string value) { m_value = value; }
   virtual std::string toString() { return m_value;}
};

You might then derive more complex classes like PluginVersion etc. and store a map<std::string, AbstractPluginInfoElement*>.

Philipp
+6  A: 

What about this, or am I thinking too simple?

struct PluginInfo2: public PluginInfo
{
    public:
        std::string s_License;
};

In your application you are probably passing around only pointers to PluginInfos, so version 2 is compatible to version 1. When you need access to the version 2 members, you can test the version with either dynamic_cast<PluginInfo2 *> or with an explicit pluginAPIVersion member.

Roland Illig
This is a good idea (a simple one that works and is in the spirit of C++). Care should be taken so that the user's source code always pass *by reference* in order to avoid the [object slicing problem](http://stackoverflow.com/questions/274626/what-is-the-slicing-problem-in-c), [also here](http://stackoverflow.com/questions/274626#274636).
rwong
+1  A: 

Here's an idea, not sure whether it works with classes, it for sure works with structs: You can make the struct "reserve" some space to be used in the future like this:

struct Foo
{
  // Instance variables here.
  int bar;

  char _reserved[128]; // Make the class 128 bytes bigger.
}

An initializer would zero out whole struct before filling it, so newer versions of the class which would access fields that would now be within the "reserved" area are of sane default values.

If you only add fields in front of _reserved, reducing its size accordingly, and not modify/rearrange other fields you should be OK. No need for any magic. Older software will not touch the new fields as they don't know about them, and the memory footprint will remain the same.

DarkDust
One drawback I can think of is having to know the `sizeof()` for each data type used. But still this is good I think.
nakiya
+5  A: 

Either your plugin is compiled with the same version of C++ compiler and std library source (or its std::string implementation may not be compatible, and all your string fields will break), in which case you have to recompile the plugins anyway, and adding fields to the struct won't matter

Or you want binary compatibility with previous plugins, in which case stick to plain data and fixed size char arrays ( or provide an API to allocate the memory for the strings based on size or passing in a const char* ), in which case it's not unheard of to have a few unused fields in the struct, and then change these to be usefully named items when the need arises. In such cases, it's also common to have a field in the struct to say which version it represents.

But it's very rare to expect binary compatibility and make use of std::string. You'll never be able to upgrade or change your compiler.

Pete Kirkham
+1. Thanks for pointing out using basic types is the way.
nakiya
+2  A: 
sbi
Does this mean that the compiler guarantees that the variables will be placed in memory in the same layout as the code?
nakiya
And I am pretty sure that evolution is linear.
nakiya
Thanks for the lengthy reply. But I think you may have misinterpreted my requirements. Suppose in the first version we have header `H1.h`. This will be included in the application `A1` and shared object `D1`. The header will declare `PluginInfo1` struct with basic info. In the second version, `A` will have headers `H1.h` and `H2.h`. `H2.h` will declare `PluginInfo2` which adds another member. `A` will load `D2` which uses `PluginInfo2` from `H2.h`. And, `A` will also have to load `D1`. There will be a single function to retrieve `PluginInfo` from shared object. Which means...
nakiya
... what matters is memory layout. If `PluginInfo2` only adds a member to the end of `PluginInfo1` in memory, `A` can work with it properly I think. I maybe wrong though.
nakiya
Found what I was looking for : http://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html says : `adding optional items to the ends of structures is okay, as long as those structures are only allocated within the library`.
nakiya
@nakiya: This is designed for a scenario, where the `struct`'s name never changes (no `PluginInfo2`), but newer versions of the API simply append data to it. The functions taking this data first look at its `size` member from which they find out the data's version and then treat it according to that. Older plugins can load in newer host apps because the host app supplies reasonable defaults for the newer `struct` members that the old plugins didn't know about. (If you want, newer plugins can even load in older host apps, if the host app just ignores the additional data it doesn't know about.)
sbi
This system allows such a `struct` to be allocated by the user. (Win32 indeed does that.)
sbi
@sbi: Can you please explain a little more? Perhaps with an example? thanks.
nakiya
@sbi:`adding optional items to the ends of structures is okay, as long as those structures are only allocated within the library` I thought what this means is, if a shared lib has `PluginInfo2`, and a newer version of the app knows `PluginInfo2` too, the app can load the lib correctly in addition to the fact that it can load previous libraries - care has to be taken not to access fields which weren't in previous libs - and load even future libraries because it will only be using a subset of the fields in `PluginInfo3`. Am I wrong?
nakiya
@nakiya: I've tried to add a more thorough explanation. To be honest, I'm not sure what's so hard about this. You might want to explain this, so I can address it properly.
sbi
Or is it that you're used to dealing with languages that feature reflection? C++ has next to nothing of that. When you pass a pointer to some data to another function, that function interprets the binary data found at the address using the code the compiler has generated from its source. If the caller and callee were compiled using different versions of the data, then neither of them will detect that. Officially this invokes undefined behavior, although I suppose you can get away with a scheme like this on maybe every platform.
sbi
I think your answer is right. I'm going in circles :). Thanks for the lengthy explanation.
nakiya
@sbi: I totally read it just now. Yesterday I was just skimming. I don't need inter-plugin calls. Plugins only work with the app so only the app has to know how to handle a `PuginInfo` which makes my life easier. Your answer even considers plugin-plugin connections which is nice!!!
nakiya