Keywords: DLL export | name decoration | extern C | dllexport | calling convention
Abstract: This article provides a comprehensive exploration of correctly exporting C-style functions from C++ DLLs on Windows to achieve undecorated export names. It focuses on the combination of __declspec(dllexport) and extern "C", avoiding .def files while ensuring compatibility with GetProcAddress, PInvoke, and other cross-language calls. By comparing the impact of different calling conventions on name decoration, it offers practical code examples and best practices to help developers create user-friendly cross-platform DLL interfaces.
When developing dynamic-link libraries (DLLs) on the Windows platform, exporting functions for use by other applications or modules is a common requirement. Particularly when dynamic loading via GetProcAddress, .NET PInvoke, or FFI mechanisms from other languages is needed, ensuring undecorated export names becomes crucial. Based on practical development experience, this article systematically explains how to achieve this using __declspec(dllexport) and extern "C", avoiding the maintenance complexity of .def files.
Fundamentals of DLL Export Mechanisms
There are two primary methods for exporting functions from Windows DLLs: using module definition files (.def) or the __declspec(dllexport) attribute. The latter, as an extension of Microsoft compilers, provides a more intuitive in-code declaration approach. When a function is marked with dllexport, the linker automatically adds it to the DLL's export table.
However, C++ compilers by default mangle function names to support features like overloading and namespaces. This decoration results in export names containing extra characters, such as ?foo@@YAHH@Z, making dynamic loading by name difficult. For example, the following code:
__declspec(dllexport) int foo(int bar) {
return bar * 2;
}
When viewed with dumpbin, may show a mangled name. To solve this, extern "C" is used to suppress name decoration.
Achieving Undecorated Exports with extern "C"
extern "C" instructs the compiler to use C language linkage conventions, thereby avoiding C++ name mangling. Combined with __declspec(dllexport), it creates a straightforward export interface. The basic pattern is:
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) int addNumbers(int a, int b);
#ifdef __cplusplus
}
#endif
In the implementation file:
__declspec(dllexport) int addNumbers(int a, int b) {
return a + b;
}
This keeps the export name as addNumbers, facilitating dynamic loading. This method is particularly suitable for scenarios requiring calls via FFI from languages like Python, .NET, or VB6.
Impact and Handling of Calling Conventions
Calling conventions determine how function parameters are passed and the stack is cleaned up, and on x86 architecture, they also affect name decoration. Common conventions include __stdcall, __cdecl, etc. When using __stdcall (often defined via the WINAPI macro), even with extern "C", x86 platforms still decorate names, e.g., _addNumbers@8 (where 8 represents the total parameter byte count).
Example:
extern "C" __declspec(dllexport) int __stdcall addNumbers(int a, int b);
On x86, the export name may become _addNumbers@8. This can break compatibility with callers expecting undecorated names. Notably, on x64 architecture, __stdcall is ignored, and a unified calling convention is used, so no extra decoration occurs.
If __stdcall is required along with undecorated names, consider these solutions:
- Use a .def file to explicitly specify export names, but this adds maintenance overhead.
- Use the compiler's
#pragma comment(linker, ...)directive, such as:
#define EXPORT comment(linker, "/EXPORT:" __FUNCTION__ "=" __FUNCDNAME__)
int __stdcall addNumbers(int a, int b) {
#pragma EXPORT
return a + b;
}
This approach maintains undecorated names on both x86 and x64 while preserving __stdcall semantics.
Cross-Platform Compatibility Considerations
To ensure DLL portability across Windows and non-Windows platforms (e.g., Linux, macOS), conditional compilation can be used to safely define export macros. For example:
#ifdef _WIN32
#ifdef MATHLIB_EXPORTS
#define MATHLIB_API __declspec(dllexport)
#else
#define MATHLIB_API __declspec(dllimport)
#endif
#else
#define MATHLIB_API
#endif
#ifdef __cplusplus
extern "C" {
#endif
MATHLIB_API int multiply(int x, int y);
#ifdef __cplusplus
}
#endif
Define the MATHLIB_EXPORTS macro in the DLL project, so functions are exported during DLL build and imported when used by clients. On non-Windows platforms, MATHLIB_API is defined as empty to avoid compilation errors.
Practical Application Example
Suppose we need to create a math library DLL exporting addition and multiplication functions for dynamic loading. The complete code is as follows:
Header file mathlib.h:
#ifndef MATHLIB_H
#define MATHLIB_H
#ifdef _WIN32
#ifdef MATHLIB_EXPORTS
#define MATHLIB_API __declspec(dllexport)
#else
#define MATHLIB_API __declspec(dllimport)
#endif
#else
#define MATHLIB_API
#endif
#ifdef __cplusplus
extern "C" {
#endif
MATHLIB_API int add(int a, int b);
MATHLIB_API int multiply(int a, int b);
#ifdef __cplusplus
}
#endif
#endif // MATHLIB_H
Implementation file mathlib.cpp:
#define MATHLIB_EXPORTS
#include "mathlib.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
After compilation, use dumpbin to view exports:
dumpbin /exports mathlib.dll
ordinal hint RVA name
1 0 00001000 add
2 1 00001010 multiply
Client-side dynamic loading example (C++):
#include <windows.h>
#include <iostream>
typedef int (*AddFunc)(int, int);
typedef int (*MultiplyFunc)(int, int);
int main() {
HMODULE hDll = LoadLibrary("mathlib.dll");
if (!hDll) {
std::cerr << "Failed to load DLL" << std::endl;
return 1;
}
AddFunc pAdd = (AddFunc)GetProcAddress(hDll, "add");
MultiplyFunc pMultiply = (MultiplyFunc)GetProcAddress(hDll, "multiply");
if (pAdd && pMultiply) {
std::cout << "3 + 4 = " << pAdd(3, 4) << std::endl;
std::cout << "3 * 4 = " << pMultiply(3, 4) << std::endl;
}
FreeLibrary(hDll);
return 0;
}
This example demonstrates how to create and use a DLL with undecorated export names, ensuring compatibility with dynamic loading mechanisms.
Summary and Best Practices
By combining __declspec(dllexport) and extern "C", C-style functions can be effectively exported with undecorated names, simplifying dynamic loading. Key points include:
- Use
extern "C"to suppress C++ name mangling, ensuring predictable export names. - Be aware of calling convention impacts on x86 name decoration; use .def files or compiler directives if necessary.
- Implement cross-platform compatibility via conditional compilation, allowing code to work on both Windows and non-Windows systems.
- Use export macros (e.g.,
LIBRARY_API) to simplify dllexport/dllimport switching, improving code maintainability.
Following these practices enables developers to create easily integrable, cross-language compatible DLL interfaces that meet modern software development needs.