Reputation: 2982
Basic problem description:
A 1.0
.B
uses newer version of library A 2.0
with incompatible ABI/behavior.A 1.0
and B
as well, with the latter implicitly using newer version of library A 2.0
in the same application context.The real library A
is a C++ framework with more than 14k source files. The sample below is for testing approaches to the problem.
Currently, this scenario fails due to symbols conflicts on Linux platform. Some answers to similar questions suggest using --version-script
- the way how glibc
solves versioning issues.
I have tried feasibility of this approach to my scenario, and found it 'almost' working.
Consider that we have library A 1.0
like this:
// liba1.cpp
const char* doSomeA() { return "liba1::doSomeA()"; }
library A 2.0
like this:
// liba2.cpp
const char* doSomeA() { return "liba2::doSomeA()"; }
library B
like this:
// libb.cpp
#include <stdio.h>
extern const char* doSomeA();
void doSomeB() { printf("libb::doSomeB(%s)\n", doSomeA()); }
and application:
// app.cpp
#include <stdio.h>
extern const char* doSomeA();
extern const char* doSomeB();
int main() {
printf("%s .. ", doSomeA());
doSomeB();
return 0;
}
Now we can try to build library A 1.0
and 2.0
with and without symbols versioning liba1.map
:
LIBA_1.0 {
global: *;
};
and liba2.map
:
LIBA_2.0 {
global: *;
};
Building:
rm -f lib*.so
rm -f app00 app10 app11
# build library 'a 1.0' with (liba1v.so) and without (liba1.so) symbol versions
g++ liba1.cpp -g -fpic -shared -o liba1.so -Wl,-soname,liba1.so
g++ liba1.cpp -g -fpic -shared -o liba1v.so -Wl,-soname,liba1v.so -Wl,--version-script,liba1.map
# build library 'a 2.0' with (liba2v.so) and without (liba2.so) symbol versions
g++ liba2.cpp -g -fpic -shared -o liba2.so -Wl,-soname,liba2.so
g++ liba2.cpp -g -fpic -shared -o liba2v.so -Wl,-soname,liba2v.so -Wl,--version-script,liba2.map
# build library 'b' linked to either versioned (liba2v.so) or unversioned (liba2.so) library 'a'
g++ libb.cpp -g -fpic -shared -o libb2.so -Wl,-soname,libb2.so -Wl,--no-undefined -L. -la2
g++ libb.cpp -g -fpic -shared -o libb2v.so -Wl,-soname,libb2v.so -Wl,--no-undefined -L. -la2v
# build application linking to libraries 'b' (implicitly depending on 'a 2.0') and 'a 1.0'.
g++ app.cpp -g -o app00 -Wl,-rpath,`pwd` -L. -lb2 -la1
g++ app.cpp -g -o app10 -Wl,-rpath,`pwd` -L. -lb2v -la1
g++ app.cpp -g -o app11 -Wl,-rpath,`pwd` -L. -lb2v -la1v
LD_LIBRARY_PATH=$PWD ./app00
LD_LIBRARY_PATH=$PWD ./app10
LD_LIBRARY_PATH=$PWD ./app11
Output:
liba1::doSomeA() .. libb::doSomeB(liba1::doSomeA())
liba1::doSomeA() .. libb::doSomeB(liba1::doSomeA())
liba1::doSomeA() .. libb::doSomeB(liba2::doSomeA())
The 1st combination (library A
has unversioned symbols) shows the original problem (wrong version of library A
is used).
The 3rd combination (both libraries A 1.0
and 2.0
have versioned symbols) shows problem solved. But this option requires both versions 1.0
and 2.0
of library A
to be modified.
The 2nd combination (library A 1.0
uses unversioned symbols and A 2.0
- versioned) still fails. It seems that even when library refers to versioned symbols, linker still accepts unversioned symbols, if they are already loaded.
Considering that library A 1.0
cannot be modified (but A 2.0
and library B
could be modified) - is there any option to avoid this behavior?
Upvotes: 2
Views: 90
Reputation: 213376
If the number of symbols in A 1.0
which the application is actually using is relatively small, you can achieve your desired result by building an interposer library.
The source of the interposer would look something like this:
#include <dlfcn.h>
const char* doSomeA()
{
static void* handle = dlopen("liba1.so", RTLD_LAZY|RTLD_LOCAL|RTLD_DEEPBIND);
static char* (*fn)(void) = (char* (*)())dlsym(handle, "_Z7doSomeAv");
return (*fn)();
}
You would then build this interposer library with versioned symbols (same way you built liba1v.so
), and link your application as you did app11
. (You don't even have to rebuild app11
at all -- just replace liba1v.so
with the interposer.)
The fact that you are using liba1.so
"behind the scenes" will be hidden behind the liba1v
"facade" -- due to RTLD_LOCAL
.
Upvotes: 0
Reputation: 2361
This is a comment expanded to answer.
but A 2.0 and library B could be modified
If you can modify source files of A 2.0 then you could move all code in library A 2.0 to some unique namespace, say liba_2_0
. Basically it means inserting a single line "namespace liba_2_0" at the top (but after #includes) of every source file in library A, and two lines "} using namespace liba_2_0;" at the bottom. That's what I meant by suggesting "wrapping" library A in a namespace.
// liba2.h
namespace liba_2_0 {
const char* doSomeA();
// 999 other C++ classes, heavily templated maybe.
} // namespace liba_2_0
using namespace liba_2_0;
The goal of the namespace is to change the name of all symbols in library A from "plainName" to "liba_2_0::plainName" to avoid linker coillisions.
The goal of using-directive is to allow unqualified lookup in library B find those names, without modifying sources of library B.
P.S. I'm not suggesting inline namespaces on purpose. They have some downsides...
Upvotes: 0