The early history of redundant function pointer casts: MakeProcInstance


If you look through old code, you see a lot of redundant function pointer casts. (If you're writing new code, you should get rid of as many function pointer casts as possible, because a function pointer cast is a bug waiting to happen.) Why does old code have so many redundant function pointer casts?

Because back in the old days, they weren't redundant.

In the days of 16-bit Windows, function prologues were required to take very specific forms in order to make stack walking work, and stack walking was necessary in order to simulate an MMU on a CPU that didn't have one.

Another rule for prologues has to do with state management. The full prologue for a far function looks like this:

    mov     ax, ds
    nop
    inc     bp
    push    bp
    mov     bp, sp
    push    ds
    mov     ds, ax

Before we can dig into those instructions, we need to know a bit about how code segments worked in real-mode 16-bit Windows. In real-mode 16-bit Windows, there was a single address space for all applications because the CPU had no concept of per-process address spaces. The kernel simulated separate address spaces by managing instances. The instance (represented by an instance handle) specified the location of the data segment the code should operate on. If you have two copies of a program running, the code is shared, but each program has its own data. The instance handle tells you where that data is.

And the instance handle is kept in the ds register.

Therefore, it is essential that every function have its ds register set to the instance handle that describes where the code should find its data. You can think of it as a "global this pointer for the process."

Okay, so let's look at the function prologue again. First, it copies ds to ax via a two-byte mov ax, ds instruction. Then there is a nop. This pads the prologue size to three bytes.

The next four instructions build the stack frame: The inc bp marks the stack frame as a far frame. The push bp and mov bp, sp build the bp chain. And the push ds saves the original ds register, which also provides breathing room for return address patching.

And then we move ax back into ds. The instance handle just took a little tour of the ax register and then returned back home. What was the point of that?

Recall that in 16-bit Windows, every far function called from another segment was listed in the module's Entry Table.

When a far function is placed in the exported function table, the loader patches the first three bytes of the function to three nop instructions. Non-exported functions remain unchanged. This means that non-exported functions do the redundant ds rigamarole. It's a little extra work, but it's ultimately harmless.

The effect of patching out the initial mov ax, ds is that the function ends up doing this:

  • Build a far stack frame, which includes saving the original ds.

  • Set ds to whatever was passed in the ax register.

The second step means that the code, when it executes, operates on the data associated with the handle passed in the ax register.

Okay, great, but this means that you can't call an exported function directly, because it will set the ds register to whatever value is passed in the ax register. Since the ax register is not part of the calling convention, its value is garbage.

But that's okay. We made things worse so we can make them better.

The Make­Proc­Instance function creates a stub function that loads the ax register with the instance handle you provide, and then jumps to the function you provide. Really. That's all it did. (When you're done, you call Free­Proc­Instance to free the memory back to the system.)

This stub function was known as a procedure instance thunk, or a proc instance for short. Hence the name Make­Proc­Instance.

Okay, finally the punch line. The Make­Proc­Instance function didn't care what kind of function pointer you passed it. Whatever you passed in, it returned the same kind of pointer back out, because all the stub did was twiddle the ax register and then jump to the real function. The parameters on the stack didn't change, the cleanup convention didn't change, nothing else changed.

The Make­Proc­Instance function was declared as returning a FARPROC, which is a typedef for a far function that takes no parameters and returns nothing. The parameters and return value are irrelevant; it just had to be something.

But what this means is that when you take your function, like a window enumeration callback, and create a procedure instance for it, the thing you get back has been type-erased to a generic function pointer. To make it useful again, you need to cast it back to what it was originally.

For example, if what you passed was a WNDENUMPROC, then you need to cast the procedure instance back to a WNDENUMPROC. If you passed a TIMERPROC, then you need to cast the procedure instance back to a TIMERPROC. You could anachronistically express this as

template<typename R, typename ...Args>
auto MakeProcInstanceT(R (FAR *func)(Args...), HINSTANCE inst)
{
  return (decltype(func))MakeProcInstance((FARPROC)func, inst);
}

Of course, you didn't have this fancy template deduction in 1983-era C, so you had to cast the return value manually.

And that brings us to today. Even though Make­Proc­Instance has been obsolete for decades, some people imprinted on the "gotta cast your function pointers to get them to compile" pattern, either because they wrote code when the cast was required and fell into the habit, or or (more likely) they learned from code that was written by someone who inherited this habit from somebody else. And yes, this inherited folk wisdom can even be found in MSDN.

The redundant function pointer cast is now a type of folklore, passed down from developer to developer, even though it's no longer needed and in fact will mask problems caused by mismatched prototypes.

Comments (10)
  1. skSdnW says:

    The cast should have been a part of the function signature.

    Quick, to the time machine so we can have:

    #if _WIN32
    #ifdef __cplusplus
    template<class T> inline T MakeProcInstanceHelper(T func, HINSTANCE inst) { return func; }
    #define MakeProcInstance(functype,func,inst) MakeProcInstanceHelper((func),(inst))
    #else
    #define MakeProcInstance(functype,func,inst) ((inst),(func))
    #endif
    #else
    #define MakeProcInstance(functype,func,inst) ( (functype) RealMakeProcInstance((func,(inst)) )
    #endif

    (The c++ version will verify that the cast is correct so porting back to 16-bit would be painless for codebases supporting both)

    1. Joshua says:

      Try this way:

      $ git clone timestream
      $ git branch makeprocinstnace3 `git log –after “1981-12-07” –pretty oneline | head -n 1 | sed ‘s/ .*//’`
      $ git patch < makeprocinstance3.patch
      $ git commit -m "safe MkaeProcInstance casts"
      $ git checkout master
      $ tar -cf – .git | ssh scp155 tar -xf – \&\& git merge -r resove-propigate-changes \&\& tar -cf – .git | tar -xf –
      $ git push timestream

    2. skSdnW says:

      Well, that came out almost readable. It is supposed to be MakeProcInstanceHelper<functype>((func),(inst)) of course.

      Raymond, are we supposed to use code and/or pre tags for code here? I can never remember.

    3. Antonio Rodríguez says:

      When, oh when, will that time machine be ready? Not only we could avoid 11-S, the Haiti earthquake and the Japan tsunami. Above all, we could fix all those Win16 wrinkles that are carried onto Win32 and Win64…

      1. ZLB says:

        It really doesn’t matter when the time machine will be ready…ask me again last week.

        Any parameter will fit any function if you hit it hard enough with the cast-hammer!

    4. Neil says:

      Why bother when the time machine can prevent MakeProcInstance from being created?

      1. Joshua says:

        DS=SS does not appear to be required for Win16 executables; it was just required for Win16 C executables. So MakeProcInstance is merely rare not non-extant.

        1. Neil says:

          I’m not convinced that that’s a scenario in which MakeProcInstance would have been useful.

  2. Darran Rowe says:

    This post was really interesting, I have always wondered where these function casts came from and why they persisted so much.

  3. Yuhong Bao says:

    I think older K&R C would let you call any FARPROC with any arguments without any casts.

Comments are closed.

Skip to main content