1

I have a piece of legacy C++ code which lives in a library and is loaded as DLL by an application:

// Foobar.h (old)
struct Foobar
{
    const char* foo;
    const char* bar;
    int apple;
    int banana;
};

// Foobar.cpp (old)
void func(Foobar* ptrFoobar) {
    cout << ptrFoobar -> foo << endl;
    cout << ptrFoobar -> bar << endl;
    cout << ptrFoobar -> apple << endl;
    cout << ptrFoobar -> banana << endl;
}

And I am hoping to add a new field to the end of this struct in the new release for this library (which I know breaks the ABI):

// Foobar.h (new)
struct Foobar
{
    const char* foo;
    const char* bar;
    int apple;
    int banana;
    int orange
};

// Foobar.cpp (new)
void func(Foobar* ptrFoobar) {
    cout << ptrFoobar -> foo << endl;
    cout << ptrFoobar -> bar << endl;
    cout << ptrFoobar -> apple << endl;
    cout << ptrFoobar -> banana << endl;
    cout << ptrFoobar -> orange << endl;
}

Suppose it's known that the only way how this library is used by the application is:

// app.cpp (linked with new library)
Foobar foobar { ... }; 
func(&foobar);

And app.cpp is linked with the new library (the one with extra member variable orange).

While I know it is unsafe in general to append a member variable to a C++ class, since it breaks the ABI in many terrible ways, so code above should never have been written in the first place, I am still wondering under the extremely restrictive conditions:

  1. The new field orange is appended to the end of the struct
  2. The fields in the struct are ordered by their sizeof(type) value non-increasingly.
  3. The only way how Foobar is used in application code is constructing a new object (using the new Foobar.h) and passing it to func(..) by pointer. And the library (either the old or new one) does not do anything more than reading the value of known member variables as shown in the samples above (so no malloc, no sizeof, no other crazy pointer maths).

Is it safe for the application (linked against the new library) to load the old binary at runtime?

Lifu Huang
  • 11,930
  • 14
  • 55
  • 77
  • 4
    You should create a new `struct` whose base is this `struct` and which contains the extra data member. Should work OK under the conditions you state, and it doesn't require this kind of vandalism. – user207421 Apr 06 '21 at 01:46
  • The problem is that the new code can't tell if the last member is there or not. Windows solved this by having each struct have a size member, which can be used to determine which members exist. – Mooing Duck Apr 06 '21 at 02:01
  • 1
    The problem comes up if using a new version of the DLL with an application built against the old. The old application generates an instance that doesn't have the new member that the (I assume DLL-provided) function expects. – SoronelHaetir Apr 06 '21 at 03:17
  • @MooingDuck the last member will always be there since the application is built using the new library. But the consumer of the struct (I.e. func(..)) could be either the old or new library. – Lifu Huang Apr 06 '21 at 04:36
  • @SoronelHaetir right, though the application is ALWAYS built with the new library so the situation you mentioned should never happen. – Lifu Huang Apr 06 '21 at 04:38
  • @user207421 thanks! that’s a good point and I do understand C++ ensures the base object layout during inheritance. But I am still hoping to understand practically speaking whether there is any case when the forward compatibility of the old dll would be compromised under the precautious usage as I mentioned. – Lifu Huang Apr 06 '21 at 04:41
  • If the application and DLL are built (and distributed/installed/run) in lock-step it really doesn't matter. In that case it is no different from code that is directly part of the application (so far as compatibility is concerned). – SoronelHaetir Apr 06 '21 at 05:01
  • @SoronelHaetir No, the new dll and application are built in lock step, but not the old dll which uses the version of Foobar without the appended member. – Lifu Huang Apr 06 '21 at 05:14

0 Answers0