C

Advanced Windows C++20 DLL Development: Export Definitions, WinAPI, and Best Practices. Create dll library

OsderdaDev
May 24, 2026
8 views

Dynamic-Link Libraries (DLLs) are the lifeblood of the Windows operating system. From the low-level ntdll.dll to high-level UI frameworks, DLLs allow developers to modularize code, reduce memory footprints, and build extensible plugin architectures. While the core concept of DLLs hasn't changed in decades, the techniques, compiler flags, and best practices for authoring them in modern C++20 within Visual Studio have evolved significantly.

In this comprehensive guide, we will explore the WinAPIs, export techniques, module definitions, and linker flags required to build robust, high-performance C++20 libraries on Windows.

1. The Entry Point: Understanding `DllMain`

Unlike a standard executable that starts at main() or WinMain(), a Windows DLL uses an optional entry point called DllMain. The operating system calls this function whenever a process or thread loads or unloads the DLL.

cpp
#include <windows.h>

// The standard signature for a Windows DLL entry point
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACH:
            // Called when the DLL is mapped into a process's address space.
            // Optimization: Disable thread library calls if you don't need DLL_THREAD_ATTACH
            DisableThreadLibraryCalls(hModule);
            break;
        case DLL_THREAD_ATTACH:
            // Called when a new thread is created in the host process.
            break;
        case DLL_THREAD_DETACH:
            // Called when a thread cleanly exits.
            break;
        case DLL_PROCESS_DETACH:
            // Called when the DLL is unmapped from the process.
            // lpReserved is non-null if the process is terminating.
            break;
    }
    return TRUE; // Return FALSE to fail the DLL load
}

The Loader Lock Warning

A critical architectural rule for Windows engineers: Do not do complex initialization inside `DllMain`.

When DllMain executes, the OS holds the Loader Lock, a system-wide mutex. If you call LoadLibrary, wait on synchronization objects, or interact with other threads inside DllMain, you risk causing a deadlock. Initialize your heavy C++20 objects via a dedicated exported Initialize() function instead.

2. Defining Exports: `__declspec` and Macros

To make a function or class available to the outside world, you must export it. Windows uses __declspec(dllexport) to export a symbol, and __declspec(dllimport) to import it.

The industry standard is to use a preprocessor macro to toggle these flags automatically based on whether you are compiling the DLL or consuming it.

cpp
// MyLibrary.h
#pragma once

// Exclude rarely-used stuff from Windows headers
#define WIN32_LEAN_AND_MEAN 
#include <windows.h>

// If MYLIB_EXPORTS is defined in Visual Studio Preprocessor Definitions,
// we are building the DLL. Otherwise, we are importing it.
#ifdef MYLIB_EXPORTS
    #define MYLIB_API __declspec(dllexport)
#else
    #define MYLIB_API __declspec(dllimport)
#endif

// Use EXTERN_C (extern "C") to prevent C++ name mangling
EXTERN_C MYLIB_API int InitializeSystem();
EXTERN_C MYLIB_API double CalculateMetrics(const double* data, size_t size);

The C++ Name Mangling Problem

C++ compilers "mangle" function names to support function overloading (e.g., CalculateMetrics might become ?CalculateMetrics@@YANNPEBN_K@Z). If you want other languages (C#, Python, Rust) to call your DLL, you must wrap your exports in extern "C" (or the Windows macro EXTERN_C) to force a flat, standard C ABI.

3. Advanced Exporting: Module Definition (.DEF) Files

While __declspec(dllexport) is convenient, it can sometimes leave unnecessary metadata in the export table. For ultimate control, senior engineers use .DEF files.

A .DEF file allows you to export functions by ordinal (number) rather than name, hiding the function name entirely (using the NONAME flag). This is highly useful for proprietary security or minimizing DLL size.

Create a file named Exports.def and add it to your Visual Studio project:

def
LIBRARY "MyAdvancedLib"
EXPORTS
    InitializeSystem   @1
    CalculateMetrics   @2
    SecretFunction     @3 NONAME

By passing this to the linker (/DEF:Exports.def), SecretFunction will only be accessible if the caller knows it is located at ordinal 3.

4. Explicit vs. Implicit Linking

When a client application uses your DLL, they can link to it in two ways.

Implicit Linking (Load-Time)

The client links against the .lib file generated alongside your .dll. The OS automatically loads your DLL when the client application starts. If the DLL is missing, the application immediately crashes with a "DLL not found" error.

Explicit Linking (Run-Time)

The client manually loads the DLL at runtime using WinAPIs. This is safer, allows for fallback mechanisms, and is essential for plugin architectures.

Here is a modern C++20 approach to explicit linking, utilizing RAII with std::unique_ptr and custom deleters to ensure handles are never leaked:

cpp
#include <windows.h>
#include <iostream>
#include <memory>
#include <functional>

// Define the function pointer type
using CalculateMetricsFn = double(*)(const double*, size_t);

void ExecutePlugin() {
    // C++20 RAII wrapper for HMODULE
    std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)> 
        hModule(LoadLibraryW(L"MyAdvancedLib.dll"), &FreeLibrary);

    if (!hModule) {
        std::cerr << "Failed to load DLL. Error: " << GetLastError() << '\n';
        return;
    }

    // Retrieve the exported function
    auto CalculateMetrics = reinterpret_cast<CalculateMetricsFn>(
        GetProcAddress(hModule.get(), "CalculateMetrics")
    );

    if (CalculateMetrics) {
        double data[] = { 1.5, 2.5, 3.5 };
        double result = CalculateMetrics(data, std::size(data));
        std::cout << "Result: " << result << '\n';
    }
}

5. The Golden Rule: Respecting the ABI Boundary

When writing a C++20 DLL, you have access to incredible features: concepts, ranges, coroutines, and std::format. However, never pass C++ Standard Template Library (STL) objects (like `std::vector` or `std::string`) across the DLL boundary exported API.

Why? Because the client executable and the DLL might be compiled with different versions of the C++ Runtime (CRT), or one might be compiled in Debug and the other in Release mode. An std::string allocated in the DLL and deleted in the Exe will corrupt the heap.

Best Practice:

Keep your internal DLL code in pure C++20. For your exported functions, expose a pure C API, or use pure abstract base classes (COM-lite architecture) where the DLL provides custom deletion functions (e.g., DestroyString(char* str)).

6. Visual Studio Compiler and Linker Flags

To optimize your C++ DLL project in Visual Studio, you must configure specific linker flags.

* Delay-Loaded DLLs (`/DELAYLOAD`):

If your DLL depends on other heavy DLLs, you can tell the Visual Studio Linker to delay loading them until a function from them is actually called. Go to *Properties -> Linker -> Input -> Delay Loaded Dlls* and add HeavyDependency.dll.

* Runtime Library (`/MT` vs `/MD`):

By default, DLLs use /MD (Multi-threaded DLL), meaning they depend on the external Visual C++ Redistributable. If you want a standalone, portable DLL, compile with /MT (Multi-threaded). This statically bakes the CRT into your DLL, increasing file size but eliminating dependency nightmares.

* Security Flags: Ensure /NXCOMPAT (Data Execution Prevention) and /DYNAMICBASE (Address Space Layout Randomization) are enabled in the Linker settings to protect against memory exploits.

Summary

Building a robust Windows DLL with C++20 requires more than just knowing language syntax. It requires a deep understanding of the operating system's loader, the WinAPI, memory management across boundaries, and the Visual Studio toolchain. By leveraging .DEF files, designing flat ABI boundaries, and utilizing modern RAII wrappers for Windows handles, you can engineer highly performant libraries ready for enterprise deployment.

Osderda

Senior Software Engineer

With a diverse background in Windows system programming, scalable Backend infrastructure, native Android development, and server management, I strive to bridge the gap between different technology stacks. My goal is to simplify complex coding concepts and share actionable knowledge through concise, technical documentation.

Discussion (0)