There are two problem types I can think of:
- Using standard library types in the interface of
A.so
- (Accidentally) exporting symbols with standard library types from
A.so
.
Let's focus on one platform, say Linux. On Linux, shared libraries follow the ELF specification. They export a subset of their symbols ("ELF-visible" symbols). Only those exported symbols can be linked against, or found at runtime via the dynamic loader, by consumers like B.out
.
libstdc++ is meant to be backwards-compatible. That is, if you link libstdc++ dynamically, your program usually just will load one libstd++.so library. There can be multiple modules (the executable, dynamic libraries) consuming symbols from this libstdc++. Because of the backwards-compatibility, all of them should work. This backwards-compatibility is achieved by ELF symbol versioning. Symbol versioning is however only done for symbols inside libstdc++, and not the standard library types which just live in headers (e.g. templates).
Regarding the first problem type: The layout of standard-library types is not stable in libstdc++. If you construct an object of type std::vector<int>
in A.so
and pass it out to B.out
, they might assume different binary layouts for that type and it will break in wonderful ways. This even affects members of classes in A.so
, if the space for the class object is allocated/freed by B.out
. Here's one example.
Regarding the second problem type: By default, all symbols with external linkage are also exported from a shared object. Therefore, standard library templates instantiations can also get exported, usually by accident. If you've accidentally created an ELF-visible symbol like std::vector<Foo>::push_back(..)
there's a chance both A.so
and B.out
both provide that symbol. But since we're linking dynamically, the one in B.out
will be preferred and also used by A.so
. Of course this is bad since either Foo
or std::vector
might have different layouts in A.so
and B.out
. This can also happen for things like global/static variables such as the empty string for CoW-std::string, or locale facets. It can mostly be dealt with by disabling default ELF-visibility (-fvisibility=hidden
) and then explicitly exporting individual symbols. But even then it's tricky and you should check which symbols are exported in the end. Maybe a linker version script helps here.
Regarding clang vs gcc: If you use both libstdc++ and libc++, you'll most likely run into symbol conflicts between libstdc++ and libc++. For example, both might provide a function like operator new(size_t)
. Maybe you're lucky and all symbols are properly versioned, and the version contains the library name.
In simple shared object schemes, you only specify which libraries you need. Separately, you specify the symbols you need. There is usually no association between those two. For every library loaded, the dynamic loader maintains a list of libraries to search for a symbol. It is possible to resolve symbols from "the wrong library" (not the one you intended to use), however I think that would not happen if A.so
and libc++.so
are sibling dependencies (i.e. B.out
loads both libc++.so
and A.so
, and A.so
requires libstdc++.so
).
It should be possible to use linker namespaces to completely isolate dependencies.
In the end, I think it's possible to make it work. Question is, are you willing to solve the weird and wonderful issues arising from accidental symbol collisions? Is it worth it?