How do I forward an exported function to an ordinal in another DLL?


The syntax for specifying that requests to import a function from your DLL should be forwarded to another DLL is

; A.DEF
EXPORTS
 Dial = B.Call

This says that if somebody tries to call Dial() from A.DLL, they are really calling Call() in B.DLL. This forwarding is done in the loader. Normally, when a client links to the function A!Dial, the loader says, "Okay, let me get the address of the Dial function in A.DLL and store it into the __imp__Dial variable." It's the logical equivalent of

client::__imp__Dial = GetProcAddress(hinstA, "Dial");

When you use a forwarder, the loader sees the forwarder entry and says, "Whoa, I'm not actually supposed to get the function from A.DLL at all! I'm supposed to get the function Call from B.DLL!" So it loads B.DLL and gets the function Call from it.

hinstB = LoadLibrary("B.DLL");
client::__imp__Dial = GetProcAddress(B, "Call");

(Of course, the loader doesn't actually do it this way, but this is a good way of thinking about it.)

But what if the function Call was exported by ordinal? How do you tell the linker, "Please create a forwarder entry for Dial that forwards to function 42 in B.DLL?"

I didn't know, but I was able to guess.

Back in the days of 16-bit Windows, there were two ways to obtain the address of a function exported by ordinal. The first way is the way most people are familiar with:

FARPROC fp = GetProcAddress(hinst, MAKEINTRESOURCE(42));

The second way uses an alternate formulation, passing the desired ordinal as a string prefixed with the number-sign:

FARPROC fp = GetProcAddress(hinst, "#42");

You can hide a number inside a string by using MAKEINTRESOURCE, and you can hide a string inside a number by using the '#' character.

Given that the number sign has been used in the past to hide a number inside a string, I figured it was worth a shot to see if the loader carried this convention forward. (No pun intended.)

; A.DEF
EXPORTS
 Dial = B.#1

Hey, check it out. It works.

Sometimes a little knowledge of history actually helps you solve problems in the present day.

Comments (16)
  1. Henke37 says:

    Of course, just because you can doesn't mean that you should.

  2. parkrrrr says:

    Isn't "Hey, check it out; it works" the rallying cry of every programmer who ever made it necessary for you to make compromises for the sake of compatibility?

  3. Random832 says:

    I was going to check whether this syntax is documented or not, to reply to parkrrrr, but I couldn't find where the regular forwarding syntax was documented.

  4. David Walker says:

    I never thought the loader would be saying "Whoa!" but it's an amusing thought.  Anthropomorphizing is fun!

  5. Joshua says:

    @parkrrr, He's depending on compiler internals rather than Windows internals. Having read the PE spec, I can see this will continue to work on newer versions of Windows. I have no idea if this will work on newer versions of compilers and I'd wager Raymond doesn't either. (You'll notice on recompile if this gets broken in a newer compiler version.)

  6. @Parkrrrr: That's true when someone uses an undocumented/abusive method to achieve something, but in this case the "#x" method is documented (at least it was in Win16, and Windows is known for its back-compat). Doing some digging, I did find a VS6 doc page that uses this format for specifying ordinals: msdn.microsoft.com/…/aa264882%28v=vs.60%29.aspx

  7. parkrrrr says:

    Whether this specific behavior is documented or not is beside the point, really.

    If you discovered something works (on at least one computer, in at least one test case, for at least one sheep in Scotland who is black on at least one side) by writing the code and seeing what it did, two things are possible. Either you've discovered undocumented behavior, or you've discovered documented behavior but don't know that you have. Short of looking for the documentation, you have no way to know whether you're in the "bad" or "okay" case. So you should check the documentation to make sure. But since that's the case, why not just check the documentation first?

    [It's much easier to find an answer once you know what the answer is. (In other words: Now that you know the answer, you have a better chance of finding it in the documentation since you know what search keywords are likely to work.) -Raymond]
  8. John says:

    Failure to find documentation does not mean documentation does not exist.  You can prove that something is documented.  You cannot prove that something is not documented (unless explicitly documented as being undocumented).  Of course if you A) assume everything is undocumented by default and B) do not rely on undocumented behavior then you will be safe.

  9. me2 says:

    "Windows is known for its back-compat" Try this FAIL

    IMPORTS

    Dial = B.#1

  10. [QUOTE]You can hide a number inside a string by using MAKEINTRESOURCE, and you can hide a string inside a number by using the '#' character.

    Given that the number sign has been used in the past to hide a number inside a string[/QUOTE]

    I'm confused. Are the "string in number" and "number in string" reversed in the first sentence?

  11. Joshua says:

    @me2: I'm pretty sure there's a more appropriate syntax for import by ordinal.

  12. Neil says:

    If it was so hard for Raymond to find out the appropriate documentation, I wonder how hard it is going to be for me to find out what GetProcAddress does. (It amuses me to think that it could return ERROR_FILE_NOT_FOUND for instance.)

  13. Joshua says:

    @Neil: You did GetProcAddress on a forwarded function and the DLL it was forwarded to was not found.

  14. Cheong says:

    @Erik: FYI, DllImport in latest version of Visual Studio uses it as well. Seems fundamental blocks like this won't change a lot.

    msdn.microsoft.com/…/f5xe74x8(v=vs.110).aspx

  15. 640k says:

    [It's much easier to find an answer once you know what the answer is. (In other words: Now that you know the answer, you have a better chance of finding it in the documentation since you know what search keywords are likely to work.) -Raymond]

    This is an indication of that the documentations is almost worthless. Although most blame has to be assigned to the search engine used.

  16. Cheong says:

    @640k: Indeed.

    In a recent question asked in MS forums, someone called Control.Invoke() and meet "Invoke or BeginInvoke cannot be called on a control until the window handle has been created." exception, and doesn't know what to do.

    In fact, there's a line in Remarks section of Control.InvokeRequired property tell you that you should also check for Control.IsHandleCreated if it'll be run at a place you're not sure whether target window is created.

    That line does not exist on the Control.Invoke() page itself.

Comments are closed.