How were DLL functions exported in 16-bit Windows?


The whole point of dynamic link libraries (DLLs) is that the linkage is dynamic. Whereas statically-linked libraries are built into the final product, a module that uses a dynamically-linked library merely says, "I would like function X from Y.DLL, please." This technique has advantages and disadvantages. One advantage is more efficient use of storage, since there is only one copy of Y.DLL in memory rather than a separate copy bound into each module. Another advantage is that an update to Y.DLL can be made without having to re-compile all the programs that used it. On the other hand, the ability to swap in functionality automatically is also one of the main disadvantages of dynamic link libraries, because one program can change a DLL that has cascade effects on other clients of that DLL.

Anyway, let's start with how 16-bit Windows managed imports and exports. After that, we'll see how things changed during the switch to 32-bit Windows, and then we'll take a look at the compiler-specific dllimport declaration specifier. (I already discussed dllexport earlier.)

A 16-bit DLL has not one but three export tables. (Things are actually more complicated than I describe them here, but I'm going to skip over the nitpicky details just to keep everyone's heads from exploding.) The most important table is a sparse array of functions, indexed by a 1-based integer (the "ordinal"). It is this function table that is the master list of all exported functions. If you request a function by ordinal, the ordinal is looked up in this table. The table is physically rather complicated due to the sparseness, but logically, it looks like this:

Ordinal  Address other goo
1 02:0014 ...
2 04:0000 ...
5 02:02C8 ...

The first column in the table is the ordinal of the function, and the second function describes where the function can be found. (Notice that there is no function 3 or 4 in this DLL.)

Things get interesting when you want to export a function by name. The exported names table is a list of function names with their associated ordinal equivalents. For example, a section of the exported names table for the 16-bit window manager (USER) went like this:

...
ClipCursor 16
GetCursorPos 17
SetCapture 18
...

If somebody asks for the address of the function ClipCursor, the exported names table is consulted, the value 16 is retrieved, and the function at position 16 in the ordinal export table is returned. Although you can't see it here, there was no requirement that the names in the exported names table be in any particular order, or that every ordinal have a corresponding name.

Wait, did I say the exported names table? I'm sorry, that was an oversimplification. There are actually two exported names tables, the resident names table and the non-resident names table. As their names suggest, the names in the resident names table remain in memory as long as the DLL is loaded, whereas the names in the non-resident names table are loaded into memory only when somebody calls GetProcAddress (or one of its moral equivalents). This distinction is a reflection of the extremely tight memory constraints that Windows had to run within back in those days. For example, the window manager (USER) has over six hundred export functions; if all the exported names were kept resident, that would be over ten kilobytes of data. You'd be wasting four percent of the memory of your 256KB machine remembering things you don't need most of the time.

The large size of the table for exported function names meant that only functions that are passed to GetProcAddress with high frequency deserve to be placed in the resident names table. For most DLLs, no function falls into this category, and the resident names table is empty. (Head-exploding details deleted for sanity's sake.)

Since obtaining a function by name is so expensive (requiring the non-resident names table to be loaded from disk so it can be searched), all functions exported by operating system DLLs are exported both by name and by ordinal, with the ordinal taking precedence in the import library table. Obtaining a procedure address by ordinal avoids the name tables entirely.

Notice that every named function has a corresponding ordinal. If you do not assign an ordinal to your named function in your module definition file, the linker will make one up for you. (However, the value that it makes up need not be the same from build to build.) This situation did not occur in practice, for as we noted above, everybody explicitly assigned an ordinal to their exports and put that ordinal in the import library in order to avoid the huge cost of a name-based function lookup.

That's a quick look at how functions were exported in 16-bit Windows. Next time, we'll look at how they are imported.

Comments (20)
  1. Mike says:

    Oh man, 16-bit. This awakes some memories. Like that time I was woken by the phone in an ungodly hour by the FoxPro Norwegian team, responding to my (quite serious) complain that their import .lib exported the WEP!

    For the ones lucky enough to never having explored these deep and dark corners of Windows 16-bit, WEP was the “Windows Exit Procedure” (IIRC – Raymond will correct me if I’m wrong). It was a very special named function exported by DLLs to be called when all references to the module had been released, i.e. no programs used the DLL anymore. This was required as all applications and the system itself shared a single address space resource, and the DLL had to have a way to clean up what resources it allocated before it was unloaded.

    The thing was, while you usually did write a WEP for moderately and more complex DLLs, you never EVER exported it like these morons did. You put it in the exported names table in the DLL, but you NEVER put it in the import library for that DLL. Doing so would make it effectively impossible for anyone else writing a DLL to use your DLL.

    Well, guess what. That was a scenario the FoxPro developers never considered (one could rightfully, and in the most polite way, say “they didn’t really and fully know what they were doing”), and so I was left with a DLL I couldn’t link, and a phone call at an hour I didn’t appreciate.

    What really made me see the quality of this was when I presented, again, the error for this person over the phone, and the response was “OK, we export the WEP from the import lib? So what?”.

    [Ah, the WEP… The modern analog to this mistake would be putting your DllMain into your import library. Now everybody who tries to link to your DLL gets a “duplicate definition conflict” between their DllMain and yours. -Raymond]
  2. Adam says:

    Just out of curiosity, what sort of function /would/ GetProcAddress() be called for with high frequency?

    And do you have any idea what made it impractical/impossible for callers to remember the returned pointer(s) themselves? If there aren’t that many of those functions, and it’s only 4 bytes per pointer (GetProcAddress returns a FAR pointer?), then that wouldn’t have eaten too much mem at the time, would it?

    [I can’t think of a function designed to be GetProcAddress’d with high frequency; I left that as a hedge in case one existed. -Raymond]
  3. Mike says:

    For 16-bit, I also can’t think of one. For 32-bit though (even that it works differently), I’d say GetDisk(Free?)SpaceEx is at the top of the list. Oh the shortsightedness of what is/was known as Win32c (the "Chicago", aka Win95, version of the Win32 API).

    Adam:

    There is no reason a single process wouldn’t hold on to the pointer, once it got it. But remember, 16-bit. All the applications in the system shared a single address space. The DLL was loaded once, at an address that then became unavailable for all other use in all other processes. So the issue wasn’t at all a single process not being able to keep track of what it got – it was the whole system using that single and very small adddress space and needing to share it between all applications (we’re talking pre-386 here).

  4. Gabe says:

    A base-1 integer? What’s that? 0 for 0, 00 for 1, 000 for 2, etc?

    Wait, you said "1-based integer"? Never mind. :)

    BTW Mike, was that back when FoxPro was an MS app, or was that before MS bought them?

  5. BryanK says:

    As far as a function that gets GetProcAddress()ed frequently in 16-bit, I’d say that DefWindowProc might be a good candidate.  Programs won’t call GetProcAddress() on it directly, but they will import it, so the system needs to resolve the link every time any program starts.

    However, it may be that the programs linked to DefWindowProc by ordinal number, in which case it’s not one of the frequent functions.  Hmm…

  6. Eric Brown says:

    Mike:

    I don’t believe that any version of Windows was written in Pascal.  The reason that Pascal calling conventions were used was, IIRC, the Pascal calling convention (callee cleans up after the call) was a few bytes smaller (per call site), and a few clock ticks faster than C calling conventions (caller cleans up after the call).

    When you’re trying to shoehorn a system into 640K, every few bytes help.

    Of course, C calling conventions allow for varargs.

  7. Mike says:

    On the subject of 1-based, so I don’t die wondering: Raymond, was Windows at one time written in/for PASCAL (specifically the API or API-handling code)? I’m thinking of the many clues in the form of calling convention for system functions, the one-based indices and other implementation details that seems to have leaked through. If not, do you know (or can you find out) why many of these design decisions were made at the time? I’d hate to die not knowing this piece of history.

    Gabe:

    IIRC it was around 1991, so I’m quite confident MS was the owner of FoxPro then. Another clue could be; top score on getting back to me on the issue quickly, but a rock bottom for giving me a "Huh, what you say goes *whoosh* over our head". I see it as just another proof of what Raymond recently wrote in another entry – many are the groups that ship Windows software that break (the) rules of Windows programming, and not all within MS writing software are Windows API gurus.

    Still, not exporting the WEP into the import library might perhaps not have been considered a "guru" issue. ;-)

    (I btw think MS never fixed this. IIRC I had to fix it manually by introducing, for no other reason than to fix this FoxPro bug, yet another intermediate DLL that had no data whatsoever and only forwarded my calls from my real DLL to the FoxPro DLL).

  8. Ken Hagan says:

    The WEP problem is easily fixed. You simply load the import library
    into a binary editor, find the WEP text and replace it with (say) XXX.

    One should never treat third party tools with more respect than
    your own code. (There was a time when our company had MSVC6 checked
    into SourceSafe to make it easy to deploy all the necessary patches.)

    [And then a new version of the import library is released, you update your copy, and then your program doesn’t work. After two weeks of fighting, you realize, “Oh right, I have to patch the import library first.” That’s two weeks of your life you can’t get back. -Raymond]
  9. Tony says:

    Hello Raymond,

    sorry to be off-topic to this post, but could you look into this
    and correct this on your tooltip post? I had the same problem (missing
    black border most of the time, but with XP) and the suggestion by
    Martin Filteau made it go away.

    Original posting tooltip: http://blogs.msdn.com/oldnewthing/archive/2006/06/26/647365.aspx

    Martin Filteau’s suggestion: http://blogs.msdn.com/oldnewthing/archive/2006/06/27/648493.aspx#649674

    Thanks,

    Tony

    [I’m not sure that removing WS_EX_TRANSPARENT is the correct answer (since it messes up hit-testing) but I haven’t had the interest in finding out. People can find Martin’s comment and decide whether or not they want to follow it. -Raymond]
  10. Gabe says:

    Sorry Mike, but MS didn’t buy FoxPro until the middle 1992, and the Windows version that was released a few months later was already mostly complete by the time MS moved in. It would almost certainly have been a Fox Software engineer you were talking to.

  11. Mike says:

    Gabe, right, my bad. I’m fairly sure I was using MSVC1.x for that project, and it seems it was released early 1993 (?), why I’d guesstimate sometime after Q1-Q2 1993.

  12. Neil says:

    For most DLLs, no function falls into this category, and the resident names table is empty.

    WEP was in fact one of those names that had to be in the resident names table (and the code segment had to be preload nondiscardable for the same reason).

    >I can’t think of a function designed to be GetProcAddress’d with high frequency
    Although I never used PenWindows, I’d say RegisterPenApp would have been a candidate for the resident names table – even taskman.exe calls GetProcAddress(GetSystemMetrics(SM_PENWINDOWS), “RegisterPenApp”) and it’s only 3K.

    [Once per process isn’t high frequency enough to be worth being made a resident name. It would have to be something that is GetProcAddress’d at least several times per minute to be worth keeping resident. -Raymond]
  13. A threaded linked list.

  14. glonq says:

    256KB machine?  Did some version of Windows (version 1?) actually work with that?

  15. A table of function pointers.

  16. Ken Hagan says:

    [And then a new version of the import library is released, you update your copy, and then your program doesn’t work. After two weeks of fighting, you realize, “Oh right, I have to patch the import library first.” That’s two weeks of your life you can’t get back. -Raymond]

    Unlikely in this case, since the error is 100% certain to recur the very first time you link after updating the 3rd party product, and the error message will say something like “You’ve got two WEPs – one in your code and one in this library over here.”. I think two minutes is a more reasonable estimate of the loss of life in this case.

    [This assumes that the person who knows how to fix the problem (1) still works on the team, and (2) remembers what this error message means. Imagine: You’re a new hire, you’re asked to take advantage of some new functionality, so you update the import library and—uh oh—you get this “WEP multiply defined” error. You ask your colleagues and none of them knows what this means. Prepare to lose two weeks of your life. -Raymond]
  17. Preserving the spirit while accommodating separate address spaces and new processors.

  18. In case you’ve missed it and are interested, Raymond Chen has started a series on how DLL imports/exports…

  19. I found this list of article on Raymond's blog . Raymond's blog is one of the more interesting

Comments are closed.

Skip to main content