4
votes

Note that this is not a duplicate of Multiple instances of singleton across shared libraries on Linux since adding -rdynamic flag doesn't change what is described in the question.

I have a static c++ library which has some static initialization code (for variables requiring a constructor call in an unnammed namespace). I then have 2 (or more) shared library using that static library internally.

Any executable using both shared libraries will call the initialization code of the static library twice, making things like folly::Singleton or gflags global variables misbehave. Is that the intended behavior? Is that a bug/weakness of dynamic linkers?

edit: Note that on macos it only initialize once.

Example:

static.h

int get();

static.cpp

static int y = 0;
struct C { C() : x(y++) {}; int x; };
namespace { C c; }
int get() { return c.x; }

shared.h

int shared_get();

shared.cpp

#include "static.h"
int shared_get() { return get(); }

shared2.h

int shared2_get();

shared2.cpp

#include "static.h"
int shared2_get() { return get(); }

main.cpp

#include "shared.h"
#include "shared2.h"
#include <iostream>
int main() {
    std::cout << shared_get() << " " << shared2_get() << std::endl;
    return 0;
}

Compile:

g++ -fPIC -g -c  -o static.o static.cpp
ar rcs libstatic.a static.o
g++ -g -fPIC -shared -o libshared.so shared.cpp ./libstatic.a
g++ -g -fPIC -shared -o libshared2.so shared2.cpp ./libstatic.a
g++ main.cpp ./libshared.so ./libshared2.so

Run :

LD_LIBRARY_PATH=. ./a.out

The result is "1 1" when I would expect it to be "0 0".

Looking at the symbols with nm, libshared.so and libshared2.so do both contain:

t _GLOBAL__sub_I_static.cpp

Linking the static library only to the executable solves the behaviour but doesn't explain how two shared libraries can use a static library internally without interfering. For completion here is how to get "0 0":

Compile:

g++ -fPIC -g -c  -o static.o static.cpp
ar rcs libstatic.a static.o
g++ -g -fPIC -shared -o libshared.so shared.cpp
g++ -g -fPIC -shared -o libshared2.so shared2.cpp
g++ main.cpp ./libshared.so ./libshared2.so ./libstatic.a
1
Each shared library has its own copy of the static library. They don't know about each other. - Pete Becker
@PeteBecker Even with two copies, shouldn't each of them have their own y and c, and shouldn't both c.x be equal to 0? - Aconcagua
@Leonard: Please post an minimal reproducible example (i. e. code of main as well). - Aconcagua
@Aconcagua — yes; I didn’t read the question carefully enough. - Pete Becker
@PeteBecker They don't know about each other until they are loaded at the same time: the symbol for get for example will not be loaded twice, only the first one loaded will be used, right? So I am kind of puzzled about why the call to the initialization is called twice too. - Léonard

1 Answers

3
votes

To further simplify the problem you're getting results equivalent to having

static.cpp:

static int y = 0;
struct C { C() : x(y++) {}; int x; };
static C c; 
int get() { return c.x; }

and

shared1.cpp:

#include "static.h"
#include "static.cpp"
int shared1_get() { return get(); }

shared2.cpp:

#include "static.h"
#include "static.cpp"
int shared2_get() { return get(); }

(IOW, each of the 2 DSOs will include all of the static library's code, so we may just skip the static library part to make the example a little simpler.)

In your case

static C c; 

is essentially a per-DSO call to struct C's constructor, which in your case (without a hidden attribute on the struct) is exported.

Since you have two libraries with static C c;, a constructor for c will get called once in each DSO, and since the constructor symbol is exported (default), one of the constructors will win (along with its associated int get();) and will drag its statics with it.

Essentially, exactly one of the static C c; will be used and it will be constructed twice. That may sound weird but as far as shared libs are concerned, you should be thinking C, not C++, and see static C c; as

/*C*/
static int y;
static struct C c;
__attribute__((constructor))
void C__ctor(struct C *this) /*exported function*/
{
    C.x=y++;
}

To fix the problem you can mark the whole C struct as hidden so that that attribute applies to the constructor too

struct __attribute__((visibility("hidden")))  C { C() : x(y++) {}; int x; };

or better yet, compile with -fvisibility=hidden and apply visibility("default") attributes explicitly.

Below is an executable (shell script) example:

#!/bin/sh -eu
echo 'int get();' > static.h
cat > static.cpp <<EOF
static int y = 0;
#if 1 /*toggle to play with the visibility attribute*/
    #define MAYBE_HIDDEN __attribute__((visibility("hidden")))
#else
    #define MAYBE_HIDDEN
#endif
struct MAYBE_HIDDEN C { C() : x(y++) {}; int x; };
static C c; 
int get() { return c.x; }
EOF
cat > shared.h <<EOF
int shared_get();
EOF

cat > shared.cpp <<EOF
#include "static.h"
#include "static.cpp"
int shared_get() { return get(); }
EOF

cat > shared2.h <<EOF
int shared2_get();
EOF

cat > shared2.cpp <<EOF
#include "static.h"
#include "static.cpp"
int shared2_get() { return get(); }
EOF
cat > main.cpp <<EOF
#include "shared.h"
#include "shared2.h"
#include <iostream>
int main() {
    std::cout << shared_get() << " " << shared2_get() << std::endl;
    return 0;
}
EOF
g++ -fPIC -g -c  -o static.o static.cpp
#ar rcs libstatic.a static.o
g++ -g -fPIC -shared -o libshared.so shared.cpp #./libstatic.a
g++ -g -fPIC -shared -o libshared2.so shared2.cpp #./libstatic.a
g++ main.cpp ./libshared.so ./libshared2.so
./a.out

I've skipped the static lib, and included the C++ code directly, but you might as well revert that -- it doesn't change the results.

If you compile without the hidden attribute, you might want to try and run nm -D libshared.so libshared1.so. The _ZN1CC1Ev or the _ZN1CC2Ev symbol you should be getting there (or not, if you've applied the hidden attribute) should be the exported constructor function.