Search code examples
c++dllbinary-compatibility

What's the difference in usage between shared libraries built in debug and release configuration (c++)?


I've come with problems using shared libraries (.dll in Win 10).

I build a lib called xlib in two different configurations, and try to test use them via CMake in a project called xlibtest.

the work flow is:

Step A. build xlib => xlib.dll + xlib.lib(only symbol) + xlib.pdb[optional]

Step B. build xlibtest and link xlib.lib => xlibtest.exe

If build configuration is different in Step A and Step B, then when I'm running xlibtest.exe, there will be problems. For example, I have a function

//xlib.h    
void foo(std::string input); 

in xlib, it will crash when I call this function.

//xlibtest.cpp
int main() {
    xlib::foo("test"); // in debug mode I found the string pass to foo is wrong
}

I've searched and found some explanation about this problem. It may because of the difference in memory pattern of debug and release configuration. And a suggestion is not mix these two configurations, i.e. use debug lib in debug configuration and release lib in release configuration.

Then here is the problem. I can build two version of my own library. However what can I do for those external libraries?

I've used intel MKL libraries in my project, and linking the same libraries in both debug and release configuration. It seems works fine.

But when I'm using Boost libraries, there may be difference between debug and release version, since we can decide the version we build when install Boost libraries.

P.S. The reason why I don't link with static libraries is that there is error is compile time and requires same configuration (i.e. debug/release).

--Update--

I'm using:

CMake 3.5.2;

Compiler: Intel icl 16.0 update 3 for Intel 64 VS2015 environment;

IDE: VS 2015 Community;

OS: Windows 10, 64 bit.

CPU: Intel i7 4930K

Sorry for not providing these information about the tool chain. I used to thought this is a general problem without relation to the tool chain I use. @Hans Passant

I think answer from @John D may end my confusion. After I learnt there are problem in mixing debug and release configuration, I can't understand why there is no difference in using Intel MKL libs, that's why I post this question. Is it the reason that Intel MKL lib is a C lib that has no std::string in it?


Solution

  • Compiling in Debug/Release - impact on code

    Technically, "Debug" and "Release" builds of a library are builds with different compiler switches and preprocessor macros. It's not fundamentally different from, say, compiling a library with optional features.

    • compiler switches generally do not affect how code works at high level. There are slight differences on low level where a relevant spec doesn't specify the behavior (in e.g. order of independent operations, memory map, physical location of variables, compiler-generated instrumentation code) but if you depend on these, you're doing it wrong.
    • preprocessor macros, on the other hand, can alter code behavior in any way. Typically, the DEBUG macro enables chunks of code with various runtime checks and diagnostics. The latter may include adding members to data types.

    So, Debug and Release versions of C++ code that you build are, in the general case, binary incompatible.

    This stands for the code of the C++ standard library. Although it's typically not compiled from stratch each time but one of the few precompiled versions are inserted (including a "thunk" version that delegates to a DLL).

    The real part: pitfalls and solutions

    Problems will occur if you have two modules that

    1. use different versions or binary representations of a type and
    2. exchange objects of that type

    The problem predominantly occurs in dynamic linking (but might happen in static linking too if, say, a pre-built static library doesn't correspond to its headers).

    This happens if the modules have been compiled (to use the problematic type) and linked (to use library code working with the problematic type) against binary incompatible versions of the library defining that type.

    So, specifically for the C++ standard library, to avoid and/or fix those, you need to

    • make sure that all your modules that exchange C++ types are/have been compiled and linked against binary-compatible (for the types being exchanged) versions of C++ runtime
      • This is relatively easy to check if all such modules are linked to it dynamically, but not so much if some have it compiled in statically
      • Basically, in the ideal case, all modules should even use the same instance of the runtime (in Linux, it includes libgcc) - e.g. it may be required for exceptions to pass between them. If your environment is so heterogeneous that it's impossible to achieve, it may be wise to forfeit C++ types as an exchange format altogether.

    Distinguishing incompatible library versions at link time

    (to specify correct ones and name your own modules so that they can be distinguished)

    At link time (static or dynamic), there's no such thing as "build configurations". All there is are a number of modules. All the linker sees is references to other modules (dynamic linking only) and exported/imported entries. And all it does is find the referenced modules (again, dynamic only; in static, the modules are typically specified explicitly), then match exported and imported entries together. C++ export/import entry names contain the entire signature of the entry in encoded ("mangled") form (specifically, incompatible compilers have to use different mangling schemes so entries produced by them don't clash.

    The only ways for the linker to distinguish two versions of the library are:

    1. feed it different search paths/ explicitly specify different modules
    2. use different module names to link against
    3. use different entry names (implies different type names/function signatures)

    3) isn't generally practical (requires lots of boilerplate code) and in dynamic linking , is unusable without 2) 'cuz a linker first matches module names. 1) (search paths flavor) doesn't scale.

    To summarize:

    • 2) is the standard practice for system-wide dynamic linking (_d suffix, version suffix)
    • 1) is the standard practice for build environments (static and dynamic)
    • 1) (search path flavor) is used to link against private copies of libraries (as it was said, this doesn't scale)