12

Motivating background info: I maintain a C++ library, and I spent way too much time this weekend tracking down a mysterious memory-corruption problem in an application that links to this library. The problem eventually turned out to be caused by the fact that the C++ library was built with a particular -DBLAH_BLAH compiler-flag, while the application's code was being compiled without that -DBLAH_BLAH flag, and that led to the library-code and the application-code interpreting the classes declared in the library's header-files differently in terms of data-layout. That is: sizeof(ThisOneParticularClass) would return a different value when invoked from a .cpp file in the application than it would when invoked from a .cpp file in the library.

So far, so unfortunate -- I have addressed the immediate problem by making sure that the library and application are both built using the same preprocessor-flags, and I also modified the library so that the presence or absence of the -DBLAH_BLAH flag won't affect the sizeof() its exported classes... but I feel like that wasn't really enough to address the more general problem of a library being compiled with different preprocessor-flags than the application that uses that library. Ideally I'd like to find a mechanism that would catch that sort of problem at compile-time, rather than allowing it to silently invoke undefined behavior at runtime. Is there a good technique for doing that? (All I can think of is to auto-generate a header file with #ifdef/#ifndef tests for the application code to #include, that would deliberately #error out if the necessary #defines aren't set, or perhaps would automatically-set the appropriate #defines right there... but that feels a lot like reinventing automake and similar, which seems like potentially opening a big can of worms)

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • What consumes those defines? At first I assumed it's for your own library, but then you're saying you could redefine them in your header, so I'm probably wrong. – HolyBlackCat Apr 04 '22 at 12:20
  • Not really a solution to *catching* mismatched macro definitions, but to help prevent them from occurring things like CMake can be helpful here in enforcing macro definitions propagate through while building and linking dependencies. – Cory Kramer Apr 04 '22 at 12:21
  • 1
    We had the exact same problem with boost's sockets. We linked to a static lib that had a define that changed the sockets implementation and the linker was picking half the definition from the static lib and would generate the rest. During runtime it would fail and it was nasty to debug. We modified the library to not expose its internals and used dynamic linking. – Zaiborg Apr 04 '22 at 12:24
  • 7
    One good technique (the best technique, imo) I learned from Titus Winters. "Build EVERYTHING from source, with the same flags, at the same time!" ~Titus Winters – Eljay Apr 04 '22 at 12:24
  • Do not make the library API configurable. – j6t Apr 04 '22 at 12:46
  • @HolyBlackCat you have it right, it’s the code in the library that consumes them. My header-file idea would have the header file generated (somehow) when the library is built, and the header file would be for the use of applications using the library. – Jeremy Friesner Apr 04 '22 at 14:01
  • In general, it should not be harmful if the application is built a `DEBUG` flag and the library is not, for example. If using different preprocessor macros in different translation units cause problems, I'd say something is wrong in the design – Hagen von Eitzen Apr 04 '22 at 21:30
  • 2
    @HagenvonEitzen In an ideal world I would agree with you, but in practise a library (especially a C++ one - as opposed to C) might want to pass objects across the API that involves exposing (to the compiler) the layout of their private data. There are ways round this (the PIMPL idiom, most notably) but they usually make life harder both for the author and the consumer(s) of the library since they don't marry well with modern C++'s value semantics. And try mixing Windows code where some of it is built in Release mode and some in Debug mode. The usual result: bang! – Paul Sanders Apr 04 '22 at 21:38
  • 1
    @HagenvonEitzen in this case the preprocessor macro was named ENABLE_OPENSSL_SUPPORT, to be specified if OpenSSL was to be used for networking, or left unspecified if OpenSSL support was unnecessary (and the user wanted to avoid the hassle of dealing with an OpenSSL dependency). That said, I agree that it's better if the presence/absence of the macro doesn't break ABI(?) compatibility, and I did modify my library to achieve that. – Jeremy Friesner Apr 04 '22 at 23:10

3 Answers3

10

One way of implementing such a check is to provide definition/declaration pairs for global variables that change, according to whether or not particular macros/tokens are defined. Doing so will cause a linker error if a declaration in a header, when included by a client source, does not match that used when building the library.

As a brief illustration, consider the following section, to be added to the "MyLibrary.h" header file (included both when building the library and when using it):

#ifdef FOOFLAG
extern int fooflag;
static inline int foocheck = fooflag;    // Forces a reference to the above external
#else
extern int nofooflag;
static inline int foocheck = nofooflag;  //                <ditto>
#endif

Then, in your library, add the following code, either in a separate ".cpp" module, or in an existing one:

#include "MyLibrary.h"

#ifdef FOOFLAG
int fooflag = 42;
#else
int nofooflag = 42;
#endif

This will (or should) ensure that all component source files for the executable are compiled using the same "state" for the FOOFLAG token. I haven't actually tested this when linking to an object library, but it works when building an EXE file from two separate sources: it will only build if both or neither have the -DFOOFLAG option; if one has but the other doesn't, then the linker fails with (in Visual Studio/MSVC):

error LNK2001: unresolved external symbol "int fooflag" (?fooflag@@3HA)

The main problem with this is that the error message isn't especially helpful (to a third-party user of your library); that can be ameliorated (perhaps) by appropriate use of names for those check variables.1

An advantage is that the system is easily extensible: as many such check variables as required can be added (one for each critical macro token), and the same idea can also be used to check for actual values of said macros, with code like the following:

#if FOOFLAG == 1
int fooflag1 = 42;
#elif FOOFLAG == 2
int fooflag2 = 42;
#elif FOOFLAG == 3
int fooflag3 = 42;
#else
int fooflagX = 42;
#endif

1 For example, something along these lines (with suitable modifications in the header file):

#ifdef FOOFLAG
int CANT_DEFINE_FOOFLAG = 42;
#else
int MUST_DEFINE_FOOFLAG = 42;
#endif

Important Note: I have just tried this technique using the clang-cl compiler (in Visual Studio 2019) and the linker failed to catch a mismatch, because it is completely optimizing away all references to the foocheck variable (and, thus, to the dependent fooflag). However, there is a fairly trivial workaround, using clang's __attribute__((used)) directive (which also works for the GCC C++ compiler). Here is the header section for the last code snippet shown, with that workaround added:

#if defined(__clang__) || defined(__GNUC__)
#define KEEPIT __attribute__((used))
// Equivalent directives may be available for other compilers ...
#else
#define KEEPIT
#endif

#ifdef FOOFLAG
extern int CANT_DEFINE_FOOFLAG;
KEEPIT static inline int foocheck = CANT_DEFINE_FOOFLAG; // Forces reference to above
#else
extern int MUST_DEFINE_FOOFLAG;
KEEPIT static inline int foocheck = MUST_DEFINE_FOOFLAG; //         <ditto>
#endif
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
  • Maybe I'm missing something from not reading the question carefully enough, but why not use a static assertion? – Cody Gray - on strike Apr 04 '22 at 21:14
  • 1
    @Cody Because that relies on *compile-time* knowledge on the compiler's part. What does the compiler know about how the library was built? So, when a third-party "client" is being linked with the pre-compiled/shipped object library, how would a `static_assert` help? – Adrian Mole Apr 04 '22 at 21:34
  • 1
    ... a *run-time* assertion could do the trick, probably, but that would not stop a build that's using the wrong flag/macro settings. – Adrian Mole Apr 04 '22 at 21:41
  • 1
    Might get (slightly) nicer linker errors if those variables are declared `extern "C"`. – Paul Sanders Apr 06 '22 at 05:40
7

In the Microsoft C++ frontend and linker, the #pragma detect_mismatch directive can be used in a very similar spirit as the solution presented in Adrian Mole's answer. Like that answer, mismatches are detected at link time, not at compilation time. It "places a record in an object. The linker checks these records for potential mismatches."

Say something like this is in a header file that is included in different compilation units:

#ifdef BLAH_BLAH
#pragma detect_mismatch("blah_blah_enabled", "true")
#else
#pragma detect_mismatch("blah_blah_enabled", "false")
#endif

Attempting to link object files with differing values of "blah_blah_enabled" will fail with LNK2038:

mismatch detected for 'name': value 'value_1' doesn't match value 'value_2' in filename.obj

Based on the mention of automake in the question, I assume that the asker isn't using the Microsoft C++ toolchain. I'm posting this here in case it helps someone in a similar situation who is using that toolchain.

I believe the closest MSVC analogue to the __attribute__((used)) in Adrian Mole's answer is the /INCLUDE:symbol-name linker option, which can be injected from a compilation unit via #pragma comment(linker, "/include:symbol-name").

chwarr
  • 6,777
  • 1
  • 30
  • 57
6

As an alternative to @adrian's (excellent) answer, here's a suggestion for a runtime check which might be of interest.

For the sake of example, let's assume there are two flags, FOO1 and FOO2. First of all, for my scheme to work, and since the OP seems to be using #ifdef rather than #if, the library needs to provide a header file that looks like this (header guards omitted for clarity):

// MyLibrary_config_check.h

#ifdef FOO1
    #define FOO1_VAL 1
#else
    #define FOO1_VAL 0
#endif

#ifdef FOO2
    #define FOO2_VAL 1
#else
    #define FOO2_VAL 0
#endif

... etc ...

Then, the same header file declares the following function:

bool CheckMyLibraryConfig (int expected_flag1, int expected_flag2 /* , ... */);

The library then implements this like so:

bool CheckMyLibraryConfig (int expected_flag1, int expected_flag2 /* , ... */)
{
    static const int configured_flag1 = FOO1_VAL;
    static const int configured_flag2 = FOO2_VAL;
    // ...

    if (expected_flag1 != configured_flag1)
        return false;
    if (expected_flag2 != configured_flag2)
        return false;
    // ...
    return true;
}

And the consumer of the library can then do:

    if (!CheckMyLibraryConfig (FOO1_VAL, FOO2_VAL /* , ... */))
        halt_and_catch_fire ();

On the downside, it's a runtime check, and that's not what was asked for. On the upside, CheckMyLibraryConfig could instead be implemented something like this:

std::string CheckMyLibraryConfig (int expected_flag1, int expected_flag2 /* , ... */)
{
    if (expected_flag1 != configured_flag1)
        return std::string ("Expected value of FOO1 does not match configured value, expected: ") + std::to_string (expected_flag1) + ", configured: " + std::to_string (expected_flag2);

    ...

    return "";
}

And the consumer can then check for and display any non-empty string returned. Get as fancy as you like (that code could certainly be factored better) and check all the flags before returning a string reporting all the mis-matches, go crazy.

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • It's a nice answer. But, whatever you do, OP's goal can *only* be achieved (I guess) either at link time (as in my answer) or at run time (as in yours). Because the C++ *compiler* doesn't really 'know' anything about how the library was compiled, I can't see a compile time option for this. – Adrian Mole Apr 04 '22 at 20:52
  • 2
    Yes, I agree completely. The only other solution might be to do what some Linux (and macOS) libraries do and make the name of the library version-specific. – Paul Sanders Apr 04 '22 at 21:33