macosglobaldylib

Two macOS dylibs with Shared Static Library: Why Are Global Variables Shared?


To my surprise, when compiling two separate dylibs that happen to share a static library, the global variables defined in that static library seem to be shared. This SO article would seem to indicate that every dynamic library will keep its global variables separate, but in the case described above, test code included below proves that this is not the true. I am seeking confirmation that this is to be expected on macOS (and probably Linux as well).

Given this scenario:

I expected that when "AAA" printed "Bar" it would be 111, and that when "BBB" printed bar it would still be 123. Instead, when "BBB" prints "Bar", it is 111, indicating that – from MyApp's point of view – there is only a single, shared instance of "Bar".

My suspicion is that since "Bar" is exposed by both "AAA" and "BBB", when you dynamically link the two dylibs, one of the two "wins" because the name is exactly the same and the linker can't distinguish the two.

This suspicion seems to be proved by setting the "-fvisibility=hidden" flag in the "Other C++ Flags" in Xcode. If I do this for the dylibs "AAA" and "BBB", then the two global variables seem to be distinct. I expect this is because 'visibility=hidden' hides the two copies of "Bar", thus resolving the conflict described in the paragraph above.

Can someone confirm my understanding of this?

--- SAMPLE CODE ---

The static library CGlobalTest has a C++ class as shown below. The class declares a global inside a function, a class global, and static global in the .cpp file. The function GetGlobal() returns a reference to one of these based on the GlobalType parameter.

CGlobalTest.cpp:

class CGlobalTest
{
public:
    CGlobalTest() { }
    
    static int& GetFunctionGlobal()
                {
                    static int sFunctionGlobal = 123;
                    return sFunctionGlobal;
                }
    
    static int& GetClassGlobal()
                {
                    return sClassGlobal;
                }
    
    static int& GetFileGlobal();
    
    static int& GetGlobal(
                    GlobalType  inType)
                {
                    switch (inType) {
                    case kFunctionGlobal:
                        return GetFunctionGlobal();
                        break;
                    case kClassGlobal:
                        return GetClassGlobal();
                        break;
                    case kFileGlobal:
                        return GetFileGlobal();
                        break;
                    }
                }
    
    static int  sClassGlobal;
};

CGlobalTest.h

#include "static_lib.h"

int CGlobalTest::sClassGlobal = 456;

int sFileGlobal = 789;

int&
CGlobalTest::GetFileGlobal()
{
    return sFileGlobal;
}

I've then got two dynamic libraries that use the CGlobalTest static library, called global_test_dynamic_1 and global_test_dynamic_2. The code for 1 and 2 are essentially the same, so I'm including just the first one.

dynamic_lib_1.cpp:

#include "dynamic_lib_1.h"
#include "static_lib.h"
#include "stdio.h"

const char*
GlobalTypeToString(
    GlobalType  inType)
{
    const char* type = "";
    switch (inType) {
    case kFunctionGlobal:
        type = "Function Global";
        break;
    case kClassGlobal:
        type = "Class Global";
        break;
    case kFileGlobal:
        type = "File Global";
        break;
    }
    
    return type;
}

void dynamic_lib_1_set_global(enum GlobalType inType, int value)
{
    int& global = CGlobalTest::GetGlobal((GlobalType) inType);
    global = value;
    printf("Dynamic Lib 1: Set %s: %d (%p)\n", GlobalTypeToString(inType), global, &global);
}

void dynamic_lib_1_print_global(enum GlobalType inType)
{
    const int& global = CGlobalTest::GetGlobal((GlobalType) inType);
    printf("Dynamic Lib 1: %s = %d (%p)\n", GlobalTypeToString(inType), global, &global);
}

dynamic_lib_1.h

#ifdef __cplusplus
#define EXPORT extern "C" __attribute__((visibility("default")))
#else
#define EXPORT
#endif

#include "global_type.h"

EXPORT void dynamic_lib_1_set_global(enum GlobalType inType, int value);
EXPORT void dynamic_lib_1_print_global(enum GlobalType inType);

Finally, there is an application that links to the two dylibs.

#include "dynamic_lib_1.h"
#include "dynamic_lib_2.h"
#include "global_type.h"

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

typedef void (*print_func)(enum GlobalType inType);
typedef void (*set_func)(enum GlobalType inType, int value);

int main()
{
    printf("App is starting up...\n");

    // LOAD DYNAMIC LIBRARY 1

    void* handle1 = dlopen("libglobal_test_dynamic_1.dylib", RTLD_NOW);
    assert(handle1 != NULL);

    print_func d1_print = (print_func) dlsym(handle1, "dynamic_lib_1_print_global");
    assert(d1_print != NULL);
    
    set_func d1_set = (set_func) dlsym(handle1, "dynamic_lib_1_set_global");
    assert(d1_set != NULL);
    
    // LOAD DYNAMIC LIBRARY 2

    void* handle2 = dlopen("libglobal_test_dynamic_2.dylib", RTLD_NOW);
    assert(handle1 != NULL);

    print_func d2_print = (print_func) dlsym(handle2, "dynamic_lib_2_print_global");
    assert(d2_print != NULL);
    
    set_func d2_set = (set_func) dlsym(handle2, "dynamic_lib_2_set_global");
    assert(d2_set != NULL);
    
    enum GlobalType type;
    
    printf("**************************************************\n");
    printf("** FUNCTION GLOBAL\n");
    printf("**************************************************\n");
    
    type = kFunctionGlobal;
    
    (d1_print)(type);
    (d2_print)(type);
    
    printf("** SET D1 TO 111 - THEN PRINT FROM D2\n");
    d1_set(type, 111);
    d1_print(type);
    d2_print(type);

    printf("** SET D2 TO 222 - THEN PRINT FROM D1\n");
    d2_set(type, 222);
    d2_print(type);
    d1_print(type);

    printf("**************************************************\n");
    printf("** CLASS GLOBAL\n");
    printf("**************************************************\n");
    
    type = kClassGlobal;
    
    (d1_print)(type);
    (d2_print)(type);
    
    printf("** SET D1 TO 111 - THEN PRINT FROM D2\n");
    d1_set(type, 111);
    d1_print(type);
    d2_print(type);

    printf("** SET D2 TO 222 - THEN PRINT FROM D1\n");
    d2_set(type, 222);
    d2_print(type);
    d1_print(type);

    printf("**************************************************\n");
    printf("** FILE GLOBAL\n");
    printf("**************************************************\n");
    
    type = kFileGlobal;
    
    (d1_print)(type);
    (d2_print)(type);
    
    printf("** SET D1 TO 111 - THEN PRINT FROM D2\n");
    d1_set(type, 111);
    d1_print(type);
    d2_print(type);

    printf("** SET D2 TO 222 - THEN PRINT FROM D1\n");
    d2_set(type, 222);
    d2_print(type);
    d1_print(type);

    return 0;
}

Solution

  • This is a feature, not a bug. The symbol in each dylib is explicitly marked "weak-coalesce" (in addition to being exported):

    % xcrun dyld_info -fixups libglobal_test_dynamic_1.dylib 
    libglobal_test_dynamic_1.dylib [arm64]:
        -fixups:
            segment      section          address                 type   target
            __DATA_CONST __got            0x00004000              bind  weak-coalesce/__ZZN11CGlobalTest17GetFunctionGlobalEvE15sFunctionGlobal
            __DATA_CONST __got            0x00004008              bind  libSystem.B.dylib/_printf
            __DATA_CONST __const          0x00004010            rebase  0x00003F45
            __DATA_CONST __const          0x00004018            rebase  0x00003F55
            __DATA_CONST __const          0x00004020            rebase  0x00003F62
    

    This makes the dynamic linker choose a single definition for all binaries using this symbol across the process.

    Note that this has to be done explicitly though. Mach-Os by default use a two-level namespace, where you have both the symbol name and the library you expect to find it in. And on top of this, the library has to explicitly ask to import a symbol that it knows it's already exporting. With any ordinary symbol (like a basic function definition), that wouldn't happen, everything within that library would just use the local definition.

    But the core problem here are inline functions containing static variables. The C++ standard has this to say ("dcl.inline/6"):

    A static local variable in an inline function with external linkage always refers to the same object.

    This is precisely your case. If you move the definition of GetFunctionGlobal() out of the header and into your source file, then you'll get the exact same behaviour as with GetFileGlobal().

    And it has to work this way, because otherwise static local variables within inline functions would just be broken, as every translation unit would get its own copy of that variable.

    Now, as far as I know, dynamic linking is still to this day very much implementation-defined, so how this behaves with respect to dylibs isn't mandated by the standard, but again if it weren't implemented this way, it would just be broken. Because consider the case where you have a dynamic library that exports the CGlobalTest class, and a binary that imports it. Since your function definitions are still in the header, the binary could now either get its own copy of the static local variable (broken), or it could be aliased to that of the library (sane). And by linking against your static library, you're doing exactly that: exporting it. If you don't want to export it, then that's what -fvisibility=hidden is for.