Xunsongh
Xunsongh

Reputation: 49

Are there hidden dangers to link libraries compiled with different C++ standard?

Let's assume we have prepared a shared library which was compiled with gcc9 in std-c++=14 called A.so. And we wanted to link this shared library to a new project whose source file would be compiled with clang++13 in std-c++=17. Let's call the target of the new project B.out.

I am wondering whether there would be hidden dangers in this scheme? If we defined functions with the same name in both A and B, will they conflict with each other or produce unexpected output?

Upvotes: 4

Views: 1735

Answers (2)

dyp
dyp

Reputation: 39121

There are two problem types I can think of:

  1. Using standard library types in the interface of A.so
  2. (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?

Upvotes: 1

PFee
PFee

Reputation: 273

There are a few dangers to be aware of.

C++ name mangling is deliberately non-standard. Different compilers may produce different symbols for the same code. Hence calling code compiled with one compiler from code compiled by another can lead to linker errors.

Having a standard mangling scheme that all compilers use may initially appear useful, but there are many other areas where compilers fail to interoperate (e.g. exception, virtual functions) therefore early breakage due to linker errors can protect against more subtle issues.

GCC and Clang are very close wrt mangling, see here: https://jplehr.de/2019/10/25/name-mangling-in-c-with-clang-and-gcc/

However a further danger is the standard library interoperability. If mixing libstdc++ (from GCC) with libc++ (from LLVM), then more issues arise. For example if you pass a objects from the standard library (e.g. std::string, std::map) between the two bodies of code, it's likely they won't interoperate. The objects from each library may have different memory layout, leading to corruption and (ideally, since obvious bugs are better than subtle ones) crashes.

It may be best to keep the two portions of C++ code entirely separate and only let them meet each other over C APIs.

Upvotes: 1

Related Questions