Destruction order of static objects in shared libraries

2.7k Views Asked by At

I have a main program (main.cpp) and a shared library (test.h and test.cpp):

test.h:

#include <stdio.h>

struct A {
    A() { printf("A ctor\n"); }
    ~A() { printf("A dtor\n"); }
};

A& getA();

test.cpp:

#include "test.h"

A& getA() {
    static A a;
    return a;
}

main.cpp:

#include "test.h"

struct B {
    B() { printf("B ctor\n"); }
    ~B() { printf("B dtor\n"); }
};

B& getB() {
    static B b;
    return b;
}

int main() {
    B& b = getB();
    A& a = getA();
    return 0;
}

This is how I compile these sources on Linux:

g++ -shared -fPIC test.cpp -o libtest.so
g++ main.cpp -ltest

Output on Linux:

B ctor
A ctor
A dtor
B dtor

When I run this example on Windows (after some adjustments like adding dllexport) I get with MSVS 2015/2017:

B ctor
A ctor
B dtor
A dtor

To me the first output seems to be compliant with the standard. For example see: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf

From paragraph 3.6.3.1:

If the completion of the constructor or dynamic initialization of an object with static storage duration is sequenced before that of another, the completion of the destructor of the second is sequenced before the initiation of the destructor of the first.

That is if B object is constructed first it should be destroyed last - that what we see on Linux. But the Windows output is different. Is it a MSVC bug or am I missing something?

4

There are 4 best solutions below

2
1201ProgramAlarm On

The whole concept of a DLL is outside the scope of the C++ standard.

With Windows, DLLs can be unloaded dynamically during program execution. To help support this, each DLL will handle the destruction of static variables constructed while it was loaded. The result is that the static variables will be destroyed in an order that depends on the unload order of the DLLs (when they receive the DLL_PROCESS_DETACH notification). DLLs and Visual C++ run-time library behavior describes this process.

0
JaMiT On

I see two things that are missing from your analysis.

Program: The standard places requirements on how a program is executed. Your program consists of the (executable) file produced by the command g++ main.cpp -ltest, presumably a.out or a.exe. In particular, your program does not contain any of the shared libraries it is linked against. So whatever is done by a shared library falls outside the scope of the standard.

Well, almost. Since you wrote your shared library in C++, your libtest.so or test.dll file does fall within the scope of the standard, but it does so by itself, independent of the executable that invokes it. That is, the observable behavior of a.exe, ignoring the observable behavior of the shared libraries, must comply with the standard, and the observable behavior of test.dll, ignoring the observable behavior of the executable, must comply with the standard.

You have two related, but technically independent programs. The standard applies to each of them separately. The C++ standard does not cover how independent programs interact with each other.

If you want a reference for this, I would look at clause 9 of "Phases of translation" ([lex.phases] -- section 2.2 in the version of the standard you referenced). The result, a.out, of linking is a program image, while test.dll is part of the execution environment.

Sequenced before: You seem to have missed the definition of "sequenced before". Yes, the output has "B ctor" before "A ctor". However, this by itself does not mean that the constructor of b was sequenced before the constructor of a. The C++ standard gives a precise meaning to "sequenced before" in [intro.execution] (clause 13 of section 1.9 in the version of the standard you referenced). Using the precise meaning, one could conclude that if the constructor of b is sequenced before the constructor of a, then the output should have "B ctor" before "A ctor". However, the converse (what you assumed) does not hold.

In the comments, you suggested that it was a minor change when "sequenced before" was replaced by "strongly happens before". Not so, as "strongly happens before" also has a precise meaning in the newer version of the standard (clause 12 of section 6.8.2.1 [intro.races]). It turns out that "strongly happens before" means either "sequenced before" or one of three additional cases. So the wording change was an intentional broadening of that part of the standard, encompassing more cases than it had before.

4
Kai Petzke On

Even on Linux, you can encounter the crossing of static constructor and destructor calls, if you open and close the DLL manually with dlopen() and dlclose():

testa.cpp:

#include <stdio.h>

struct A {
    A() { printf("A ctor\n"); }
    ~A() { printf("A dtor\n"); }
};

A& getA() {
    static A a;
    return a;
}

(testb.cpp is analog, except for A changed to B and a to b)

main.cpp:

#include <stdio.h>
#include <dlfcn.h>

class A;
class B;

typedef A& getAtype();
typedef B& getBtype();

int main(int argc, char *argv[])
{
    void* liba = dlopen("./libtesta.so", RTLD_NOW);
    printf("dll libtesta.so opened\n");
    void* libb = dlopen("./libtestb.so", RTLD_NOW);
    printf("dll libtestb.so opened\n");
    getAtype* getA = reinterpret_cast<getAtype*>(dlsym(liba, "_Z4getAv"));
    printf("gotten getA\n");
    getBtype* getB = reinterpret_cast<getBtype*>(dlsym(libb, "_Z4getBv"));
    printf("gotten getB\n");
    A& a = (*getA)();
    printf("gotten a\n");
    B& b = (*getB)();
    printf("gotten b\n");

    dlclose(liba);
    printf("dll libtesta.so closed\n");
    dlclose(libb);
    printf("dll libtestb.so closed\n");

    return 0;
}

And the output is:

dll libtesta.so opened
dll libtestb.so opened
gotten getA
gotten getB
A ctor
gotten a
B ctor
gotten b
A dtor
dll libtesta.so closed
B dtor
dll libtestb.so closed

Interestingly, execution of the constructor of a is deferred to when getA() is actually called. The same for b. If the static declaration of a and b is moved from inside their getter-Functions to the module level, then the constructors is already be called upon loading of the DLL, automatically, though.

Of course, the application would crash, if a or b was still used in the main() function after the call to dlclose(liba) or dlclose(libb), respectively.

If you compile and link your application normally, then the calls to dlopen() and dlclose() will be performed by the code in the runtime environment. It seems, that your tested Windows version performs those calls in an order, that was unexpected by you. The reason, why Microsoft choose to do it this way, was probably, that upon program exit, there is a higher tendency for anything in the main application to still depend on anything from a DLL than the other way round. So static objects from libraries should generally be destructed AFTER the main application is destructed.

With the same reasoning, the initalization order should also be reversed: DLLs should be first, the main application second. So Linux gets it wrong on both initialization and cleanup, and Windows gets it right at least on cleanup.

0
Serge On

Relative order of the constructors and destructors is only defined within a statically linked executable or a (shared) library. It is defined by the scoping rules and order of the static objects at the liking time. The latter is also vague because sometimes it is difficult to guarantee the order of linking.

Shared libraries (dlls) are loaded by either the operating system at the beginning of execution or can be loaded on demand by the program. So, there is no known order in which those libraries would be loaded. As a consequence, there is no known order in which they would be unloaded. As a result, the order of constructors and destructors between the libraries can vary. Only the relative order of them is guaranteed within a single library.

Usually, when an order of constructors or destructors is important across libraries or across different files, there are simple techniques which allows you doing it. One of them is to use pointers to the objects. For example, if object A requires that object B is constructed before it, one can do this:

A *aPtr = nullptr;
class B {
public:
    B() {
      if (aPtr == nullptr) 
         aPtr = new A();
      aPtr->doSomething();
    }
 };
 ...
 B *b = new B();

The above will guarantee that A is constructed before it is used. While doing so, you can keep a list of allocated objects, or keep pointers, shared_pointers, ... in other objects to orchestrate an orderly destructions, say before exiting main.

So, to illustrate the above i re-implemented your example in a basic way. There are definitely multiple ways for handle it. In this example the destruction list is constructed following the above technique, the allocated A and B are put on the list and get destroyed at the end in a particular order.

test.h

#include <stdio.h>
#include <list>
using namespace std;

// to create a simple list for destructios. 
struct Destructor {
  virtual ~Destructor(){}
};

extern list<Destructor*> *dList;

struct A : public Destructor{
 A() {
  // check existencd of the destruction list.
  if (dList == nullptr)
    dList = new list<Destructor*>();
  dList->push_front(this);

  printf("A ctor\n"); 
 }
 ~A() { printf("A dtor\n"); }
};

A& getA();

test.cpp

#include "test.h"

A& getA() {
    static A *a = new A();;
    return *a;
}

list<Destructor *> *dList = nullptr;

main.cpp

#include "test.h"

struct B : public Destructor {
  B() {
   // check existence of the destruciton list
  if (dList == nullptr)
    dList = new list<Destructor*>();
  dList->push_front(this);

  printf("B ctor\n");
 }
 ~B() { printf("B dtor\n"); }
};

B& getB() {
  static B *b = new B();;
  return *b;
}


int main() {
 B& b = getB();
 A& a = getA();

 // run destructors
 if (dList != nullptr) {
  while (!dList->empty()) {
    Destructor *d = dList->front();
    dList->pop_front();
    delete d;
  }
  delete dList;
 }
 return 0;
}