1

I have a C++ program that has a global variable. The constructor and destructor of this global variable call functions from a shared library that has a constructor and destructor defined.

I am obeserving differing orderings of function calls depending on whether I run this program on Linux or MacOS.

When running the program, I would have expected the following calling sequence:

shared library constructor
shared library function called from global object constructor
...
shared library function called from global object destructor
shared library destructor

And indeed, this is what I get on Linux. However, when running on MacOS, the two last lines are swapped. That is, the shared library destructor is called before the global object is destroyed. As a consequence, the destructor of the global object calls a function in the shared library that was already destroyed.

Here is example code to reproduce the differing behavior:

  • Shared library implementation (in C): shlib.c
#include <stdio.h>

static int initialized = 0; /* Is library initialized? */

/** Shared library constructor. */
__attribute__((constructor)) void shlib_init(void)
{
  printf("Constructor\n");
  fflush(stdout);
  initialized = 1;
}

/** Shared library destructor. */
__attribute__((destructor)) void shlib_fini(void)
{
  printf("Destructor[%d]\n", initialized);
  fflush(stdout);
  initialized = 0;
}

/** Shared library function that creates something. */
void shlib_create_function(void)
{
  printf("create function[%d]\n", initialized);
  fflush(stdout);
}

/** Undo shlib_create_function(). */
void shlib_destroy_function(void)
{
  printf("destroy function[%d]\n", initialized);
  fflush(stdout);
}
  • Shared library header:
#ifndef SHLIB_H
#define SHLIB_H 1

#ifdef __cplusplus
extern "C" {
#endif

void shlib_create_function(void);
void shlib_destroy_function(void);

#ifdef __cplusplus
}
#endif

#endif /* !SHLIB_H */
  • Main program (in C++) app.cpp:
#include "shlib.h"
#include <iostream>

class Object {
public:
  Object() { shlib_create_function(); }
  ~Object() { shlib_destroy_function(); }
};

Object global_object;

int
main(void)
{
  std::cout << "BEGIN main()" << std::endl << std::flush;
  std::cout << "END main()" << std::endl << std::flush;
  return 0;
}
  • Makefile
# Select suffix for shared library (.so on Linux, .dylib on Mac)
ifneq ($(linux),)
so    = so
else
so    = dylib
endif

# Default compiler is clang.
ifeq ($(COMPILER),)
COMPILER = clang
endif

# Setup compiler.
ifeq ($(COMPILER),gcc)
CC    = gcc
CXX   = g++
DYLIB = libshlib.$(so)
DYLD  = gcc -shared
endif
ifeq ($(COMPILER),icc)
CC    = icc
CXX   = icc
DYLIB = libshlib.$(so)
DYLD  = icc -shared
endif
ifeq ($(COMPILER),clang)
CC    = clang
CXX   = clang++
DYLIB = libshlib.$(so)
DYLD  = clang -shared
endif

.PHONY: clean

app: app.cpp shlib.h $(DYLIB)
    $(CXX) -o app app.cpp -L. -lshlib

shlib.o: shlib.c shlib.h
    $(CC) -c -fPIC -o shlib.o shlib.c

$(DYLIB): shlib.o
    $(DYLD) -o $(DYLIB) shlib.o

clean:
    rm -f $(DYLIB) shlib.o app

Running on Ubuntu 20.04.02 LTS with gcc 9.3.0 I get this output (which is what I would expect):

Constructor
create function[1]
BEGIN main()
END main()
destroy function[1]
Destructor[1]

Running instead on macOS 10.13.6 (17G14042) (Kernel Version: Darwin 17.7.0) with clang (LLVM version 9.1.0 (clang-902.0.39.2)) I get this unexpected output instead:

Constructor
create function[1]
BEGIN main()
END main()
Destructor[1]
destroy function[0]

As can be seen, the global variable destructor invokes a function on the shared library that has already been destroyed.

So how is the order of these destructors (shared library and global variable) defined? Why are they not executed in opposite order of the respective constructors on MacOS? Is there a way I can force the shared library destructor to run after global object destructors?

I tried using __attribute__((destructor(65535))) to give the shared library destructor the smallest possible priority, but that did not help.

The best solution would of course be to get rid of the global variable. However, I am dealing with legacy code for which this is currently not an option.

Edit: Thread safety is not an issue here. Once I have a reliable ordering of constructors/destructors, I can take care of thread-safety without problem.

Edit: I just tried on a machine that has MacOS version 10.15.7 and there the order of function calls is the same as for Linux. So this may be a problem that depends on the MacOS version.

Daniel Junglas
  • 5,830
  • 1
  • 5
  • 22
  • 2
    It sounds a lot like [Static Initialization Order Fiasco](https://en.cppreference.com/w/cpp/language/siof). It sometimes helps to do lazy initialization. Hide the global in a function: `variable_type& variable_name() { static variable_type instance; return instance; }` - then use `variable_name()` instead `variable_name` everywhere. – Ted Lyngmo Jun 29 '21 at 05:27
  • If you don't want to add parentheses in 10000 places, you could hide the function behind a macro: `variable_type& hidden() { static variable_type instance; return instance; }` and `#define variable_name hidden()` – Ted Lyngmo Jun 29 '21 at 05:45
  • It's worth noting that the Singleton behaviour suggested by @TedLyngmo isn't generally thread safe. – Ryan Pepper Jun 29 '21 at 06:50
  • @RyanPepper It is a single instance already. Using lazy initialization doesn't change that. – Ted Lyngmo Jun 29 '21 at 07:09
  • @RyanPepper _It's worth noting that the Singleton behaviour suggested by \@TedLyngmo isn't generally thread safe._ I wonder about your statement. This is like [Meyers Singleton](https://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton), and this is explicitly said to be thread-safe (since C++11, and if compilers support the standard correctly concerning this what the recent major do). FYI: [Is Meyers' implementation of the Singleton pattern thread safe?](https://stackoverflow.com/a/1661564/7478597) – Scheff's Cat Jun 29 '21 at 07:37
  • @Scheff'sCat The *initialisation* is thread safe in the Meyers singleton. Any internals of the object still need to be handled by the developer though – Ryan Pepper Jun 29 '21 at 07:57
  • @RyanPepper I'm not quite sure how the Static Initialization Order Fiasco is related to thread-safety. Of course, you have to guard things which are used for inter-thread communication but that's not what the OP is asking about. (Or did I overlook something?) – Scheff's Cat Jun 29 '21 at 08:04
  • @Scheff'sCat we can't know, because we don't know what the objects are like that the functions `void shlib_create_function(void)` and `void shlib_destroy_function(void)` are creating/destroying. Presumably there's also some accessing going on... – Ryan Pepper Jun 29 '21 at 08:06
  • @TedLyngmo, thanks a lot for your suggestion. I tried it and it seems to fix the problem. Unfortunately, I cannot use it since in my setup I only provide the shared library and it is my users who write the application. I cannot ask them to avoid those global variables. My shared library did not have a constructor/destructor in the past. I only want to introduce that now. But what I am wondering is mostly why on MacOS destructors are not invoked in inverse order of constructors. This looks wrong to me - independent of the order of constructors and potential static init order fiasco. – Daniel Junglas Jun 29 '21 at 08:39
  • @RyanPepper, I updated my question: thread-safety is not an issue here. I can take care of that. The problem really is the (undefined or mixed up?) order of ctors/dtors. – Daniel Junglas Jun 29 '21 at 08:41
  • @DanielJunglas Great that it seems to work. Re: "_Unfortunately, I cannot use it_" - Would the users of the library even notice if you replace the global variable with a macro with the same name as the old variable? I'd take this further and remove the need for the `__attribute__((constructor))` and `__attribute__((destructor))` functions too. Whatever those functions do, you can place in an internal class that you also initialize lazily. – Ted Lyngmo Jun 29 '21 at 08:50
  • [Example](https://godbolt.org/z/nd8fofPYn) of replacing the old variable with a macro – Ted Lyngmo Jun 29 '21 at 09:00
  • @TedLyngmo, your example is exactly what I tried. The problem is that it is not me who creates that global variable. It is the users of my shared library. I am adding constructor/destructor to the shared library as that simplifies things internally. However, there are existing users who have code with these global vars. They will not be happy if changing their code becomes mandatory. Basically, I cannot change app.cpp. The only thing I can change is the library. I may have to give up on shlib ctor/dtor but I first want to figure out whether the observed behavior is a bug or just undefined. – Daniel Junglas Jun 29 '21 at 10:59
  • @DanielJunglas Oh, ok, there are more than one. If users create globals depending on globals they will often get bit by the static init fiasco. It's very hard to do something about but to have the users get upset and do something about it. If you've made sure that your library is safe by using lazy initialization you can't do much more on that end. The users need to adapt to reality too. – Ted Lyngmo Jun 29 '21 at 11:08
  • I have no proof atm but this behavior sounds like a bug in OSX's loader. Normally program modules (i.e. app + it's shared libraries) should be destroyed in topological order i.e. library should not be destroyed until all of it's users are. – yugr Jul 02 '21 at 04:40
  • 1
    Thanks a lot @yugr. I came more or less to the same conclusion, in particular since I found many newer versions of OSX that behaved as expected (and like Linux). So I guess I'll declare this a bug in the OS and not in my code. In the potentially buggy version the shared library destructor is invoked from `__cxa_finalize_ranges`. I could not find a corresponding bug report for that function, though. – Daniel Junglas Jul 02 '21 at 05:04

0 Answers0