DEV Community

Martin Vejdarski
Martin Vejdarski

Posted on

DLL Symbol Visibility in C++

Static and Dynamic Libraries

  • Static libraries are a set of symbols (variables and functions), which get resolved and embedded into an application right after compiling, at link-time.
  • Dynamic libraries get resolved and linked at load-time, when the application starts, or later on, at run-time (e.g. as a plugin).
  • Having the symbols embedded reduces the load time, however, using dynamic libraries allows for these libraries to be changed without having to recompile the entire app.

Symbol Visibility

  • To allow for symbols to be linked dynamically, the dynamic library has to contain information that would map its symbols to the ones needed in the app.
  • This is achieved through the relocation and symbol tables.
  • By specifying which symbols are visible/exported (to be used by other libraries) and which ones are hidden (only used internally), the sizes of these tables can be greatly reduced, which results in much faster application load times.

DLLs and linking errors (LNK2001, LNK2019, and LNK1120)

Working with dynamic-link libraries on Windows requires that symbols shared across boundaries be explicitly marked as exported or imported. Failing to do so results in errors, such as error LNK2019: unresolved external symbol ... referenced in .... This typically comes as a surprise at first, especially when coming from a different OS where all symbols are exported by default and limited symbol visibility is an option.

How are symbols exported/imported?

The most common way is by using the keywords __declspec(dllimport) and __declspec(dllexport) to mark every symbol that should be usable by other libraries.

For example:

// foo.h
__declspec(dllexport) void bar();

When compiling the DLL, the compiler must see the __declspec(dllexport) keyword. However, when later on using that DLL in another library or executable, the compiler has to see the __declspec(dllimport) keyword for those same definitions instead.

// foo.h - compiling library A
__declspec(dllexport) void bar();

-------------------------------------------------------------------------------

// foo.h - compiling library B, which links to library A
__declspec(dllimport) void bar();

The easiest way to achieve this is by wrapping these keywords in macros so that they can be flipped conditionally.

// common.h

#if defined(A_IMPLEMENTATION)
#    define A_API __declspec(dllexport)
#else
#    define A_API __declspec(dllimport)
#endif

// foo.h
A_API void bar();

When compiling library A, A_IMPLEMENTATION would be passed as a compiler definition, and when compiling library B, it would be left out.

While this macro is sufficient when only targeting Windows, if the library were to be used on other OSs, the macro would have to be extended to handle other platforms accordingly.

In CMake, there is a GenerateExportHeader utility, which, as the name would suggest, generates all the necessary macros automatically. CMake also automatically defines <libname>_EXPORTS (analogous to A_IMPLEMENTATION) for each library.

Where should the macros be placed?

The answer depends on the symbol type, and while this is not an exhaustive list, it should cover most of the typical use cases:

// free functions declarations - before the return type
A_API void freeFunction();

// class or struct definitions
//   to export the entire class with all of its members, 
//   add the macro after the class keyword
class A_API FullClass {
public:
    void method();
    static void staticFunction();
    int memberVariable;
}
//   to export only some of the members, decorate each member
//   Note: exporting the whole class is more common and less prone to issues
class PartialClass {
public:
    A_API void method();
    A_API static void staticFunction();
    A_API int memberVariable;
}

// extern variables - same as free functions
A_API extern int externVariable;

// inline functions - these will compile with or without an export,
// however, if an inline function hasn't been exported, its address
// will be different depending on which library accesses it
// See References below for more information.
A_API inline void inlineFunction() {}

// template functions without definition in the header:
// each specialization that must be available from the DLL, must be 
// explicitly instantiated and exported
//   foo.h - declaration
template<class T>
void templateFunction(T);
//   foo.cpp - definition and exports
template<class T>
void templateFunction(T) {}
template A_API templateFunction<int>(int);
template A_API templateFunction<double>(double);

// template classes without implementation in the header:
// same as the template functions above - each specialization must be exported
//   foo.h
template<class T>
class TemplateClass {
public:
    TemplateClass();
}
//    foo.cpp
template<class T>
TemplateClass::TemplateClass() = default;
template class A_API TemplateClass<int>;
template class A_API TemplateClass<double>;

References:

Top comments (0)