C++ two libraries depend on same lib but different versions?

I found this question in my searching for answers and as @Component-10 suggested, I have created a minimal set of files to investigate this behavior and tested with MacOS + CLANG.

  • If building A and B as shared libraries, you can get the proper resolution to a dependent library, C, to which are dependencies of A and B, but at different versions.
  • If building A and B as static, it fails.

EDIT

As pointed out in the comments, the shared library approach is not cross platform and does not work in Linux.

@SergA has created a solution with the Dynamically Loaded Library (dl) API (https://www.dwheeler.com/program-library/Program-Library-HOWTO/x172.html).

@SergA's solution using dlopen

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

// #define DLOPEN_FLAGS RTLD_LAZY | RTLD_LOCAL
#define DLOPEN_FLAGS RTLD_LAZY

#if defined(_WIN32) || defined(__CYGWIN__)
    // Windows (x86 or x64)
    const char* libA = "libA.shared.dll";
    const char* libB = "libB.shared.dll";
#elif defined(__linux__)
    // Linux
    const char* libA = "libA.shared.so";
    const char* libB = "libB.shared.so";
#elif defined(__APPLE__) && defined(__MACH__)
    // Mac OS
    const char* libA = "libA.shared.dylib";
    const char* libB = "libB.shared.dylib";
#elif defined(unix) || defined(__unix__) || defined(__unix)
    // Unix like OS
    const char* libA = "libA.shared.so";
    const char* libB = "libB.shared.so";
#else
    #error Unknown environment!
#endif

int main(int argc, char **argv)
{
  (void)argc;
  (void)argv;

  void *handle_A;
  void *handle_B;
  int (*call_A)(void);
  int (*call_B)(void);
  char *error;

  handle_B = dlopen(libB, DLOPEN_FLAGS);
  if(handle_B == NULL) {
    fprintf(stderr, "%s\n", dlerror());
    exit(EXIT_FAILURE);
  }

  handle_A = dlopen(libA, DLOPEN_FLAGS);
  if(handle_A == NULL) {
    fprintf(stderr, "%s\n", dlerror());
    exit(EXIT_FAILURE);
  }


  call_A = dlsym(handle_A, "call_A");
  error = dlerror();
  if(error != NULL) {
    fprintf(stderr, "%s\n", error);
    exit(EXIT_FAILURE);
  }
  call_B = dlsym(handle_B, "call_B");
  error = dlerror();
  if(error != NULL) {
    fprintf(stderr, "%s\n", error);
    exit(EXIT_FAILURE);
  }

  printf(" main_AB->");
  call_A();
  printf(" main_AB->");
  call_B();

  dlclose(handle_B);
  dlclose(handle_A);

  return 0;
}

Previous solution showing static vs. shared

Here is my set of files. I will not show them all here for brevity.

$ tree .
.
├── A
│   ├── A.cc
│   └── A.hh
├── B
│   ├── B.cc
│   └── B.hh
├── C
│   ├── v1
│   │   ├── C.cc
│   │   └── C.hh
│   └── v2
│       ├── C.cc
│       └── C.hh
├── compile_shared_works.sh
├── compile_static_fails.sh
├── main_A.cc
├── main_AB.cc
└── main_B.cc

A depends on C version 1 and B depends on C version 2. Each library contains a single function, e.g. libA contains call_A which calls libC v1's call_C, and libB contains call_B which calls libC v1's call_C.

Then main_A links to only libA, main_B to only lib_B, and main_AB to both.

compile_static_fails.sh

The following set of commands builds libA and libB statically.

#clean slate
rm -f *.o *.so *.a *.exe

#generate static libA
g++ -I . -c C/v1/C.cc A/A.cc
ar rvs libA.a *.o
rm -f *.o

#generate static libB
g++ -I . -c C/v2/C.cc B/B.cc
ar rvs libB.a *.o
rm -f *.o

#generate 3 versions of exe
g++ -L . -lA main_A.cc -o main_A.exe
g++ -L . -lB main_B.cc -o main_B.exe
g++ -L . -lA -lB main_AB.cc -o main_AB.exe
./main_A.exe
./main_B.exe
./main_AB.exe

The output is

main_A->call_A->call_C [v1]
main_B->call_B->call_C [v2]
main_AB->call_A->call_C [v1]
main_AB->call_B->call_C [v1]

When main_AB executes call_B it goes to the wrong place!

compile_shared_works.sh

#clean slate
rm -f *.o *.so *.a *.exe

#generate shared libA
g++ -I . -c -fPIC C/v1/C.cc A/A.cc
g++ -shared *.o -o libA.so
rm *.o

#generate shared libB
g++ -I . -c -fPIC C/v2/C.cc B/B.cc
g++ -shared *.o -o libB.so
rm *.o

#generate 3 versions of exe
g++ -L . -lA main_A.cc -o main_A.exe
g++ -L . -lB main_B.cc -o main_B.exe
g++ -L . -lA -lB main_AB.cc -o main_AB.exe
./main_A.exe
./main_B.exe
./main_AB.exe

The output is

main_A->call_A->call_C [v1]
main_B->call_B->call_C [v2]
main_AB->call_A->call_C [v1]
main_AB->call_B->call_C [v2]

It works (on MacOS)!


@SergA's solution also works in Linux if we open the shared library with flag RTLD_LAZY | RTLD_LOCAL
The output is:
1. Main_AB_dlopen -> CallA -> callC(v1)
2. Main_AB_dlopen -> callB -> callC(v2)


Dynamic libraries don't do strong version checking which means that if the entry points that A uses in C haven't changed then it will still be able to use a later version of C. That being said, often Linux distros use a symbol link filesystem method of providing version support. This means that if an executable is designed only to work with 1.2.2 then it can be specifically linked to find /usr/lib/mylib-1.2.2.

Mostly programs are linked to find the general case, eg. /usr/lib/mylib and this will be symbolically linked to the version which is on the machine. E.g. /usr/lib/mylib -> /usr/lib/mylib-1.2.2. Providing you don't link to a specific version and the actuall interfaces don't change, forward compatibility shouldn't be a problem.

If you want to check whether libraries A and B are bound to a specifically named version of C, you can use the ldd command on them to check the dll search path.


I'm assuming that you're linking dynamically. If both A and B completely encapsulate their respective versions of C then it might be possible to do this. You might have to make sure that the different versions of C are named differently (i.e. libMyC.1.so and libMyC.2.so) to avoid confusion when they are loaded at runtime.

You could also investigate statically building A and B to avoid the possiblility of runtime load confusion.

Simplest way to find out is simply to try it. It shouldn't take to long to determine if it'll work or not.

Lastly, of course, by far the easiest solution, and best from a maintenance perspective is to bring A, or B, up to the level of the other so that they both use the same version of C. This is better in so many ways and I strongly urge you to do that rather than to try working around a real problem.