Now that we've learned what the dllimport
declaration specifier does, what if you get it wrong?
If you forget to declare a function as dllimport
, then you're basically making the compiler act like a naive compiler that doesn't understand dllimport
. When the linker goes to resolve the external reference for the function, it will use the stub from the import library, and everything will work as before. You do miss out on the optimization that dllimport
enables, but the code will still run. You're just running in naive mode.
(There are still some header files in the Platform SDK that neglect to use the dllimport
declaration specifier. As a result, anybody who uses those header files to import functions from the corresponding DLL will be operating in "naive mode". Hopefully the people responsible for those header files will recognize themselves in this parenthetical and fix the problem for a future release of the Platform SDK.)
Now, what about the reverse problem? What if you declare a function as dllimport
when it really isn't? The linker detects this since it sees an attempt to import a __imp__FunctionName
symbol and can't find one, though it can find the normal FunctionName
symbol. When this happens, the linker raises warning LNK4217. It recovers from this error by simply manufacturing a fake __imp__FunctionName
variable and initializing it with the address of the FunctionName
function. In effect, you've imported the function from yourself. Your code now goes through all the gyrations associated with calling an imported function unnecessarily; it could have just called FunctionName
directly.
(There are cases where the linker can be a little smarter. For example, if it sees a call [__imp__FunctionName]
, it can change it to call FunctionName + nop
. The nop
is necessary because the call [__imp__FunctionName]
instruction is six bytes long, whereas call FunctionName
is only five. The extra nop
gets everything back in sync.)
Thus, in both cases where you mess up the dllimport
declaration specifier, the linker manages to recover from your mistake, and your program does run fine, though the patching up did cost you in code size and efficiency.
(All this discussion is for x86, by the way. Other architectures have different quirks.)
Next time, more on import libraries, and exposing some "little white lies" I've been telling.
This seems like a problem for the CRT. As far as I know, VC gives you the option of statically or dynamically linking the CRT. But it seems like the headers will have to make a choice to support one thing better than the other. Conditional compilation would work, but then people would have to remember to include a #define somewhere. Is this dllimport vs. static linking thing something the compiler could figure out on its own if you’re doing Link-time codegen?
“All this discussion is for x86, by the way. Other architectures have different quirks.”
Huh? Surely you mean this is for PE/x86. Other formats (e.g. ELF/x86) will naturally have a different shared library ABI (with related quirks). But given that you’re talking about PE/x86, what other architectures does PE run on? (Aside from PE/x86-64, which will be mostly running 32-bit userspace apps anyway)
Adam: IA-64? PowerPC (XBox)?
My guess would be PE/PPC, on the XBOX 360.
Don’t forget the DEC Alpha, and every other processor that NT used to support. Each one has there own quirks.
…and don’t forget the bunch of CPU’s that WinCE runs on.
nksingh:
You bet this is an issue for the CRT. The CRT headers use a lot of conditional compilation. Look at the file "crtdefs.h". This defines "_CRTIMP" as either "__declspec(dllimport)" or nothing, depending on whether or not you’re using the DLL version of the CRT. Every CRT export has "_CRTIMP" (or some variation) in front of it. Luckily, most people don’t have to remember how this works, so most people don’t have to worry about forgetting to put the _CRTIMP prefix on the functions.
You do have to do something so the compiler knows what you want, though. The VC 8.0 CRT uses the _DLL symbol to decide whether or not to import the functions. You can get this symbol defined (or not) based on a #define in a common header (probably not the best way), by passing -D_DLL or some other symbol as a command-line argument, or by using one of the -M?? switches (which is like passing -D_DLL plus some other stuff).
Right – the context *is* well-established now.
*That’s what confused me!*
I’m trying to figure out why the clarification was necessary, or what other information it gave us than that which was already implied by the *last couple of weeks* posts (which have been excellent). I couldn’t figure out was the reason for the clarification was, so assumed I must have been missing something. (Happens a lot :)
Sorry to come across as one of the Nitpick Police.
/me tiptoes away, trying to not draw any more attention…
Raymond, you mentioned in a previous entry in this series “Next time, we’ll look at the dllexport declaration specifier…”. I have yet to find a mention of it. Did you decide to save it for later, did it somehow got lost, or am I just blind?
(Me, I think dllexport simply tags the name for publication, much like a .def file would do, but without the power of the .def file to rename exported symbols and so on)
On the subject of __imp prepended function names – is that the only thing __dllimport does – renaming the entry in the import-lib (actually, more like providing the mapping __imp_X -> X) to be externally visible as __imp__sym for the symbol sym in the DLL the import library is generated for?
How appropriate that I wrote "__dllimport" when I intended __dllexport to generate the names. :-)
Still, does __dllexport do anything else but create the name __imp__X for X in the import library, even if subtle things like "slightly" modify optimizations or anything else? Even if not, how exactly does that work (on a COFF level if you don’t mind)? There must be a "mapping" stored in the COFF object file for the linker to pick it up, no? Is it simply that addr X get referred by the name real_fn as tatic, and __imp__real_fn as public?
Does anyone have any idea of how much the __dllimport optimisation is worth these days? I mean how much faster is the optimised case than the "naive mode" case on modern CPUs?
Is the optimized call always slower than a local call? File size increases one extra byte, why?