Windows 3.0 expanded the CreateÂWindow
function by adding a new extended style parameter, resulting in the CreateÂWindowÂEx
function. You would expect that the new parameter would go at the end of the parameter list, but that’s not where it ended up. Instead, it became a bonus first parameter.
CreateWindow( lpszClass, lpszName, dwStyle, x, y, cx, cy, hwndParent, hMenu, hInstance, lpCreateParams ); |
CreateWindowEx( dwExStyle, lpszClass, lpszName, dwStyle, x, y, cx, cy, hwndParent, hMenu, hInstance, lpCreateParams ); |
Why did the extra parameter go at the start rather than at the end?
If you’re familiar with the __stdcall
calling convention, you would know that the parameters are pushed onto the stack from right to left. Adding a new parameter to the front would therefore permit the old function to forward to the new function by inserting an extra parameter on the stack:
CreateWindow: pop eax ; pop return address push 0 ; dwExStyle push eax ; restore return address jmp CreateWindowEx ; continue as if CreateWindowEx
However, this theory doesn’t hold up because Windows 3.0 used the 16-bit Pascal calling convention, which pushes parameters from left to right.
But you’re close. The calling convention does play a role.
The other half of the puzzle is the in the lParam
of the WM_
and NCCREATE
WM_
messages. That parameter is a pointer to a CREATE
CREATESTRUCT
structure, which originally looked like this:
struct tagCREATESTRUCT { LPVOID lpCreateParams; HINSTANCE hInstance; HMENU hMenu; HWND hwndParent; int cy; int cx; int y; int x; LONG style; LPCSTR lpszName; LPCSTR lpszClass; } CREATESTRUCT;
Look familiar?
It’s the parameter list of the CreateÂWindow
function, but backward.
Why is it backward?
Since the Pascal calling convention pushes parameters from left to right, it means that the first parameter has the highest address, and the last parameter has the lowest address. If you take all the parameters and treat them as a structure, they end up in reverse order.
And that’s the missing link.
Back in the days of 16-bit Windows, the CREATESTRUCT
that was passed to the WM_
and NCCREATE
WM_
messages was just a pointer to the “structure” on the stack formed by all of the parameters.CREATE
For backward compatibility, the new dwExStyle
structure member needs to go to the end, so that old code which understood the old structure would have all the old data at the old offsets.
struct tagCREATESTRUCT { LPVOID lpCreateParams; HINSTANCE hInstance; HMENU hMenu; HWND hwndParent; int cy; int cx; int y; int x; LONG style; LPCSTR lpszName; LPCSTR lpszClass; DWORD dwExStyle; // Now with extended style support! } CREATESTRUCT;
Backward compatibility dictates that the new structure member goes at the end, which means that the corresponding new parameter must go at the beginning.
It’s another example of the lengths that 16-bit Windows went in order to run in a very memory-constrained system.
Bonus chatter: This means that converting a classic CreateÂWindow
to the new CreateÂWindowÂEx
is not a simply matter of inserting a new parameter under the return address. The return address plus all of the existing parameters need to be popped off, the new parameter inserted, and then all the parameters and return address pushed back on. Alternatively, the code could simply have pushed another frame onto the stack:
HWND CreateWindow( LPCSTR lpszClass, LPCSTR lpszName, DWORD dwStyle, int x, int y, int cx, int cy, HWND hwndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpCreateParams) { return CreateWindowEx( 0, lpszClass, lpszName, dwStyle, x, y, cx, cy, hwndParent, hMenu, hInstance, lpCreateParams); }
The code chose to do the “pop, insert, push” because the C wrapper function to push a new frame was 59 bytes long, whereas the pop/insert/push mechanism, written in hand-tuned assembly, was faster and consumed only 32 bytes.
I have a sneaky suspicion that this little trick must have caused some cursing for the NT folks when they found it. It probably meant they had to re-write it the way the ABI describes it and not the fun little way the prior dev did. That said… even in the early NT days bytes cost serious money.
I always wondered about that and thought it must have something to do with the calling convention and compatibility. Thanks for clearing it up.
It also shows that the design of this function call is not optimal. The team did a better job on the RegisterWindow/RegisterWindowEx functions, where you only pass a pointer to a struct instead of a dozen or more parameters. But the structs (WNDCLASS/WNDCLASSEX) are still inconsistent because the latter has an additional...
Agree. My first thought was also “why not a struct?”