STANDARDIZING Win32 CALLBACK PROCEDURES by Jeremy Gordon JG at JGnet.co.uk http://www.GoDevTool.com This short article describes my preferred method for coding CALLBACK procedures in a large assembler program for Windows 32. First I describe what Win32 callback procedures are, and then get down to some code. At run time the Win32 system will call your program on a regular and frequent basis. The procedures you supply for the system to call are called CALLBACK procedures. Here are examples of when these are used:- 1. To manage a window you created. In this case the system will send many messages to the Window Procedure for the window. The Window Procedure is the code label you provide when you register your window class (by calling RegisterClass). For example the message WM_SIZE is sent by the system when the window is resized. 2. To inform the owner of a child window of events in the child window. For example WM_PARENTNOTIFY (with a notify code) is sent to the Window Procedure of the owner of a window when the child window is being created or destroyed, or if the user clicks a mouse button while the cursor is over the child window. 3. To inform the owner of a common control of events in the control. For example if you create a button owned by your window the Window Procedure for that window receives BN_CLICKED messages if the button is clicked. 4. Messages sent to a dialog you have created. These are messages relating to the creation of the dialog and of the various controls. The dialog procedure is informed of events in the controls. 5. If you "Superclass" or "Subclass" a common control, you receive messages for that common control like a hook procedure but your window procedure has the responsibility of passing them on to the control. 6. If you create "Hook" procedures you can intercept messages about to be sent to other windows. The system will call your hook procedure and will pass the message on only when your hook procedure returns. 7. You can ask the system to provide your program with information to be sent to a CALLBACK procedure. Examples are EnumWindows (enumerate all top-level windows) or EnumFonts (enumerate all available fonts). In cases 1 to 5 above, just before the system calls the CALLBACK procedure, it PUSHES 4 dwords on the stack (ie. 4 "parameters"). Traditionally the names given to these parameters are:- hWnd = handle of window being called uMsg = message number wParam = a parameter sent with the message lParam = another parameter sent with the message. The number of parameters sent to hook procedures and emumeration callbacks varies - see the Window SDK. Since your Window (or Dialog) procedure will need to react in a certain way depending on the message being sent, your code will need to divert execution to the correct place for a particular message. "C" programmers have the advantage of being able to code this simply, using "switch" and "case". Assembler programmers use various techniques. Perhaps the worst if there are a lot of messages to handle is the chain of compares, eg. (in GoAsm format):- MOV EAX,[EBP+0Ch] ;get message number CMP EAX,1h ;see if WM_CREATE JNZ >L2 ;no XOR EAX,EAX ;ensure eax is zero on exit JMP >L32 ;finish L2: CMP EAX,116h ;see if WM_INITMENU JNZ >L4 ;no CALL INITIALISE_MENU JMP >L30 ;correct exit code L4: CMP EAX,47h ;see if WM_WINDOWPOSCHANGED JNZ >L8 and so on ........ To avoid these long chains, assembler programmers have developed various techniques. You will have seen many of these in sample code around Win32 assembler web sites and in the asm journal, using conditional jumps, macros or table scans. I do not wish to compare these various methods, merely to put forward my own current favourite, which I believe has these advantages:- 1. It works on all assemblers 2. It is modular, ie. the code for each window can be concentrated in a particular part of your source code 3. It is easy to follow from the source code what message causes what result 4. The same function can easily be called from within different window procedures My method results in a very simple Window Procedure as follows (GoAsm format):- WndProc: MOV EDX,OFFSET MAINMESSAGES CALL GENERAL_WNDPROC RET 10h where the messages and functions (specific to this particular window procedure) are set out in a table such as this:- ;---------------------------------------------------------- DATA SECTION ;assembler to put following in data section ;--------------------------- WNDPROC message functions MAINMESSAGES DD (ENDOF_MAINMESSAGES-$-4)/8 ;=number to be done DD 312h,HOTKEY,116h,INITMENU,117h,INITMENUPOPUP,11Fh,MENUSELECT DD 1h,CREATE,2h,DESTROY, 410h,OWN410,411h,OWN411 DD 231h,ENTERSIZEMOVE,47h,WINDOWPOSCHANGED,24h,GETMINMAXINFO DD 1Ah,SETTINGCHANGE,214h,SIZING,46h,WINDOWPOSCHANGING DD 2Bh,DRAWITEM,0Fh,PAINT,113h,TIMER,111h,COMMAND DD 104h,SYSKEYDOWN,100h,KEYDOWN,112h,SYSCOMMAND DD 201h,LBUTTONDOWN,202h,LBUTTONUP,115h,SCROLLMESS DD 204h,RBUTTONDOWNUP,205h,RBUTTONDOWNUP DD 200h,MOUSEMOVE,0A0h,NCMOUSEMOVE,20h,SETCURSORM DD 4Eh,NOTIFY,210h,PARENTNOTIFY,86h,NCACTIVATE,6h,ACTIVATE DD 1Ch,ACTIVATEAPP ENDOF_MAINMESSAGES: ;label used to work out how many messages ;---------------------------------------------------------- CODE SECTION ;assembler to put following in code section ;---------------------------------------------------------- and where each of the functions here are procedures, for example:- CREATE: XOR EAX,EAX ;ensure zero and nc return RET and where GENERAL_WINDPROC is as follows:- GENERAL_WNDPROC: PUSH EBP MOV EBP,[ESP+10h] ;get uMsg in ebp MOV ECX,[EDX] ;get number of messages to do ADD EDX,4 ;jump over size dword L33: DEC ECX JS >L46 ;s=message not found CMP [EDX+ECX*8],EBP ;see if its the correct message JNZ L33 ;no MOV EBP,ESP PUSH ESP,EBX,EDI,ESI ;save registers as required by Windows ADD EBP,4 ;allow for the extra call to here ;now [EBP+8]=hWnd,[EBP+0Ch]=uMsg,[EBP+10h]=wParam,[EBP+14h]=lParam CALL [EDX+ECX*8+4] ;call correct procedure for the message POP ESI,EDI,EBX,ESP JNC >L48 ;nc=don't call DefWindowProc eax=exit code L46: PUSH [ESP+18h],[ESP+18h],[ESP+18h],[ESP+18h] ;ESP changes on push CALL DefWindowProcA L48: POP EBP RET NOTES: 1. Instead of giving the actual message value, you can, of course, give the name of an EQUATE. For example (in GoAsm syntax) WM_CREATE EQU 1h or if you prefer WM_CREATE=1h or if you prefer #define WM_CREATE 1h enables you to use WM_CREATE,CREATE instead of 1h,CREATE if you wish. 2. It is tempting to keep the message table in the CODE SECTION. This is perfectly possible because the only difference to the Win32 system between the code section and the data section is that the code section area of memory is marked read only, whereas the data section is read/write. However, you should expect some loss of performance if you do this because the processor is designed to read data more quickly from the data section. I performed some tests on this and found that having the table in the code section rather than the data section slowed the code considerably:- 486 processor - 22% to 36% slower AMD-K6-3D processor - 78% to 193% slower These tests were carried out on a table of 60 messages and the range here relates to how quickly in the table scan the message was found. 3. The procedure names must not be the names of API imports to avoid confusion! For example change SETCURSOR slightly to avoid confusion with the API SetCursor. 4. If a function returns c (carry flag set) the window procedure will call DefWindowProc. An nc return (carry flag not set) will merely return to the system with the return code in eax. (Some messages must be dealt with in this way). 5. You can send a parameter of your own to GENERAL_WNDPROC using EAX. This is useful if you wish to identify a particular window. For example:- SpecialWndProc: MOV EAX,OFFSET hSpecialWnd MOV EDX,OFFSET SPECIALWND_MESSAGES CALL GENERAL_WNDPROC RET 10h 6. The ADD EBP,4 just before the call to the function is to ensure that EBP points to the parameters the stack in the same way as if the window procedure had been entered normally. This is intended to ensure that the function will be compatible if called by an ordinary window procedure written in assembler, for example:- WndProc: PUSH EBP MOV EBP,ESP ;now [EBP+8]=hWnd,[EBP+0Ch]=uMsg,[EBP+10h]=wParam,[EBP+14h]=lParam 7. A standardized procedure for dealing with messages to a dialog procedure can also be created in the same way, except that it should return TRUE (eax=1) if the message is processed and FALSE (eax=0) if it is not, without calling DefWindowProc. The same coding method can be applied to hooks and to enumerator CALLBACKS although these will vary. [Adapted from article first published in Asm Journal No. 5]