hanshq.net

Notes on dllexport and dllimport
(5 October 2026)

These are some notes about how dllimport/export works. They describe the behaviour of Microsoft Visual C++ (MSVC) and Clang's MSVC-compatibility mode, clang-cl. The attributes are also supported when targeting MinGW, but with slightly different behaviour.

Table of Contents

Building and Using a DLL

Let's build a dynamic-link library (DLL).

Here is add.c:

__declspec(dllexport) int add(int x, int y) {
  return x + y;
}

__declspec(dllexport) int nbr = 42;

We compile and link it into a DLL using Microsoft Visual C++ (MSVC) in a command prompt:

> cl /O2 /LD add.c /link /nodefaultlib /noentry

/O2 enables optimizations, /LD tells the compiler that we're building a DLL, /link /nodefaultlib /noentry instructs the linker not to link against any default libraries (such as the C runtime) and that there's no entry point (such as DllMain) to run when loading the DLL.

This creates add.dll (the name is based on the source file) and we can see our function and variable in the Export Directory:

> dumpbin /exports add.dll
...
  Section contains the following exports for add.dll
...
    ordinal hint RVA      name

          1    0 00001000 add
          2    1 00003000 nbr

(0x1000 and 0x3000 are the relative virtual addresses of the symbols: at the start of the .text and .data sections, respectively.)

The cl invocation above also created add.lib, which is the import library for our DLL. It's a little hard to see what's going on, but we can take a look like this:

> dumpbin /linkermember add.lib
...
      188 __IMPORT_DESCRIPTOR_add
      3A2 __NULL_IMPORT_DESCRIPTOR
      4D4 ⌂add_NULL_THUNK_DATA
      67C __imp__nbr
      61E __imp__add
      61E _add
...

When linking an import library, the Import Descriptor tells the linker to add the DLL to the executable's Import Directory Table, and the import library provides symbols for accessing exported functions and variables in the DLL (add and nbr). Use dumpbin /exports add.lib do just see the exported symbols.

(For details on the format of import libraries, see LLVM's COFFImportFile.cpp.)

Now, let's write a program that links against our DLL. Here is main.c:

#include <stdio.h>

extern __declspec(dllimport) int add(int x, int y);
extern __declspec(dllimport) int nbr;

int main(void) {
  printf("1 + 1 is %d\n", add(1, 1));
  printf("nbr is %d\n", nbr);
  return 0;
}

We compile it and inspect the disassembly:

> cl /O2 /c main.c
> dumpbin /disasm main.obj
...
_main:
  00000000: 6A 01              push        1
  00000002: 6A 01              push        1
  00000004: FF 15 00 00 00 00  call        dword ptr [__imp__add]
  0000000A: 50                 push        eax
  0000000B: 68 00 00 00 00     push        offset ??_C@_0N@GIANGGIM@1?5?$CL?51?5is?5?$CFd?6@
  00000010: E8 00 00 00 00     call        _printf
  00000015: A1 00 00 00 00     mov         eax,dword ptr [__imp__nbr]
  0000001A: FF 30              push        dword ptr [eax]
  0000001C: 68 00 00 00 00     push        offset ??_C@_0L@BEGPCDEC@nbr?5is?5?$CFd?6@
  00000021: E8 00 00 00 00     call        _printf
  00000026: 83 C4 18           add         esp,18h
  00000029: 33 C0              xor         eax,eax
  0000002B: C3                 ret
...

Note how add and nbr are accessed via symbols starting with __imp__. That's because they are declared dllimport. The __imp__ symbols will point to the imported symbols' addresses in the executable's Import Address Table (IAT).

The symbols are referenced indirectly: call dword ptr [__imp__add] loads add's address from __imp__add in the IAT and then calls it. mov eax,dword ptr [__imp__nbr] loads nbr's address from the IAT; push dword ptr [eax] then loads nbr's value and pushes it on the stack for the printf call.

(The ??_C@... symbols are the string constants passed as format arguments to printf.)

If we try to compile and link our program on its own, it will fail:

> cl /O2 main.c
...
main.obj : error LNK2019: unresolved external symbol __imp__add referenced in function _main
main.obj : error LNK2019: unresolved external symbol __imp__nbr referenced in function _main
main.exe : fatal error LNK1120: 2 unresolved externals

We need to link against the import library, which defines those symbols:

> cl /O2 main.c add.lib

We can see how the executable imports the symbols from our DLL:

> dumpbin /imports main.exe
...
  Section contains the following imports:

    add.dll
                415128 Import Address Table
                41B718 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                    1 nbr
                    0 add
...

And how the __imp__ symbol references in main have been resolved to addresses in the Import Address Table (IAT) at 0x415128: (add at offset 4 and nbr at offset 0)

> dumpbin /disasm main.exe
...
  00401010: 6A 01              push        1
  00401012: 6A 01              push        1
  00401014: FF 15 2C 51 41 00  call        dword ptr ds:[0041512Ch]
  0040101A: 50                 push        eax
  0040101B: 68 90 51 41 00     push        415190h
  00401020: E8 1B 00 00 00     call        00401040
  00401025: A1 28 51 41 00     mov         eax,dword ptr ds:[00415128h]
  0040102A: FF 30              push        dword ptr [eax]
  0040102C: 68 A0 51 41 00     push        4151A0h
  00401031: E8 0A 00 00 00     call        00401040
  00401036: 83 C4 18           add         esp,18h
  00401039: 33 C0              xor         eax,eax
  0040103B: C3                 ret
...

And the program works:

> main.exe
1 + 1 is 2
nbr is 42

Importing Without dllimport (thunks)

It's not strictly necessary to dllimport the add function in order to call it. In addition to __imp__add, the import library also defines _add (the underscore is part of the name mangling for C functions), a thunk which forwards the call to add in the DLL. So we could write our program as:

#include <stdio.h>

int add(int x, int y);

int main(void) {
  printf("1 + 1 is %d\n", add(1, 1));
  return 0;
}
> cl /O2 main.c add.lib

The code now looks like:

> dumpbin /disasm main.exe
...
  00401010: 6A 01              push        1
  00401012: 6A 01              push        1
  00401014: E8 43 00 00 00     call        0040105C
  00401019: 50                 push        eax
  0040101A: 68 80 51 41 00     push        415180h
  0040101F: E8 0C 00 00 00     call        00401030
  00401024: 83 C4 10           add         esp,10h
  00401027: 33 C0              xor         eax,eax
  00401029: C3                 ret
...
  0040105C: FF 25 28 51 41 00  jmp         dword ptr ds:[00415128h]
...

Our call to add becomes a direct call to the thunk at 0x40105C, which in turn loads the function address from the IAT and jumps to it. We have effectively imported it implicitly via the thunk.

The extra call in the thunk is a little less efficient, but the main downside is that if our program were to take the address of add, it would get the address of the thunk, not the real add function in the DLL.

Another downside is that these thunks only work for functions, not variables. There is no way for the import library to provide a nbr variable which would forward loads and stores to nbr in the DLL.

Other Ways of Exporting Symbols

Besides dllexport, there are other ways of specifying what symbols to export. For example, we can pass /export flags directly to the linker:

int add(int x, int y) {
  return x + y;
}

int nbr = 42;
> cl /O2 /LD add.c /link /nodefaultlib /noentry /export:add /export:nbr

(When using dllexport, the compiler puts /export flags in the object file's .drectve section, to be applied by the linker. The directives can be inspected with dumpbin /directives.)

Or we can use a .def (module-definition) file. Here is add.def:

LIBRARY add
EXPORTS
  add
  nbr
> cl /O2 /LD add.c /link /nodefaultlib /noentry /def:add.def

Exporting symbols "manually" like this can work for some libraries, but it gets less practical for C++ code due to name mangling or if we want to export all the members of a class etc.

There is no direct way of exporting all symbols from a DLL (like with shared objects on Linux), but it's possible to use approaches like CMake's WINDOWS_EXPORT_ALL_SYMBOLS which creates a .def file based on what symbols are found in the object files. Note that DLLs are limited to exporting 64k symbols, due to using 16-bit values for the Export Ordinal Table. Exporting all symbols from large projects can (and does) hit that limit.

API Annotation with Macros

When possible, it's best to annotate a library's API explicitly. Typically this is done with a macro that expands to dllexport when building the library and dllimport when using it. Such macros can also be used to set the ELF visibility on Unix systems. (See the GCC wiki and an example in Chromium.) For example:

add.h:

#ifndef ADD_H
#define ADD_H

#ifdef _WIN32
#ifdef ADD_IMPL
#define ADD_API __declspec(dllexport)
#else
#define ADD_API __declspec(dllimport)
#endif
#else
#define ADD_API __attribute__((visibility("default")))
#endif

ADD_API int add(int x, int y);
ADD_API int nbr;

#endif

add.c:

#include "add.h"

int add(int x, int y) {
  return x + y;
}

int nbr = 42;

main.c:

#include <stdio.h>
#include "add.h"

int main(void) {
  printf("1 + 1 is %d\n", add(1, 1));
  printf("nbr is %d\n", nbr);
  return 0;
}

Building and running:

> cl /O2 /LD /DADD_IMPL add.c /link /nodefaultlib /noentry
> cl /O2 main.c add.lib
> main
1 + 1 is 2
nbr is 42

Semantics of dllexport and dllimport

So far this seems straightforward: put dllexport or dllimport on a variable or function to export or import it. However, there are some rules. For example, exported or imported symbols need to have external linkage:

// error C2201: 'f': must have external linkage in order to be exported/imported
static __declspec(dllexport) int f() {}

And except for inline functions, dllimport declarations should not be definitions:

// error C2491: 'f': definition of dllimport function not allowed
__declspec(dllimport) void f() {}

// error C2491: 'x': definition of dllimport data not allowed
__declspec(dllimport) int x = 42;

As we dig into the details below, it gets more complicated.

Redeclarations

It's common to declare a function or variable multiple times, for example declaring it in a header file and defining it later in a different file. One doesn't need to repeat the declspec each time, but the linkage needs to stay consistent. (In Clang, most of this checking is done in checkDLLAttributeRedeclaration)

For example, this is fine:

__declspec(dllexport) void func();
void func() {} // Exported.

But this is not:

void func();
__declspec(dllexport) void func() {} // error C2375: 'func': redefinition; different linkage

For dllimport, dropping the declspec on a redeclaration results in a warning and the compiler not treating the symbol as dllimport:

__declspec(dllimport) void func();
void func(); // warning C4273: 'func': inconsistent dll linkage (not imported)

It gets weird if we provide a definition after dllimport; in this case the function gets exported!

__declspec(dllimport) void func();
void func() {} // warning C4273: 'func': inconsistent dll linkage (exported!)

Is this surprising behavior a historical accident, or is there a good motivation?

(clang-cl may not be able to change the function from imported to exported if it was already used before the definition.)

Inline Functions

Things get more complicated as we start considering C++ features. Let's start with inline functions:

inline __declspec(dllexport) void func() {} // Exported.

func will get exported by the DLL, But does it make sense to export an inline function? Consider the "consumer" side:

inline __declspec(dllimport) void func() {}

void use() {
  func(); // May be inlined.
}

The compiler can choose whether to call func in the DLL or inline the call into use directly. (MSVC will inline at /O2, call at /Od with the version I used.)

Note that if the call is inlined, we cannot change the behavior of func by just rebuilding the DLL; we now have a source dependency on the inline function, not just a binary dependency.

Also, consider:

// In library.h
void library_stuff();

inline __declspec(dllimport) void func() {
  library_stuff();
}

// In user.c
#include "library.h"
void use() {
  func(); // Not inlined.
}

Although func is an inline function, the compiler has to be careful here. The function body references a symbol which is not exported from the library. That means the function body cannot be inlined outside the library, as it would then reference an unexported function from another library. (In Clang, this check is performed by a DLLImportFunctionVisitor.)

(If the function were delcared __forceinline the compiler would inline it anyway, assuming that the author knows what they're asking for.)

Static Local Variables in Inline Functions

In the example below, var is a static local variable. That means it can only be directly accessed from within the function (local scope), but that the same instance is used in all invocations of the function (static storage duration). Repeated calls to func() will return 0, 1, 2, and so on.

inline __declspec(dllexport) int func() { // Exported.
  static int var = 0; // Exported.
  return var++;
}

To ensure that func() can be inlined outside the defining DLL and still reference the same static local, var needs to be exported too.

Furthermore, if a static local requires dynamic initialization (static int var = foo()), the compiler will (at least since C++11) create an implicit lock variable for var to ensure that only a single thread performs the initialization. For the same reason as above, that lock variable needs to be exported too.

(In Clang, see CheckStaticLocalForDllExport.)

Classes

Member functions and (static) member variables can be exported and imported just like regular functions and global variables:

struct S {
  __declspec(dllexport) void f();
  static __declspec(dllexport) int x;
};

But it's also possible to put dllexport or dllimport on the class itself to export or import all its members:

struct __declspec(dllexport) MyClass {
  void func();
  static void static_func();
  int var;
private:
  static int static_var;
};

In the example above, func(), static_func(), and static_var get exported. var, does not get exported since it's only defined when instances of the class are created.

Access specifiers (private, public, and protected) do not affect dllexport/import.

With clang-cl, /Zc:dllexportInlines- can be used to suppress dllexporting inline class member functions, reducing compile times significantly.

If a class has implicit non-trivial constructors or destructor, those get exported too. For example:

#include <string>
struct __declspec(dllexport) ClassWithString {
  std::string str;
};

Even though ClassWithString doesn't explicitly declare any constructor or destructor, because std::string has non-trivial constructors and destructor, ClassWithString will get implicitly generated structors, and they all get exported:

ClassWithString::ClassWithString()
ClassWithString::~ClassWithString()
ClassWithString::ClassWithString(const ClassWithString&)
ClassWithString::ClassWithString(const ClassWithString&&)
ClassWithString::operator=(const ClassWithString&)
ClassWithString::operator=(const ClassWithString&&)

The copy and assignment operators get exported even when they are trivial, in case an importing library takes their address which should then be unique throughout the whole program. (Taking the address of a constructor or destructor is not possible.)

Class level dllexport/imports do not affect inner classes. This can be a common pitfall:

struct __declspec(dllexport) Outer {
  void foo();
  struct Inner {
    void bar();
  };
};

In the example above, Outer::foo() is exported, but Outer::Inner::bar() is not.

The attributes are also not inherited from base to derived classes:

struct __declspec(dllexport) Base {
  void foo();
};
struct Derived : public Base {
  void bar();
};

(In Clang, see checkClassLevelDLLAttribute, and ReferenceDllExportedMembers.)

Templates

Templates themselves cannot be exported or imported, but template specializations—the functions and classes generated when instantiating templates—can.

It's possible to put the attributes directly on a template, but it's not a good idea. For example:

template <typename T> __declspec(dllimport) T add(T x, T y);
int f(int x, int y) { return add(x, y); }

Here, the implicitly instantiated add<int>(int, int) will be dllimport'ed. However, this will only work if the exporting side also made the same template instantiation.

Instead, it's usually better to either not try to import/export the template, or to manually instantiate the specializations that should be imported/exported. For example, on the exporting side we can but dllexport on an explicit instantiation definition:

template <typename T> T add(T x, T y) { return x + y; }
template __declspec(dllexport) int add<int>(int x, int y);

On the importing side we put dllimport on an implicit instantiation declaration:

template <typename T> T add(T x, T y) { return x + y; }
extern template __declspec(dllimport) int add<int>(int x, int y);

int f(int x, int y) { return add(x, y); }

Templates is where the implementation details get hairy. (In Clang, see Sema::ActOnExplicitInstantiation and dllExportImportClassTemplateSpecialization.) One particularly interesting case is exported classes which inherit from class templates:

template <typename T> struct Base {
  void foo() {}
};

struct __declspec(dllexport) Derived : public Base<int> {
  void bar();
};

Derived::bar is exported as expected, but so is Base<int>::foo, because the attribute gets reverse-inherited by the base class!

This is documented as a way to help developers ensure the full interface of Derived, including that inherited from the base class, gets exported. In Clang, this is tricky to do in case Base<int> was already instantiated without dllimport/export. (See Sema::propagateDLLAttrToBaseClassTemplate.)

(C++ Templates - The Complete Guide was an invaluable guide when working on this. There is now a .)

Further Reading

  • Vandevoorde & Josuttis C++ Templates: The Complete Guide was invaluable when working on the template aspects of this. There is now a second edition.
  • James McNellis's talk, Everything You Ever Wanted to Know about DLLs (video, slides) has a lot of good information.