How dllimport and dllexport Work in Windows C++

In Windows C++ development, dynamic-link libraries (DLLs) use an explicit symbol visibility model where functions, classes, and variables are hidden by default. The Microsoft Visual C++ compiler provides the storage-class attributes __declspec(dllexport) and __declspec(dllimport) to manage this boundary. This article explains how these attributes function under the PE/COFF binary format, why they are necessary for cross-module communication, and how they optimize binary execution.

The Windows Symbol Visibility Model

Unlike ELF-based systems (such as Linux) where symbols are typically exported globally by default, the Windows PE (Portable Executable) format requires explicit declarations to expose symbols across executable boundaries. A dynamic-link library produces two primary files during compilation:

  1. The .dll file: The executable module containing compiled machine code and an export table listing exposed symbols and their entry points.
  2. The .lib file: An import library containing metadata that maps exported symbols to their corresponding DLL names and ordinals, used by the client linker during build time.

The Role of __declspec(dllexport)

The __declspec(dllexport) attribute instructs the compiler and linker to place the declared symbol into the export table of the resulting DLL.

// Exporting a function
__declspec(dllexport) void LogMessage(const char* message);

// Exporting a class
class __declspec(dllexport) NetworkClient {
public:
    void Connect();
};

When applied:

  • The linker emits an entry in the export directory of the DLL.
  • The linker generates an import library (.lib) containing stub records for the exported symbols.
  • It eliminates the need to manually maintain a module-definition (.def) file for symbol exports.

The Role of __declspec(dllimport)

The __declspec(dllimport) attribute informs the compiler that a symbol is not defined in the current translation unit or binary, but is instead located in an external DLL.

// Importing a function
__declspec(dllimport) void LogMessage(const char* message);

// Importing a class
class __declspec(dllimport) NetworkClient {
public:
    void Connect();
};

Using __declspec(dllimport) is optional for functions, but mandatory for global data and static member variables:

  • For Functions: If __declspec(dllimport) is omitted, the compiler assumes the function is internal or resolved statically. The linker satisfies the reference via an intermediate jump thunk (a small stub function) provided by the import library. When __declspec(dllimport) is explicitly used, the compiler skips the stub and generates a direct indirect call through the Import Address Table (IAT) (e.g., call dword ptr [__imp_LogMessage]), reducing instructions and improving execution speed.
  • For Data: The compiler cannot create intermediate jump thunks for memory addresses. Without __declspec(dllimport), referencing an external variable results in an address pointing to the import stub within the .lib rather than the actual variable inside the DLL, causing link-time errors or runtime crashes.

The Unified Header Macro Pattern

Because headers are shared between the DLL project (which exports symbols) and the consumer project (which imports them), C++ developers use preprocessor definitions to toggle the attribute automatically:

#if defined(MYLIBRARY_EXPORTS)
    #define MYLIBRARY_API __declspec(dllexport)
#else
    #define MYLIBRARY_API __declspec(dllimport)
#endif

// Shared interface declaration
MYLIBRARY_API void InitializeEngine();

class MYLIBRARY_API EngineContext {
public:
    void Start();
};

When compiling the DLL, the build system defines MYLIBRARY_EXPORTS, expanding the macro to __declspec(dllexport). When a client application includes the header without defining that symbol, the macro expands to __declspec(dllimport).

Name Mangling and extern "C"

C++ applies name decoration (mangling) to support function overloading and namespaces. If a DLL must be consumed by C code or loaded dynamically at runtime via LoadLibrary and GetProcAddress, the export must prevent name mangling by combining the attribute with extern "C":

extern "C" MYLIBRARY_API void PerformAction();

This guarantees that the symbol is stored in the export table as clean, undecorated text, making direct dynamic symbol lookup predictable.