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 A386 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 (A386 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 SEGMENT FLAT ;assembler to put following in data section
;--------------------------- WNDPROC message functions
MAINMESSAGES DD ENDOF_MAINMESSAGES-$ ;=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
;----------------------------------------------------------
_TEXT SEGMENT FLAT ;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 8 (+4)
SHR ECX,3 ;get number of messages to do
ADD EDX,4 ;jump over size dword
- L33
- DEC ECX
JS >L46 ;s=message not found
CMP [EDX+ECX8],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
WM_CREATE EQU 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 may well get some loss of performance if you do this because
most processors will 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 could slow the code considerably:-
486 processor - 22% to 36% slower
Pentium processor - 94% to 161% slower
AMD-K6-3D processor - 78% to 193% slower
(but Pentium Pro - from 7% faster to 9% slower)
(and Pentium II - from 29% faster to 5% slower)
These tests were carried out on a table of 60 messages and the range of
results is because tests were carried out varying the number of scans
required before a find and also testing a no-find.
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.
{
This e-mail address is being protected from spam bots, you need JavaScript enabled to view it
}
{http://ourworld.compuserve.com/homepages/jorgon}
|