Destruction order of static objects in shared libraries
Asked Answered
N

4

15

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?

Noaccount answered 6/2, 2019 at 21:34 Comment(11)
none of them is complaint. there is no standard which orders dynamically loaded libraries. it depends on the system, command line, search order, and env variables.Forte
Which version of C++ are you targeting? For instance your quoted section has been changed since N4296: timsong-cpp.github.io/cppwp/basic.start.term#3Christiano
@Christiano I see the only difference is "is sequenced before" (was) vs "strongly happens before" (in your link). Seems like a minor change - maybe just to minimize confusion for concurrent construction scenarios.Noaccount
are code of test.cpp in dll ? this is in separate binary modules ? main.cpp in exe and test.cpp in dll ? if yes - this and must be and all okSkat
@Skat Yes, test.cpp is in dll and main.cpp is in exeNoaccount
@Noaccount - in this case such order in windows and must be all correct. and this is unrelated to compilerSkat
@Forte I don't think shared libraries are covered by the standard at all. But nevertheless on Linux this part of the standard is implemented properly even with shared libraries.Noaccount
@Skat Can you please elaborate on this? I would like to have a portable way to enforce order of construction/destruction for singleton objects. The standard seems to provide some guarantees on that but they do not work on Windows. I understand that DLLs have their specifics but I would like to know if it is something that is intended/impossible to fix or just a gap in implementation.Noaccount
@Noaccount - ok i can give answer but situation is absolute clear and unrelated to standard. your objects in different binary modules. destructors in dll called from DLL_PROCESS_DETACHSkat
Avoid global objects. If you cannot avoid global objects, them nest them as a static inside a singleton instance getter global function. Note that there is a possible tiny bit of performance hit, because the static initializer may have a secret bool and/or mutex.Hughmanick
@Forte forgive me for nitpicking, but since the standard doesn't define how dynamic libraries behave, I think it's more correct to say that both behaviors are compliant, not that neither is compliant. You can't be non-compliant with a nonexistent specification.Alcoholometer
U
9

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.

Undercroft answered 6/2, 2019 at 23:13 Comment(2)
absolute correct. how this internal implemented we can look in crt\src\vcruntime\utility.cpp. search here by module_local_atexit_table symbol and about On-Exit Table :Skat
When a module uses the Universal CRT DLL, the behavior of _onexit(), atexit(), and at_quick_exit() is different depending on whether the module is an EXE or a DLL. If it is an EXE, then calls to these functions are passed to the Universal CRT DLL and the callbacks are registered with its atexit function tables. This way, functions registered by the EXE are called whenever one of the exit() functions is called. If the module is a DLL, it has its own table of registered functions. This table is executed when the DLL is unloaded (during DLL_PROCESS_DETACH).Skat
T
4

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.

Theatrician answered 11/2, 2019 at 3:19 Comment(0)
F
2

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;
}
Forte answered 15/2, 2019 at 2:16 Comment(0)
A
1

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.

Amelia answered 14/2, 2019 at 12:54 Comment(4)
Regarding deferring the execution of the constructors, I would call it "as expected" rather than "interestingly". That behavior is why using function-level static variables can sometimes avoid a static initialization fiasco.Theatrician
Why should DLLs be loaded first? (Think of the function-level static variables -- no need to initialize anything before it could possibly be used.) Better yet, how is the operating system supposed to know which DLLs to load before the program loads and gets a chance to request that the DLL be loaded?Theatrician
@Theatrician Of course ,the program has to be loaded before DLLs can be loaded. But the order of high level initializations (and I call creation of C++ static objects high level here) is not bound by OS needs, but is rather determined by the runtime environment. And there, my personal opinion is, that the best initialization order would be to initialize the C++ library first (so that everybody can use std::cout for example) and then any generic DLLs, and then DLLs, that depend on other, already initialized DLLs, and then the app itself last.Amelia
That smells of a straw man argument. The question at hand gives a context with exactly one DLL, so all this talk of the order in which multiple DLLs are initialized is spurious. Also, the only "OS need" that had been mentioned is that the OS needs the executable (after being loaded) to specify which dynamic libraries to load -- nothing about how the libraries execute. I'm left even more confused about what your answer is trying to convey than I was before.Theatrician

© 2022 - 2024 — McMap. All rights reserved.