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:
- The
.dllfile: The executable module containing compiled machine code and an export table listing exposed symbols and their entry points. - The
.libfile: 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.librather 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.