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.