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.