CodeMonkey
CodeMonkey

Reputation: 3668

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

If i have libs A, B and C in C++ using GCC compiler. Lib A and B both depend on C but on different versions of it. Can i then use A and B together in another program? or will the different versions required of C by A and B conflict? And how do i resolve this and can i?

Upvotes: 35

Views: 14444

Answers (5)

vrqq
vrqq

Reputation: 566

I have a solution by dlopen(RTLD_LOCAL | RTLD_DEEPBIND) or dlmopen(LM_ID_NEWLM, "filename.so", ...) (familiar as other answers above)

First we should read this article to know .dynsym is 'exported symbol table' in linux ELF: https://blogs.oracle.com/solaris/post/inside-elf-symbol-tables

Each elf file include executable and dynamic library has .dynsym in header. The function declared with __attribute__((visibility("default"))) will recorded in this header zone.
But if the -fvisibility=hidden has not assigned to compiler (gcc), all functions would declare with __attribute__((visibility("default"))) by compiler automatically. (all functions marked as export)

How's the .so loading?

All .so would be loaded in memory before the program start int main(), all file marked as DT_NEEDED in ELF header one by one.
There has a global symbol table in system for current program, all .so loadad and fill the address of function name their hold in this table, if two .so file has same function name, only the first one would accepted.

dlopen() The program loaded in dlopen() is no difference with loading by ELF header, the global symbol table are also filled by dlopen().

For example

// main.cpp
__attribute__((visibility("default"))) int fn() { return 10; }
void print();
int main() { print(); return 0; }

// libfn.cpp => libfn.so
__attribute__((visibility("default"))) int fn() { return 999; }
__attribute__((visibility("default"))) void print() { cout<<fn(); }

The 10 would print, since the fn() in main.cpp would loaded before libfn.so.

If using dlopen(libfn.so, RTLD_LOCAL) without RTLD_DEEPBIND, the number 10 still would be printed.

If using normal link in compiling stage, even if using something like lib_try_in_middle.so to separate the .dynsym in ELF-header, the functions in any file marked as __attribute__((visibility("default"))) always appear in global symbol table.

Known issue

address sanitizer cannot work with RTLD_DEEPBIND, and I haven't found any solution to let asan run in new namespace by dlmopen().

Reference: https://man7.org/linux/man-pages/man3/dlopen.3.html

Upvotes: 0

Leiwu
Leiwu

Reputation: 21

@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)

Upvotes: 2

wawiesel
wawiesel

Reputation: 180

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)!

Upvotes: 2

Benj
Benj

Reputation: 32398

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.

Upvotes: 2

Component 10
Component 10

Reputation: 10487

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.

Upvotes: 8

Related Questions