Date: | April 23, 2018 / year-entry #94 |
Tags: | history |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20180423-00/?p=98575 |
Comments: | 10 |
Summary: | The generic function pointer. |
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
Therefore, it is essential that every function have its
Okay, so let's look at the function prologue again.
First, it copies
The next four instructions build the stack frame:
The
And then we move 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
The effect of patching out the initial
The second step means that the code, when it executes,
operates on the data associated with the handle passed in the
Okay, great, but this means that you can't call an exported function
directly,
because it will set the But that's okay. We made things worse so we can make them better.
The
This stub function was known as
a procedure instance thunk,
or a proc instance for short.
Hence the name
Okay, finally the punch line.
The
The 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 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
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)
Comments are closed. |
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)
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
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.
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…
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!
Why bother when the time machine can prevent MakeProcInstance from being created?
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.
I’m not convinced that that’s a scenario in which MakeProcInstance would have been useful.
This post was really interesting, I have always wondered where these function casts came from and why they persisted so much.
I think older K&R C would let you call any FARPROC with any arguments without any casts.