Sommaire

BONJOUR
Musique "Invention BWV784" J.S. BACH 1685-1750

Tutoriel ICZELION

Masm32 et API Win32

Nous veillons aux respects des droits d'auteur : si par erreur un contenu protégé est disponible sans indication sur ce site, merci de contacter l'administrateur, il sera retiré ou mis à jour dans les plus brefs délais.


Iczelion's Win32asm tutorial part 1: The Basic

Tutorial 1: The Basics

This tutorial assumes that the reader knows how to use MASM. If you're not familiar with MASM, download win32asm.exe and study the text inside the package before going on with the tutorial. Good. You're now ready. Let's go!

Theory:

Win32 programs run in protected mode which is available since 80286. But 80286 is now history. So we only have to concern ourselves with 80386 and its descendants. Windows runs each Win32 program in separated virtual space. That means each Win32 program will have its own 4 GB address space. However, this doesn't mean every win32 program has 4GB of physical memory, only that the program can address any address in that range. Windows will do anything necessary to make the memory the program references valid. Of course, the program must adhere to the rules set by Windows, else it will cause the dreaded General Protection Fault. Each program is alone in its address space. This is in contrast to the situation in Win16. All Win16 programs can *see* each other. Not so under Win32. This feature helps reduce the chance of one program writing over other program's code/data.
Memory model is also drastically different from the old days of the 16-bit world. Under Win32, we need not be concerned with memory model or segments anymore! There's only one memory model: Flat memory model. There's no more 64K segments. The memory is a large continuous space of 4 GB. That also means you don't have to play with segment registers. You can use any segment register to address any point in the memory space. That's a GREAT help to programmers. This is what makes Win32 assembly programming as easy as C.
When you program under Win32, you must know some important rules. One such rule is that, Windows uses esi, edi, ebp and ebx internally and it doesn't expect the values in those registers to change. So remember this rule first: if you use any of those four registers in your callback function, don't ever forget to restore them before returning control to Windows. A callback function is your own function which is called by Windows. The obvious example is the windows procedure. This doesn't mean that you cannot use those four registers, you can. Just be sure to restore them back before passing control back to Windows.

Content:

Here's the skeleton program. If you don't understand some of the codes, don't panic. I'll explain each of them later.

.386
.MODEL Flat, STDCALL
.DATA
<Your initialized data>
......
.DATA?
<Your uninitialized data>
......
.CONST
<Your constants>
......
.CODE
<label>
<Your code>
.....
end <label>

That's all! Let's analyze this skeleton program.

.386
This is an assembler directive, telling the assembler to use 80386 instruction set. You can also use .486, .586 but the safest bet is to stick to .386. There are actually two nearly identical forms for each CPU model. .386/.386p, .486/.486p. Those "p" versions are necessary only when your program uses privileged instructions. Privileged instructions are the instructions reserved by the CPU/operating system when in protected mode. They can only be used by privileged code, such as the virtual device drivers. Most of the time, your program will work in non-privileged mode so it's safe to use non-p versions.

.MODEL FLAT, STDCALL
.MODEL is an assembler directive that specifies memory model of your program. Under Win32, there's only on model, FLAT model.
STDCALL tells MASM about parameter passing convention. Parameter passing convention specifies the order of parameter passing, left-to-right or right-to-left, and also who will balance the stack frame after the function call.
Under Win16, there are two types of calling convention, C and PASCAL
C calling convention passes parameters from right to left, that is , the rightmost parameter is pushed first. The caller is responsible for balancing the stack frame after the call. For example, in order to call a function named foo(int first_param, int second_param, int third_param) in C calling convention the asm codes will look like this:

push [third_param] ; Push the third parameter
push [second_param] ; Followed by the second
push [first_param] ; And the first
call foo
add sp, 12 ; The caller balances the stack frame
PASCAL calling convention is the reverse of C calling convention. It passes parameters from left to right and the callee is responsible for the stack balancing after the call.
Win16 adopts PASCAL convention because it produces smaller codes. C convention is useful when you don't know how many parameters will be passed to the function as in the case of wsprintf(). In the case of wsprintf(), the function has no way to determine beforehand how many parameters will be pushed on the stack, so it cannot do the stack balancing.
STDCALLis the hybrid of C and PASCAL convention. It passes parameter from right to left but the callee is responsible for stack balancing after the call.Win32 platform use STDCALL exclusively. Except in one case: wsprintf(). You must use C calling convention with wsprintf().

.DATA
.DATA?
.CONST
.CODE
All four directives are what's called section. You don't have segments in Win32, remember? But you can divide your entire address space into logical sections. The start of one section denotes the end of the previous section. There'are two groups of section: data and code. Data sections are divided into 3 categories:

  • .DATA This section contains initialized data of your program.
  • .DATA? This section contains uninitialized data of your program. Sometimes you just want to preallocate some memory but don't want to initialize it. This section is for that purpose. The advantage of uninitialized data is: it doesn't take space in the executable file. For example, if you allocate 10,000 bytes in your .DATA? section, your executable is not bloated up 10,000 bytes. Its size stays much the same. You only tell the assembler how much space you need when the program is loaded into memory, that's all.
  • .CONST This section contains declaration of constants used by your program. Constants in this section can never be modified in your program. They are just *constant*.

You don't have to use all three sections in your program. Declare only the section(s) you want to use.

There's only one section for code: .CODE. This is where your codes reside.
<label>
end <label>
where <label> is any arbitrary label is used to specify the extent of your code. Both labels must be identical. All your codes must reside between <label> and end <label>

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 ASM tutorial 2: MessageBox

Tutorial 2: MessageBox

In this tutorial, we will create a fully functional Windows program that displays a message box saying "Win32 assembly is great!".

Download the example file here.

Theory:

Windows prepares a wealth of resources for Windows programs. Central to this is the Windows API (Application Programming Interface). Windows API is a huge collection of very useful functions that reside in Windows itself, ready for use by any Windows programs. These functions are stored in several dynamic-linked libraries (DLLs) such as kernel32.dll, user32.dll and gdi32.dll. Kernel32.dll contains API functions that deal with memory and process management. User32.dll controls the user interface aspects of your program. Gdi32.dll is responsible for graphics operations. Other than "the main three", there are other DLLs that your program can use, provided you have enough information about the desired API functions.
Windows programs dynamically link to these DLLs, ie. the codes of API functions are not included in the Windows program executable file. In order for your program to know where to find the desired API functions at runtime, you have to embed that information into the executable file. The information is in import libraries. You must link your programs with the correct import libraries or they will not be able to locate API functions.
When a Windows program is loaded into memory, Windows reads the information stored in the program. That information includes the names of functions the program uses and the DLLs those functions reside in. When Windows finds such info in the program, it'll load the DLLs and perform function address fixups in the program so the calls will transfer control to the right function.
There are two categoriesof API functions: One for ANSI and the other for Unicode. The names of API functions for ANSI are postfixed with "A", eg. MessageBoxA. Those for Unicode are postfixed with "W" (for Wide Char, I think). Windows 95 natively supports ANSI and Windows NT Unicode.
We are usually familiar with ANSI strings, which are arrays of characters terminated by NULL. ANSI character is 1 byte in size. While ANSI code is sufficient for European languages, it cannot handle several oriental languages which have several thousands of unique characters. That's why UNICODE comes in. A UNICODE character is 2 bytes in size, making it possible to have 65536 unique characters in the strings.
But most of the time, you will use an include file which can determine and select the appropriate API functions for your platform. Just refer to API function names without the postfix.

Example:

I'll present the bare program skeleton below. We will flesh it out later.

.386
.model flat, stdcall
.data
.code
start:
end start

The execution starts from the first instruction immediately below the label specified after end directive. In the above skeleton, the execution will start at the first instruction immediately below start label. The execution will proceed instruction by instruction until some flow-control instructions such as jmp, jne, je, ret etc is found. Those instructions redirect the flow of execution to some other instructions. When the program needs to exit to Windows, it should call an API function, ExitProcess.

ExitProcess proto uExitCode:DWORD

The above line is called a function prototype. A function prototype defines the attributes of a function to the assembler/linker so it can do type-checking for you. The format of a function prototype is like this:

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

In short, the name of the function followed by the keyword PROTO and then by the list of data types of the parameters,separated by commas. In the ExitProcess example above, it defines ExitProcess as a function which takes only one parameter of type DWORD. Functions prototypes are very useful when you use the high-level call syntax, invoke. You can think of invoke as a simple call with type-checking. For example, if you do:

call ExitProcess

without pushing a dword onto the stack, the assembler/linker will not be able to catch that error for you. You'll notice it later when your program crashes. But if you use:

invoke ExitProcess

The linker will inform you that you forgot to push a dword on the stack thus avoiding error. I recommend you use invoke instead of simple call. The syntax of invoke is as follows:

INVOKE expression [,arguments]

expression can be the name of a function or it can be a function pointer. The function parameters are separated by commas.

Most of function prototypes for API functions are kept in include files. If you use hutch's MASM32, they will be in MASM32/include folder. The include files have .inc extension and the function prototypes for functions in a DLL is stored in .inc file with the same name as the DLL. For example, ExitProcess is exported by kernel32.lib so the function prototype for ExitProcess is stored in kernel32.inc.
You can also create function prototypes for your own functions.
Throughout my examples, I'll use hutch's windows.inc which you can download from http://win32asm.cjb.net

Now back to ExitProcess, uExitCode parameter is the value you want the program to return to Windows after the program terminates. You can call ExitProcess like this:

invoke ExitProcess, 0

Put that line immediately below start label, you will get a win32 program which immediately exits to Windows, but it's a valid program nonetheless.

.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
start:
invoke ExitProcess,0
end start

option casemap:none tells MASM to make labels case-sensitive so ExitProcess and exitprocess are different. Note a new directive, include. This directive is followed by the name of a file you want to insert at the place the directive is. In the above example, when MASM processes the line include \masm32\include\windows.inc, it will open windows.inc which is in \MASM32\include folder and process the content of windows.inc as if you paste the content of windows.inc there. hutch's windows.inc contains definitions of constants and structures you need in win32 programming. It doesn't contain any function prototype. windows.inc is by no means comprehensive. hutch and I try to put as many constants and structures into it as possible but there are still many left to be included. It'll be constantly updated. Check out hutch's and my homepage for updates.
From windows.inc, your program got constant and structure definitions. Now for function prototypes, you need to include other include files. They are all stored in \masm32\include folder.

In our example above, we call a function exported by kernel32.dll, so we need to include the function prototypes from kernel32.dll. That file is kernel32.inc. If you open it with a text editor, you will see that it's full of function prototypes for kernel32.dll. If you don't include kernel32.inc, you can still call ExitProcess but only with simple call syntax. You won't be able to invoke the function. The point here is that: in order to invoke a function, you have to put its function prototype somewhere in the source code. In the above example, if you don't include kernel32.inc, you can define the function prototype for ExitProcess anywhere in the source code above the invoke command and it will work. The include files are there to save you the work of typing out the prototypes yourself so use them whenever you can.
Now we encounter a new directive, includelib. includelib doesn't work like include. It 's only a way to tell the assembler what import library your program uses. When the assembler sees an includelib directive, it puts a linker command into the object file so that the linker knows what import libraries your program needs to link with. You're not forced to use includelib though. You can specify the names of the import libraries in the command line of the linker but believe me, it's tedious and the command line can hold only 128 characters.

Now save the example under the name msgbox.asm. Assuming that ml.exe is in your path, assemble msgbox.asm with:

    ml /c /coff /Cp msgbox.asm
  • /c tells MASM to assemble only. Do not invoke link.exe. Most of the time, you would not want to call link.exe automatically since you may have to perform some other tasks prior to calling link.exe.

    • /coff tells MASM to create .obj file in COFF format. MASM uses a variation of COFF (Common Object File Format) which is used under Unix as its own object and executable file format.
    /Cp tells MASM to preserve case of user identifiers. If you use hutch's MASM32 package, you may put "option casemap:none" at the head of your source code, just below .model directive to achieve the same effect.
After you successfully assemble msgbox.asm, you will get msgbox.obj. msgbox.obj is an object file. An object file is only one step away from an executable file. It contains the instructions/data in binary form. What is lacking is some fixups of addresses by the linker.

Then go on with link:

    link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj
/SUBSYSTEM:WINDOWS informs Link what sort of executable this program is
/LIBPATH:<path to import library> tells Link where the import libraries are. If you use MASM32, they will be in MASM32\lib folder.
Link reads in the object file and fixes it with addresses from the import libraries. When the process is finished you get msgbox.exe.

Now you get msgbox.exe. Go on, run it. You'll find that it does nothing. Well, we haven't put anything interesting into it yet. But it's a Windows program nonetheless. And look at its size! In my PC, it is 1,536 bytes.

Next we're going to put in a message box. Its function prototype is:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd is the handle to parent window. You can think of a handle as a number that represents the window you're referrring to. Its value is not important to you. You only remember that it represents the window. When you want to do anything with the window, you must refer to it by its handle.
lpText is a pointer to the text you want to display in the client area of the message box. A pointer is really an address of something. A pointer to text string==The address of that string.
lpCaption is a pointer to the caption of the message box
uType specifies the icon and the number and type of buttons on the message box
Let's modify msgbox.asm to include the message box.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0

.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start

Assemble and run it. You will see a message box displaying the text "Win32 Assembly is Great!".

Let's look again at the source code.
We define two zero-terminated strings in .data section. Remember that every ANSI string in Windows must be terminated by NULL (0 hexadecimal).
We use two constants, NULL and MB_OK. Those constants are documented in windows.inc. So you can refer to them by name instead of the values. This improves readability of your source code.
The addr operator is used to pass the address of a label to the function. It's valid only in the context of invoke directive. You can't use it to assign the address of a label to a register/variable, for example. You can use offset instead of addr in the above example. However, there are some differences between the two:

  1. addr cannot handle forward reference while offset can. For example, if the label is defined somewhere further in the source code than the invoke line, addr will not work.
    2. invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
    ......
    MsgBoxCaption db "Iczelion Tutorial No.2",0
    MsgBoxText db "Win32 Assembly is Great!",0
    MASM will report error. If you use offset instead of addr in the above code snippet, MASM will assemble it happily.
  2. addr can handle local variables while offset cannot. A local variable is only some reserved space in the stack. You will only know its address during runtime. offset is interpreted during assembly time by the assembler. So it's natural that offset won't work for local variables. addr is able to handle local variables because of the fact that the assembler checks first whether the variable referred to by addr is a global or local one. If it's a global variable, it puts the address of that variable into the object file. In this regard, it works like offset. If it's a local variable, it generates an instruction sequence like this before it actually calls the function:
    3. lea eax, LocalVar
    push eax

    Since lea can determine the address of a label at runtime, this works fine.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 ASM tutorial 3: A Simple Window

Tutorial 3: A Simple Window

In this tutorial, we will build a Windows program that displays a fully functional window on the desktop.
Download the example file here

Theory:

Windows programs rely heavily on API functions for their GUI. This approach benefits both users and programmers. For users, they don't have to learn how to navigate the GUI of each new programs, the GUI of Windows programs are alike. For programmers, the GUI codes are already there,tested, and ready for use. The downside for programmers is the increased complexity involved. In order to create or manipulate any GUI objects such as windows, menu or icons, programmers must follow a strict recipe. But that can be overcome by modular programming or OOP paradigm.
I'll outline the steps required to create a window on the desktop below:
  1. Get the instance handle of your program (required)
  2. Get the command line (not required unless your program wants to process a command line)
  3. Register window class (required ,unless you use predefined window types, eg. MessageBox or a dialog box)
  4. Create the window (required)
  5. Show the window on the desktop (required unless you don't want to show the window immediately)
  6. Refresh the client area of the window
  7. Enter an infinite loop, checking for messages from Windows
  8. If messages arrive, they are processed by a specialized function that is responsible for the window
  9. Quit program if the user closes the window
As you can see, the structure of a Windows program is rather complex compared to a DOS program. But the world of Windows is drastically different from the world of DOS. Windows programs must be able to coexist peacefully with each other. They must follow stricter rules. You, as a programmer, must also be more strict with your programming style and habit.

Content:

Below is the source code of our simple window program. Before jumping into the gory details of Win32 ASM programming, I'll point out some fine points which will ease your programming.
  • You should put all Windows constants, structures and function prototypes in an include file and include it at the beginning of your .asm file. It'll save you a lot of effort and typo. Currently, the most complete include file for MASM is hutch's windows.inc which you can download from his page or my page. You can also define your own constants & structure definitions but you should put them into a separate include file.
  • Use includelib directive to specify the import library used in your program. For example, if your program calls MessageBox, you should put the line:
      • includelib user32.lib
    at the beginning of your .asm file. This directive tells MASM that your program will make uses of functions in that import library. If your program calls functions in more than one library, just add an includelib for each library you use. Using IncludeLib directive, you don't have to worry about import libraries at link time. You can use /LIBPATH linker switch to tell Link where all the libs are.
  • When declaring API function prototypes, structures, or constants in your include file, try to stick to the original names used in Windows include files, including case. This will save you a lot of headache when looking up some item in Win32 API reference.
  • Use makefile to automate your assembling process. This will save you a lot of typing.
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib ; calls to functions in user32.lib and kernel32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.DATA ; initialized data
ClassName db "SimpleWinClass",0 ; the name of our window class
AppName db "Our First Window",0 ; the name of our window

.DATA? ; Uninitialized data
hInstance HINSTANCE ? ; Instance handle of our program
CommandLine LPSTR ?
.CODE ; Here begins our code
start:
invoke GetModuleHandle, NULL ; get the instance handle of our program.
; Under Win32, hmodule==hinstance mov hInstance,eax
mov hInstance,eax
invoke GetCommandLine ; get the command line. You don't have to call this function IF
; your program doesn't process the command line.
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; call the main function
invoke ExitProcess, eax ; quit our program. The exit code is returned in eax from WinMain.

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX ; create local variables on stack
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX ; fill values in members of wc
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc ; register our window class
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; display our window on desktop
invoke UpdateWindow, hwnd ; refresh the client area

.WHILE TRUE ; Enter message loop
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam ; return exit code in eax
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY ; if the user closes our window
invoke PostQuitMessage,NULL ; quit our application
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Default message processing
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

Analysis:

You may be taken aback that a simple Windows program requires so much coding. But most of those codes are just *template* codes that you can copy from one source code file to another. Or if you prefer, you could assemble some of these codes into a library to be used as prologue and epilogue codes. You can write only the codes in WinMain function. In fact, this is what C compilers do. They let you write WinMain codes without worrying about other housekeeping chores. The only catch is that you must have a function named WinMain else C compilers will not be able to combine your codes with the prologue and epilogue. You do not have such restriction with assembly language. You can use any function name instead of WinMain or no function at all.
Prepare yourself. This's going to be a long, long tutorial. Let's analyze this program to death!
    .386
    .model flat,stdcall
    option casemap:none

    WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

    include \masm32\include\windows.inc
    include \masm32\include\user32.inc
    include \masm32\include\kernel32.inc
    includelib \masm32\lib\user32.lib
    includelib \masm32\lib\kernel32.lib

The first three lines are "necessities". .386 tells MASM we intend to use 80386 instruction set in this program. .model flat,stdcall tells MASM that our program uses flat memory addressing model. Also we will use stdcall parameter passing convention as the default one in our program.
Next is the function prototype for WinMain. Since we will call WinMain later, we must define its function prototype first so that we will be able to invoke it.
We must include windows.inc at the beginning of the source code. It contains important structures and constants that are used by our program. The include file , windows.inc, is just a text file. You can open it with any text editor. Please note that windows.inc does not contain all structures, and constants (yet). hutch and I are working on it. You can add in new items if they are not in the file.
Our program calls API functions that reside in user32.dll (CreateWindowEx, RegisterWindowClassEx, for example) and kernel32.dll (ExitProcess), so we must link our program to those two import libraries. The next question : how can I know which import library should be linked to my program? The answer: You must know where the API functions called by your program reside. For example, if you call an API function in gdi32.dll, you must link with gdi32.lib.
This is the approach of MASM. TASM 's way of import library linking is much more simpler: just link to one and only one file: import32.lib.
    .DATA
    ClassName db "SimpleWinClass",0
    AppName db "Our First Window",0

    .DATA?
    hInstance HINSTANCE ?
    CommandLine LPSTR ?

Next are the "DATA" sections.
In .DATA, we declare two zero-terminated strings(ASCIIZ strings): ClassName which is the name of our window class and AppName which is the name of our window. Note that the two variables are initialized.
In .DATA?, two variables are declared: hInstance (instance handle of our program) and CommandLine (command line of our program). The unfamiliar data types, HINSTANCE and LPSTR, are really new names for DWORD. You can look them up in windows.inc. Note that all variables in .DATA? section are not initialized, that is, they don't have to hold any specific value on startup, but we want to reserve the space for future use.
    .CODE
    start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke GetCommandLine
    mov CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax
    .....
    end start
.CODE contains all your instructions. Your codes must reside between <starting label>: and end <starting label>. The name of the label is unimportant. You can name it anything you like so long as it is unique and doesn't violate the naming convention of MASM.
Our first instruction is the call to GetModuleHandle to retrieve the instance handle of our program. Under Win32, instance handle and module handle are one and the same. You can think of instance handle as the ID of your program. It is used as parameter to several API functions our program must call, so it's generally a good idea to retrieve it at the beginning of our program.
Note: Actually under win32, instance handle is the linear address of your program in memory.
Upon returning from a Win32 function, the function's return value, if any, can be found in eax. All other values are returned through variables passed in the function parameter list you defined for the call.
A Win32 function that you call will nearly always preserve the segment registers and the ebx, edi, esi and ebp registers. Conversely, ecx and edx are considered scratch registers and are always undefined upon return from a Win32 function.
Note: Don't expect the values of eax, ecx, edx to be preserved across API function calls.
The bottom line is that: when calling an API function, expects return value in eax. If any of your function will be called by Windows, you must also play by the rule: preserve and restore the values of the segment registers, ebx, edi, esi and ebp upon function return else your program will crash very shortly, this includes your window procedure and windows callback functions.
The GetCommandLine call is unnecessary if your program doesn't process a command line. In this example, I show you how to call it in case you need it in your program.
Next is the WinMain call. Here it receives four parameters: the instance handle of our program, the instance handle of the previous instance of our program, the command line and window state at first appearance. Under Win32, there's NO previous instance. Each program is alone in its address space, so the value of hPrevInst is always 0. This is a leftover from the day of Win16 when all instances of a program run in the same address space and an instance wants to know if it's the first instance. Under win16, if hPrevInst is NULL, then this instance is the first one.
Note: You don't have to declare the function name as WinMain. In fact, you have complete freedom in this regard. You don't have to use any WinMain-equivalent function at all. You can paste the codes inside WinMain function next to GetCommandLine and your program will still be able to function perfectly.
Upon returning from WinMain, eax is filled with exit code. We pass that exit code as the parameter to ExitProcess which terminates our application.

WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

The above line is the function declaration of WinMain. Note the parameter:type pairs that follow PROC directive. They are parameters that WinMain receives from the caller. You can refer to these parameters by name instead of by stack manipulation. In addition, MASM will generate the prologue and epilogue codes for the function. So we don't have to concern ourselves with stack frame on function enter and exit.

LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

LOCAL directive allocates memory from the stack for local variables used in the function. The bunch of LOCAL directives must be immediately below the PROC directive. The LOCAL directive is immediately followed by <the name of local variable>:<variable type>. So LOCAL wc:WNDCLASSEX tells MASM to allocate memory from the stack the size of WNDCLASSEX structure for the variable named wc. We can refer to wc in our codes without any difficulty involved in stack manipulation. That's really a godsend, I think. The downside is that local variables cannot be used outside the function they're created and will be automatically destroyed when the function returns to the caller. Another drawback is that you cannot initialize local variables automatically because they're just stack memory allocated dynamically when the function is entered . You have to manually assign them with desired values after LOCAL directives.

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc

The inimidating lines above are really simple in concept. It just takes several lines of instruction to accomplish. The concept behind all these lines is window class. A window class is nothing more than a blueprint or specification of a window. It defines several important characteristics of a window such as its icon, its cursor, the function responsible for it, its color etc. You create a window from a window class. This is some sort of object oriented concept. If you want to create more than one window with the same characteristics, it stands to reason to store all these characteristics in only one place and refer to them when needed. This scheme will save lots of memory by avoiding duplication of information. Remember, Windows is designed in the past when memory chips are prohibitive and most computers have 1 MB of memory. Windows must be very efficient in using the scarce memory resource. The point is: if you define your own window, you must fill the desired characteristics of your window in a WNDCLASS or WNDCLASSEX structure and call RegisterClass or RegisterClassEx before you're able to create your window. You only have to register the window class once for each window type you want to create a window from.
Windows has several predefined Window classes, such as button and edit box. For these windows (or controls), you don't have to register a window class, just call CreateWindowEx with the predefined class name.
The single most important member in the WNDCLASSEX is lpfnWndProc. lpfn stands for long pointer to function. Under Win32, there's no "near" or "far" pointer, just pointer because of the new FLAT memory model. But this is again a leftover from the day of Win16. Each window class must be associated with a function called window procedure. The window procedure is responsible for message handling of all windows created from the associated window class. Windows will send messages to the window procedure to notify it of important events concerning the windows it 's responsible for,such as user keyboard or mouse input. It's up to the window procedure to respond intelligently to each window message it receives. You will spend most of your time writing event handlers in window procedure.
I describe each member of WNDCLASSEX below:

WNDCLASSEX STRUCT DWORD
cbSize DWORD ?
style DWORD ?
lpfnWndProc DWORD ?
cbClsExtra DWORD ?
cbWndExtra DWORD ?
hInstance DWORD ?
hIcon DWORD ?
hCursor DWORD ?
hbrBackground DWORD ?
lpszMenuName DWORD ?
lpszClassName DWORD ?
hIconSm DWORD ?
WNDCLASSEX ENDS

cbSize: The size of WNDCLASSEX structure in bytes. We can use SIZEOF operator to get the value.
style: The style of windows created from this class. You can combine several styles together using "or" operator.
lpfnWndProc: The address of the window procedure responsible for windows created from this class.
cbClsExtra: Specifies the number of extra bytes to allocate following the window-class structure. The operating system initializes the bytes to zero. You can store window class-specific data here.
cbWndExtra: Specifies the number of extra bytes to allocate following the window instance. The operating system initializes the bytes to zero. If an application uses the WNDCLASS structure to register a dialog box created by using the CLASS directive in the resource file, it must set this member to DLGWINDOWEXTRA.
hInstance: Instance handle of the module.
hIcon: Handle to the icon. Get it from LoadIcon call.
hCursor: Handle to the cursor. Get it from LoadCursor call.
hbrBackground: Background color of windows created from the class.
lpszMenuName: Default menu handle for windows created from the class.
lpszClassName: The name of this window class.
hIconSm: Handle to a small icon that is associated with the window class. If this member is NULL, the system searches the icon resource specified by the hIcon member for an icon of the appropriate size to use as the small icon.

invoke CreateWindowEx, NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL

After registering the window class, we can call CreateWindowEx to create our window based on the submitted window class. Notice that there are 12 parameters to this function.

CreateWindowExA proto dwExStyle:DWORD,\
lpClassName:DWORD,\
lpWindowName:DWORD,\
dwStyle:DWORD,\
X:DWORD,\
Y:DWORD,\
nWidth:DWORD,\
nHeight:DWORD,\
hWndParent:DWORD ,\
hMenu:DWORD,\
hInstance:DWORD,\
lpParam:DWORD

Let's see detailed description of each parameter:
dwExStyle: Extra window styles. This is the new parameter that is added to the old CreateWindow. You can put new window styles for Windows 95 & NT here.You can specify your ordinary window style in dwStyle but if you want some special styles such as topmost window, you must specify them here. You can use NULL if you don't want extra window styles.
lpClassName: (Required). Address of the ASCIIZ string containing the name of window class you want to use as template for this window. The Class can be your own registered class or predefined window class. As stated above, every window you created must be based on a window class.
lpWindowName: Address of the ASCIIZ string containing the name of the window. It'll be shown on the title bar of the window. If this parameter is NULL, the title bar of the window will be blank.
dwStyle: Styles of the window. You can specify the appearance of the window here. Passing NULL is ok but the window will have no system menu box, no minimize-maximize buttons, and no close-window button. The window would not be of much use at all. You will need to press Alt+F4 to close it. The most common window style is WS_OVERLAPPEDWINDOW. A window style is only a bit flag. Thus you can combine several window styles by "or" operator to achieve the desired appearance of the window. WS_OVERLAPPEDWINDOW style is actually a combination of the most common window styles by this method.
X,Y: The coordinate of the upper left corner of the window. Normally this values should be CW_USEDEFAULT, that is, you want Windows to decide for you where to put the window on the desktop.
nWidth, nHeight: The width and height of the window in pixels. You can also use CW_USEDEFAULT to let Windows choose the appropriate width and height for you.
hWndParent: A handle to the window's parent window (if exists). This parameter tells Windows whether this window is a child (subordinate) of some other window and, if it is, which window is the parent. Note that this is not the parent-child relationship of multiple document interface (MDI). Child windows are not bound to the client area of the parent window. This relationship is specifically for Windows internal use. If the parent window is destroyed, all child windows will be destroyed automatically. It's really that simple. Since in our example, there's only one window, we specify this parameter as NULL.
hMenu: A handle to the window's menu. NULL if the class menu is to be used. Look back at the a member of WNDCLASSEX structure, lpszMenuName. lpszMenuName specifies *default* menu for the window class. Every window created from this window class will have the same menu by default. Unless you specify an *overriding* menu for a specific window via its hMenu parameter. hMenu is actually a dual-purpose parameter. In case the window you want to create is of a predefined window type (ie. control), such control cannot own a menu. hMenu is used as that control's ID instead. Windows can decide whether hMenu is really a menu handle or a control ID by looking at lpClassName parameter. If it's the name of a predefined window class, hMenu is a control ID. If it's not, then it's a handle to the window's menu.
hInstance: The instance handle for the program module creating the window.
lpParam: Optional pointer to a data structure passed to the window. This is used by MDI window to pass the CLIENTCREATESTRUCT data. Normally, this value is set to NULL, meaning that no data is passed via CreateWindow(). The window can retrieve the value of this parameter by the call to GetWindowLong function.

mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow
invoke UpdateWindow, hwnd

On successful return from CreateWindowEx, the window handle is returned in eax. We must keep this value for future use. The window we just created is not automatically displayed. You must call ShowWindow with the window handle and the desired *display state* of the window to make it display on the screen. Next you can call UpdateWindow to order your window to repaint its client area. This function is useful when you want to update the content of the client area. You can omit this call though.

.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW

At this time, our window is up on the screen. But it cannot receive input from the world. So we have to *inform* it of relevant events. We accomplish this with a message loop. There's only one message loop for each module. This message loop continually checks for messages from Windows with GetMessage call. GetMessage passes a pointer to a MSG structure to Windows. This MSG structure will be filled with information about the message that Windows want to send to a window in the module. GetMessage function will not return until there's a message for a window in the module. During that time, Windows can give control to other programs. This is what forms the cooperative multitasking scheme of Win16 platform. GetMessage returns FALSE if WM_QUIT message is received which, in the message loop, will terminate the loop and exit the program.
TranslateMessage is a utility function that takes raw keyboard input and generates a new message (WM_CHAR) that is placed on the message queue. The message with WM_CHAR contains the ASCII value for the key pressed, which is easier to deal with than the raw keyboard scan codes. You can omit this call if your program doesn't process keystrokes.
DispatchMessage sends the message data to the window procedure responsible for the specific window the message is for.

mov eax,msg.wParam
ret
WinMain endp

If the message loop terminates, the exit code is stored in wParam member of the MSG structure. You can store this exit code into eax to return it to Windows. At the present time, Windows does not make use of the return value, but it's better to be on the safe side and plays by the rule.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

This is our window procedure. You don't have to name it WndProc. The first parameter, hWnd, is the window handle of the window that the message is destined for. uMsg is the message. Note that uMsg is not a MSG structure. It's just a number, really. Windows defines hundreds of messages, most of which your programs will not be interested in. Windows will send an appropriate message to a window in case something relevant to that window happens. The window procedure receives the message and reacts to it intelligently. wParam and lParam are just extra parameters for use by some messages. Some messages do send accompanying data in addition to the message itself. Those data are passed to the window procedure by means of lParam and wParam.

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

Here comes the crucial part. This is where most of your program's intelligence resides. The codes that respond to each Windows message are in the window procedure. Your code must check the Windows message to see if it's a message it's interested in. If it is, do anything you want to do in response to that message and then return with zero in eax. If it's not, you MUST call DefWindowProc, passing all parameters you received to it for default processing.. This DefWindowProc is an API function that processes the messages your program are not interested in.
The only message that you MUST respond to is WM_DESTROY. This message is sent to your window procedure whenever your window is closed. By the time your window procedure receives this message, your window is already removed from the screen. This is just a notification that your window was destroyed, you should prepare yourself to return to Windows. In response to this, you can perform housekeeping prior to returning to Windows. You have no choice but to quit when it comes to this state. If you want to have a chance to stop the user from closing your window, you should process WM_CLOSE message. Now back to WM_DESTROY, after performing housekeeping chores, you must call PostQuitMessage which will post WM_QUIT back to your module. WM_QUIT will make GetMessage return with zero value in eax, which in turn, terminates the message loop and quits to Windows. You can send WM_DESTROY message to your own window procedure by calling DestroyWindow function.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 4: Painting with Text

Tutorial 4: Painting with Text

In this tutorial, we will learn how to "paint" text in the client area of a window. We'll also learn about device context.
You can download the source code here.

Theory:

Text in Windows is a type of GUI object. Each character is composed of numerous pixels (dots) that are lumped together into a distinct pattern. That's why it's called "painting" instead of "writing". Normally, you paint text in your own client area (actually, you can paint outside client area but that's another story). Putting text on screen in Windows is drastically different from DOS. In DOS, you can think of the screen in 80x25 dimension. But in Windows, the screen are shared by several programs. Some rules must be enforced to avoid programs writing over each other's screen. Windows ensures this by limiting painting area of each window to its own client area only. The size of client area of a window is also not constant. The user can change the size anytime. So you must determine the dimensions of your own client area dynamically.
Before you can paint something on the client area, you must ask for permission from Windows. That's right, you don't have absolute control of the screen as you were in DOS anymore. You must ask Windows for permission to paint your own client area. Windows will determine the size of your client area, font, colors and other GDI attributes and sends a handle to device context back to your program. You can then use the device context as a passport to painting on your client area.
What is a device context? It's just a data structure maintained internally by Windows. A device context is associated with a particular device, such as a printer or video display. For a video display, a device context is usually associated with a particular window on the display.
Some of the values in the device context are graphic attributes such as colors, font etc. These are default values which you can change at will. They exist to help reduce the load from having to specify these attributes in every GDI function calls.
You can think of a device context as a default environment prepared for you by Windows. You can override some default settings later if you so wish.
When a program need to paint, it must obtain a handle to a device context. Normally, there are several ways to accomplish this.
    call BeginPaint in response to WM_PAINT message.
    call GetDC in response to other messages.
    call CreateDC to create your own device context
One thing you must remember, after you're through with the device context handle, you must release it during the processing of a single message. Don't obtain the handle in response to one message and release it in response to another.
Windows posts WM_PAINT messages to a window to notify that it's now time to repaint its client area. Windows does not save the content of client area of a window. Instead, when a situation occurs that warrants a repaint of client area (such as when a window was covered by another and is just uncovered), Windows puts WM_PAINT message in that window's message queue. It's the responsibility of that window to repaint its own client area. You must gather all information about how to repaint your client area in the WM_PAINT section of your window procedure, so the window procudure can repaint the client area when WM_PAINT message arrives.
Another concept you must come to terms with is the invalid rectangle. Windows defines an invalid rectangle as the smallest rectangular area in the client area that needs to be repainted. When Windows detects an invalid rectangle in the client area of a window , it posts WM_PAINT message to that window. In response to WM_PAINT message, the window can obtain a paintstruct structure which contains, among others, the coordinate of the invalid rectangle. You call BeginPaint in response to WM_PAINT message to validate the invalid rectangle. If you don't process WM_PAINT message, at the very least you must call DefWindowProc or ValidateRect to validate the invalid rectangle else Windows will repeatedly send you WM_PAINT message.
Below are the steps you should perform in response to a WM_PAINT message:
    Get a handle to device context with BeginPaint.
    Paint the client area.
    Release the handle to device context with EndPaint
Note that you don't have to explicitly validate the invalid rectangle. It's automatically done by the BeginPaint call. Between BeginPaint-Endpaint pair, you can call any GDI functions to paint your client area. Nearly all of them require the handle to device context as a parameter.

Content:

We will write a program that displays a text string "Win32 assembly is great and easy!" in the center of the client area.
.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
OurText db "Win32 assembly is great and easy!",0

.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax, eax
ret
WndProc endp
end start

Analysis:

The majority of the code is the same as the example in tutorial 3. I'll explain only the important changes.

LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT

These are local variables that are used by GDI functions in our WM_PAINT section. hdc is used to store the handle to device context returned from BeginPaint call. ps is a PAINTSTRUCT structure. Normally you don't use the values in ps. It's passed to BeginPaint function and Windows fills it with appropriate values. You then pass ps to EndPaint function when you finish painting the client area. rect is a RECT structure defined as follows:

RECT Struct
left LONG ?
top LONG ?
right LONG ?
bottom LONG ?
RECT ends
Left and top are the coordinates of the upper left corner of a rectangle Right and bottom are the coordinates of the lower right corner. One thing to remember: The origin of the x-y axes is at the upper left corner of the client area. So the point y=10 is BELOW the point y=0.

invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps

In response to WM_PAINT message, you call BeginPaint with handle to the window you want to paint and an uninitialized PAINTSTRUCT structure as parameters. After successful call, eax contains the handle to device context. Next you call GetClientRect to retrieve the dimension of the client area. The dimension is returned in rect variable which you pass to DrawText as one of its parameters. DrawText's syntax is:

DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD, uFormat:DWORD

DrawText is a high-level text output API function. It handles some gory details such as word wrap, centering etc. so you could concentrate on the string you want to paint. Its low-level brother, TextOut, will be examined in the next tutorial. DrawText formats a text string to fit within the bounds of a rectangle. It uses the currently selected font,color and background (in the device context) to draw the text.Lines are wrapped to fit within the bounds of the rectangle. It returns the height of the output text in device units, in our case, pixels. Let's see its parameters:

    hdc handle to device context
    lpString The pointer to the string you want to draw in the rectangle. The string must be null-terminated else you would have to specify its length in the next parameter, nCount.
    nCount The number of characters to output. If the string is null-terminated, nCount must be -1. Otherwise nCount must contain the number of characters in the string you want to draw.
    lpRect The pointer to the rectangle (a structure of type RECT) you want to draw the string in. Note that this rectangle is also a clipping rectangle, that is, you could not draw the string outside this rectangle.
    uFormat The value that specifies how the string is displayed in the rectangle. We use three values combined by "or" operator:
    • DT_SINGLELINE specifies a single line of text
    • DT_CENTER centers the text horizontally.
    • DT_VCENTER centers the text vertically. Must be used with DT_SINGLELINE.
After you finish painting the client area, you must call EndPaint function to release the handle to device context.
That's it. We can summarize the salient points here:
  • You call BeginPaint-EndPaint pair in response to WM_PAINT message.
  • Do anything you like with the client area between the calls to BeginPaint and EndPaint.
  • If you want to repaint your client area in response to other messages, you have two choices:
    • Use GetDC-ReleaseDC pair and do your painting between these calls
    • Call InvalidateRect or UpdateWindow to invalidate the entire client area, forcing Windows to put WM_PAINT message in the message queue of your window and do your painting in WM_PAINT section
[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 5: More about Text

Tutorial 5: More about Text

We will experiment more with text attributes, ie. font and color.

Download the example file here.

Theory:

Windows color system is based on RGB values, R=red, G=Green, B=Blue. If you want to specify a color in Windows, you must state your desired color in terms of these three major colors. Each color value has a range from 0 to 255 (a byte value). For example, if you want pure red color, you should use 255,0,0. Or if you want pure white color, you must use 255,255,255. You can see from the examples that getting the color you need is very difficult with this system since you have to have a good grasp of how to mix and match colors.
For text color and background, you use SetTextColor and SetBkColor, both of them require a handle to device context and a 32-bit RGB value. The 32-bit RGB value's structure is defined as:

RGB_value struct
unused db 0
blue db ?
green db ?
red db ?
RGB_value ends

Note that the first byte is not used and should be zero. The order of the remaining three bytes is reversed,ie. blue, green, red. However, we will not use this structure since it's cumbersome to initialize and use. We will create a macro instead. The macro will receive three parameters: red, green and blue values. It'll produce the desired 32-bit RGB value and store it in eax. The macro is as follows:

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

You can put this macro in the include file for future use.
You can "create" a font by calling CreateFont or CreateFontIndirect. The difference between the two functions is that CreateFontIndirect receives only one parameter: a pointer to a logical font structure, LOGFONT. CreateFontIndirect is the more flexible of the two especially if your programs need to change fonts frequently. However, in our example, we will "create" only one font for demonstration, we can get away with CreateFont. After the call to CreateFont, it will return a handle to a font which you must select into the device context. After that, every text API function will use the font we have selected into the device context.

Content:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
TestString db "Win32 assembly is great and easy!",0
FontName db "script",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL hfont:HFONT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName
invoke SelectObject, hdc, eax
mov hfont,eax
RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax
invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString
invoke SelectObject,hdc, hfont
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

Analysis:

invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName

CreateFont creates a logical font that is the closest match to the given parameters and the font data available. This function has more parameters than any other function in Windows. It returns a handle to logical font to be used by SelectObject function. We will examine its parameters in detail.

CreateFont proto nHeight:DWORD,\
nWidth:DWORD,\
nEscapement:DWORD,\
nOrientation:DWORD,\
nWeight:DWORD,\
cItalic:DWORD,\
cUnderline:DWORD,\
cStrikeOut:DWORD,\
cCharSet:DWORD,\
cOutputPrecision:DWORD,\
cClipPrecision:DWORD,\
cQuality:DWORD,\
cPitchAndFamily:DWORD,\
lpFacename:DWORD

nHeight The desired height of the characters . 0 means use default size.
nWidth The desired width of the characters. Normally this value should be 0 which allows Windows to match the width to the height. However, in our example, the default width makes the characters hard to read, so I use the width of 16 instead.
nEscapement Specifies the orientation of the next character output relative to the previous one in tenths of a degree. Normally, set to 0. Set to 900 to have all the characters go upward from the first character, 1800 to write backwards, or 2700 to write each character from the top down.
nOrientation Specifies how much the character should be rotated when output in tenths of a degree. Set to 900 to have all the characters lying on their backs, 1800 for upside-down writing, etc.
nWeight Sets the line thickness of each character. Windows defines the following sizes:

    FW_DONTCARE equ 0
    FW_THIN equ 100
    FW_EXTRALIGHT equ 200
    FW_ULTRALIGHT equ 200
    FW_LIGHT equ 300
    FW_NORMAL equ 400
    FW_REGULAR equ 400
    FW_MEDIUM equ 500
    FW_SEMIBOLD equ 600
    FW_DEMIBOLD equ 600
    FW_BOLD equ 700
    FW_EXTRABOLD equ 800
    FW_ULTRABOLD equ 800
    FW_HEAVY equ 900
    FW_BLACK equ 900
cItalic 0 for normal, any other value for italic characters.
cUnderline 0 for normal, any other value for underlined characters.
cStrikeOut 0 for normal, any other value for characters with a line through the center.
cCharSet The character set of the font. Normally should be OEM_CHARSET which allows Windows to select font which is operating system-dependent.
cOutputPrecision Specifies how much the selected font must be closely matched to the characteristics we want. Normally should be OUT_DEFAULT_PRECIS which defines default font mapping behavior.
cClipPrecision Specifies the clipping precision. The clipping precision defines how to clip characters that are partially outside the clipping region. You should be able to get by with CLIP_DEFAULT_PRECIS which defines the default clipping behavior.
cQuality Specifies the output quality. The output quality defines how carefully GDI must attempt to match the logical-font attributes to those of an actual physical font. There are three choices: DEFAULT_QUALITY, PROOF_QUALITY and DRAFT_QUALITY.
cPitchAndFamily Specifies pitch and family of the font. You must combine the pitch value and the family value with "or" operator.
lpFacename A pointer to a null-terminated string that specifies the typeface of the font.

The description above is by no means comprehensive. You should refer to your Win32 API reference for more details.

invoke SelectObject, hdc, eax
mov hfont,eax

After we get the handle to the logical font, we must use it to select the font into the device context by calling SelectObject. SelectObject puts the new GDI objects such as pens, brushs, and fonts into the device context to be used by GDI functions. It returns the handle to the replaced object which we should save for future SelectObject call. After SelectObject call, any text output function will use the font we just selected into the device context.

RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax

Use RGB macro to create a 32-bit RGB value to be used by SetColorText and SetBkColor.

invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString

Call TextOut function to draw the text on the client area. The text will be in the font and color we specified previously.

invoke SelectObject,hdc, hfont

When we are through with the font, we should restore the old font back into the device context. You should always restore the object that you replaced in the device context.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 6: Keyboard Input

Tutorial 6: Keyboard Input

We will learn how a Windows program receives keyboard input.

Download the example here.

Theory:

Since normally there's only one keyboard in each PC, all running Windows programs must share it between them. Windows is responsible for sending the key strokes to the window which has the input focus.
Although there may be several windows on the screen, only one of them has the input focus. The window which has input focus is the only one which can receive key strokes. You can differentiate the window which has input focus from other windows by looking at the title bar. The title bar of the window which has input focus is highlighted.
Actually, there are two main types of keyboard messages, depending on your view of the keyboard. You can view a keyboard as a collection of keys. In this case, if you press a key, Windows sends a WM_KEYDOWN message to the window which has input focus, notifying that a key is pressed. When you release the key, Windows sends a WM_KEYUP message. You treat a key as a button. Another way to look at the keyboard is that it's a character input device. When you press "a" key, Windows sends a WM_CHAR message to the window which has input focus, telling it that the user sends "a" character to it. In fact, Windows sends WM_KEYDOWN and WM_KEYUP messages to the window which has input focus and those messages will be translated to WM_CHAR messages by TranslateMessage call. The window procedure may decide to process all three messages or only the messages it's interested in. Most of the time, you can ignore WM_KEYDOWN and WM_KEYUP since TranslateMessage function call in the message loop translate WM_KEYDOWN and WM_KEYUP messages to WM_CHAR messages. We will focus on WM_CHAR in this tutorial.

Example:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
char WPARAM 20h ; the character the program receives from keyboard

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke TextOut,hdc,0,0,ADDR char,1
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

char WPARAM 20h ; the character the program receives from keyboard

This is the variable that will store the character received from the keyboard. Since the character is sent in WPARAM of the window procedure, we define the variable as type WPARAM for simplicity. The initial value is 20h or the space since when our window refreshes its client area the first time, there is no character input. So we want to display space instead.

.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE

This is added in the window procedure to handle the WM_CHAR message. It just puts the character into the variable named "char" and then calls InvalidateRect. InvalidateRect makes the specified rectangle in the client area invalid which forces Windows to send WM_PAINT message to the window procedure. Its syntax is as follows:

InvalidateRect proto hWnd:HWND,\
lpRect:DWORD,\
bErase:DWORD

lpRect is a pointer to the rectagle in the client area that we want to declare invalid. If this parameter is null, the entire client area will be marked as invalid.
bErase is a flag telling Windows if it needs to erase the background. If this flag is TRUE, then Windows will erase the backgroud of the invalid rectangle when BeginPaint is called.

So the strategy we used here is that: we store all necessary information relating to painting the client area and generate WM_PAINT message to paint the client area. Of course, the codes in WM_PAINT section must know beforehand what's expected of them. This seems a roundabout way of doing things but it's the way of Windows.
Actually we can paint the client area during processing WM_CHAR message by calling GetDC and ReleaseDC pair. There is no problem there. But the fun begins when our window needs to repaint its client area. Since the codes that paint the character are in WM_CHAR section, the window procedure will not be able to repaint our character in the client area. So the bottom line is: put all necessary data and codes that do painting in WM_PAINT. You can send WM_PAINT message from anywhere in your code anytime you want to repaint the client area.

invoke TextOut,hdc,0,0,ADDR char,1

When InvalidateRect is called, it sends a WM_PAINT message back to the window procedure. So the codes in WM_PAINT section is called. It calls BeginPaint as usual to get the handle to device context and then call TextOut which draws our character in the client area at x=0, y=0. When you run the program and press any key, you will see that character echo in the upper left corner of the client window. And when the window is minimized and maximized again, the character is still there since all the codes and data essential to repaint are all gathered in WM_PAINT section.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 7: Mouse Input

Tutorial 7: Mouse Input

We will learn how to receive and respond to mouse input in our window procedure. The example program will wait for left mouse clicks and display a text string at the exact clicked spot in the client area.

Download the example here.

Theory:

As with keyboard input, Windows detects and sends notifications about mouse activities that are relevant to each window. Those activities include left and right clicks, mouse cursor movement over window, double clicks. Unlike keyboard input which is directed to the window that has input focus, mouse messages are sent to any window that the mouse cursor is over, active or not. In addition, there are mouse messages about the non-client area too. But most of the time, we can blissfully ignore them. We can focus on those relating to the client area.
There are two messages for each mouse button: WM_LBUTTONDOWN,WM_RBUTTONDOWN and WM_LBUTTONUP, WM_RBUTTONUP messages. For a mouse with three buttons, there are also WM_MBUTTONDOWN and WM_MBUTTONUP. When the mouse cursor moves over the client area, Windows sends WM_MOUSEMOVE messages to the window under the cursor.
A window can receive double click messages, WM_LBUTTONDBCLK or WM_RBUTTONDBCLK, if and only if its window class has CS_DBLCLKS style flag, else the window will receive only a series of mouse button up and down messages.
For all these messages, the value of lParam contains the position of the mouse. The low word is the x-coordinate, and the high word is the y-coordinate relative to upper left corner of the client area of the window. wParam indicates the state of the mouse buttons and Shift and Ctrl keys.

Example:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MouseClick db 0 ; 0=no click yet

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hitpoint POINT <>

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE

The window procedure waits for left mouse button click. When it receives WM_LBUTTONDOWN, lParam contains the coordinate of the mouse cursor in the client area. It saves the coordinate in a variable of type POINT which is defined as:

POINT STRUCT
x dd ?
y dd ?
POINT ENDS

and sets the flag, MouseClick, to TRUE, meaning that there's at least a left mouse button click in the client area.

mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax

Since x-coordinate is the low word of lParam and the members of POINT structure are 32-bit in size, we have to zero out the high word of eax prior to storing it in hitpoint.x.

shr eax,16
mov hitpoint.y,eax

Because y-coordinate is the high word of lParam, we must put it in the low word of eax prior to storing it in hitpoint.y. We do this by shifting eax 16 bits to the right.
After storing the mouse position, we set the flag, MouseClick, to TRUE in order to let the painting code in WM_PAINT section know that there's at least a click in the client area so it can draw the string at the mouse position. Next we call InvalidateRect function to force the window to repaint its entire client area.

.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF

The painting code in WM_PAINT section must check if MouseClick is true, since when the window was created, it received a WM_PAINT message which at that time, no mouse click had occurred so it should not draw the string in the client area. We initialize MouseClick to FALSE and change its value to TRUE when an actual mouse click occurs.
If at least one mouse click has occurred, it draws the string in the client area at the mouse position. Note that it calls lstrlen to get the length of the string to display and sends the length as the last parameter of TextOut function.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 8: Menu

Tutorial 8: Menu

In this tutorial, we will learn how to incorporate a menu into our window.
Download the example 1 and example 2.

Theory:

Menu is one of the most important component in your window. Menu presents a list of services a program offers to the user. The user doesn't have to read the manual included with the program to be able to use it, he can peruse the menu to get an overview of the capability of a particular program and start playing with it immediately. Since a menu is a tool to get the user up and running quickly, you should follow the standard. Succintly put, the first two menu items should be File and Edit and the last should be Help. You can insert your own menu items between Edit and Help. If a menu item invokes a dialog box, you should append an ellipsis (...) to the menu string.
Menu is a kind of resource. There are several kinds of resources such as dialog box, string table, icon, bitmap, menu etc. Resources are described in a separated file called a resource file which normally has .rc extension. You then combine the resources with the source code during the link stage. The final product is an executable file which contains both instructions and resources.
You can write resource scripts using any text editor. They're composed of phrases which describe the appearances and other attributes of the resources used in a particular program Although you can write resource scripts with a text editor, it's rather cumbersome. A better alternative is to use a resource editor which lets you visually design resources with ease. Resource editors are usually included in compiler packages such as Visual C++, Borland C++, etc.
You describe a menu resource like this:
    MyMenu MENU
    {
    [menu list here]
    }
C programmers may recognize that it is similar to declaring a structure. MyMenu being a menu name followed by MENU keyword and menu list within curly brackets. Alternatively, you can use BEGIN and END instead of the curly brackets if you wish. This syntax is more palatable to Pascal programmers.
Menu list can be either MENUITEM or POPUP statement.
MENUITEM statement defines a menu bar which doesn't invoke a popup menu when selected.The syntax is as follows:
    MENUITEM "&text", ID [,options]
It begins by MENUITEM keyword followed by the text you want to use as menu bar string. Note the ampersand. It causes the character that follows it to be underlined. Following the text string is the ID of the menu item. The ID is a number that will be used to identify the menu item in the message sent to the window procedure when the menu item is selected. As such, each menu ID must be unique among themselves.
Options are optional. Available options are as follows:
  • GRAYED The menu item is inactive, and it does not generate a WM_COMMAND message. The text is grayed.
  • INACTIVE The menu item is inactive, and it does not generate a WM_COMMAND message. The text is displayed normally.
  • MENUBREAK This item and the following items appear on a new line of the menu.
  • HELP This item and the following items are right-justified.
You can use one of the above option or combine them with "or" operator. Beware that INACTIVE and GRAYED cannot be combined together.
POPUP statement has the following syntax:
POPUP "&text" [,options]
{
[menu list]
}
POPUP statement defines a menu bar that, when selected, drops down a list of menu items in a small popup window. The menu list can be a MENUTIEM or POPUP statement. There's a special kind of MENUITEM statement, MENUITEM SEPARATOR, which will draw a horizontal line in the popup window.
The next step after you are finished with the menu resource script is to reference it in your program.
You can do this in two different places in your program.
  • In lpszMenuName member of WNDCLASSEX structure. Say, if you have a menu named "FirstMenu", you can assigned the menu to your window like this:
        • .DATA
          MenuName db "FirstMenu",0
        ...........................
        ...........................
        .CODE
          ...........................
          mov wc.lpszMenuName, OFFSET MenuName
          ...........................
  • In menu handle parameter of CreateWindowEx like this:
        • .DATA
          MenuName db "FirstMenu",0
          hMenu HMENU ?
        ...........................
        ...........................
        .CODE
          ...........................
          invoke LoadMenu, hInst, OFFSET MenuName
          mov hMenu, eax
          invoke CreateWindowEx,NULL,OFFSET ClsName,\
          OFFSET Caption, WS_OVERLAPPEDWINDOW,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          NULL,\
          hMenu,\
          hInst,\
          NULL\
          ...........................
So you may ask, what's the difference between these two methods?
When you reference the menu in the WNDCLASSEX structure, the menu becomes the "default" menu for the window class. Every window of that class will have the same menu.
If you want each window created from the same class to have different menus, you must choose the second form. In this case, any window that is passed a menu handle in its CreateWindowEx function will have a menu that "overrides" the default menu defined in the WNDCLASSEX structure.
Next we will examine how a menu notifies the window procedure when the user selects a menu item.
When the user selects a menu item, the window procedure will receive a WM_COMMAND message. The low word of wParam contains the menu ID of the selected menu item.
Now we have sufficient information to create and use a menu. Let's do it.

Example:

The first example shows how to create and use a menu by specifying the menu name in the window class.

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.const
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName ; Put our menu name here
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start
**************************************************************************************************************************

Menu.rc
**************************************************************************************************************************

#define IDM_TEST 1
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4

FirstMenu MENU
{
POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}
MENUITEM "&Test", IDM_TEST
}

Analysis:

Let's analyze the resource file first.
#define IDM_TEST 1 /* equal to IDM_TEST equ 1*/
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4
The above lines define the menu IDs used by the menu script. You can assign any value to the ID as long as the value is unique in the menu.

FirstMenu MENU

Declare your menu with MENU keyword.

POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}

Define a popup menu with four menu items, the third one is a menu separator.

MENUITEM "&Test", IDM_TEST

Define a menu bar in the main menu.
Next we will examine the source code.

MenuName db "FirstMenu",0 ; The name of our menu in the resource file.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0
MenuName is the name of the menu in the resource file. Note that you can define more than one menu in the resource file so you must specify which menu you want to use. The remaining three lines define the text strings to be displayed in message boxes that are invoked when the appropriate menu item is selected by the user.
IDM_TEST equ 1 ; Menu IDs
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4
Define menu IDs for use in the window procedure. These values MUST be identical to those defined in the resource file.

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF

In the window procedure, we process WM_COMMAND messages. When the user selects a menu item, the menu ID of that menu item is sended to the window procedure in the low word of wParam along with the WM_COMMAND message. So when we store the value of wParam in eax, we compare the value in ax to the menu IDs we defined previously and act accordingly. In the first three cases, when the user selects Test, Say Hello, and Say GoodBye menu items, we just display a text string in a message box.
If the user selects Exit menu item, we call DestroyWindow with the handle of our window as its parameter which will close our window.
As you can see, specifying menu name in a window class is quite easy and straightforward. However you can also use an alternate method to load a menu in your window. I won't show the entire source code here. The resource file is the same in both methods. There are some minor changes in the source file which I 'll show below.

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HMENU ? ; handle of our menu
Define a variable of type HMENU to store our menu handle.

invoke LoadMenu, hInst, OFFSET MenuName
mov hMenu,eax
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\
hInst,NULL

Before calling CreateWindowEx, we call LoadMenu with the instance handle and a pointer to the name of our menu. LoadMenu returns the handle of our menu in the resource file which we pass to CreateWindowEx.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 9: Child Window Controls

Tutorial 9: Child Window Controls

In this tutorial, we will explore child window controls which are very important input and output devices of our programs.

Download the example here.

Theory:

Windows provides several predefined window classes which we can readily use in our own programs. Most of the time we use them as components of a dialog box so they're usually called child window controls. The child window controls process their own mouse and keyboard messages and notify the parent window when their states have changed. They relieve the burden from programmers enormously so you should use them as much as possible. In this tutorial, I put them on a normal window just to demonstrate how you can create and use them but in reality you should put them in a dialog box.
Examples of predefined window classes are button, listbox, checkbox, radio button,edit etc.
In order to use a child window control, you must create it with CreateWindow or CreateWindowEx. Note that you don't have to register the window class since it's registered for you by Windows. The class name parameter MUST be the predefined class name. Say, if you want to create a button, you must specify "button" as the class name in CreateWindowEx. The other parameters you must fill in are the parent window handle and the control ID. The control ID must be unique among the controls. The control ID is the ID of that control. You use it to differentiate between the controls.
After the control was created, it will send messages notifying the parent window when its state has changed. Normally, you create the child windows during WM_CREATE message of the parent window. The child window sends WM_COMMAND messages to the parent window with its control ID in the low word of wParam, the notification code in the high word of wParam, and its window handle in lParam. Each child window control has different notification codes, refer to your Win32 API reference for more information.
The parent window can send commands to the child windows too, by calling SendMessage function. SendMessage function sends the specified message with accompanying values in wParam and lParam to the window specified by the window handle. It's an extremely useful function since it can send messages to any window provided you know its window handle.
So, after creating the child windows, the parent window must process WM_COMMAND messages to be able to receive notification codes from the child windows.

Example:

We will create a window which contains an edit control and a pushbutton. When you click the button, a message box will appear showing the text you typed in the edit box. There is also a menu with 4 menu items:
  1. Say Hello -- Put a text string into the edit box
  2. Clear Edit Box -- Clear the content of the edit box
  3. Get Text -- Display a message box with the text in the edit box
  4. Exit -- Close the program.
.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0
ButtonClassName db "button",0
ButtonText db "My First Button",0
EditClassName db "edit",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndButton HWND ?
hwndEdit HWND ?
buffer db 512 dup(?) ; buffer to store the text retrieved from the edit box

.const
ButtonID equ 1 ; The control ID of the button control
EditID equ 2 ; The control ID of the edit control
IDM_HELLO equ 1
IDM_CLEAR equ 2
IDM_GETTEXT equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \
ADDR AppName, WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT, CW_USEDEFAULT,\
300,200,NULL,NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
ES_AUTOHSCROLL,\
50,35,200,25,hWnd,8,hInstance,NULL
mov hwndEdit,eax
invoke SetFocus, hwndEdit
invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
75,70,140,25,hWnd,ButtonID,hInstance,NULL
mov hwndButton,eax
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

Let's analyze the program.
    .ELSEIF uMsg==WM_CREATE
    invoke CreateWindowEx,WS_EX_CLIENTEDGE, \
    ADDR EditClassName,NULL,\
    WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
    or ES_AUTOHSCROLL,\
    50,35,200,25,hWnd,EditID,hInstance,NULL
    mov hwndEdit,eax
    invoke SetFocus, hwndEdit
    invoke CreateWindowEx,NULL, ADDR ButtonClassName,\
    ADDR ButtonText,\
    WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
    75,70,140,25,hWnd,ButtonID,hInstance,NULL
    mov hwndButton,eax
We create the controls during processing of WM_CREATE message. We call CreateWindowEx with an extra window style, WS_EX_CLIENTEDGE, which makes the client area look sunken. The name of each control is a predefined one, "edit" for edit control, "button" for button control. Next we specify the child window's styles. Each control has extra styles in addition to the normal window styles. For example, the button styles are prefixed with "BS_" for "button style", edit styles are prefixed with "ES_" for "edit style". You have to look these styles up in a Win32 API reference. Note that you put a control ID in place of the menu handle. This doesn't cause any harm since a child window control cannot have a menu.
After creating each control, we keep its handle in a variable for future use.
SetFocus is called to give input focus to the edit box so the user can type the text into it immediately.
Now comes the really exciting part. Every child window control sends notification to its parent window with WM_COMMAND.

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0

Recall that a menu also sends WM_COMMAND messages to notify the window about its state too. How can you differentiate between WM_COMMAND messages originated from a menu or a control? Below is the answer

 | Low word of wParam | High word of wParam | lParam
Menu | Menu ID | 0 | 0
Control | Control ID | Notification code | Child Window Handle

You can see that you should check lParam. If it's zero, the current WM_COMMAND message is from a menu. You cannot use wParam to differentiate between a menu and a control since the menu ID and control ID may be identical and the notification code may be zero.

.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK

You can put a text string into an edit box by calling SetWindowText. You clear the content of an edit box by calling SetWindowText with NULL. SetWindowText is a general purpose API function. You can use SetWindowText to change the caption of a window or the text on a button.
To get the text in an edit box, you use GetWindowText.

.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF

The above code snippet deals with the condition when the user presses the button. First, it checks the low word of wParam to see if the control ID matches that of the button. If it is, it checks the high word of wParam to see if it is the notification code BN_CLICKED which is sent when the button is clicked.
The interesting part is after it's certain that the notification code is BN_CLICKED. We want to get the text from the edit box and display it in a message box. We can duplicate the code in the IDM_GETTEXT section above but it doesn't make sense. If we can somehow send a WM_COMMAND message with the low word of wParam containing the value IDM_GETTEXT to our own window procedure, we can avoid code duplication and simplify our program. SendMessage function is the answer. This function sends any message to any window with any wParam and lParam we want. So instead of duplicating the code, we call SendMessage with the parent window handle, WM_COMMAND, IDM_GETTEXT, and 0. This has identical effect to selecting "Get Text" menu item from the menu. The window procedure doesn't perceive any difference between the two.
You should use this technique as much as possible to make your code more organized.
Last but not least, do not forget the TranslateMessage function in the message loop. Since you must type in some text into the edit box, your program must translate raw keyboard input into readable text. If you omit this function, you will not be able to type anything into your edit box.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 10: Dialog Box as Main Window

Tutorial 10: Dialog Box as Main Window

Now comes the really interesting part about GUI, the dialog box. In this tutorial (and the next), we will learn how to use a dialog box as our main window.

Download the first example here, the second example here.

Theory:

If you play with the examples in the previous tutorial long enough, you 'll find out that you cannot change input focus from one child window control to another with Tab key. The only way you can do that is by clicking the control you want it to gain input focus. This situation is rather cumbersome. Another thing you might notice is that I changed the background color of the parent window to gray instead of normal white as in previous examples. This is done so that the color of the child window controls can blend seamlessly with the color of the client area of the parent window. There is a way to get around this problem but it's not easy. You have to subclass all child window controls in your parent window.
The reason why such inconvenience exists is that child window controls are originally designed to work with a dialog box, not a normal window. The default color of child window controls such as a button is gray because the client area of a dialog box is normally gray so they blend into each other without any sweat on the programmer's part.
Before we get deep into the detail, we should know first what a dialog box is. A dialog box is nothing more than a normal window which is designed to work with child window controls. Windows also provides internal "dialog box manager" which is responsible for most of the keyboard logic such as shifting input focus when the user presses Tab, pressing the default pushbutton if Enter key is pressed, etc so programmers can deal with higher level tasks. Dialog boxes are primarily used as input/output devices. As such a dialog box can be considered as an input/output "black box" meaning that you don't have to know how a dialog box works internally in order to be able to use it, you only have to know how to interact with it. That's a principle of object oriented programming (OOP) called information hiding. If the black box is *perfectly* designed, the user can make use of it without any knowledge on how it operates. The catch is that the black box must be perfect, that's hard to achieve in the real world. Win32 API is also designed as a black box too.
Well, it seems we stray from our path. Let's get back to our subject. Dialog boxes are designed to reduce workload of a programmer. Normally if you put child window controls on a normal window, you have to subclass them and write keyboard logic yourself. But if you put them on a dialog box, it will handle the logic for you. You only have to know how to get the user input from the dialog box or how to send commands to it.
A dialog box is defined as a resource much the same way as a menu. You write a dialog box template describing the characteristics of the dialog box and its controls and then compile the resource script with a resource editor.
Note that all resources are put together in the same resource script file. You can use any text editor to write a dialog box template but I don't recommend it. You should use a resource editor to do the job visually since arranging child window controls on a dialog box is hard to do manually. Several excellent resource editors are available. Most of the major compiler suites include their own resource editors. You can use them to create a resource script for your program and then cut out irrelevant lines such as those related to MFC.
There are two main types of dialog box: modal and modeless. A modeless dialog box lets you change input focus to other window. The example is the Find dialog of MS Word. There are two subtypes of modal dialog box: application modal and system modal. An application modal dialog box doesn't let you change input focus to other window in the same application but you can change the input focus to the window of OTHER application. A system modal dialog box doesn't allow you to change input focus to any other window until you respond to it first.
A modeless dialog box is created by calling CreateDialogParam API function. A modal dialog box is created by calling DialogBoxParam. The only distinction between an application modal dialog box and a system modal one is the DS_SYSMODAL style. If you include DS_SYSMODAL style in a dialog box template, that dialog box will be a system modal one.
You can communicate with any child window control on a dialog box by using SendDlgItemMessage function. Its syntax is like this:
SendDlgItemMessage proto hwndDlg:DWORD,\
idControl:DWORD,\
uMsg:DWORD,\
wParam:DWORD,\
lParam:DWORD
This API call is immensely useful for interacting with a child window control. For example, if you want to get the text from an edit control, you can do this:
call SendDlgItemMessage, hDlg, ID_EDITBOX, WM_GETTEXT, 256, ADDR text_buffer
In order to know which message to send, you should consult your Win32 API reference.
Windows also provides several control-specific API functions to get and set data quickly, for example, GetDlgItemText, CheckDlgButton etc. These control-specific functions are provided for programmer's convenience so he doesn't have to look up the meanings of wParam and lParam for each message. Normally, you should use control-specific API calls when they're available since they make source code maintenance easier. Resort to SendDlgItemMessage only if no control-specific API calls are available.
The Windows dialog box manager sends some messages to a specialized callback function called a dialog box procedure which has the following format:
DlgProc proto hDlg:DWORD ,\
iMsg:DWORD ,\
wParam:DWORD ,\
lParam:DWORD
The dialog box procedure is very similar to a window procedure except for the type of return value which is TRUE/FALSE instead of LRESULT. The internal dialog box manager inside Windows IS the true window procedure for the dialog box. It calls our dialog box procedure with some messages that it received. So the general rule of thumb is that: if our dialog box procedure processes a message,it MUST return TRUE in eax and if it does not process the message, it must return FALSE in eax. Note that a dialog box procedure doesn't pass the messages it does not process to the DefWindowProc call since it's not a real window procedure.
There are two distinct uses of a dialog box. You can use it as the main window of your application or use it as an input device. We 'll examine the first approach in this tutorial.
"Using a dialog box as main window" can be interpreted in two different senses.
  1. You can use the dialog box template as a class template which you register with RegisterClassEx call. In this case, the dialog box behaves like a "normal" window: it receives messages via a window procedure referred to by lpfnWndProc member of the window class, not via a dialog box procedure. The benefit of this approach is that you don't have to create child window controls yourself, Windows creates them for you when the dialog box is created. Also Windows handles the keyboard logic for you such as Tab order etc. Plus you can specify the cursor and icon of your window in the window class structure.

    2. Your program just creates the dialog box without creating any parent window. This approach makes a message loop unnecessary since the messages are sent directly to the dialog box procedure. You don't even have to register a window class!
This tutorial is going to be a long one. I'll present the first approach followed by the second.

Examples:

dialog.asm

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "DLGCLASS",0
MenuName db "MyMenu",0
DlgName db "MyDialog",0
AppName db "Our First Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hDlg:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,DLGWINDOWEXTRA
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
mov hDlg,eax
invoke ShowWindow, hDlg,SW_SHOWNORMAL
invoke UpdateWindow, hDlg
invoke GetDlgItem,hDlg,IDC_EDIT
invoke SetFocus,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.IF dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Dialog.rc

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our First Dialog Box"
CLASS "DLGCLASS"
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13, WS_GROUP
END

MyMenu MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

Analysis:

Let's analyze this first example.
This example shows how to register a dialog template as a window class and create a "window" from that class. It simplifies your program since you don't have to create the child window controls yourself.
Let's first analyze the dialog template.

MyDialog DIALOG 10, 10, 205, 60

Declare the name of a dialog, in this case, "MyDialog" followed by the keyword "DIALOG". The following four numbers are: x, y , width, and height of the dialog box in dialog box units (not the same as pixels).

STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK

Declare the styles of the dialog box.

CAPTION "Our First Dialog Box"

This is the text that will appear in the dialog box's title bar.

CLASS "DLGCLASS"

This line is crucial. It's this CLASS keyword that allows us to use the dialog box template as a window class. Following the keyword is the name of the "window class"

BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

The above block defines the child window controls in the dialog box. They're defined between BEGIN and END keywords. Generally the syntax is as follows:

    control-type "text" ,controlID, x, y, width, height [,styles]
control-types are resource compiler's constants so you have to consult the manual.
Now we go to the assembly source code. The interesting part is in the window class structure:
    mov wc.cbWndExtra,DLGWINDOWEXTRA
    mov wc.lpszClassName,OFFSET ClassName
Normally, this member is left NULL, but if we want to register a dialog box template as a window class, we must set this member to the value DLGWINDOWEXTRA. Note that the name of the class must be identical to the one following the CLASS keyword in the dialog box template. The remaining members are initialized as usual. After you fill the window class structure, register it with RegisterClassEx. Seems familiar? This is the same routine you have to do in order to register a normal window class.
    invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
After registering the "window class", we create our dialog box. In this example, I create it as a modeless dialog box with CreateDialogParam function. This function takes 5 parameters but you only have to fill in the first two: the instance handle and the pointer to the name of the dialog box template. Note that the 2nd parameter is not a pointer to the class name.
At this point, the dialog box and its child window controls are created by Windows. Your window procedure will receive WM_CREATE message as usual.
    invoke GetDlgItem,hDlg,IDC_EDIT
    invoke SetFocus,eax
After the dialog box is created, I want to set the input focus to the edit control. If I put these codes in WM_CREATE section, GetDlgItem call will fail since at that time, the child window controls are not created yet. The only way you can do this is to call it after the dialog box and all its child window controls are created. So I put these two lines after the UpdateWindow call. GetDlgItem function gets the control ID and returns the associated control's window handle. This is how you can get a window handle if you know its control ID.

invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF

The program enters the message loop and before we translate and dispatch messages, we call IsDialogMessage function to let the dialog box manager handles the keyboard logic of our dialog box for us. If this function returns TRUE , it means the message is intended for the dialog box and is processed by the dialog box manager. Note another difference from the previous tutorial. When the window procedure wants to get the text from the edit control, it calls GetDlgItemText function instead of GetWindowText. GetDlgItemText accepts a control ID instead of a window handle. That makes the call easier in the case you use a dialog box.

Now let's go to the second approach to using a dialog box as a main window. In the next example, I 'll create an application modal dialog box. You'll not find a message loop or a window procedure because they're not necessary!
dialog.asm (part 2)

.386
.model flat,stdcall
option casemap:none

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
DlgName db "MyDialog",0
AppName db "Our Second Dialog Box",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL
invoke ExitProcess,eax

DlgProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSEIF ax==IDM_EXIT
invoke EndDialog, hWnd,NULL
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.if dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
mov eax,FALSE
ret
.ENDIF
mov eax,TRUE
ret
DlgProc endp
end start

dialog.rc (part 2)

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDR_MENU1 3003

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our Second Dialog Box"
MENU IDR_MENU1
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Say Hello", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

IDR_MENU1 MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

The analysis follows:

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

We declare the function prototype for DlgProc so we can refer to it with addr operator in the line below:

invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL

The above line calls DialogBoxParam function which takes 5 parameters: the instance handle, the name of the dialog box template, the parent window handle, the address of the dialog box procedure, and the dialog-specific data. DialogBoxParam creates a modal dialog box. It will not return until the dialog box is destroyed.

.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0

The dialog box procedure looks like a window procedure except that it doesn't receive WM_CREATE message. The first message it receives is WM_INITDIALOG. Normally you can put the initialization code here. Note that you must return the value TRUE in eax if you process the message.
The internal dialog box manager doesn't send our dialog box procedure the WM_DESTROY message by default when WM_CLOSE is sent to our dialog box. So if we want to react when the user presses the close button on our dialog box, we must process WM_CLOSE message. In our example, we send WM_COMMAND message with the value IDM_EXIT in wParam. This has the same effect as when the user selects Exit menu item. EndDialog is called in response to IDM_EXIT.
The processing of WM_COMMAND messages remains the same.
When you want to destroy the dialog box, the only way is to call EndDialog function. Do not try DestroyWindow! EndDialog doesn't destroy the dialog box immediately. It only sets a flag for the internal dialog box manager and continues to execute the next instructions.
Now let's examine the resource file. The notable change is that instead of using a text string as menu name we use a value, IDR_MENU1. This is necessary if you want to attach a menu to a dialog box created with DialogBoxParam. Note that in the dialog box template, you have to add the keyword MENU followed by the menu resource ID.
A difference between the two examples in this tutorial that you can readily observe is the lack of an icon in the latter example. However, you can set the icon by sending the message WM_SETICON to the dialog box during WM_INITDIALOG.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 10: More about Dialog Box

Tutorial 11: More about Dialog Box

We will learn more about dialog box in this tutorial. Specifically, we will explore the topic of how to use dialog boxs as our input-output devices. If you read the previous tutorial, this one will be a breeze since only a minor modification is all that's needed to be able to use dialog boxes as adjuncts to our main window. Also in this tutorial, we will learn how to use common dialog boxes.

Download the dialog box examples here and here. Download Common Dialog Box example here.

Theory:

Very little is to be said about how to use dialog boxes as input-output devices of our program. Your program creates the main window as usual and when you want to display the dialog box, just call CreateDialogParam or DialogBoxParam. With DialogBoxParam call, you don't have to do anything more, just process the messages in the dialog box procedure. With CreateDialogParam, you must insert IsDialogMessage call in the message loop to let dialog box manager handle the keyboard navigation in your dialog box for you. Since the two cases are trivial, I'll not put the source code here. You can download the examples and examine them yourself, here and here.
Let's go on to the common dialog boxes. Windows has prepared predefined dialog boxes for use by your applications. These dialog boxes exist to provide standardized user interface. They consist of file, print, color, font, and search dialog boxes. You should use them as much as possible. The dialog boxes reside in comdlg32.dll. In order to use them, you have to link to comdlg32.lib. You create these dialog boxes by calling appropriate functions in the common dialog library. For open file dialog, it is GetOpenFileName, for save as dialog it is GetSaveFileName, for print dialog it is PrintDlg and so on. Each one of these functions takes a pointer to a structure as its parameter. You should look them up in Win32 API reference. In this tutorial, I'll demonstrate how to create and use an open file dialog.
Below is the function prototype of GetOpenFileName function:
GetOpenFileName proto lpofn:DWORD
You can see that it receives only one parameter, a pointer to an OPENFILENAME structure. The return value TRUE means the user selected a file to open, it's FALSE otherwise. We will look at OPENFILENAME structure next.
OPENFILENAME STRUCT
lStructSize DWORD ?
hwndOwner HWND ?
hInstance HINSTANCE ?
lpstrFilter LPCSTR ?
lpstrCustomFilter LPSTR ?
nMaxCustFilter DWORD ?
nFilterIndex DWORD ?
lpstrFile LPSTR ?
nMaxFile DWORD ?
lpstrFileTitle LPSTR ?
nMaxFileTitle DWORD ?
lpstrInitialDir LPCSTR ?
lpstrTitle LPCSTR ?
Flags DWORD ?
nFileOffset WORD ?
nFileExtension WORD ?
lpstrDefExt LPCSTR ?
lCustData LPARAM ?
lpfnHook DWORD ?
lpTemplateName LPCSTR ?
OPENFILENAME ENDS
Let's see the meaning of the frequently used members.
lStructSize | The size of the OPENFILENAME structure , in bytes
hwndOwner | The window handle of the open file dialog box.
hInstance | Instance handle of the application that creates the open file dialog box
lpstrFilter | The filter strings in the format of pairs of null terminated strings. The first string in each pair is the description. The second string is the filter pattern. for example: FilterString db "All Files (*.*)",0, "*.*",0 db "Text Files (*.txt)",0,"*.txt",0,0 Note that only the pattern in the second string in each pair is actually used by Windows to filter out the files. Also noted that you have to put an extra 0 at the end of the filter strings to denote the end of it.
nFilterIndex | Specify which pair of the filter strings will be initially used when the open file dialog is first displayed. The index is 1-based, that is the first pair is 1, the second pair is 2 and so on. So in the above example, if we specify nFilterIndex as 2, the second pattern, "*.txt" will be used.
lpstrFile | Pointer to the buffer that contains the filename used to initialize the filename edit control on the dialog box. The buffer should be at least 260 bytes long. After the user selects a file to open, the filename with full path is stored in this buffer. You can extract the information from it later.
nMaxFile | The size of the lpstrFile buffer.
lpstrTitle | Pointer to the title of the open file dialog box
Flags | Determine the styles and characteristics of the dialog box.
nFileOffset | After the user selects a file to open, this member contains the index to the first character of the actual filename. For example, if the full name with path is "c:\windows\system\lz32.dll", the this member will contain the value 18.
nFileExtension | After the user selects a file to open, this member contains the index to the first character of the file extension

Example:

The following program displays an open file dialog box when the user selects File-> Open from the menu. When the user selects a file in the dialog box, the program displays a message box showing the full name, filename,and extension of the selected file.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_EXIT equ 2
MAXSIZE equ 260
OUTPUTSIZE equ 512

.data
ClassName db "SimpleWinClass",0
AppName db "Our Main Window",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
OurTitle db "-=Our First Open File Dialog Box=-: Choose the file to open",0
FullPathName db "The Full Filename with Path is: ",0
FullName db "The Filename is: ",0
ExtensionName db "The Extension is: ",0
OutputString db OUTPUTSIZE dup(0)
CrLf db 0Dh,0Ah,0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if ax==IDM_OPEN
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
mov ofn.lpstrTitle, OFFSET OurTitle
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset ExtensionName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileExtension
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK
invoke RtlZeroMemory,offset OutputString,OUTPUTSIZE
.endif
.else
invoke DestroyWindow, hWnd
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance

We fill in the routine members of ofn structures.

mov ofn.lpstrFilter, OFFSET FilterString

This FilterString is the filename filter that we specify as follows:

FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
Note that All four strings are zero terminated. The first string is the description of the following string. The actual pattern is the even number string, in this case, "*.*" and "*.txt". Actually we can specify any pattern we want here. We MUST put an extra zero after the last pattern string to denote the end of the filter string. Don't forget this else your dialog box will behave strangely.

mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

We specify where the dialog box will put the filename that the user selects. Note that we must specify its size in nMaxFile member. We can later extract the filename from this buffer.

mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

Flags specifies the characteristics of the dialog box.
OFN_FILEMUSTEXIST and OFN_PATHMUSTEXIST flags demand that the filename and path that the user types in the filename edit control MUST exist.
OFN_LONGNAMES flag tells the dialog box to show long filenames.
OFN_EXPLORER flag specifies that the appearance of the dialog box must be explorer-like.
OFN_HIDEREADONLY flag hides the read-only checkbox on the dialog box.
There are many more flags that you can use. Consult your Win32 API reference.

mov ofn.lpstrTitle, OFFSET OurTitle

Specify the title of the dialog box.

invoke GetOpenFileName, ADDR ofn

Call the GetOpenFileName function. Passing the pointer to the ofn structure as its parameter.
At this time, the open file dialog box is displayed on the screen. The function will not return until the user selects a file to open or presses the cancel button or closes the dialog box.
It 'll return the value TRUE in eax if the user selects a file to open. It returns FALSE otherwise.

.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName

In case the user selects a file to open, we prepare an output string to be displayed in a message box. We allocate a block of memory in OutputString variable and then we use an API function, lstrcat, to concatenate the strings together. In order to put the strings into several lines, we must separate each line with a carriage return-line feed pair.

mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax

The above lines require some explanation. nFileOffset contains the index into the ofn.lpstrFile. But you cannot add them together directly since nFileOffset is a WORD-sized variable and lpstrFile is a DWORD-sized one. So I have to put the value of nFileOffset into the low word of ebx and add it to the value of lpstrFile.

invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK

We display the string in a message box.

invoke RtlZerolMemory,offset OutputString,OUTPUTSIZE

We must *clear* the OutputString before we can fill in another string. So we use RtlZeroMemory function to do the job.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 12: Memory Management and File I/O

Tutorial 12: Memory Management and File I/O

We will learn the rudimentary of memory management and file i/o operation in this tutorial. In addition we'll use common dialog boxes as input-output devices.

Download the example here.

Theory:

Memory management under Win32 from the application's point of view is quite simple and straightforward. Each process owns a 4 GB memory address space. The memory model used is called flat memory model. In this model, all segment registers (or selectors) point to the same starting address and the offset is 32-bit so an application can access memory at any point in its own address space without the need to change the value of selectors. This simplifies memory management a lot. There's no "near" or "far" pointer anymore.
Under Win16, there are two main categories of memory API functions: Global and Local. Global-type API calls deal with memory allocated in other segments thus they're "far" memory functions. Local-type API calls deal with the local heap of the process so they're "near" memory functions. Under Win32, these two types are identical. Whether you call GlobalAlloc or LocalAlloc, you get the same result.
Steps in allocating and using memory are as follows:
  1. Allocate a block of memory by calling GlobalAlloc. This function returns a handle to the requested memory block.
  2. "Lock" the memory block by calling GlobalLock. This function accepts a handle to the memory block and returns a pointer to the memory block.
  3. You can use the pointer to read or write memory.
  4. "Unlock" the memory block by calling GlobalUnlock . This function invalidates the pointer to the memory block.
  5. Free the memory block by calling GlobalFree. This function accepts the handle to the memory block.
You can also substitute "Global" by "Local" such as LocalAlloc, LocalLock,etc.
The above method can be further simplified by using a flag in GlobalAlloc call, GMEM_FIXED. If you use this flag, the return value from Global/LocalAlloc will be the pointer to the allocated memory block, not the memory block handle. You don't have to call Global/LocalLock and you can pass the pointer to Global/LocalFree without calling Global/LocalUnlock first. But in this tutorial, I'll use the "traditional" approach since you may encounter it when reading the source code of other programs.

File I/O under Win32 bears remarkable semblance to that under DOS. The steps needed are the same. You only have to change interrupts to API calls and it's done. The required steps are the followings:

  1. Open or Create the file by calling CreateFile function. This function is very versatile: in addition to files, it can open communication ports, pipes, disk drives or console. On success, it returns a handle to file or device. You can then use this handle to perform operations on the file or device.

    2. Move the file pointer to the desired location by calling SetFilePointer.
  2. Perform read or write operation by calling ReadFile or WriteFile. These functions transfer data from a block of memory to or from the file. So you have to allocate a block of memory large enough to hold the data.
  3. Close the file by calling CloseHandle. This function accepts the file handle.

Content:

The program listed below displays an open file dialog box. It lets the user select a text file to open and shows the content of that file in an edit control in its client area. The user can modify the text in the edit control as he wishes, and can choose to save the content in a file.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260
MEMSIZE equ 65535

EditID equ 1 ; ID of the edit control

.data
ClassName db "Win32ASMEditClass",0
AppName db "Win32 ASM Edit",0
EditClass db "edit",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndEdit HWND ? ; Handle to the edit control
hFile HANDLE ? ; File handle
hMemory HANDLE ? ;handle to the allocated memory block
pMemory DWORD ? ;pointer to the allocated memory block
SizeReadWrite DWORD ? ; number of bytes actually read or write

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,hwndEdit
;==============================================
; Initialize the members of OPENFILENAME structure
;==============================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax

In WM_CREATE section, we create an edit control. Note that the parameters that specify x, y, width,height of the control are all zeroes since we will resize the control later to cover the whole client area of the parent window.
Note that in this case, we don't have to call ShowWindow to make the edit control appear on the screen because we include WS_VISIBLE style. You can use this trick in the parent window too.

;==============================================
; Initialize the members of OPENFILENAME structure
;==============================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

After creating the edit control, we take this time to initialize the members of ofn. Because we want to reuse ofn in the save as dialog box too, we fill in only the *common* members that're used by both GetOpenFileName and GetSaveFileName.
WM_CREATE section is a great place to do once-only initialization.

.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE

We receive WM_SIZE messages when the size of the client area of our main window changes. We also receive it when the window is first created. In order to be able to receive this message, the window class styles must include CS_VREDRAW and CS_HREDRAW styles. We use this opportunity to resize our edit control to the same size as the client area of the parent window. First we have to know the current width and height of the client area of the parent window. We get this info from lParam. The high word of lParam contains the height and the low word of lParam the width of the client area. We then use the information to resize the edit control by calling MoveWindow function which, in addition to changing the position of the window, can alter the size too.

.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn

When the user selects File/Open menu item, we fill in the Flags member of ofn structure and call GetOpenFileName function to display the open file dialog box.

.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax

After the user selects a file to open, we call CreateFile to open the file. We specifies that the function should try to open the file for read and write. After the file is opened, the function returns the handle to the opened file which we store in a global variable for future use. This function has the following syntax:

CreateFile proto lpFileName:DWORD,\
dwDesiredAccess:DWORD,\
dwShareMode:DWORD,\
lpSecurityAttributes:DWORD,\
dwCreationDistribution:DWORD\,
dwFlagsAndAttributes:DWORD\,
hTemplateFile:DWORD

dwDesiredAccess specifies which operation you want to perform on the file.

  • 0 Open the file to query its attributes. You have to rights to read or write the data.
  • GENERIC_READ Open the file for reading.
  • GENERIC_WRITE Open the file for writing.
dwShareMode specifies which operation you want to allow other processes to perform on the file that 's being opened.
  • 0 Don't share the file with other processes.
  • FILE_SHARE_READ allow other processes to read the data from the file being opened
  • FILE_SHARE_WRITE allow other processes to write data to the file being opened.
lpSecurityAttributes has no significance under Windows 95.
dwCreationDistribution specifies the action CreateFile will perform when the file specified in lpFileName exists or when it doesn't exist.
  • CREATE_NEW Creates a new file. The function fails if the specified file already exists.
  • CREATE_ALWAYS Creates a new file. The function overwrites the file if it exists.
  • OPEN_EXISTING Opens the file. The function fails if the file does not exist.
  • OPEN_ALWAYS Opens the file, if it exists. If the file does not exist, the function creates the file as if dwCreationDistribution were CREATE_NEW.
  • TRUNCATE_EXISTING Opens the file. Once opened, the file is truncated so that its size is zero bytes. The calling process must open the file with at least GENERIC_WRITE access. The function fails if the file does not exist.
dwFlagsAndAttributes specifies the file attributes
  • FILE_ATTRIBUTE_ARCHIVE The file is an archive file. Applications use this attribute to mark files for backup or removal.
  • FILE_ATTRIBUTE_COMPRESSED The file or directory is compressed. For a file, this means that all of the data in the file is compressed. For a directory, this means that compression is the default for newly created files and subdirectories.
  • FILE_ATTRIBUTE_NORMAL The file has no other attributes set. This attribute is valid only if used alone.
  • FILE_ATTRIBUTE_HIDDEN The file is hidden. It is not to be included in an ordinary directory listing.
  • FILE_ATTRIBUTE_READONLY The file is read only. Applications can read the file but cannot write to it or delete it.
  • FILE_ATTRIBUTE_SYSTEM The file is part of or is used exclusively by the operating system.
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax

When the file is opened, we allocate a block of memory for use by ReadFile and WriteFile functions. We specify GMEM_MOVEABLE flag to let Windows move the memory block around to consolidate memory. GMEM_ZEROINIT flag tells GlobalAlloc to fill the newly allocated memory block with zeroes.
When GlobalAlloc returns successfully, eax contains the handle to the allocated memory block. We pass this handle to GlobalLock function which returns a pointer to the memory block.

invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory

When the memory block is ready for use, we call ReadFile function to read data from the file. When a file is first opened or created, the file pointer is at offset 0. So in this case, we start reading from the first byte in the file onwards. The first parameter of ReadFile is the handle of the file to read, the second is the pointer to the memory block to hold the data, next is the number of bytes to read from the file, the fourth param is the address of the variable of DWORD size that will be filled with the number of bytes actually read from the file.
After we fill the memory block with the data, we put the data into the edit control by sending WM_SETTEXT message to the edit control with lParam containing the pointer to the memory block. After this call, the edit control shows the data in its client area.

invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif

At this point, we have no need to keep the file open any longer since our purpose is to write the modified data from the edit control to another file, not the original file. So we close the file by calling CloseHandle with the file handle as its parameter. Next we unlock the memory block and free it. Actually you don't have to free the memory at this point, you can reuse the memory block during the save operation later. But for demonstration purpose , I choose to free it here.

invoke SetFocus,hwndEdit

When the open file dialog box is displayed on the screen, the input focus shifts to it. So after the open file dialog is closed, we must move the input focus back to the edit control.
This end the read operation on the file. At this point, the user can edit the content of the edit control.And when he wants to save the data to another file, he must select File/Save as menuitem which displays a save as dialog box. The creation of the save as dialog box is not much different from the open file dialog box. In fact, they differ in only the name of the functions, GetOpenFileName and GetSaveFileName. You can reuse most members of the ofn structure too except the Flags member.

mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

In our case, we want to create a new file, so OFN_FILEMUSTEXIST and OFN_PATHMUSTEXIST must be left out else the dialog box will not let us create a file that doesn't already exist.
The dwCreationDistribution parameter of the CreateFile function must be changed to CREATE_NEW since we want to create a new file.
The remaining code is identical to those in the open file section except the following:

invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL

We send WM_GETTEXT message to the edit control to copy the data from it to the memory block we provide, the return value in eax is the length of the data inside the buffer. After the data is in the memory block, we write them to the new file.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 13: Memory Mapped Files

Tutorial 13: Memory Mapped Files

I'll show you what memory mapped files are and how to use them to your advantages. Using a memory mapped file is quite easy as you'll see in this tutorial.

Download the example here.

Theory:

If you examine the example in the previous tutorial closely, you'll find that it has a serious shortcoming: what if the file you want to read is larger than the allocated memory block? or what if the string you want to search for is cut off in half at the end of the memory block? The traditional answer for the first question is that you should repeatedly read in the data from the file until the end of file is encountered. The answer to the second question is that you should prepare for the special case at the end of the memory block. This is called a boundary value problem. It presents headaches to programmers and causes innumerable bugs.
It would be nice if we can allocate a very large block of memory, enough to store the whole file but our program would be a resource hog. File mapping to the rescue. By using file mapping, you can think of the whole file as being already loaded into memory and you can use a memory pointer to read or write data from the file. As easy as that. No need to use memory API functions and separate File I/O API functions anymore, they are one and the same under file mapping. File mapping is also used as a means to share data between processes. Using file mapping in this way, there's no actual file involved. It's more like a reserved memory block that every process can *see*. But sharing data between processes is a delicate subject, not to be treated lightly. You have to implement process and thread synchronization else your applications will crash in very short order.
We will not touch the subject of file mapping as a means to create a shared memory region in this tutorial. We'll concentrate on how to use file mapping as a means to "map" a file into memory. In fact, the PE loader uses file mapping to load executable files into memory. It is very convenient since only the necessary portions can be selectively read from the file on the disk. Under Win32, you should use file mapping as much as possible.
There are some limitation to file mapping though. Once you create a memory mapped file, its size cannot be changed during that session. So file mapping is great for read-only files or file operations that don't affect the file size. That doesn't mean that you cannot use file mapping if you want to increase the file size. You can estimate the new size and create the memory mapped file based on the new size and the file will grow to that size. It's just inconvenient, that's all.
Enough for the explanation. Let's dive into implementation of file mapping. In order to use file mapping, these steps must be performed:
  1. call CreateFile to open the file you want to map.
  2. call CreateFileMapping with the file handle returned by CreateFile as one of its parameter. This function creates a file mapping object from the file opened by CreateFile.
  3. call MapViewOfFile to map a selected file region or the whole file to memory. This function returns a pointer to the first byte of the mapped file region.
  4. Use the pointer to read or write the file
  5. call UnmapViewOfFile to unmap the file.
  6. call CloseHandle with the handle to the mapped file as the parameter to close the mapped file.
  7. call CloseHandle again this time with the file handle returned by CreateFile to close the actual file.

Example:

The program listed below lets you open a file via an open file dialog box. It opens the file using file mapping, if it's successful, the window caption is changed to the name of the opened file. You can save the file in another name by select File/Save as menuitem. The program will copy the whole content of the opened file to the new file. Note that you don't have to call GlobalAlloc to allocate a memory block in this program.

.386
.model flat,stdcall
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260

.data
ClassName db "Win32ASMFileMappingClass",0
AppName db "Win32 ASM File Mapping Example",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
hMapFile HANDLE 0 ; Handle to the memory mapped file, must be
;initialized with 0 because we also use it as
;a flag in WM_DESTROY section too

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hFileRead HANDLE ? ; Handle to the source file
hFileWrite HANDLE ? ; Handle to the output file
hMenu HANDLE ?
pMemory DWORD ? ; pointer to the data in the source file
SizeWritten DWORD ? ; number of bytes actually written by WriteFile

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke GetMenu,hWnd ;Obtain the menu handle
mov hMenu,eax
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileRead,eax
invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
.endif
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileWrite,eax
invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax
invoke GetFileSize,hFileRead,NULL
invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL
invoke UnmapViewOfFile,pMemory
call CloseMapFile
invoke CloseHandle,hFileWrite
invoke SetWindowText,hWnd,ADDR AppName
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED
.endif
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

end start

Analysis:

invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL

When the user selects a file in the open file dialog, we call CreateFile to open it. Note that we specify GENERIC_READ to open this file for read-only access and dwShareMode is zero because we don't want some other process to modify the file during our operation.

invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL

Then we call CreateFileMapping to create a memory mapped file from the opened file. CreateFileMapping has the following syntax:

CreateFileMapping proto hFile:DWORD,\
lpFileMappingAttributes:DWORD,\
flProtect:DWORD,\
dwMaximumSizeHigh:DWORD,\
dwMaximumSizeLow:DWORD,\
lpName:DWORD

You should know first that CreateFileMapping doesn't have to map the whole file to memory. You can use this function to map only a part of the actual file to memory. You specify the size of the memory mapped file in dwMaximumSizeHigh and dwMaximumSizeLow params. If you specify the size that 's larger than the actual file, the actual file will be expanded to the new size. If you want the memory mapped file to be the same size as the actual file, put zeroes in both params.
You can use NULL in lpFileMappingAttributes parameter to have Windows creates a memory mapped file with default security attributes.
flProtect defines the protection desired for the memory mapped file. In our example, we use PAGE_READONLY to allow only read operation on the memory mapped file. Note that this attribute must not contradict the attribute used in CreateFile else CreateFileMapping will fail.
lpName points to the name of the memory mapped file. If you want to share this file with other process, you must provide it a name. But in our example, our process is the only one that uses this file so we ignore this parameter.

mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax

If CreateFileMapping is successful, we change the window caption to the name of the opened file. The filename with full path is stored in buffer, we want to display only the filename in the caption so we must add the value of nFileOffset member of the OPENFILENAME structure to the address of buffer.

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED

As a precaution, we don't want the user to open multiple files at once, so we gray out the Open menu item and enable the Save menu item. EnableMenuItem is used to change the attribute of menu item.
After this, we wait for the user to select File/Save as menu item or close our program. If the user chooses to close our program, we must close the memory mapped file and the actual file like the code below:

.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL

In the above code snippet, when the window procedure receives the WM_DESTROY message, it checks the value of hMapFile first whether it is zero or not. If it's not zero, it calls CloseMapFile function which contains the following code:

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

CloseMapFile closes the memory mapped file and the actual file so that there 'll be no resource leakage when our program exits to Windows.
If the user chooses to save that data to another file, the program presents him with a save as dialog box. After he types in the name of the new file, the file is created by CreateFile function.

invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax

Immediately after the output file is created, we call MapViewOfFile to map the desired portion of the memory mapped file into memory. This function has the following syntax:

MapViewOfFile proto hFileMappingObject:DWORD,\
dwDesiredAccess:DWORD,\
dwFileOffsetHigh:DWORD,\
dwFileOffsetLow:DWORD,\
dwNumberOfBytesToMap:DWORD

dwDesiredAccess specifies what operation we want to do to the file. In our example, we want to read the data only so we use FILE_MAP_READ.
dwFileOffsetHigh and dwFileOffsetLowspecify the starting file offset of the file portion that you want to map into memory. In our case, we want to read in the whole file so we start mapping from offset 0 onwards.
dwNumberOfBytesToMap specifies the number of bytes to map into memory. If you want to map the whole file (specified by CreateFileMapping), pass 0 to MapViewOfFile.
After calling MapViewOfFile, the desired portion is loaded into memory. You'll be given the pointer to the memory block that contains the data from the file.

invoke GetFileSize,hFileRead,NULL

Find out how large the file is. The file size is returned in eax. If the file is larger than 4 GB, the high DWORD of the file size is stored in FileSizeHighWord. Since we don't expect to handle such large file, we can ignore it.

invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL

Write the data that is mapped into memory into the output file.

invoke UnmapViewOfFile,pMemory

When we're through with the input file, unmap it from memory.

call CloseMapFile
invoke CloseHandle,hFileWrite

And close all the files.

invoke SetWindowText,hWnd,ADDR AppName

Restore the original caption text.

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED

Enable the Open menu item and gray out the Save As menu item.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 14: Process

Tutorial 14: Process

We will learn what a process is and how to create and terminate it.

Download the example here.

Preliminary:

What is a process? I quote this definition from Win32 API reference:
"A process is an executing application that consists of a private virtual address space, code, data, and other operating system resources, such as files, pipes, and synchronization objects that are visible to the process."
As you can see from the definition above, a process "owns" several objects: the address space, the executing module(s), and anything that the executing modules create or open. At the minimum, a process must consist of an executing module, a private address space and a thread. Every process must have at least one thread. What's a thread? A thread is actually an execution queue. When Windows first creates a process, it creates only one thread per process. This thread usually starts execution from the first instruction in the module. If the process later needs more threads, it can explicitly create them.
When Windows receives a command to create a process, it creates the private memory address space for the process and then it maps the executable file into the space. After that it creates the primary thread for the process.
Under Win32, you can also create processes from your own programs by calling CreateProcess function. CreateProcess has the following syntax:

CreateProcess proto lpApplicationName:DWORD,\
lpCommandLine:DWORD,\
lpProcessAttributes:DWORD,\
lpThreadAttributes:DWORD,\
bInheritHandles:DWORD,\
dwCreationFlags:DWORD,\
lpEnvironment:DWORD,\
lpCurrentDirectory:DWORD,\
lpStartupInfo:DWORD,\
lpProcessInformation:DWORD

Don't be alarmed by the number of parameters. We can ignore most of them.

lpApplicationName --> The name of the executable file with or without pathname that you want to execute. If this parameter is null, you must provide the name of the executable file in the lpCommandLine parameter.
lpCommandLine --> The command line arguments to the program you want to execute. Note that if the lpApplicationName is NULL, this parameter must contain the name of the executable file too. Like this: "notepad.exe readme.txt"
lpProcessAttributes and lpthreadAttributes --> Specify the security attributes for the process and the primary thread. If they're NULLs, the default security attributes are used.
bInheritHandles --> A flag that specify if you want the new process to inherit all opened handles from your process.
dwCreationFlags --> Several flags that determine the behavior of the process you want to created, such as, do you want to process to be created but immediately suspended so that you can examine or modify it before it runs? You can also specify the priority class of the thread(s) in the new process. This priority class is used to determine the scheduling priority of the threads within the process. Normally we use NORMAL_PRIORITY_CLASS flag.
lpEnvironment --> A pointer to the environment block that contains several environment strings for the new process. If this parameter is NULL, the new process inherits the environment block from the parent process.
lpCurrentDirectory --> A pointer to the string that specifies the current drive and directory for the child process. NULL if you want the child process to inherit from the parent process.
lpStartupInfo --> Points to a STARTUPINFO structure that specifies how the main window for the new process should appear. The STARTUPINFO structure contains many members that specifies the appearance of the main window of the child process. If you don't want anything special, you can fill the STARTUPINFO structure with the values from the parent process by calling GetStartupInfo function.
lpProcessInformation --> Points to a PROCESS_INFORMATION structure that receives identification information about the new process. The PROCESS_INFORMATION structure has the following members:

PROCESS_INFORMATION STRUCT
hProcess HANDLE ? ; handle to the child process
hThread HANDLE ? ; handle to the primary thread of the child process
dwProcessId DWORD ? ; ID of the child process
dwThreadId DWORD ? ; ID of the primary thread of the child process
PROCESS_INFORMATION ENDS
Process handle and process ID are two different things. A process ID is a unique identifier for the process in the system. A process handle is a value returned by Windows for use with other process-related API functions. A process handle cannot be used to identify a process since it's not unique.

After the CreateProcess call, a new process is created and the CreateProcess call return immediately. You can check if the new process is still active by calling GetExitCodeProcess function which has the following syntax:

GetExitCodeProcess proto hProcess:DWORD, lpExitCode:DWORD

If this call is successful, lpExitCode contains the termination status of the process in question. If the value in lpExitCode is equal to STILL_ACTIVE, then that process is still running.

You can forcibly terminate a process by calling TerminateProcess function. It has the following syntax:

TerminateProcess proto hProcess:DWORD, uExitCode:DWORD

You can specify the desired exit code for the process, any value you like. TerminateProcess is not a clean way to terminate a process since any dll attached to the process will not be notified that the process was terminated.

Example:

The following example will create a new process when the user selects the "create process" menu item. It will attempt to execute "msgbox.exe". If the user wants to terminate the new process, he can select the "terminate process" menu item. The program will check first if the new process is already destroyed, if it is not, the program will call TerminateProcess function to destroy the new process.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_PROCESS equ 1
IDM_TERMINATE equ 2
IDM_EXIT equ 3

.data
ClassName db "Win32ASMProcessClass",0
AppName db "Win32 ASM Process Example",0
MenuName db "FirstMenu",0
processInfo PROCESS_INFORMATION <>
programname db "msgbox.exe",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HANDLE ?
ExitCode DWORD ? ; contains the process exitcode status from GetExitCodeProcess call.

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL startInfo:STARTUPINFO
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread
.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analysis:

The program creates the main window and retrieves the menu handle for future use. It then waits for the user to select a command from the menu. When the user selects "Process" menu item in the main menu, we process WM_INITMENUPOPUP message to modify the menu items inside the popup menu before it's displayed.

.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif

Why do we want to process this message? Because we want to prepare the menu items in the popup menu before the user can see them. In our example, if the new process is not started yet, we want to enable the "start process" and gray out the "terminate process" menu items. We do the reverse if the new process is already active.
We first check if the new process is still running by calling GetExitCodeProcess function with the process handle that was filled in by CreateProcess function. If GetExitCodeProcess returns FALSE, it means the process is not started yet so we gray out the "terminate process" menu item. If GetExitCodeProcess returns TRUE, we know that a new process has been started, but we have to check further if it is still running. So we compare the value in ExitCode to the value STILL_ACTIVE, if they're equal, the process is still running: we must gray out the "start process" menu item since we don't want to start several concurrent processes.

.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread

When the user selects "start process" menu item, we first check if hProcess member of PROCESS_INFORMATION structure is already closed. If this is the first time, the value of hProcess will always be zero since we define PROCESS_INFORMATION structure in .data section. If the value of hProcess member is not 0, it means the child process has exited but we haven't closed its process handle yet. So this is the time to do it.
We call GetStartupInfo function to fill in the startupinfo structure that we will pass to CreateProcess function. After that we call CreateProcess function to start the new process. Note that I haven't checked the return value of CreateProcess because it will make the example more complex. In real life, you should check the return value of CreateProcess. Immediately after CreateProcess, we close the primary thread handle returned in processInfo structure. Closing the handle doesn't mean we terminate the thread, only that we don't want to use the handle to refer to the thread from our program. If we don't close it, it will cause a resource leak.

.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0

When the user selects "terminate process" menu item, we check if the new process is still active by calling GetExitCodeProcess function. If it's still active, we call TerminateProcess function to kill the process. Also we close the child process handle since we have no need for it anymore.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 15: Multithreading Programming

Tutorial 15: Multithreading Programming

We will learn how to create a multithreading program in this tutorial. We also study the communication methods between the threads.

Download the example here.

Theory:

In previous tutorial, you learn the a process consists of at least one thread: the primary thread. A thread is a chain of execution. You can also create additional threads in your program. You can view multithreading as multitasking within one program. In term of implementation, a thread is a function that runs concurrently with the main program. You can run several instances of the same function or you can run several functions simultaneously depending on your requirement. Multithreading is specific to Win32, no Win16 counterpart exists.
Threads run in the same process so they can access any resources in the process such as global variables, handles etc. However, each thread has its own stack so local variables in each thread are private. Each thread also owns its private register set so when Windows switches to other threads, the thread can "remember" its last status and can "resume" the task when it gains control again. This is handled internally by Windows.
We can divide threads into two caterories:
  1. User interface thread: This type of thread creates its own window so it receives windows messages. It can respond to the user via its own window hence the name. This type of thread is subject to Win16 Mutex rule which allows only one user interface thread in 16-bit user and gdi kernel. While a user interface thread is executing code in 16-bit user and gdi kernel, other UI threads cannot use the service of the 16-bit user and gdi kernel. Note that this Win16 Mutex is specific to Windows 95 since underneath, Windows 95 API functions thunk down to 16-bit code. Windows NT has no Win16 Mutex so the user interface threads under NT work more smoothly than under Windows 95.
  2. Worker thread: This type of thread does not create a window so it cannot receive any windows message. It exists primarily to do the assigned job in the background hence the name worker thread.
I advise the following strategy when using multithreading capability of Win32: Let the primary thread do user interface stuff and the other threads do the hard work in the background. In this way, the primary thread is like a Governor, other threads are like the Governor's staff. The Governor delegates jobs to his staff while he maintains contact with the public. The Governor staff obediently performs the work and reports back to the Governor. If the Governor were to perform every task himself, he would not be able to give much attention to the public or the press. That's akin to a window which is busy doing a lengthy job in its primary thread: it doesn't respond to the user until the job is completed. Such a program can benefit from creating an additonal thread which is responsible for the lengthy job, allowing the primary thread to respond to the user's commands.
We can create a thread by calling CreateThread function which has the following syntax:

CreateThread proto lpThreadAttributes:DWORD,\
dwStackSize:DWORD,\
lpStartAddress:DWORD,\
lpParameter:DWORD,\
dwCreationFlags:DWORD,\
lpThreadId:DWORD

CreateThread function looks a lot like CreateProcess.
lpThreadAttributes --> You can use NULL if you want the thread to have default security descriptor.
dwStackSize --> specify the stack size of the thread. If you want the thread to have the same stack size as the primary thread, use NULL as this parameter.
lpStartAddress--> Address of the thread function.It's the function that will perform the work of the thread. This function MUST receive one and only one 32-bit parameter and return a 32-bit value.
lpParameter --> The parameter you want to pass to the thread function.
dwCreationFlags --> 0 means the thread runs immediately after it's created. The opposite is CREATE_SUSPENDED flag.
lpThreadId --> CreateThread function will fill the thread ID of the newly created thread at this address.

If CreateThread call is sucessful, it returns the handle of the newly created thread. Otherwise, it returns NULL.
The thread function runs as soon as CreateThread call is success ful unless you specify CREATE_SUSPENDED flag in dwCreationFlags. In that case, the thread is suspended until ResumeThread function is called.
When the thread function returns with ret instruction, Windows calls ExitThread function for the thread function implicitly. You can call ExitThread function with in your thread function yourself but there' s little point in doing so.
You can retrieve the exit code of a thread by calling GetExitCodeThread function.
If you want to terminate a thread from other thread, you can call TerminateThread function. But you should use this function under extreme circumstance since this function terminates the thread immediately without giving the thread any chance to clean up after itself.

Now let's move to the communication methods between threads.
There are three of them:

  • Using global variables
  • Windows messages
  • Event
Threads share the process's resources including global variables so the threads can use global varibles to communicate with each other. However this method must be used with care. Thread synchronization must enter into consideration. For example, if two threads use the same structure of 10 members , what happens when Windows suddenly yanks the control from one of the thread when it was in the middle of updating the structure? The other thread will be left with an inconsistent data in the structure! Don't make any mistake, multithreading programs are harder to debug and maintain. This sort of bug seems to happen at random which is very hard to track down.
You can also use Windows messages to communicate between threads. If the threads are all user interface ones, there's no problem: this method can be used as a two-way communication. All you have to do is defining one or more custom windows messages that are meaningful to the threads. You define a custom message by using WM_USER message as the base value say , you can define it like this:

WM_MYCUSTOMMSG equ WM_USER+100h

Windows will not use any value from WM_USER upward for its own messages so you can use the value WM_USER and above as your own custom message value.
If one of the thread is a user interface thread and the other is a worker one, you cannot use this method as two-way communication since a worker thread doesn't have its own window so it doesn't have a message queue. You can use the following scheme:

User interface Thread ------> global variable(s)----> Worker thread
Worker Thread ------> custom window message(s) ----> User interface Thread

In fact, we will use this method in our example.
The last communication method is an event object. You can view an event object as a kind of flag. If the event object is in "unsignalled" state, the thread is dormant or sleeping, in this state, the thread doesn't receive CPU time slice. When the event object is in "signalled" state,Windows "wakes up" the thread and it starts performing the assigned task.

Example:

You should download the example zip file and run thread1.exe. Click the "Savage Calculation" menu item. This will instruct the program to perform "add eax,eax " for 600,000,000 times. Note that during that time, you cannot do anything with the main window: you cannot move it, you cannot activate its menu, etc. When the calculation is completed, a message box appears. After that the window accepts your command normally.
To avoid this type of inconveniece to the user, we can move the "calculation" routine into a separate worker thread and let the primary thread continue with its user interface task. You can see that even though the main window responds more slowly than usual, it still responds

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
0,\
ADDR ThreadID
invoke CloseHandle,eax
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

end start

Analysis:

The main program presents the user with a normal window with a menu. If the user selects "Create Thread" menu item, the program creates a thread as below:

.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

The above function creates a thread that will run a procedure named ThreadProc concurrently with the primary thread. After the successful call, CreateThread returns immediately and ThreadProc begins to run. Since we do not use thread handle, we should close it else there'll be some leakage of memory. Note that closing the thread handle doesn't terminate the thread. Its only effect is that we cannot use the thread handle anymore.

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

As you can see, ThreadProc performs a savage calculation which takes quite a while to finish and when it finishs it posts a WM_FINISH message to the main window. WM_FINISH is our custom message defined like this:

    WM_FINISH equ WM_USER+100h
You don't have to add WM_USER with 100h but it's safer to do so.
The WM_FINISH message is meaningful only within our program. When the main window receives the WM_FINISH message, it respons by displaying a message box saying that the calculation is completed.
You can create several threads in succession by selecting "Create Thread" several times.
In this example, the communication is one-way in that only the thread can notify the main window. If you want the main thread to send commands to the worker thread, you can so as follows:
  • add a menu item saying something like "Kill Thread" in the menu
  • a global variable which is used as a command flag. TRUE=Stop the thread, FALSE=continue the thread
  • Modify ThreadProc to check the value of the command flag in the loop.
When the user selects "Kill Thread" menu item, the main program will set the value TRUE in the command flag. When ThreadProc sees that the value of the command flag is TRUE, it exits the loop and returns thus ends the thread.
[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 16: Event Object

Tutorial 16: Event Object

We will learn what an event object is and how to use it in a multithreaded program.

Download the example here.

Theory:

From the previous tutorial, I demonstrated how threads communicate with a custom window message. I left out two other methods: global variable and event object. We will use both of them in this tutorial.
An event object is like a switch: it has only two states: on or off. When an event object is turned on, it's in the "signalled" state. When it is turned off, it's in the "nonsignalled" state. You create an event object and put in a code snippet in the relevant threads to watch for the state of the event object. If the event object is in the nonsignalled state, the threads that wait for it will be asleep.When the threads are in wait state, they consume little CPU time.
You create an event object by calling CreateEvent function which has the following syntax:

CreateEvent proto lpEventAttributes:DWORD,\
bManualReset:DWORD,\
bInitialState:DWORD,\
lpName:DWORD

lpEventAttribute--> If you specify NULL value, the event object is created with default security descriptor.
bManualReset--> If you want Windows to automatically reset the event object to nonsignalled state after WaitForSingleObject call, you must specify FALSE as this parameter. Else you must manually reset the event object with the call to ResetEvent.
bInitialState--> If you want the event object to be created in the signalled state, specify TRUE as this parameter else the event object will be created in the nonsignalled state.
lpName --> Pointer to an ASCIIZ string that is the name of the event object. This name is used when you want to call OpenEvent.

If the call is successful, it returns the handle to the newly created event object else it returns NULL.
You can modify the state of an event object with two API calls: SetEvent and ResetEvent. SetEvent function sets the event object into signalled state. ResetEvent does the reverse.
When the event object is created, you must put the call to WaitForSingleObject in the thread that wants to watch for the state of the event object. WaitForSingleObject has the following syntax:

WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD

hObject --> A handle to one of the synchronization object. Event object is a type of synchronization object.
dwTimeout --> specify the time in milliseconds that this function will wait for the object to be in signalled state. If the specified time has passed and the event object is still in nonsignalled state, WaitForSingleObject returns the the caller. If you want to wait for the object indefinitely, you must specify the value INFINITE as this parameter.

Example:

The example below displays a window waiting for the user to select a command from the menu. If the user selects "run thread", the thread starts the savage calculation. When it's finished, a message box appears informing the user that the job is done. During the time that the thread is running, the user can select "stop thread" to stop the thread.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_START_THREAD equ 1
IDM_STOP_THREAD equ 2
IDM_EXIT equ 3
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMEventClass",0
AppName db "Win32 ASM Event Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0
StopString db "The thread is stopped",0
EventStop BOOL FALSE

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
hMenu HANDLE ?
ThreadID DWORD ?
ExitCode DWORD ?
hEventStart HANDLE ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_ENABLED
.elseif ax==IDM_STOP_THREAD
mov EventStop,TRUE
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000
.WHILE ecx!=0
.if EventStop!=TRUE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif
.ENDW
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
jmp ThreadProc
ret
ThreadProc ENDP
end start

Analysis:

In this example, I demonstrate another thread technique.

.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

You can see that I create the event object and the thread during the processing of WM_CREATE message. I create the event object in the nonsignalled state with automatic reset. After the event object is created, I create the thread. However the thread doesn't run immediately because it waits for the event object to be in the signalled state as the code below:

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000

The first line of the thread procedure is the call to WaitForSingleObject. It waits infinitely for the signalled state of the event object before it returns. This means that even when the thread is created, we put it into a dormant state.
When the user selects "run thread" command from the menu, we set the event object into signalled state as below:

.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart

The call to SetEvent turns the event object into the signalled state which in turn makes the WaitForSingleObject call in the thread procedure return and the thread starts running. When the user selects "stop thread" command, we set the value of the global variable "EventStop" to TRUE.

.if EventStop==FALSE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif

This stops the thread and jumps to the WaitForSingleObject call again. Note that we don't have to manually reset the event object into nonsignalled state because we specify the bManualReset parameter of the CreateEvent call as FALSE.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32asm tutorial 17: Dynamic Link Libraries

Tutorial 17: Dynamic Link Libraries

In this tutorial, we will learn about DLLs , what are they and how to create them.
You can download the example here.

Theory:

If you program long enough, you'll find that the programs you wrote usually have some code routines in common. It's such a waste of time to rewrite them everytime you start coding new programs. Back in the old days of DOS, programmers store those commonly used routines in one or more libraries. When they want to use the functions, they just link the library to the object file and the linker extracts the functions from the library and inserts them into the final executable file. This process is called static linking. C runtime libraries are good examples. The drawback of this method is that you have identical functions in every program that calls them. Your disk space is wasted storing several identical copies of the functions. But for DOS programs, this method is quite acceptable since there is usually only one program that's active in memory. So there is no waste of precious memory.
Under Windows, the situation becomes much more critical because you can have several programs running simultaneously. Memory will be eat up quickly if your program is quite large. Windows has a solution for this type of problem: dynamic link libraries. A dynamic link library is a kind of common pool of functions. Windows will not load several copies of a DLL into memory so even if there are many instances of your program running at the same time, there'll be only one copy of the DLL that program uses in memory. And I should clarify this point a bit. In reality, all processes that use the same dll will have their own copies of that dll. It will look like there are many copies of the DLL in memory. But in reality, Windows does it magic with paging and all processes share the same DLL code.So in physical memory, there is only one copy of DLL code. However, each process will have its own unique data section of the DLL.
The program links to a DLL at runtime unlike the old static library. That's why it's called dynamic link library. You can also unload a DLL at runtime as well when you don't need it. If that program is the only one that uses the DLL, it'll be unloaded from memory immediately. But if the DLL is still used by some other program, the DLL remains in memory until the last program that uses its service unloads it.
However, the linker has a more difficult job when it performs address fixups for the final executable file. Since it cannot "extract" the functions and insert them into the final executable file, somehow it must store enough information about the DLL and functions into the final execuable file for it to be able to locate and load the correct DLL at runtime.
That's where import library comes in. An import library contains the information about the DLL it represents. The linker can extract the info it needs from the import libraries and stuff it into the executable file. When Windows loader loads the program into memory, it sees that the program links to a DLL so it searches for that DLL and maps it into the address space of the process as well and performs the address fixups for the calls to the functions in the DLL.
You may choose to load the DLL yourself without relying on Windows loader. This method has its pros and cons:
  • It doesn't need an import library so you can load and use any DLL even if it comes with no import library. However, you still have to know about the functions inside it, how many parameters they take and the likes.
  • When you let the loader load the DLL for your program, if the loader cannot find the DLL it will report "A required .DLL file, xxxxx.dll is missing" and poof! your program doesn't have a chance to run even if that DLL is not essential to its operation. If you load the DLL yourself, when the DLL cannot be found and it's not essential to the operation, your program can just tell the user about the fact and go on.
  • You can call *undocumented* functions that are not included in the import libraries. Provided that you know enough info about the functions.
  • If you use LoadLibrary, you have to call GetProcAddress for every function that you want to call. GetProcAddress retrieves the entrypoint address of a function in a particular DLL. So your code might be a little bit larger and slower but by not much.
Seeing the advantages/disadvantages of LoadLibrary call, we go into detail how to create a DLL now.
The following code is the DLL skeleton.

;--------------------------------------------------------------------------------------
; DLLSkeleton.asm
;--------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
.code
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp
;---------------------------------------------------------------------------------------------------
; This is a dummy function
; It does nothing. I put it here to show where you can insert functions into
; a DLL.
;----------------------------------------------------------------------------------------------------
TestFunction proc
ret
TestFunction endp

End DllEntry

;-------------------------------------------------------------------------------------
; DLLSkeleton.def
;-------------------------------------------------------------------------------------
LIBRARY DLLSkeleton
EXPORTS TestFunction

The above program is the DLL skeleton. Every DLL must have an entrypoint function. Windows will call the entrypoint function everytime that:

  • The DLL is first loaded
  • The DLL is unloaded
  • A thread is created in the same process
  • A thread is destroyed in the same process
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp

You can name the entrypoint function anything you wish so long as you have a matching END <Entrypoint function name>. This function takes three parameters, only the first two of which are important.
hInstDLL is the module handle of the DLL. It's not the same as the instance handle of the process. You should keep this value if you need to use it later. You can't obtain it again easily.
reason can be one of the four values:

  • DLL_PROCESS_ATTACH The DLL receives this value when it is first injected into the process address space. You can use this opportunity to do initialization.
  • DLL_PROCESS_DETACH The DLL receives this value when it is being unloaded from the process address space. You can use this opportunity to do some cleanup such as deallocate memory and so on.
  • DLL_THREAD_ATTACH The DLL receives this value when the process creates a new thread.
  • DLL_THREAD_DETACH The DLL receives this value when a thread in the process is destroyed.
You return TRUE in eax if you want the DLL to go on running. If you return FALSE, the DLL will not be loaded. For example, if your initialization code must allocate some memory and it cannot do that successfully, the entrypoint function should return FALSE to indicate that the DLL cannot run.
You can put your functions in the DLL following the entrypoint function or before it. But if you want them to be callable from other programs, you must put their names in the export list in the module definition file (.def).
A DLL needs a module definition file in its developmental stage. We will take a look at it now.

LIBRARY DLLSkeleton
EXPORTS TestFunction

Normally you must have the first line.The LIBRARY statement defines the internal module name of the DLL. You should match it with the filename of the DLL.
The EXPORTS statement tells the linker which functions in the DLL are exported, that is, callable from other programs. In the example, we want other modules to be able to call TestFunction, so we put its name in the EXPORTS statement.
Another change is in the linker switch. You must put /DLL switch and /DEF:<your def filename> in your linker switches like this:

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def /LIBPATH:c:\masm32\lib DLLSkeleton.obj

The assembler switches are the same, namely /c /coff /Cp. So after you link the object file, you will get .dll and .lib. The .lib is the import library which you can use to link to other programs that use the functions in the DLL.
Next I'll show you how to use LoadLibrary to load a DLL.

;---------------------------------------------------------------------------------------------
; UseDLL.asm
;----------------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

.data
LibName db "DLLSkeleton.dll",0
FunctionName db "TestHello",0
DllNotFound db "Cannot load library",0
AppName db "Load Library",0
FunctionNotFound db "TestHello function not found",0

.data?
hLib dd ? ; the handle of the library (DLL)
TestHelloAddr dd ? ; the address of the TestHello function

.code
start:
invoke LoadLibrary,addr LibName
;---------------------------------------------------------------------------------------------------------
; Call LoadLibrary with the name of the desired DLL. If the call is successful
; it will return the handle to the library (DLL). If not, it will return NULL
; You can pass the library handle to GetProcAddress or any function that requires
; a library handle as a parameter.
;------------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax
invoke GetProcAddress,hLib,addr FunctionName
;-------------------------------------------------------------------------------------------------------------
; When you get the library handle, you pass it to GetProcAddress with the address
; of the name of the function in that DLL you want to call. It returns the address
; of the function if successful. Otherwise, it returns NULL
; Addresses of functions don't change unless you unload and reload the library.
; So you can put them in global variables for future use.
;-------------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
.else
mov TestHelloAddr,eax
call [TestHelloAddr]
;-------------------------------------------------------------------------------------------------------------
; Next, you can call the function with a simple call with the variable containing
; the address of the function as the operand.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke FreeLibrary,hLib
;-------------------------------------------------------------------------------------------------------------
; When you don't need the library anymore, unload it with FreeLibrary.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke ExitProcess,NULL
end start

So you can see that using LoadLibrary is a little more involved but it's also more flexible.

[Iczelion's Win32 Assembly Homepage]


Iczelion Win32asm Tutorial 18: Common Controls

Tutorial 18: Common Controls

We will learn what common controls are and how to use them. This tutorial will be a quick introduction to them only.
Download the example source code here.

Theory:

Windows 95 comes with several user-interface enhancements over Windows 3.1x. They make the GUI richer. Several of them are in widely used before Windows 95 hit the shelf, such as status bar, toolbars etc. Programmers have to code them themselves. Now Microsoft has included them with Windows 9x and NT. We will learn about them here.
These are the new controls:
  • Toolbar
  • Tooltip
  • Status bar
  • Property sheet
  • Property page
  • Tree view
  • List view
  • Animation
  • Drag list
  • Header
  • Hot-key
  • Image list
  • Progress bar
  • Right edit
  • Tab
  • Trackbar
  • Up-down
Since there are many of them, loading them all into memory and registering them would be a waste of resource. All of them, with the exception of rich edit control, are stored in comctl32.dll with applications can load when they want to use the controls. Rich edit control resides in its own dll, richedXX.dll, because it's very complicated and hence larger than its brethren.
You can load comctl32.dll by including a call to InitCommonControls in your program. InitCommonControls is a function in comctl32.dll, so referring to it anywhere in your code will make PE loader load comctl32.dll when your program runs.You don't have to execute it, just include it in your code somewhere. This function does NOTHING! Its only instruction is "ret". Its sole purpose is to include reference to comctl32.dll in the import section so that PE loader will load it whenever the program is loaded. The real workhorse is the DLL entrypoint function which registers all common control classes when the dll is loaded. Common controls are created based on those classes just like other child window controls such as edit, listbox etc.
Rich edit is another matter entirely. If you want to use it, you have to call LoadLibrary to load it explicitly and call FreeLibrary to unload it.
Now we learn how to create them. You can use a resource editor to incorporate them into dialog boxes or you can create them yourself. Nearly all common controls are created by calling CreateWindowEx or CreateWindow, passing it the name of the control class. Some common controls have specific creation functions , however, they are just wrappers around CreateWindowEx to make it easier to create those controls. Existing specific creation functions are listed below:
  • CreateToolbarEx
  • CreateStatusWindow
  • CreatePropertySheetPage
  • PropertySheet
  • ImageList_Create
In order to create common controls, you have to know their class names. They are listed below:
Class Name | Common Control
ToolbarWindow32 | Toolbar
tooltips_class32 | Tooltip
msctls_statusbar32 | Status bar
SysTreeView32 | Tree view
SysListView32 | List view
SysAnimate32 | Animation
SysHeader32 | Header
msctls_hotkey32 | Hot-key
msctls_progress32 | Progress bar
RICHEDIT | Rich edit
msctls_updown32 | Up-down
SysTabControl32 | Tab
Property sheets and property pages and image list control have their own specific creation functions. Drag list control are souped-up listbox so it doesn't have its own class. The above class names are verified by checking resource script generated by Visual C++ resource editor. They differ from the class names listed by Borland's win32 api reference and Charles Petzold's Programming Windows 95. The above list is the accurate one.
Those common controls can use general window styles such as WS_CHILD etc. They also have their own specific styles such as TVS_XXXXX for tree view control, LVS_xxxx for list view control, etc. Win32 api reference is your best friend in this regard.
Now that we know how to create common controls, we can move on to communication method between common controls and their parent. Unlike child window controls, common controls don't communicate with the parent via WM_COMMAND. Instead they send WM_NOTIFY messages to the parent window when some interesting events occur with the common controls. The parent can control the children by sending messages to them. There are also many new messages for those new controls. You should consult your win32 api reference for more detail.
Let's examine progress bar and status bar controls in the following example.

Sample code:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDC_PROGRESS equ 1 ; control IDs
IDC_STATUS equ 2
IDC_TIMER equ 3

.data
ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0
ProgressClass db "msctls_progress32",0 ; the class name of the progress bar
Message db "Finished!",0
TimerID dd 0

.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
WS_CHILD+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
mov eax,1000 ; the lParam of PBM_SETRANGE message contains the range
mov CurrentStep,eax
shl eax,16 ; the high range is in the high word
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
mov TimerID,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.if TimerID!=0
invoke KillTimer,hWnd,TimerID
.endif
.elseif uMsg==WM_TIMER ; when a timer event occurs
invoke SendMessage,hwndProgress,PBM_STEPIT,0,0 ; step up the progress in the progress bar
sub CurrentStep,10
.if CurrentStep==0
invoke KillTimer,hWnd,TimerID
mov TimerID,0
invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analysis:

    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax
    invoke InitCommonControls
I deliberately put InitCommonControls after ExitProcess to demonstrate that InitCommonControls is just there for putting a reference to comctl32.dll in the import section. As you can see, the common controls work even if InitCommonControls doesn't execute.
    .if uMsg==WM_CREATE
    invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
    WS_CHILD+WS_VISIBLE,100,\
    200,300,20,hWnd,IDC_PROGRESS,\
    hInstance,NULL
    mov hwndProgress,eax
Here is where we create the common control. Note that this CreateWindowEx call contains hWnd as the parent window handle. It also specifies a control ID for identifying this control. However, since we have the control's window handle, this ID is not used. All child window controls must have WS_CHILD style.
    mov eax,1000
    mov CurrentStep,eax
    shl eax,16
    invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
    invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
After the progress bar is created, we can set its range. The default range is from 0 to 100. If you are not satisfied with it, you can specify your own range with PBM_SETRANGE message. lParam of this message contains the range, the maximum range is in the high word and the minimum one is in the low word. You can specify how much a step takes by using PBM_SETSTEP message. The example sets it to 10 which means that when you send a PBM_STEPIT message to the progress bar, the progress indicator will rise by 10. You can also set your own indicator level by sending PBM_SETPOS messages. This message gives you tighter control over the progress bar.
    invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
    mov hwndStatus,eax
    invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; create a timer
    mov TimerID,eax
Next, we create a status bar by calling CreateStatusWindow. This call is easy to understand so I'll not comment on it. After the status window is created, we create a timer. In this example, we will update the progress bar at a regular interval of 100 ms so we must create a timer control. Below is the function prototype of SetTimer.
    SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD
hWnd : Parent window handle
TimerID : a nonzero timer identifier. You can create your own identifier.
TimerInterval : the timer interval in milliseconds that must pass before the timer calls the timer procedure or sends a WM_TIMER message
lpTimerProc : the address of the timer function that will be called when the time interval expires. If this parameter is NULL, the timer will send WM_TIMER message to the parent window instead.

If this call is successful, it will return the TimerID. If it failed, it returns 0. This is why the timer identifer must be a nonzero value.

    .elseif uMsg==WM_TIMER
    invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
    sub CurrentStep,10
    .if CurrentStep==0
    invoke KillTimer,hWnd,TimerID
    mov TimerID,0
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
    invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
    invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
    .endif
When the specified time interval expires, the timer sends a WM_TIMER message. You will put your code that will be executed here. In this example, we update the progress bar and then check if the maximum limit has been reached. If it has, we kill the timer and then set the text in the status window with SB_SETTEXT message. A message box is displayed and when the user clicks OK, we clear the text in the status bar and the progress bar.
[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 19: Tree View Control

Tutorial 19: Tree View Control

In this tutorial, we will learn how to use tree view control. Moreover, we will also learn how to do drag and drop under tree view control and how to use an image list with it.
Download the example here.

Theory:

A tree view control is a special kind of window that represents objects in hierarchical order. An example of it is the left pane of Windows Explorer. You can use this control to show relationships between objects.
You can create a tree view control by calling CreateWindowEx, passing "SysTreeView32" as the class name or you can incorporate it into a dialog box. Don't forget to put InitCommonControls call in your code.
There are several styles specific to the tree view control. These three are the ones mostly used.
    TVS_HASBUTTONS == Displays plus (+) and minus (-) buttons next to parent items. The user clicks the buttons to expand or collapse a parent item's list of child items. To include buttons with items at the root of the tree view, TVS_LINESATROOT must also be specified.
    TVS_HASLINES == Uses lines to show the hierarchy of items.
    TVS_LINESATROOT == Uses lines to link items at the root of the tree-view control. This value is ignored if TVS_HASLINES is not also specified.
The tree view control, like other common controls, communicates with the parent window via messages. The parent window can send various messages to it and the tree view control can send "notification" messages to its parent window. In this regard, the tree view control is not different that any window.
When something interesting occurs to it, it sends a WM_NOTIFY message to the parent window with accompanying information.
    WM_NOTIFY
    wParam == Control ID, this value is not guaranteed to be unique so we don't use it.
    Instead, we use hwndFrom or IDFrom member of the NMHDR structure
    pointed to by lParam
    lParam == Pointer to NMHDR structure. Some controls may pass a pointer to larger
    structure but it must have a NMHDR structure as its first member.
    That is, when you have lParam, you can be sure that it points to a
    NMHDR structure at least.
Next we will examine NMHDR structure.
    NMHDR struct DWORD
    hwndFrom DWORD ?
    idFrom DWORD ?
    code DWORD ?
    NMHDR ends
hwndFrom is the window handle of the control that sends this WM_NOTIFY message.
idFrom is the control ID of the control that sends this WM_NOTIFY message.
code is the actual message the control wants to send to the parent window.
Tree view notifications are those with TVN_ at the beginning of the name. Tree view messages are those with TVM_, like TVM_CREATEDRAGIMAGE. The tree view control sends TVN_xxxx in the code member of NMHDR. The parent window can send TVM_xxxx to control it.

Adding items to a tree view control

After you create a tree view control, you can add items to it. You can do this by sending TVM_INSERTITEM to it.
    TVM_INSERTITEM
    wParam = 0;
    lParam = pointer to a TV_INSERTSTRUCT;
You should know some terminology at this point about the relationship between items in the tree view control.
An item can be parent, child, or both at the same time. A parent item is the item that has some other subitem(s) associated with it. At the same time, the parent item may be a child of some other item. An item without a parent is called a root item. There can be many root items in a tree view control. Now we examine TV_INSERTSTRUCT structure
    TV_INSERTSTRUCT STRUCT DWORD
    hParent DWORD ?
    hInsertAfter DWORD ?
    ITEMTYPE <>
    TV_INSERTSTRUCT ENDS
hParent = Handle to the parent item. If this member is the TVI_ROOT value or NULL, the item is inserted at the root of the tree-view control.
hInsertAfter = Handle to the item after which the new item is to be inserted or one of the following values:
  • TVI_FIRST ==> Inserts the item at the beginning of the list.
  • TVI_LAST ==> Inserts the item at the end of the list.
  • TVI_SORT ==> Inserts the item into the list in alphabetical order.
    ITEMTYPE UNION
    itemex TVITEMEX <>
    item TVITEM <>
    ITEMTYPE ENDS
We will use only TVITEM here.
    TV_ITEM STRUCT DWORD
    imask DWORD ?
    hItem DWORD ?
    state DWORD ?
    stateMask DWORD ?
    pszText DWORD ?
    cchTextMax DWORD ?
    iImage DWORD ?
    iSelectedImage DWORD ?
    cChildren DWORD ?
    lParam DWORD ?
    TV_ITEM ENDS
This structure is used to send and receive info about a tree view item, depending on messages. For example, with TVM_INSERTITEM, it is used to specify the attribute of the item to be inserted into the tree view control. With TVM_GETITEM, it'll be filled with information about the selected tree view item.
imask is used to specify which member(s) of the TV_ITEM structure is (are) valid. For example, if the value in imask is TVIF_TEXT, it means only the pszText member is valid. You can combine several flags together.
hItem is the handle to the tree view item. Each item has its own handle, like a window handle. If you want to do something with an item, you must select it by its handle.
pszText is the pointer to a null-terminated string that is the label of the tree view item.
cchTextMax is used only when you want to retrieve the label of the tree view item. Because you will supply the pointer to the buffer in pszText, Windows has to know the size of the provided buffer. You have to give the size of the buffer in this member.
iImage and iSelectedImage refers to the index into an image list that contains the images to be shown when the item is not selected and when it's selected. If you recall Windows Explorer left pane, the folder images are specified by these two members.
In order to insert an item into the tree view control, you must at least fill in the hParent, hInsertAfter and you should fill imask and pszText members as well.

Adding images to the tree view control

If you want to put an image to the left of the tree view item's label, you have to create an image list and associate it with the tree view control. You can create an image list by calling ImageList_Create.
    ImageList_Create PROTO cx:DWORD, cy:DWORD, flags:DWORD, \
    cInitial:DWORD, cGrow:DWORD
This function returns the handle to an empty image list if successful.
cx == width of each image in this image list, in pixels.
cy == height of each image in this image list, in pixels. Every image in an image list must be equal to each other in size. If you specify a large bitmap, Windows will use cx and cy to *cut* it into several pieces. So you should prepare your own image as a strip of pictures with identical dimensions.
flags == specify the type of images in this image list whether they are color or monochrome and their color depth. Consult your win32 api reference for more detail
cInitial == The number of images that this image list will initially contain. Windows will use this info to allocate memory for the images.
cGrow == Amount of images by which the image list can grow when the system needs to resize the list to make room for new images. This parameter represents the number of new images that the resized image list can contain.
An image list is not a window! It's only an image deposit for use by other windows.
After an image list is created, you can add images to it by calling ImageList_Add
    ImageList_Add PROTO himl:DWORD, hbmImage:DWORD, hbmMask:DWORD
This function returns -1 if unsuccessful.
himl == the handle of the image list you want to add images to. It is the value returned by a successful call to ImageList_Create
hbmImage == the handle to the bitmap to be added to the image list. You usually store the bitmap in the resource and load it with LoadBitmap call. Note that you don't have to specify the number of images contained in this bitmap because this information is inferred from cx and cy parameters passed to ImageList_Create call.
hbmMask == Handle to the bitmap that contains the mask. If no mask is used with the image list, this parameter is ignored.
Normally, we will add only two images to the image list for use with the tree view control: one that is used when the tree view item is not selected, and the other when the item is selected.
When the image list is ready, you associate it with the tree view control by sending TVM_SETIMAGELIST to the tree view control.
    TVM_SETIMAGELIST
    wParam = type of image list to set. There are two choices:
    • TVSIL_NORMAL Set the normal image list, which contains the selected and unselected images for the tree-view item.
    • TVSIL_STATE Set the state image list, which contains the images for tree-view items that are in a user-defined state.
    lParam = Handle to the image list

Retrieve the info about tree view item

You can retrieve the information about a tree view item by sending TVM_GETITEM message.
    TVM_GETITEM
    wParam = 0
    lParam = pointer to the TV_ITEM structure to be filled with the information
Before you send this message, you must fill imask member with the flag(s) that specifies which member(s) of TV_ITEM you want Windows to fill. And most importantly, you must fill hItem with the handle to the item you want to get information from. And this poses a problem: How can you know the handle of the item you want to retrieve info from? Will you have to store all tree view handles?
The answer is quite simple: you don't have to. You can send TVM_GETNEXTITEM message to the tree view control to retrieve the handle to the tree view item that has the attribute(s) you specified. For example, you can query the handle of the first child item, the root item, the selected item, and so on.
    TVM_GETNEXTITEM
    wParam = flag
    lParam = handle to a tree view item (only necessary for some flag values)
The value in wParam is very important so I present all the flags below:
    • TVGN_CARET Retrieves the currently selected item.
    • TVGN_CHILD Retrieves the first child item of the item specified by the hitem parameter
    • TVGN_DROPHILITE Retrieves the item that is the target of a drag-and-drop operation.
    • TVGN_FIRSTVISIBLE Retrieves the first visible item.
    • TVGN_NEXT Retrieves the next sibling item.
    • TVGN_NEXTVISIBLE Retrieves the next visible item that follows the specified item. The specified item must be visible. Use the TVM_GETITEMRECT message to determine whether an item is visible.
    • TVGN_PARENT Retrieves the parent of the specified item.
    • TVGN_PREVIOUS Retrieves the previous sibling item.
    • TVGN_PREVIOUSVISIBLE Retrieves the first visible item that precedes the specified item. The specified item must be visible. Use the TVM_GETITEMRECT message to determine whether an item is visible.
    • TVGN_ROOT Retrieves the topmost or very first item of the tree-view control.
You can see that, you can retrieve the handle to the tree view item you are interested in from this message. SendMessage returns the handle to the tree view item if successful. You can then fill the returned handle into hItem member of TV_ITEM to be used with TVM_GETITEM message.

Drag and Drop Operation in tree view control

This part is the reason I decided to write this tutorial. When I tried to follow the example in win32 api reference (the win32.hlp from InPrise), I was very frustrated because the vital information is lacking. From trial and error, I finally figured out how to implement drag & drop in a tree view control and I don't want anyone to walk the same path as myself.
Below is the steps in implementing drag & drop operation in a tree view control.
  1. When the user tries to drag an item, the tree view control sends TVN_BEGINDRAG notification to the parent window. You can use this opportunity to create a drag image which is the image that will be used to represent the item while it's being dragged. You can send TVM_CREATEDRAGIMAGE to the tree view control to tell it to create a default drag image from the image that is currently used by the item that will be dragged. The tree view control will create an image list with just one drag image and return the handle to that image list to you.
  2. After the drag image is created, you specify the hotspot of the drag image by calling ImageList_BeginDrag.
      3. ImageList_BeginDrag PROTO himlTrack:DWORD, \
      iTrack:DWORD , \
      dxHotspot:DWORD, \
      dyHotspot:DWORD
      himlTrack is the handle to the image list that contains the drag image.
      iTrack is the index into the image list that specifies the drag image
      dxHotspot specifies the relative distance of the hotspot in horizontal plance in the drag image since this image will be used in place of the mouse cursor, so we need to specify which part of the image is the hotspot.
      dyHotspot specifies the relative distance of the hotspot in the vertical plane.
      Normally, iTrack would be 0 if you tell the tree view control to create the drag image for you. and dxHotspot and dyHotspot can be 0 if you want the left upper corner of the drag image to be the hotspot.
  3. When the drag image is ready to be displayed, we call ImageList_DragEnter to display the drag image in the window.
      4. ImageList_DragEnter PROTO hwndLock:DWORD, x:DWORD, y:DWORD
      hwndLock is the handle of the window that owns the drag image. The drag image will not be able to move outside that window.
      x and y are the x-and y-coordinate of the place where the drag image should be initially displayed. Note that these values are relative to the left upper corner of the window, not the client area.
  4. Now that the drag image is displayed on the window, you will have to support the drag operation in the tree view control. However, there is a little problem here. We have to monitor the drag path with WM_MOUSEMOVE and the drop location with WM_LBUTTONUP messages. However, if the drag image is over some other child windows, the parent window will never receive any mouse message. The solution is to capture the mouse input with SetCapture. Using the call, the mouse messages will be directed to the specified window regardless of where the mouse cursor is.
  5. Within WM_MOUSEMOVE handler, you will update the drag path with ImageList_DragMove call. This function moves the image that is being dragged during a drag-and-drop operation. Furthermore, if you so desire, you can hilite the item that the drag image is over by sending TVM_HITTEST to check if the drag image is over some item. If it is, you can send TVM_SELECTITEM with TVGN_DROPHILITE flag to hilite that item. Note that before sending TVM_SELECTITEM message, you must hide the drag image first else your drag image will leave ugly traces. You can hide the drag image by calling ImageList_DragShowNolock and, after the hilite operation is finished, call ImageList_DragShowNolock again to show the drag image.
  6. When the user releases the left mouse button, you must do several things. If you hilite an item, you must un-hilite it by sending TVM_SELECTITEM with TVGN_DROPHILITE flag again, but this time, lParam MUST be zero. If you don't un-hilite the item, you will get a strange effect: when you select some other item, that item will be enclosed by a rectangle but the hilite will still be on the last hilited item. Next, you must call ImageList_DragLeave followed by ImageList_EndDrag. You must release the mouse by calling ReleaseCapture. If you create an image list, you must destroy it by calling ImageList_Destroy. After that, you can go on with what your program wants to do when the drag& drop operation is completed.

Code sample:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
.const
IDB_TREE equ 4006 ; ID of the bitmap resource
.data
ClassName db "TreeViewWinClass",0
AppName db "Tree View Demo",0
TreeViewClass db "SysTreeView32",0
Parent db "Parent Item",0
Child1 db "child1",0
Child2 db "child2",0
DragMode dd FALSE ; a flag to determine if we are in drag mode

.data?
hInstance HINSTANCE ?
hwndTreeView dd ? ; handle of the tree view control
hParent dd ? ; handle of the root tree view item
hImageList dd ? ; handle of the image list used in the tree view control
hDragImageList dd ? ; handle of the image list used to store the drag image

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,200,400,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL tvinsert:TV_INSERTSTRUCT
LOCAL hBitmap:DWORD
LOCAL tvhit:TV_HITTESTINFO
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
0,200,400,hWnd,NULL,\
hInstance,NULL ; Create the tree view control
mov hwndTreeView,eax
invoke ImageList_Create,16,16,ILC_COLOR16,2,10 ; create the associated image list
mov hImageList,eax
invoke LoadBitmap,hInstance,IDB_TREE ; load the bitmap from the resource
mov hBitmap,eax
invoke ImageList_Add,hImageList,hBitmap,NULL ; Add the bitmap into the image list
invoke DeleteObject,hBitmap ; always delete the bitmap resource
invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
mov tvinsert.hParent,NULL
mov tvinsert.hInsertAfter,TVI_ROOT
mov tvinsert.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
mov tvinsert.item.pszText,offset Parent
mov tvinsert.item.iImage,0
mov tvinsert.item.iSelectedImage,1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov hParent,eax
mov tvinsert.hParent,eax
mov tvinsert.hInsertAfter,TVI_LAST
mov tvinsert.item.pszText,offset Child1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov tvinsert.item.pszText,offset Child2
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
.elseif uMsg==WM_MOUSEMOVE
.if DragMode==TRUE
mov eax,lParam
and eax,0ffffh
mov ecx,lParam
shr ecx,16
mov tvhit.pt.x,eax
mov tvhit.pt.y,ecx
invoke ImageList_DragMove,eax,ecx
invoke ImageList_DragShowNolock,FALSE
invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
.if eax!=NULL
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
.endif
invoke ImageList_DragShowNolock,TRUE
.endif
.elseif uMsg==WM_LBUTTONUP
.if DragMode==TRUE
invoke ImageList_DragLeave,hwndTreeView
invoke ImageList_EndDrag
invoke ImageList_Destroy,hDragImageList
invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
invoke ReleaseCapture
mov DragMode,FALSE
.endif
.elseif uMsg==WM_NOTIFY
mov edi,lParam
assume edi:ptr NM_TREEVIEW
.if [edi].hdr.code==TVN_BEGINDRAG
invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
mov hDragImageList,eax
invoke ImageList_BeginDrag,hDragImageList,0,0,0
invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
invoke SetCapture,hWnd
mov DragMode,TRUE
.endif
assume edi:nothing
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analysis:

Within WM_CREATE handler, you create the tree view control
    invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
    WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
    0,200,400,hWnd,NULL,\
    hInstance,NULL
Note the styles. TVS_xxxx are the tree view specific styles.
    invoke ImageList_Create,16,16,ILC_COLOR16,2,10
    mov hImageList,eax
    invoke LoadBitmap,hInstance,IDB_TREE
    mov hBitmap,eax
    invoke ImageList_Add,hImageList,hBitmap,NULL
    invoke DeleteObject,hBitmap
    invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
Next, you create an empty image list with will accept images of 16x16 pixels in size, 16-bit color and initially, it will contain 2 images but can be expanded to 10 if need arises. We then load the bitmap from the resource and add it to the image list just created. After that, we delete the handle to the bitmap since it will not be used anymore. When the image list is all set, we associate it with the tree view control by sending TVM_SETIMAGELIST to the tree view control.
    mov tvinsert.hParent,NULL
    mov tvinsert.hInsertAfter,TVI_ROOT
    mov tvinsert.u.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
    mov tvinsert.u.item.pszText,offset Parent
    mov tvinsert.u.item.iImage,0
    mov tvinsert.u.item.iSelectedImage,1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
We insert items into the tree view control, beginning from the root item. Since it will be root item, hParent member is NULL and hInsertAfter is TVI_ROOT. imask member specifies that pszText, iImage and iSelectedImage members of the TV_ITEM structure is valid. We fill those three members with appropriate value. pszText contains the label of the root item, iImage is the index into the image in the image list that will be displayed to the left of the unselected item, and iSelectedImage is the index into the image in the image list that will be displayed when the item is selected. When all appropriate members are filled in, we send TVM_INSERTITEM message to the tree view control to add the root item to it.
    mov hParent,eax
    mov tvinsert.hParent,eax
    mov tvinsert.hInsertAfter,TVI_LAST
    mov tvinsert.u.item.pszText,offset Child1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
    mov tvinsert.u.item.pszText,offset Child2
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
After the root item is added, we can attach the child items to it. hParent member is now filled with the handle of the parent item. And we will use identical images in the image list so we don't change iImage and iSelectedImage member.
    .elseif uMsg==WM_NOTIFY
    mov edi,lParam
    assume edi:ptr NM_TREEVIEW
    .if [edi].hdr.code==TVN_BEGINDRAG
    invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
    mov hDragImageList,eax
    invoke ImageList_BeginDrag,hDragImageList,0,0,0
    invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
    invoke SetCapture,hWnd
    mov DragMode,TRUE
    .endif
    assume edi:nothing
Now when the user tries to drag an item, the tree view control sends WM_NOTIFY message with TVN_BEGINDRAG as the code. lParam is the pointer to an NM_TREEVIEW structure which contains several pieces of information we need so we put its value into edi and use edi as the pointer to NM_TREEVIEW structure. assume edi:ptr NM_TREEVIEW is a way to tell MASM to treat edi as the pointer to NM_TREEVIEW structure. We then create a drag image by sending TVM_CREATEDRAGIMAGE to the tree view control. It returns the handle to the newly created image list with a drag image inside. We call ImageList_BeginDrag to set the hotspot in the drag image. Then we enter the drag operation by calling ImageList_DragEnter. This function displays the drag image at the specified location in the specified window. We use ptDrag structure that is a member of NM_TREEVIEW structure as the point where the drag image should be initially displayed.After that, we capture the mouse input and set the flag to indicate that we now enter drag mode.
    .elseif uMsg==WM_MOUSEMOVE
    .if DragMode==TRUE
    mov eax,lParam
    and eax,0ffffh
    mov ecx,lParam
    shr ecx,16
    mov tvhit.pt.x,eax
    mov tvhit.pt.y,ecx
    invoke ImageList_DragMove,eax,ecx
    invoke ImageList_DragShowNolock,FALSE
    invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
    .if eax!=NULL
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
    .endif
    invoke ImageList_DragShowNolock,TRUE
    .endif
Now we concentrate on WM_MOUSEMOVE. When the user drags the drag image along, our parent window receives WM_MOUSEMOVE messages. In response to these messages, we update the drag image position with ImageList_DragMove. After that, we check if the drag image is over some item. We do that by sending TVM_HITTEST message to the tree view control with a point for it to check. If the drag image is over some item, we hilite that item by sending TVM_SELECTITEM message with TVGN_DROPHILITE flag to the tree view control. During the hilite operation, we hide the drag image so that it will not leave unsightly blots on the tree view control.
    .elseif uMsg==WM_LBUTTONUP
    .if DragMode==TRUE
    invoke ImageList_DragLeave,hwndTreeView
    invoke ImageList_EndDrag
    invoke ImageList_Destroy,hDragImageList
    invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
    invoke ReleaseCapture
    mov DragMode,FALSE
    .endif
When the user releases the left mouse button, the drag operation is at the end. We leave the drag mode by calling ImageList_DragLeave, followed by ImageList_EndDrag and ImageList_Destroy. To make the tree view items look good, we also check the last hilited item, and select it. We must also un-hilite it else the other items will not get hilited when they are selected. And lastly, we release the mouse capture.
[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32asm Tutorial 20: Window Subclassing

Tutorial 20: Window Subclassing

In this tutorial, we will learn about window subclassing, what it is and how to use it to your advantage.
Download the example here.

Theory:

If you program in Windows for some time, you will find some cases where a window has nearly the attributes you need in your program but not quite. Have you encountered a situation where you want some special kind of edit control that can filter out some unwanted text? The straightforward thing to do is to code your own window. But it's really hard work and time-consuming. Window subclassing to the rescue.
In a nutshell, window subclassing allows you to "take over" the subclassed window. You will have absolute control over it. Let's take an example to make this clearer. Suppose you need a text box that accepts only hex numbers. If you use a simple edit control, you have no say whatsoever when your user types something other than hex numbers into your text box, ie. if the user types "zb+q*" into your text box, you can't do anything with it except rejecting the whole text string. This is unprofessional at least. In essence, you need the ability to examine each character the user typed into the text box right at the moment he typed it.
We will examine how to do that now. When the user types something into a text box, Windows sends WM_CHAR message to the edit control's window procedure. This window procedure resides inside Windows itself so we can't modify it. But we can redirect the message flow to our own window procedure. So that our window procedure will get first shot at any message Windows sends to the edit control. If our window procedure chooses to act on the message, it can do so. But if it doesn't want to handle the message, it can pass it to the original window procedure. This way, our window procedure inserts itself between Windows and the edit control. Look at the flow below:

    Before Subclassing

    Windows ==> edit control's window procedure

    After Subclassing

    Windows ==> our window procedure -----> edit control's window procedure
Now we put our attention on how to subclass a window. Note that subclassing is not limited to controls, it can be used with any window.
Let's think about how Windows knows where the edit control's window procedure resides. A guess?......lpfnWndProc member of WNDCLASSEX structure. If we can replace this member with the address of our own window procedure, Windows will send messages to our window proc instead.
We can do that by calling SetWindowLong.
    SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD
hWnd = handle of the window to change the value in the WNDCLASSEX structure
nIndex == value to change.
    GWL_EXSTYLE Sets a new extended window style.
    GWL_STYLE Sets a new window style.
    GWL_WNDPROC Sets a new address for the window procedure.
    GWL_HINSTANCE Sets a new application instance handle.
    GWL_ID Sets a new identifier of the window.
    GWL_USERDATA Sets the 32-bit value associated with the window. Each window has a corresponding 32-bit value intended for use by the application that created the window.
dwNewLong = the replacement value.
So our job is easy: We code a window proc that will handle the messages for the edit control and then call SetWindowLong with GWL_WNDPROC flag, passing along the address of our window proc as the third parameter. If the function succeeds, the return value is the previous value of the specified 32-bit integer, in our case, the address of the original window procedure. We need to store this value for use within our window procedure.
Remember that there will be some messages we don't want to handle, we will pass them to the original window procedure. We can do that by calling CallWindowProc function.
    CallWindowProc PROTO lpPrevWndFunc:DWORD, \
    hWnd:DWORD,\
    Msg:DWORD,\
    wParam:DWORD,\
    lParam:DWORD
lpPrevWndFunc = the address of the original window procedure.
The remaining four parameters are the ones passed to our window procedure. We just pass them along to CallWindowProc.

Code Sample:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SubclassWinClass",0
AppName db "Subclassing Demo",0
EditClass db "EDIT",0
Message db "You pressed Enter in the text box!",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?
OldWndProc dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
20,300,25,hWnd,NULL,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,eax
;-----------------------------------------
; Subclass it!
;-----------------------------------------
invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
mov OldWndProc,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

Analysis:

    invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
    mov OldWndProc,eax
After the edit control is created, we subclass it by calling SetWindowLong, replacing the address of the original window procedure with our own window procedure. Note that we store the address of the original window procedure for use with CallWindowProc. Note the EditWndProc is an ordinary window procedure.
    .if uMsg==WM_CHAR
    mov eax,wParam
    .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
    .if al>="a" && al<="f"
    sub al,20h
    .endif
    invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
    ret
    .endif
Within EditWndProc, we filter WM_CHAR messages. If the character is between 0-9 or a-f, we accept it by passing along the message to the original window procedure. If it is a lower case character, we convert it to upper case by adding it with 20h. Note that, if the character is not the one we expect, we discard it. We don't pass it to the original window proc. So when the user types something other than 0-9 or a-f, the character just doesn't appear in the edit control.
    .elseif uMsg==WM_KEYDOWN
    mov eax,wParam
    .if al==VK_RETURN
    invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
    invoke SetFocus,hEdit
    .else
    invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
    ret
    .end
I want to demonstrate the power of subclassing further by trapping Enter key. EditWndProc checks WM_KEYDOWN message if it's VK_RETURN (the Enter key). If it is, it displays a message box saying "You pressed the Enter key in the text box!". If it's not an Enter key, it passes the message to the original window procedure.
You can use window subclassing to take control over other windows. It's one of the powerful techniques you should have in your arsenal.
[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 21: Pipe

Tutorial 21: Pipe

In this tutorial, we will explore pipe, what it is and what we can use it for. To make it more interesting, I throw in the technique on how to change the background and text color of an edit control.
Download the example here.

Theory:

Pipe is a communication conduit or pathway with two ends. You can use pipe to exchange the data between two different processes, or within the same process. It's like a walkie-talkie. You give the other party one set and he can use it to communicate with you.
There are two types of pipes: anonymous and named pipes. Anonymous pipe is, well, anonymous: that is, you can use it without knowing its name. A named pipe is the opposite: you have to know its name before you can use it.
You can also categorize pipes according to its property: one-way or two-way. In a one-way pipe, the data can flow only in one direction: from one end to the other. While in a two-way pipe, the data can be exchanged between both ends.
An anonymous pipe is always one-way while a named pipe can be one-way or two-way. A named pipe is usually used in a network environment where a server can connect to several clients.
In this tutorial, we will examine anonymous pipe in some detail. Anonymous pipe's main purpose is to be used as a communcation pathway between a parent and child processes or between child processes.
Anonymous pipe is really useful when you deal with a console application. A console application is a kind of win32 program which uses a console for its input & output. A console is like a DOS box. However, a console application is a fully 32-bit program. It can use any GUI function, the same as other GUI programs. It just happens to have a console for its use.
A console application has three handles it can use for its input & output. They are called standard handles. There are three of them: standard input, standard output and standard error. Standard input handle is used to read/retrieve the information from the console and standard output handle is used to output/print the information to the console. Standard error handle is used to report error condition since its output cannot be redirected.
A console application can retrieve those three standard handles by calling GetStdHandle function, specifying the handle it wants to obtain. A GUI application doesn't have a console. If you call GetStdHandle, it will return error. If you really want to use a console, you can call AllocConsole to allocate a new console. However, don't forget to call FreeConsole when you're done with the console.
Anonymous pipe is most frequently used to redirect input and/or output of a child console application. The parent process may be a console or a GUI application but the child must be a console app. for this to work. As you know, a console application uses standard handles for its input and output. If we want to redirect the input and/or output of a console application, we can replace the handle with a handle to one end of a pipe. A console application will not know that it's using a handle to one end of a pipe. It'll use it as a standard handle. This is a kind of polymorphism, in OOP jargon. This approach is powerful since we need not modify the child process in anyway.
Another thing you should know about a console application is where it gets those standard handles from. When a console application is created, the parent process has two choices: it can create a new console for the child or it can let the child inherit its own console. For the second approach to work, the parent process must be a console application or if it's a GUI application, it must call AllocConsole first to allocate a console.
Let's begin the work. In order to create an anonymous pipe you need to call CreatePipe. CreatePipe has the following prototype:
CreatePipe proto pReadHandle:DWORD, \
pWriteHandle:DWORD,\
pPipeAttributes:DWORD,\
nBufferSize:DWORD
  • pReadHandle is a pointer to a dword variable that will receive the handle to the read end of the pipe
  • pWriteHandle is a pointer to a dword variable that will receive the handle to the write end of the pipe.
  • pPipeAttributes points to a SECURITY_ATTRIBUTES structure that determines whether the returned read & write handles are inheritable by child processes
  • nBufferSize is the suggested size of the buffer the pipe will reserve for use. This is a suggested size only. You can use NULL to tell the function to use the default size.
If the call is successful, the return value is nonzero. If it failed, the return value is zero.
After the call is successful, you will get two handles, one to read end of the pipe and the other to the write end. Now I will outline the steps needed for redirecting the standard output of a child console program to your own process.Note that my method differs from the one in Borland's win32 api reference. The method in win32 api reference assumes the parent process is a console application and thus the child can inherit the standard handles from it. But most of the time, we will need to redirect output from a console application to a GUI one.
  1. Create an anonymous pipe with CreatePipe. Don't forget to set the bInheritable member of SECURITY_ATTRIBUTES to TRUE so the handles are inheritable.
  2. Now we must prepare the parameters we will pass to CreateProcess since we will use it to load the child console application. One important structure is the STARTUPINFO structure. This structure determines the appearance of the main window of the child process when it first appears. This structure is vital to our purpose. You can hide the main window and pass the pipe handle to the child console process with it. Below is the members you must fill:
    • cb : the size of STARTUPINFO structure
    • dwFlags : the binary bit flags that determine which members of the structure are valid also it governs the show/hide state of the main window. For our purpose, you should use a combination of STARTF_USESHOWWINDOW and STARTF_USESTDHANDLES
    • hStdOutput and hStdError : the handles you want the child process to use as standard output/error handles. For our purpose, we will pass write handle of the pipe as the standard output and error of the child. So when the child outputs something to the standard output/error, it actually passes the info via the pipe to the parent process.
    • wShowWindow governs the show/hide state of the main window. For our purpose, we don't want the console window of the child to show so we put SW_HIDE into this member.
  3. Call CreateProcess to load the child application. After CreateProcess is successful, the child is still dormant. It is loaded into memory but it doesn't run immediately
  4. Close the write pipe handle. This is necessary. Because the parent process has no use for the write pipe handle, and the pipe won't work if there are more than one write end, we MUST close it before reading the data from the pipe. However, don't close the write handle before calling CreateProcess, your pipe will be broken. You should close it just after CreateProcess returns and before you read data from the read end of the pipe.
  5. Now you can read data from the read end of the pipe with ReadFile. With ReadFile, you kick the child process into running mode. It will start execution and when it writes something to the standard output handle (which is actually the handle to the write end of the pipe), the data are sent through the pipe to the read end. You can think of ReadFile as sucking data from the read end of the pipe. You must call ReadFile repeatedly until it returns 0 which means there are no more data to be read. You can do anything with the data you read from the pipe. In our example, I put them into an edit control.
  6. Close the read pipe handle.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101 ; the ID of the main menu
IDM_ASSEMBLE equ 40001

.data
ClassName db "PipeWinClass",0
AppName db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError db "Error during pipe creation",0
CreateProcessError db "Error during process creation",0
CommandLine db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,IDR_MAINMENU
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL rect:RECT
LOCAL hRead:DWORD
LOCAL hWrite:DWORD
LOCAL startupinfo:STARTUPINFO
LOCAL pinfo:PROCESS_INFORMATION
LOCAL buffer[1024]:byte
LOCAL bytesRead:DWORD
LOCAL hdc:DWORD
LOCAL sat:SECURITY_ATTRIBUTES
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL
mov hwndEdit,eax
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetBkColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
.elseif uMsg==WM_SIZE
mov edx,lParam
mov ecx,edx
shr ecx,16
and edx,0ffffh
invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
.elseif uMsg==WM_COMMAND
.if lParam==0
mov eax,wParam
.if ax==IDM_ASSEMBLE
mov sat.nLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE
invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
.if eax==NULL
invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
.else
mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE
invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
.if eax==NULL
invoke MessageBox,hWnd,addr CreateProcessError,addr AppName,MB_ICONERROR+MB_OK
.else
invoke CloseHandle,hWrite
.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw
.endif
invoke CloseHandle,hRead
.endif
.endif
.endif
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analysis:

The example will call ml.exe to assemble a file named test.asm and redirect the output of ml.exe to the edit control in its client area.
When the program is loaded, it registers the window class and creates the main window as usual. The first thing it does during main window creation is to create an edit control which will be used to display the output of ml.exe.
Now the interesting part, we will change the text and background color of the edit control. When an edit control is going to paint its client area, it sends WM_CTLCOLOREDIT message to its parent.
wParam contains the handle to the device context that the edit control will use to write its own client area. We can use this opportunity to modify the characteristics of the HDC.
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetTextColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
SetTextColor changes the text color to yellow. SetTextColor changes the background color of the text to black. And lastly, we obtain the handle to the black brush which we return to Windows. With WM_CTLCOLOREDIT message, you must return a handle to a brush which Windows will use to paint the background of the edit control. In our example, I want the background to be black so I return the handle to the black brush to Windows.
Now when the user selects Assemble menuitem, it creates an anonymous pipe.

.if ax==IDM_ASSEMBLE
mov sat.nLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE

Prior to calling CreatePipe, we must fill the SECURITY_ATTRIBUTES structure first. Note that we can use NULL in lpSecurityDescriptor member if we don't care about security. And the bInheritHandle member must be TRUE so that the pipe handles are inheritable to the child process.

invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

After that, we call CreatePipe which, if successful, will fill hRead and hWrite variables with the handles to read and write ends of the pipe respectively.

mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE

Next we must fill the STARTUPINFO structure. We call GetStartupInfo to fill the STARTUPINFO structure with default values of the parent process. You MUST fill the STARTUPINFO structure with this call if you intend your code to work under both win9x and NT. After GetStartupInfo call returns, you can modify the members that are important. We copy the handle to the write end of the pipe into hStdOutput and hStdError since we want the child process to use it instead of the default standard output/error handles. We also want to hide the console window of the child process, so we put SW_HIDE value into wShowWidow member. And lastly, we must indicate that hStdOutput, hStdError and wShowWindow members are valid and must be used by specifying the flags STARTF_USESHOWWINDOW and STARTF_USESTDHANDLES in dwFlags member.

invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo

We now create the child process with CreateProcess call. Note that the bInheritHandles parameter must be set to TRUE for the pipe handle to work.

invoke CloseHandle,hWrite

After we successfully create the child process, we must close the write end of the pipe. Remember that we passed the write handle to the child process via STARTUPINFO structure. If we don't close the write handle from our end, there will be two write ends. And that the pipe will not work. We must close the write handle after CreateProcess but before we read data from the read end of the pipe.

.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw

Now we are ready to read the data from the standard output of the child process. We will stay in an infinite loop until there are no more data left to read from the pipe. We call RtlZeroMemory to fill the buffer with zeroes then call ReadFile, passing the read handle of the pipe in place of a file handle. Note that we only read a maximum of 1023 bytes since we need the data to be an ASCIIZ string which we can pass on to the edit control.
When ReadFile returns with the data in the buffer, we fill the data into the edit control. However, there is a slight problem here. If we use SetWindowText to put the data into the edit control, the new data will overwrite existing data! We want the data to append to the end of the existing data.
To achieve that goal, we first move the caret to the end of the text in the edit control by sending EM_SETSEL message with wParam==-1. Next, we append the data at that point with EM_REPLACESEL message.

invoke CloseHandle,hRead

When ReadFile returns NULL, we break out of the loop and close the read handle.

Iczelion's Win32 Assembly Homepage


Iczelion's Win32 Assembly Tutorial 22: SuperClassing

Tutorial 22: Superclassing

In this tutorial, we will learn about superclassing, what it is and what it is for. You will also learn how to provide Tab key navigation to the controls in your own window.
Download the example here

Theory:

In your programming career, you will surely encounter a situation where you need several controls with *slightly* different behavior. For example, you may need 10 edit controls which accept only number. There are several ways to achieve that goal:
  • Create your own class and instantiate the controls
  • Create those edit control and then subclass all of them
  • Superclass the edit control
The first method is too tedious. You have to implement every functionality of the edit control yourself. Hardly a task to be taken lightly. The second method is better than the first one but still too much work. It is ok if you subclass only a few controls but it's going to be a nightmare to subclass a dozen or so controls. Superclassing is the technique you should use for this occasion.
Subclassing is the method you use to *take control* of a particular window class. By *taking control*, I mean you can modify the properties of the window class to suit your purpose then then create the bunch of controls.
The steps in superclassing is outlined below:
  • call GetClassInfoEx to obtain the information about the window class you want to superclass. GetClassInfoEx requires a pointer to a WNDCLASSEX structure which will be filled with the information if the call returns successfully.
  • Modify the WNDCLASSEX members that you want. However, there are two members which you MUST modify:
    • hInstance You must put the instance handle of your program into this member.
    • lpszClassName You must provide it with a pointer to a new class name.

      • You need not modify lpfnWndProc member but most of the time, you need to do it. Just remember to save the original lpfnWndProc member if you want to call it with CallWindowProc.
  • Register the modifed WNDCLASSEX structure. You'll have a new window class which has several characteristics of the old window class.
  • Create windows from the new class
Superclassing is better than subclassing if you want to create many controls with the same characteristics.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WM_SUPERCLASS equ WM_USER+5
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SuperclassWinClass",0
AppName db "Superclassing Demo",0
EditClass db "EDIT",0
OurClass db "SUPEREDITCLASS",0
Message db "You pressed the Enter key in the text box!",0

.data?
hInstance dd ?
hwndEdit dd 6 dup(?)
OldWndProc dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,220,NULL,NULL,\
hInst,NULL
mov hwnd,eax

.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL wc:WNDCLASSEX
.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc
push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass
invoke RegisterClassEx, addr wc
xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

Analysis:

The program will create a simple window with 6 "modified" edit controls in its client area. The edit controls will accept only hex digits. Actually, I modified the subclassing example to do superclassing. The program starts normally and the interesting part is when the main window is created:

.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc

We must first fill the WNDCLASSEX structure with the data from the class which we want to superclass, in this case, it's EDIT class. Remember that you must set the cbSize member of the WNDCLASSEX structure before you call GetClassInfoEx else the WNDCLASSEX structure will not be filled properly. After GetClassInfoEx returns, wc is filled with all information we need to create a new window class.

push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass

Now we must modify some members of wc. The first one is the pointer to the window procedure. Since we need to chain our own window procedure with the original one, we have to save it into a variable so we can call it with CallWindowProc. This technique is identical to subclassing except that you modify the WNDCLASSEX structure directly without having to call SetWindowLong. The next two members must be changed else you will not be able to register your new window class, hInstance and lpsClassName. You must replace original hInstance value with hInstance of your own program. And you must choose a new name for the new class.

invoke RegisterClassEx, addr wc

When all is ready, register the new class. You will get a new class with some characteristics of the old class.

xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit

Now that we registered the class, we can create windows based on it. In the above snippet, I use ebx as the counter of the number of windows created. edi is used as the y coordinate of the left upper corner of the window. When a window is created, its handle is stored in the array of dwords. When all windows are created, set input focus to the first window.
At this point, you got 6 edit controls which accept only hex digits. The substituted window proc handles the filter. Actually, it's identical to the window proc in subclassing example. As you can see, you don't have to do extra work of subclassing them.

I throw in a code snippet to handle control navigation with tabs to make this example more juicy. Normally, if you put controls on a dialog box, the dialog box manager handles the navigation keys for you so you can tab to go to the next control or shift-tab to go back to the previous control. Alas, such feature is not available if you put your controls on a simple window. You have to subclass them so you can handle the Tab keys yourself. In our example, we need not subclass the controls one by one because we already superclassed them, so we can provide a "central control navigation manager" for them.

.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret

The above code snippet is from EditWndClass procedure. It checks if the user press Tab key, if so, it call GetKeyState to check if the SHIFT key is also pressed. GetKeyState returns a value in eax that determines whether the specified key is pressed or not. If the key is pressed, the high bit of eax is set. If not, the high bit is clear. So we test the return value against 80000000h. If the high bit is set, it means the user pressed shift+tab which we must handle separately.
If the user press Tab key alone, we call GetWindow to retrieve the handle of the next control. We use GW_HWNDNEXT flag to tell GetWindow to obtain the handle to the window that is next in line to the current hEdit. If this function returns NULL, we interpret it as no more handle to obtain so the current hEdit is the last control in the line. We will "wrap around" to the first control by calling GetWindow with GW_HWNDFIRST flag. Similar to the Tab case, shift-tab just works in reverse.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 23: Tray Icon

Tutorial 23: Tray Icon

In this tutorial, we will learn how to put icons into system tray and how to create/use a popup menu.
Download the example here.

Theory:

System tray is the rectangular region in the taskbar where several icons reside. Normally, you'll see at least a digital clock in it. You can also put icons in the system tray too. Below are the steps you have to perform to put an icon into the system tray:
  1. Fill a NOTIFYICONDATA structure which has the following members:
    • cbSize The size of this structure.
    • hwnd Handle of the window that will receive notification when a mouse event occurs over the tray icon.
    • uID A constant that is used as the icon's identifier. You are the one who decides on this value. In case you have more than one tray icons, you will be able to check from what tray icon the mouse notification is from.
    • uFlags Specify which members of this structure are valid
      • NIF_ICON The hIcon member is valid.
      • NIF_MESSAGE The uCallbackMessage member is valid.
      • NIF_TIP The szTip member is valid.
    • uCallbackMessage The custom message that Windows will send to the window specified by the hwnd member when mouse events occur over the tray icon. You create this message yourself.
    • hIcon The handle of the icon you want to put into the system tray
    • szTip A 64-byte array that contains the string that will be used as the tooltip text when the mouse hovers over the tray icon.
  2. Call Shell_NotifyIcon which is defined in shell32.inc. This function has the following prototype:

    3. Shell_NotifyIcon PROTO dwMessage:DWORD ,pnid:DWORD

    dwMessage is the type of message to send to the shell.
    NIM_ADD Adds an icon to the status area.
    NIM_DELETE Deletes an icon from the status area.
    NIM_MODIFY Modifies an icon in the status area.
    pnid is the pointer to a NOTIFYICONDATA structure filled with proper values
    If you want to add an icon to the tray, use NIM_ADD message, if you want to remove the icon, use NIM_DELETE.

That's all there is to it. But most of the time, you're not content in just putting an icon there. You need to be able to respond to the mouse events over the tray icon. You can do this by processing the message you specified in uCallbackMessage member of NOTIFYICONDATA structure. This message has the following values in wParam and lParam (special thanks to s__d for the info):
  • wParam contains the ID of the icon. This is the same value you put into uID member of NOTIFYICONDATA structure.
  • lParam The low word contains the mouse message. For example, if the user right-clicked at the icon, lParam will contain WM_RBUTTONDOWN.
Most tray icon, however, displays a popup menu when the user right-click on it. We can implement this feature by creating a popup menu and then call TrackPopupMenu to display it. The steps are described below:
  1. Create a popup menu by calling CreatePopupMenu. This function creates an empty menu. It returns the menu handle in eax if successful.
  2. Add menu items to it with AppendMenu, InsertMenu or InsertMenuItem.
  3. When you want to display the popup menu where the mouse cursor is, call GetCursorPos to obtain the screen coordinate of the cursor and then call TrackPopupMenu to display the menu. When the user selects a menu item from the popup menu, Windows sends WM_COMMAND message to your window procedure just like normal menu selection.
Note: Beware of two annoying behaviors when you use a popup menu with a tray icon:
  1. When the popup menu is displayed, if you click anywhere outside the menu, the popup menu will not disappear immediately as it should be. This behavior occurs because the window that will receive the notifications from the popup menu MUST be the foreground window. Just call SetForegroundWindow will correct it.
  2. After calling SetForegroundWindow, you will find that the first time the popup menu is displayed, it works ok but on the subsequent times, the popup menu will show up and close immediately. This behavior is "intentional", to quote from MSDN. The task switch to the program that is the owner of the tray icon in the near future is necessary. You can force this task switch by posting any message to the window of the program. Just use PostMessage, not SendMessage!

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\shell32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\shell32.lib

WM_SHELLNOTIFY equ WM_USER+5
IDI_TRAY equ 0
IDM_RESTORE equ 1000
IDM_EXIT equ 1010
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "TrayIconWinClass",0
AppName db "TrayIcon Demo",0
RestoreString db "&Restore",0
ExitString db "E&xit Program",0

.data?
hInstance dd ?
note NOTIFYICONDATA <>
hPopupMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL pt:POINT
.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
.elseif uMsg==WM_DESTROY
invoke DestroyMenu,hPopupMenu
invoke PostQuitMessage,NULL
.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif
.endif
.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

end start

Analysis:

The program will display a simple window. When you press the minimize button, it will hide itself and put an icon into the system tray. When you double-click on the icon, the program will restore itself and remove the icon from the system tray. When you right-click on it, a popup menu is displayed. You can choose to restore the program or exit it.

.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString

When the main window is created, it creates a popup menu and append two menu items. AppendMenu has the following syntax:

AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD
  • hMenu is the handle of the menu you want to append the item to
  • uFlags tells Windows about the menu item to be appended to the menu whether it is a bitmap or a string or an owner-draw item, enabled, grayed or disable etc. You can get the complete list from win32 api reference. In our example, we use MF_STRING which means the menu item is a string.
  • uIDNewItem is the ID of the menu item. This is a user-defined value that is used to represent the menu item.
  • lpNewItem specifies the content of the menu item, depending on what you specify in uFlags member. Since we specify MF_STRING in uFlags member, lpNewItem must contain the pointer to the string to be displayed in the popup menu.
After the popup menu is created, the main window waits patiently for the user to press minimize button.
When a window is minimized, it receives WM_SIZE message with SIZE_MINIMIZED value in wParam.

.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif

We use this opportunity to fill NOTIFYICONDATA structure. IDI_TRAY is just a constant defined at the beginning of the source code. You can set it to any value you like. It's not important because you have only one tray icon. But if you will put several icons into the system tray, you need unique IDs for each tray icon. We specify all flags in uFlags member because we specify an icon (NIF_ICON), we specify a custom message (NIF_MESSAGE) and we specify the tooltip text (NIF_TIP). WM_SHELLNOTIFY is just a custom message defined as WM_USER+5. The actual value is not important so long as it's unique. I use the winlogo icon as the tray icon here but you can use any icon in your program. Just load it from the resource with LoadIcon and put the returned handle in hIcon member. Lastly, we fill the szTip with the text we want the shell to display when the mouse is over the icon.
We hide the main window to give the illusion of "minimizing-to-tray-icon" appearance.
Next we call Shell_NotifyIcon with NIM_ADD message to add the icon to the system tray.

Now our main window is hidden and the icon is in the system tray. If you move the mouse over it, you will see a tooltip that displays the text we put into szTip member. Next, if you double-click at the icon, the main window will reappear and the tray icon is gone.

.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif

When a mouse event occurs over the tray icon, your window receives WM_SHELLNOTIFY message which is the custom message you specified in uCallbackMessage member. Recall that on receiving this message, wParam contains the tray icon's ID and lParam contains the actual mouse message. In the code above, we check first if this message comes from the tray icon we are interested in. If it does, we check the actual mouse message. Since we are only interested in right mouse click and double-left-click, we process only WM_RBUTTONDOWN and WM_LBUTTONDBLCLK messages.
If the mouse message is WM_RBUTTONDOWN, we call GetCursorPos to obtain the current screen coordinate of the mouse cursor. When the function returns, the POINT structure is filled with the screen coordinate of the mouse cursor. By screen coordinate, I mean the coordinate of the entire screen without regarding to any window boundary. For example, if the screen resolution is 640*480, the right-lower corner of the screen is x==639 and y==479. If you want to convert the screen coordinate to window coordinate, use ScreenToClient function.
However, for our purpose, we want to display the popup menu at the current mouse cursor position with TrackPopupMenu call and it requires screen coordinates, we can use the coordinates filled by GetCursorPos directly.
TrackPopupMenu has the following syntax:

    TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD
  • hMenu is the handle of the popup menu to be displayed
  • uFlags specifies the options of the function. Like where to position the menu relative to the coordinates specified later and which mouse button will be used to track the menu. In our example, we use TPM_RIGHTALIGN to position the popup menu to the left of the coordinates.
  • x and y specify the location of the menu in screen coordinates.
  • nReserved must be NULL
  • hWnd is the handle of the window that will receive the messages from the menu.
  • prcRect is the rectangle in the screen where it is possible to click without dismissing the menu. Normally we put NULL here so when the user clicks anywhere outside the popup menu, the menu is dismissed.
When the user double-clicks at the tray icon, we send WM_COMMAND message to our own window specifying IDM_RESTORE to emulate the user selects Restore menu item in the popup menu thereby restoring the main window and removing the icon from the system tray. In order to be able to receive double click message, the main window must have CS_DBLCLKS style.

invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif

When the user selects Restore menu item, we remove the tray icon by calling Shell_NotifyIcon again, this time we specify NIM_DELETE as the message. Next, we restore the main window to its original state. If the user selects Exit menu item, we also remove the icon from the tray and destroy the main window by calling DestroyWindow.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 24: Windows Hooks

Tutorial 24: Windows Hooks

We will learn about Windows hooks in this tutorial. Windows hooks are very powerful. With them, you can poke inside other processes and sometimes alter their behaviors.
Download the example here.

Theory:

Windows hooks can be considered one of the most powerful features of Windows. With them, you can trap events that will occur, either in your own process or in other processes. By "hooking", you tell Windows about a function, filter function also called hook procedure, that will be called everytime an event you're interested in occurs. There are two types of them: local and remote hooks.
  • Local hooks trap events that will occur in your own process.
  • Remote hooks trap events that will occur in other process(es). There are two types of remote hooks
    • thread-specific traps events that will occur in a specific thread in other process. In short, you want to observe events in a specific thread in a specific process.
    • system-wide traps all events destined for all threads in all processes in the system.
When you install hooks, remember that they affect system performance. System-wide hooks are the most notorious. Since ALL related events will be routed through your filter function, your system may slow down noticeably. So if you use a system-wide hook, you should use it judiciously and unhook it as soon as you don't need it. Also, you have a higher chance of crashing the other processes since you can meddle with other processes and if something is wrong in your filter function, it can pull the other processes down to oblivion with it. Remember: Power comes with responsibility.
You have to understand how a hook works before you can use it efficiently. When you create a hook, Windows creates a data structure in memory, containing information about the hook, and adds it to a linked list of existing hooks. New hook is added in front of old hooks. When an event occurs, if you install a local hook, the filter function in your process is called so it's rather straightforward. But if it's a remote hook, the system must inject the code for the hook procedure into the address space(s) of the other process(es). And the system can do that only if the function resides in a DLL. Thus , if you want to use a remote hook, your hook procedure must reside in a DLL. There is two exceptions to this rule: journal record and journal playback hooks. The hook procedures for those two hooks must reside in the thread that installs the hooks. The reason why it must be so is that: both hooks deal with the low-level interception of hardware input events. The input events must be recorded/playbacked in the order they appeared. If the code of those two hooks is in a DLL, the input events may scatter among several threads and it is impossible to know the order of them. So the solution: the hook procedure of those two hooks must be in a single thread only i.e. the thread that installs the hooks.
There are 14 types of hooks:
  • WH_CALLWNDPROC called when SendMessage is called
  • WH_CALLWNDPROCRET called when SendMessage returns
  • WH_GETMESSAGE called when GetMessage or PeekMessage is called
  • WH_KEYBOARD called when GetMessage or PeekMessage retrieves WM_KEYUP or WM_KEYDOWN from the message queue
  • WH_MOUSE called when GetMessage or PeekMessage retrieves a mouse message from the message queue
  • WH_HARDWARE called when GetMessage or PeekMessage retrieves some hardware message that is not related to keyboard or mouse.
  • WH_MSGFILTER called when a dialog box, menu or scrollbar is about to process a message. This hook is local. It's specifically for those objects which have their own internal message loops.
  • WH_SYSMSGFILTER same as WH_MSGFILTER but system-wide
  • WH_JOURNALRECORD called when Windows retrieves message from the hardware input queue
  • WH_JOURNALPLAYBACK called when an event is requested from the system's hardware input queue.
  • WH_SHELL called when something interesting about the shell occurs such as when the task bar needs to redraw its button.
  • WH_CBT used specifically for computer-based training (CBT).
  • WH_FOREGROUNDIDLE used internally by Windows. Little use for general applications
  • WH_DEBUG used to debug the hooking procedure
Now that we know some theory, we can move on to how to install/uninstall the hooks.
To install a hook, you call SetWindowsHookEx which has the following syntax:
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD
  • HookType is one of the values listed above, e.g., WH_MOUSE, WH_KEYBOARD
  • pHookProc is the address of the hook procedure that will be called to process the messages for the specified hook. If the hook is a remote one, it must reside in a DLL. If not, it must be in your process.
  • hInstance is the instance handle of the DLL in which the hook procedure resides. If the hook is a local one, this value must be NULL
  • ThreadID is the ID of the thread you want to install the hook to spy on. This parameter is the one that determines whether a hook is local or remote. If this parameter is NULL, Windows will interpret the hook as a system-wide remote hook that affects all threads in the system. If you specify the thread ID of a thread in your own process, this hook is a local one. If you specify the thread ID from other process, the hook is a thread-specific remote one. There are two exceptions to this rule: WH_JOURNALRECORD and WH_JOURNALPLAYBACK are always local system-wide hooks that are not required to be in a DLL. And WH_SYSMSGFILTER is always a system-wide remote hook. Actually it is identical to WH_MSGFILTER hook with ThreadID==0.
If the call is successful, it returns the hook handle in eax. If not, NULL is returned. You must save the hook handle for unhooking later.
You can uninstall a hook by callingUnhookWindowsHookEx which accepts only one parameter, the handle of the hook you want to uninstall. If the call succeeds, it returns a non-zero value in eax. Otherwise, it returns NULL.
Now that you know how to install/uninstall hooks, we can examine the hook procedure.
The hook procedure will be called whenever an event that is associated with the type of hook you have installed occurs. For example, if you install WH_MOUSE hook, when a mouse event occurs, your hook procedure will be called. Regardless of the type of hook you installed, the hook procedure always has the following prototype:
    HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
    • nCode specifies the hook code.
    • wParam and lParam contain additional information about the event
HookProc is actually a placeholder for the function name. You can name it anything you like so long as it has the above prototype. The interpretation of nCode, wParam and lParam is dependent on the type of hook you install. So as the return value from the hook procedure. For example:
WH_CALLWNDPROC
  • nCode can be only HC_ACTION which means there is a message sent to a window
  • wParam contains the message being sent, if it's not zero
  • lParam points to a CWPSTRUCT structure
  • return value: not used, return zero
WH_MOUSE
  • nCode can be HC_ACTION or HC_NOREMOVE
  • wParam contains the mouse message
  • lParam points to a MOUSEHOOKSTRUCT structure
  • return value: zero if the message should be processed. 1 if the message should be discarded.
The bottom line is: you must consult your win32 api reference for details about the meanings of the parameters and return value of the hook you want to install.
Now there is a little catch about the hook procedure. Remember that the hooks are chained in a linked list with the most recently installed hook at the head of the list. When an event occurs, Windows will call only the first hook in the chain. It's your hook procedure's responsibility to call the next hook in the chain. You can choose not to call the next hook but you'd better know what you're doing. Most of the time, it's a good practice to call the next procedure so other hooks can have a shot at the event. You can call the next hook by calling CallNextHookEx which has the following prototype:
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD
  • hHook is your own hook handle. The function uses this handle to traverse the linked list and search for the hook procedure it should call next.
  • nCode, wParam and lParam you can just pass those three values you receive from Windows to CallNextHookEx.
An important note about remote hooks: the hook procedure must reside in a DLL which will be mapped into other processes. When Windows maps the DLL into other processes, it will not map the data section(s) into the other processes. In short, all processes share a single copy of code but they will have their own private copy of the DLL's data section! This can be a big surprise to the unwary. You may think that when you store a value into a variable in the data section of a DLL, that value will be shared among all processes that load the DLL into their process address space. It's simply not true. In normal situation, this behavior is desirable since it provides the illusion that each process has its own copy of the DLL. But not when Windows hook is concerned. We want the DLL to be identical in all processes, including the data. The solution: you must mark the data section as shared. You can do this by specifying the section(s) attribute in the linker switch. For MASM, you need to use this switch:
/SECTION:<section name>, S
The name of the initialized data section is .data and the uninitialized data is .bss. For example if you want to assemble a DLL which contains a hook procedure and you want the uninitialized data section to be shared amoung processes, you must use the following line:
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........
S attribute marks the section as shared.

Example:

There are two modules: one is the main program which will do the GUI part and the other is the DLL that will install/uninstall the hook.

;--------------------------------------------- This is the source code of the main program --------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

wsprintfA proto C :DWORD,:DWORD,:VARARG
wsprintf TEXTEQU <wsprintfA>

.const
IDD_MAINDLG equ 101
IDC_CLASSNAME equ 1000
IDC_HANDLE equ 1001
IDC_WNDPROC equ 1002
IDC_HOOK equ 1004
IDC_EXIT equ 1005
WM_MOUSEHOOK equ WM_USER+6

DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0

.data?
hInstance dd ?
hHook dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
invoke ExitProcess,NULL

DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL hLib:DWORD
LOCAL buffer[128]:byte
LOCAL buffer1[128]:byte
LOCAL rect:RECT
.if uMsg==WM_CLOSE
.if HookFlag==TRUE
invoke UninstallHook
.endif
invoke EndDialog,hDlg,NULL
.elseif uMsg==WM_INITDIALOG
invoke GetWindowRect,hDlg,addr rect
invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif
.elseif uMsg==WM_COMMAND
.if lParam!=0
mov eax,wParam
mov edx,eax
shr edx,16
.if dx==BN_CLICKED
.if ax==IDC_EXIT
invoke SendMessage,hDlg,WM_CLOSE,0,0
.else
.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif
.else
invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
.endif
.endif
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgFunc endp

end start

;----------------------------------------------------- This is the source code of the DLL --------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.const
WM_MOUSEHOOK equ WM_USER+6

.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
mov eax,TRUE
ret
DllEntry Endp

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

UninstallHook proc
invoke UnhookWindowsHookEx,hHook
ret
UninstallHook endp

End DllEntry

;---------------------------------------------- This is the makefile of the DLL ----------------------------------------------

NAME=mousehook
$(NAME).dll: $(NAME).obj
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib $(NAME).obj
$(NAME).obj: $(NAME).asm
ml /c /coff /Cp $(NAME).asm

Analysis:

The example will display a dialog box with three edit controls that will be filled with the class name, window handle and the address of the window procedure associated with the window under the mouse cursor. There are two buttons, Hook and Exit. When you press the Hook button, the program hooks the mouse input and the text on the button changes to Unhook. When you move the mouse cursor over a window, the info about that window will be displayed in the main window of the example. When you press Unhook button, the program removes the mouse hook.
The main program uses a dialog box as its main window. It defines a custom message, WM_MOUSEHOOK which will be used between the main program and the hook DLL. When the main program receives this message, wParam contains the handle of the window that the mouse cursor is on. Of course, this is an arbitrary arrangement. I decide to send the handle in wParam for the sake of simplicity. You can choose your own method of communication between the main program and the hook DLL.

.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif

The program maintains a flag, HookFlag, to monitor the state of the hook. It's FALSE if the hook is not installed and TRUE if the hook is installed.
When the user presses Hook button, the program checks if the hook is already installed. If it is not, it call InstallHook function in the hook DLL to install it. Note that we pass the handle of the main dialog as the parameter of the function so the hook DLL can send the WM_MOUSEHOOK messages to the right window i.e. our own.
When the program is loaded, the hook DLL is loaded too. Actually, DLLs are loaded immediately after the program is in memory. The DLL entrypoint function is called before the first instruction in the main program is execute even. So when the main program executes the DLL(s) is/are initialized. We put the following code in the DLL entrypoint function of the hook DLL:

.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif

The code just saves the instance handle of the hook DLL itself to a global variable named hInstance for use within the InstallHook function. Since the DLL entrypoint function is called before other functions in the DLL are called , hInstance is always valid. We put hInstance in .data section so that this value is kept on per-process basis. Since when the mouse cursor hovers over a window, the hook DLL is mapped into the process. Imagine that there is already a DLL that occupies the intended load address of the hook DLL, the hook DLL would be remapped to another address. The value of hInstance will be updated to those of the new load address. When the user presses Unhook button and then Hook button, SetWindowsHookEx will be called again. However, this time, it will use the new load address as the instance handle which will be wrong because in the example process, the hook DLL's load address hasn't been changed. The hook will be a local one where you can hook only the mouse events that occur in your own window. Hardly desirable.

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

The InstallHook function itself is very simple. It saves the window handle passed as its parameter to a global variable named hWnd for future use. It then calls SetWindowsHookEx to install a mouse hook. The return value of SetWindowsHookEx is stored in a global variable named hHook for use with UnhookWindowsHookEx.
After SetWindowsHookEx is called, the mouse hook is functional. Whenever a mouse event occurs in the system, MouseProc ( your hook procedure) is called.

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

The first thing it does is to call CallNextHookEx to give the other hooks the chance to process the mouse event. After that, it calls WindowFromPoint function to retrieve the handle of the window at the specified screen coordinate. Note that we use the POINT structure in the MOUSEHOOKSTRUCT structure pointed to by lParam as the current mouse coordinate. After that we send the window handle to the main program via PostMessage with WM_MOUSEHOOK message. One thing you should remember is that: you should not use SendMessage inside the hook procedure, it can cause message deadlock. PostMessage is recommended. The MOUSEHOOKSTRUCT structure is defined below:

MOUSEHOOKSTRUCT STRUCT DWORD
pt POINT <>
hwnd DWORD ?
wHitTestCode DWORD ?
dwExtraInfo DWORD ?
MOUSEHOOKSTRUCT ENDS

  • pt is the current screen coordinate of the mouse cursor
  • hwnd is the handle of the window that will receive the mouse message. It's usually the window under the mouse cursor but not always. If a window calls SetCapture, the mouse input will be redirected to that window instead. Because of this reason, I don't use the hwnd member of this structure but choose to call WindowFromPoint instead.
  • wHitTestCode specifies the hit-test value. The hit-test value gives more information about the current mouse cursor position. It specifies on what part of window the mouse cursor is. For complete list, check your win32 api reference under WM_NCHITTEST message.
  • dwExtraInfo contains the extra information associated with the message. Normally this value is set by calling mouse_event and retrieved by calling GetMessageExtraInfo.
When the main window receives WM_MOUSEHOOK message, it uses the window handle in wParam to retrieve the information about the window.

.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif

To avoid flickers, we check the text already in the edit controls and the text we will put into them if they are identical. If they are, we skip them.
We retrieve the class name by calling GetClassName, the address of the window procedure by calling GetClassLong with GCL_WNDPROC and then format them into strings and put them into the appropriate edit controls.

invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL

When the user presses Unhook button, the program calls UninstallHook function in the hook DLL. UninstallHook just calls UnhookWindowsHookEx. After that, it changes the text of the button back to "Hook", HookFlag to FALSE and clears the content of the edit controls.
Note the linker switch in the makefile.

Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS

It specifies .bss section as a shared section to make all processes share the same uninitialized data section of the hook DLL. Without this switch, your hook DLL will not function correctly.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32asm Tutorial 25: Simple Bitmap

Tutorial 25: Simple Bitmap

In this tutorial, we will learn how to use bitmap in our program. To be exact, we will learn how to display a bitmap in the client area of our window. Download the example.

Theory

Bitmaps can be thought of as pictures stored in computer. There are many picture formats used with computers but Windows only natively supports Windows Bitmap Graphics files (.bmp). The bitmaps I'll refer to in this tutorial are Windows bitmap graphics files. The easiest way to use a bitmap is to use it as a resource. There are two ways to do that. You can include the bitmap in the resource definition file (.rc) as follows:
#define IDB_MYBITMAP 100
IDB_MYBITMAP BITMAP "c:\project\example.bmp"
This method uses a constant to represent the bitmap. The first line just creates a constant named IDB_MYBITMAP which has the value of 100. We will use this label to refer to the bitmap in the program. The next line declares a bitmap resource. It tells the resource compiler where to find the actual bmp file.
The other method uses a name to represent the bitmap as follows:
MyBitMap BITMAP "c:\project\example.bmp"
This method requires that you refer to the bitmap in your program by the string "MyBitMap" instead of a value.
Either method works fine as long as you know which method you're using.
Now that we put the bitmap in the resource file, we can go on with the steps in displaying it in the client area of our window.
  1. call LoadBitmap to get the bitmap handle. LoadBitmap has the following definition:
      LoadBitmap proto hInstance:HINSTANCE, lpBitmapName:LPSTR

    This function returns a bitmap handle. hInstance is the instance handle of our program. lpBitmapName is a pointer to the string that is the name of the bitmap (incase you use the second method to refer to the bitmap). If you use a constant to refer to the bitmap (like IDB_MYBITMAP), you can put its value here. (In the example above it would be 100). A short example is in order:

      First Method:

      .386
      .model flat, stdcall
      ................
      .const
      IDB_MYBITMAP equ 100
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,IDB_MYBITMAP
      ...........

      Second Method:

      .386
      .model flat, stdcall
      ................
      .data
      BitmapName db "MyBitMap",0
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,addr BitmapName
      ...........

  1. Obtain a handle to device context (DC). You can obtain this handle by calling BeginPaint in response to WM_PAINT message or by calling GetDC anywhere.
  2. Create a memory device context which has the same attribute as the device context we just obtained. The idea here is to create a kind of "hidden" drawing surface which we can draw the bitmap on. When we are finished with the operation, we just copy the content of the hidden drawing surface to the actual device context in one function call. It's an example of double-buffer technique used for fast display of pictures on the screen. You can create this "hidden" drawing surface by calling CreateCompatibleDC.
      CreateCompatibleDC proto hdc:HDC

    If this function succeeds, it returns the handle of the memory device context in eax. hdc is the handle to the device context that you want the memory DC to be compatible with.

  1. Now that you got a hidden drawing surface, you can draw on it by selecting the bitmap into it. This is done by calling SelectObject with the handle to the memory DC as the first parameter and the bitmap handle as the second parameter. SelectObject has the following definition:
      SelectObject proto hdc:HDC, hGdiObject:DWORD
  1. The bitmap is drawn on the memory device context now. All we need to do here is to copy it to the actual display device, namely the true device context. There are several functions that can perform this operation such as BitBlt and StretchBlt. BitBlt just copies the content of one DC to another so it's fast while StretchBlt can stretch or compress the bitmap to fit the output area. We will use BitBlt here for simplicity. BitBlt has the following definition:
      BitBlt proto hdcDest:DWORD, nxDest:DWORD, nyDest:DWORD, nWidth:DWORD, nHeight:DWORD, hdcSrc:DWORD, nxSrc:DWORD, nySrc:DWORD, dwROP:DWORD
    hdcDest is the handle of the device context that serves as the destination of bitmap transfer operation
    nxDest, nyDest are the coordinate of the upper left corner of the output area
    nWidth, nHeight are the width and height of the output area
    hdcSrc is the handle of the device context that serves as the source of bitmap transfer operation
    nxSrc, nySrc are the coordinate of the upper left corner of the source rectangle.
    dwROP is the raster-operation code (hence the acronym ROP) that governs how to combine the color data of the bitmap to the existing color data on the output area to achieve the final result. Most of the time, you only want to overwrite the existing color data with the new one.
  1. When you're done with the bitmap, delete it with DeleteObject API call.
That's it! To recapitulate, you need to put the bitmap into the resource scipt. Then load it from the resource with LoadBitmap. You'll get the bitmap handle. Next you obtain the handle to the device context of the area you want to paint the bitmap on. Then you create a memory device context that is compatible with the device context you just obtained. Select the bitmap into the memory DC then copy the content of the memory DC to the real DC.

Example Code:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
IDB_MAIN   equ 1

.data
ClassName db "SimpleWin32ASMBitmapClass",0
AppName  db "Win32ASM Simple Bitmap Example",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hBitmap dd ?

.code
start:
 invoke GetModuleHandle, NULL
 mov    hInstance,eax
 invoke GetCommandLine
 mov    CommandLine,eax
 invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
 invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
 LOCAL wc:WNDCLASSEX
 LOCAL msg:MSG
 LOCAL hwnd:HWND
 mov   wc.cbSize,SIZEOF WNDCLASSEX
 mov   wc.style, CS_HREDRAW or CS_VREDRAW
 mov   wc.lpfnWndProc, OFFSET WndProc
 mov   wc.cbClsExtra,NULL
 mov   wc.cbWndExtra,NULL
 push  hInstance
 pop   wc.hInstance
 mov   wc.hbrBackground,COLOR_WINDOW+1
 mov   wc.lpszMenuName,NULL
 mov   wc.lpszClassName,OFFSET ClassName
 invoke LoadIcon,NULL,IDI_APPLICATION
 mov   wc.hIcon,eax
 mov   wc.hIconSm,eax
 invoke LoadCursor,NULL,IDC_ARROW
 mov   wc.hCursor,eax
 invoke RegisterClassEx, addr wc
 INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
 mov   hwnd,eax
 invoke ShowWindow, hwnd,SW_SHOWNORMAL
 invoke UpdateWindow, hwnd
 .while TRUE
  invoke GetMessage, ADDR msg,NULL,0,0
  .break .if (!eax)
  invoke TranslateMessage, ADDR msg
  invoke DispatchMessage, ADDR msg
 .endw
 mov     eax,msg.wParam
 ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   LOCAL ps:PAINTSTRUCT
   LOCAL hdc:HDC
   LOCAL hMemDC:HDC
   LOCAL rect:RECT
   .if uMsg==WM_CREATE
      invoke LoadBitmap,hInstance,IDB_MAIN
      mov hBitmap,eax
   .elseif uMsg==WM_PAINT
      invoke BeginPaint,hWnd,addr ps
      mov    hdc,eax
      invoke CreateCompatibleDC,hdc
      mov    hMemDC,eax
      invoke SelectObject,hMemDC,hBitmap
      invoke GetClientRect,hWnd,addr rect
      invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
      invoke DeleteDC,hMemDC
      invoke EndPaint,hWnd,addr ps
 .elseif uMsg==WM_DESTROY
  invoke DeleteObject,hBitmap
  invoke PostQuitMessage,NULL
 .ELSE
  invoke DefWindowProc,hWnd,uMsg,wParam,lParam
  ret
 .ENDIF
 xor eax,eax
 ret
WndProc endp
end start

;---------------------------------------------------------------------
;                            The resource script
;---------------------------------------------------------------------
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"

Analysis:

There is not much to analyze in this tutorial ;)
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"
Define a constant named IDB_MAIN, assign 1 as its value. And then use that constant as the bitmap resource identifier. The bitmap file to be included in the resource is "tweety78.bmp" which resides in the same folder as the resource script.

   .if uMsg==WM_CREATE
      invoke LoadBitmap,hInstance,IDB_MAIN
      mov hBitmap,eax

In response to WM_CREATE, we call LoadBitmap to load the bitmap from the resource, passing the bitmap's resource identifier as the second parameter to the API. We get the handle to the bitmap when the function returns.
Now that the bitmap is loaded, we can paint it in the client area of our main window.

   .elseif uMsg==WM_PAINT
      invoke BeginPaint,hWnd,addr ps
      mov    hdc,eax
      invoke CreateCompatibleDC,hdc
      mov    hMemDC,eax
      invoke SelectObject,hMemDC,hBitmap
      invoke GetClientRect,hWnd,addr rect
      invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
      invoke DeleteDC,hMemDC
      invoke EndPaint,hWnd,addr ps

We choose to paint the bitmap in response to WM_PAINT message. We first call BeginPaint to obtain the handle to the device context. Then we create a compatible memory DC with CreateCompatibleDC. Next select the bitmap into the memory DC with SelectObject. Determine the dimension of the client area with GetClientRect. Now we can display the bitmap in the client area by calling BitBlt which copies the bitmap from the memory DC to the real DC. When the painting is done, we have no further need for the memory DC so we delete it with DeleteDC. End painting session with EndPaint.

 .elseif uMsg==WM_DESTROY
  invoke DeleteObject,hBitmap
  invoke PostQuitMessage,NULL
When we don't need the bitmap anymore, we delete it with DeleteObject
[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32 Assembly Tutorial 26: Splash Screen

Tutorial 26: Splash Screen

Now that we know how to use a bitmap, we can progress to a more creative use of it. Splash screen. Download the example.

Theory

A splash screen is a window that has no title bar, no system menu box, no border that displays a bitmap for a while and then disappears automatically. It's usually used during program startup, to display the program's logo or to distract the user's attention while the program does some lengthy initialization. We will implement a splash screen in this tutorial.
The first step is to include the bitmap in the resource file. However, if you think of it, it's a waste of precious memory to load a bitmap that will be used only once and keep it in memory till the program is closed. A better solution is to create a *resource* DLL which contains the bitmap and has the sole purpose of displaying the splash screen. This way, you can load the DLL when you want to display the splash screen and unload it when it's not necessary anymore. So we will have two modules: the main program and the splash DLL. We will put the bitmap into the DLL's resource.
The general scheme is as follows:
  1. Put the bitmap into the DLL as a bitmap resource
  2. The main program calls LoadLibrary to load the dll into memory
  3. The DLL entrypoint function of the DLL is called. It will create a timer and set the length of time that the splash screen will be displayed. Next it will register and create a window without caption and border and display the bitmap in the client area.
  4. When the specified length of time elapsed, the splash screen is removed from the screen and the control is returned to the main program
  5. The main program calls FreeLibrary to unload the DLL from memory and then goes on with whatever task it is supposed to do.
We will examine the mechanisms in detail.

Load/Unload DLL

You can dynamically load a DLL with LoadLibrary function which has the following syntax:
LoadLibrary proto lpDLLName:DWORD
It takes only one parameter: the address of the name of the DLL you want to load into memory. If the call is successful, it returns the module handle of the DLL else it returns NULL.
To unload a DLL, call FreeLibrary:
FreeLibrary proto hLib:DWORD
It takes one parameter: the module handle of the DLL you want to unload. Normally, you got that handle from LoadLibrary

How to use a timer

First, you must create a timer first with SetTimer:
SetTimer proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD

hWnd is the handle of a window that will receive the timer notification message. This parameter can be NULL to specify that there is no window that's associated with the timer.
TimerID is a user-defined value that is used as the ID of the timer.
uElapse is the time-out value in milliseconds.
lpTimerFunc is the address of a function that will process the timer notification messages. If you pass NULL, the timer messages will be sent to the window specified by hWnd parameter.

SetTimer returns the ID of the timer if successful. Otherwise it returns NULL. So it's best not to use the timer ID of 0.

You can create a timer in two ways:
  • If you have a window and you want the timer notification messages to go to that window, you must pass all four parameters to SetTimer (the lpTimerFunc must be NULL)
  • If you don't have a window or you don't want to process the timer messages in the window procedure, you must pass NULL to the function in place of a window handle. You must also specify the address of the timer function that will process the timer messages.
We will use the first approach in this example.
When the time-out period elapses, WM_TIMER message is sent to the window that is associated with the timer. For example, if you specify uElapse of 1000, your window will receive WM_TIMER every second.
When you don't need the timer anymore, destroy it with KillTimer:
KillTimer proto hWnd:DWORD, TimerID:DWORD

Example:

;-----------------------------------------------------------------------
;                         The main program
;-----------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SplashDemoWinClass",0
AppName  db "Splash Screen Example",0
Libname db "splash.dll",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start:
 invoke LoadLibrary,addr Libname
 .if eax!=NULL
    invoke FreeLibrary,eax
 .endif
 invoke GetModuleHandle, NULL
 mov    hInstance,eax
 invoke GetCommandLine
 mov    CommandLine,eax
 invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
 invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
 LOCAL wc:WNDCLASSEX
 LOCAL msg:MSG
 LOCAL hwnd:HWND
 mov   wc.cbSize,SIZEOF WNDCLASSEX
 mov   wc.style, CS_HREDRAW or CS_VREDRAW
 mov   wc.lpfnWndProc, OFFSET WndProc
 mov   wc.cbClsExtra,NULL
 mov   wc.cbWndExtra,NULL
 push  hInstance
 pop   wc.hInstance
 mov   wc.hbrBackground,COLOR_WINDOW+1
 mov   wc.lpszMenuName,NULL
 mov   wc.lpszClassName,OFFSET ClassName
 invoke LoadIcon,NULL,IDI_APPLICATION
 mov   wc.hIcon,eax
 mov   wc.hIconSm,eax
 invoke LoadCursor,NULL,IDC_ARROW
 mov   wc.hCursor,eax
 invoke RegisterClassEx, addr wc
 INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
 mov   hwnd,eax
 invoke ShowWindow, hwnd,SW_SHOWNORMAL
 invoke UpdateWindow, hwnd
 .while TRUE
  invoke GetMessage, ADDR msg,NULL,0,0
  .break .if (!eax)
  invoke TranslateMessage, ADDR msg
  invoke DispatchMessage, ADDR msg
 .endw
 mov     eax,msg.wParam
 ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
 .IF uMsg==WM_DESTROY
  invoke PostQuitMessage,NULL
 .ELSE
  invoke DefWindowProc,hWnd,uMsg,wParam,lParam
  ret
 .ENDIF
 xor eax,eax
 ret
WndProc endp
end start

;--------------------------------------------------------------------
;                         The Bitmap DLL
;--------------------------------------------------------------------
.386
.model flat, stdcall
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib
.data
BitmapName db "MySplashBMP",0
ClassName db "SplashWndClass",0
hBitMap dd 0
TimerID dd 0

.data
hInstance dd ?

.code

DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD
   .if reason==DLL_PROCESS_ATTACH  ; When the dll is loaded
      push hInst
      pop hInstance
      call ShowBitMap
   .endif
   mov eax,TRUE
   ret
DllEntry Endp
ShowBitMap proc
        LOCAL wc:WNDCLASSEX
        LOCAL msg:MSG
        LOCAL hwnd:HWND
        mov   wc.cbSize,SIZEOF WNDCLASSEX
        mov   wc.style, CS_HREDRAW or CS_VREDRAW
        mov   wc.lpfnWndProc, OFFSET WndProc
        mov   wc.cbClsExtra,NULL
        mov   wc.cbWndExtra,NULL
        push  hInstance
        pop   wc.hInstance
        mov   wc.hbrBackground,COLOR_WINDOW+1
        mov   wc.lpszMenuName,NULL
        mov   wc.lpszClassName,OFFSET ClassName
        invoke LoadIcon,NULL,IDI_APPLICATION
        mov   wc.hIcon,eax
        mov   wc.hIconSm,0
        invoke LoadCursor,NULL,IDC_ARROW
        mov   wc.hCursor,eax
        invoke RegisterClassEx, addr wc
        INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
           WS_POPUP,CW_USEDEFAULT,\
           CW_USEDEFAULT,250,250,NULL,NULL,\
           hInstance,NULL
        mov   hwnd,eax
        INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
        .WHILE TRUE
                INVOKE GetMessage, ADDR msg,NULL,0,0
                .BREAK .IF (!eax)
                INVOKE TranslateMessage, ADDR msg
                INVOKE DispatchMessage, ADDR msg
        .ENDW
        mov     eax,msg.wParam
        ret
ShowBitMap endp
WndProc proc hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
        LOCAL ps:PAINTSTRUCT
        LOCAL hdc:HDC
        LOCAL hMemoryDC:HDC
        LOCAL hOldBmp:DWORD
        LOCAL bitmap:BITMAP
        LOCAL DlgHeight:DWORD
        LOCAL DlgWidth:DWORD
        LOCAL DlgRect:RECT
        LOCAL DesktopRect:RECT

        .if uMsg==WM_DESTROY
                .if hBitMap!=0
                        invoke DeleteObject,hBitMap
                .endif
                invoke PostQuitMessage,NULL
        .elseif uMsg==WM_CREATE
                invoke GetWindowRect,hWnd,addr DlgRect
                invoke GetDesktopWindow
                mov ecx,eax
                invoke GetWindowRect,ecx,addr DesktopRect
                push  0
                mov  eax,DlgRect.bottom
                sub  eax,DlgRect.top
                mov  DlgHeight,eax
                push eax
                mov  eax,DlgRect.right
                sub  eax,DlgRect.left
                mov  DlgWidth,eax
                push eax
                mov  eax,DesktopRect.bottom
                sub  eax,DlgHeight
                shr  eax,1
                push eax
                mov  eax,DesktopRect.right
                sub  eax,DlgWidth
                shr  eax,1
                push eax
                push hWnd
                call MoveWindow
                invoke LoadBitmap,hInstance,addr BitmapName
                mov hBitMap,eax
                invoke SetTimer,hWnd,1,2000,NULL
                mov TimerID,eax
        .elseif uMsg==WM_TIMER
                invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
                invoke KillTimer,hWnd,TimerID
        .elseif uMsg==WM_PAINT
                invoke BeginPaint,hWnd,addr ps
                mov hdc,eax
                invoke CreateCompatibleDC,hdc
                mov hMemoryDC,eax
                invoke SelectObject,eax,hBitMap
                mov hOldBmp,eax
                invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
                invoke StretchBlt,hdc,0,0,250,250,\
                       hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
                invoke SelectObject,hMemoryDC,hOldBmp
                invoke DeleteDC,hMemoryDC
                invoke EndPaint,hWnd,addr ps
       .elseif uMsg==WM_LBUTTONDOWN
                invoke DestroyWindow,hWnd
        .else
                invoke DefWindowProc,hWnd,uMsg,wParam,lParam
                ret
        .endif
        xor eax,eax
        ret
WndProc endp

End DllEntry

Analysis:

We will examine the code in the main program first.
 invoke LoadLibrary,addr Libname
 .if eax!=NULL
    invoke FreeLibrary,eax
 .endif
We call LoadLibrary to load the DLL named "splash.dll". And after that, unload it from memory with FreeLibrary. LoadLibrary will not return until the DLL is finished with its initialization.
That's all the main program does. The interesting part is in the DLL.

   .if reason==DLL_PROCESS_ATTACH  ; When the dll is loaded
      push hInst
      pop hInstance
      call ShowBitMap

When the DLL is loaded, Windows calls its entrypoint function with DLL_PROCESS_ATTACH flag. We take this opportunity to display the splash screen. First we store the instance handle of the DLL for future use. Then call a function named ShowBitMap to do the real job. ShowBitMap registers a window class, creates a window and enters the message loop as usual. The interesting part is in the CreateWindowEx call:

        INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
           WS_POPUP,CW_USEDEFAULT,\
           CW_USEDEFAULT,250,250,NULL,NULL,\
           hInstance,NULL

Note that the window style is only WS_POPUP which will make the window borderless and without caption. We also limit the width and height of the window to 250x250 pixels.
Now when the window is created, in WM_CREATE message handler we move the window to the center of the screen with the following code.

                invoke GetWindowRect,hWnd,addr DlgRect
                invoke GetDesktopWindow
                mov ecx,eax
                invoke GetWindowRect,ecx,addr DesktopRect
                push  0
                mov  eax,DlgRect.bottom
                sub  eax,DlgRect.top
                mov  DlgHeight,eax
                push eax
                mov  eax,DlgRect.right
                sub  eax,DlgRect.left
                mov  DlgWidth,eax
                push eax
                mov  eax,DesktopRect.bottom
                sub  eax,DlgHeight
                shr  eax,1
                push eax
                mov  eax,DesktopRect.right
                sub  eax,DlgWidth
                shr  eax,1
                push eax
                push hWnd
                call MoveWindow

It retrieves the dimensions of the desktop and the window then calculates the appropriate coordinate of the left upper corner of the window to make it center.

                invoke LoadBitmap,hInstance,addr BitmapName
                mov hBitMap,eax
                invoke SetTimer,hWnd,1,2000,NULL
                mov TimerID,eax

Next it loads the bitmap from the resource with LoadBitmap and creates a timer with the timer ID of 1 and the time interval 2 seconds. The timer will send WM_TIMER messages to the window every 2 seconds.

        .elseif uMsg==WM_PAINT
                invoke BeginPaint,hWnd,addr ps
                mov hdc,eax
                invoke CreateCompatibleDC,hdc
                mov hMemoryDC,eax
                invoke SelectObject,eax,hBitMap
                mov hOldBmp,eax
                invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
                invoke StretchBlt,hdc,0,0,250,250,\
                       hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
                invoke SelectObject,hMemoryDC,hOldBmp
                invoke DeleteDC,hMemoryDC
                invoke EndPaint,hWnd,addr ps

When the window receives WM_PAINT message, it creates a memory DC, select the bitmap into the memory DC, obtain the size of the bitmap with GetObject and then put the bitmap on the window by calling StretchBlt which performs like BitBlt but it can stretch or compress the bitmap to the desired dimension. In this case, we want the bitmap to fit into the window so we use StretchBlt instead of BitBlt. We delete the memory DC after that.

        .elseif uMsg==WM_LBUTTONDOWN
                invoke DestroyWindow,hWnd

It would be frustrating to the user if he has to wait until the splash screen to disappear. We can provide the user with a choice. When he clicks on the splash screen, it will disappear. That's why we need to process WM_LBUTTONDOWN message in the DLL. Upon receiving this message, the window is destroyed by DestroyWindow call.

        .elseif uMsg==WM_TIMER
                invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
                invoke KillTimer,hWnd,TimerID

If the user chooses to wait, the splash screen will disappear when the specified time has elapsed (in our example, it's 2 seconds). We can do this by processing WM_TIMER message. Upon receiving this message, we closes the window by sending WM_LBUTTONDOWN message to the window. This is to avoid code duplication. We don't have further use for the timer so we destroy it with KillTimer.
When the window is closed, the DLL will return control to the main program.

[Iczelion's Win32 Assembly HomePage]


Iczelion's Win32asm Tutorial 27: Tooltip Control

Tutorial 27: Tooltip Control

We will learn about the tooltip control: What it is and how to create and use it. Download the example.

Theory:

A tooltip is a small rectangular window that is displayed when the mouse pointer hovers over some specific area. A tooltip window contains some text that the programmer wants to be displayed. In this regard, a tooltip servers the same role as the status window but it disappears when the user clicks or moves the mouse pointer away from the designated area. You'll probably be familiar with the tooltips that are associated with toolbar buttons. Those "tooltips" are conveniencies provided by the toolbar control. If you want tooltips for other windows/controls, you need to create your own tooltip control.
Now that you know what a tooltip is, let's go on to how we can create and use it. The steps are outlined below:
  1. Create a tooltip control with CreateWindowEx
  2. Define a region that the tooltip control will monitor for mouse pointer movement.
  3. Submit the region to the tooltip control
  4. Relay mouse messages of the submitted region to the tooltip control (this step may occur earlier, depending on the method used to relay the messages)
We wll next examine each step in detail.

Tooltip Creation

A tooltip control is a common control. As such, you need to call InitCommonControls somewhere in your source code so that MASM implicitly links your program to comctl32.dll. You create a tooltip control with CreateWindowEx. The typical scenario would be like this:
.data
TooltipClassName db "Tooltips_class32",0
.code
.....
invoke InitCommonControls
invoke CreateWindowEx, NULL, addr TooltipClassName, NULL, TIS_ALWAYSTIP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL
Note the window style: TIS_ALWAYSTIP. This style specifies that the tooltip will be shown when the mouse pointer is over the designated area regardless of the status of the window that contains the area. Put simply, if you use this flag, when the mouse pointer hovers over the area you register to the tooltip control, the tooltip window will appear even if the window under the mouse pointer is inactive.
You don't have to include WS_POPUP and WS_EX_TOOLWINDOW styles in CreateWindowEx because the tooltip control's window procedure adds them automatically. You also don't need to specify the coordinate, the height and width of the tooltip window: the tooltip control will adjust them automatically to fit the tooltip text that will be displayed, thus we supply CW_USEDEFAULT in all four parameters. The remaining parameters are not remarkable.

Specifying the tool

The tooltip control is created but it's not shown immediately. We want the tooltip window to show up when the mouse pointer hovers over some area. Now is the time to specify that area. We call such area "tool". A tool is a rectangular area on the client area of a window which the tooltip control will monitor for mouse pointer. If the mouse pointer hovers over the tool, the tooltip window will appear. The rectangular area can cover the whole client area or only a part of it. So we can divided tool into two types: one that is implemented as a window and another that is implemented as a rectangular area in the client area of some window. Both has their uses. The tool that covers the whole client area of a window is most frequently used with controls such as buttons, edit controls and so on. You don't need to specify the coordinate and the dimensions of the tool: it's assumed to be the whole client area of the window. The tool that is implemented as a rectangular area on the client area is useful when you want to divide the client area of a window into several regions without using child windows. With this type of tool, you need to specify the coordinate of the upper left corner and the width and height of the tool.
You specify the tool with theTOOLINFO structure which has the following definition:
TOOLINFO STRUCT
  cbSize             DWORD      ?
  uFlags             DWORD      ?
  hWnd               DWORD      ?
  uId                DWORD      ?
  rect               RECT      <>
  hInst              DWORD      ?
  lpszText           DWORD      ?
  lParam             LPARAM     ?
TOOLINFO ENDS
Field Name | Explanation
cbSize | The size of the TOOLINFO structure. You MUST fill this member. Windows will not flag error if this field is not filled properly but you will receive strange, unpredictable results.
uFlags The bit flags that specifies the characteristics of the tool. This value can be a combination of the following flags:
  • TTF_IDISHWND "ID is hWnd". If you specify this flag, it means you want to use a tool that covers the whole client area of a window (the first type of tool above). If you use this flag, you must fill the uId member of this structure with the handle of the window you want to use. If you don't specify this flag, it means you want to use the second type of tool, the one that is implemented as the rectangular area on the client window. In that case, you need to fill the rect member with the dimension of the rectangle.
  • TTF_CENTERTIP Normally the tooltip window will appear to the right and below the mouse pointer. If you specify this flag, the tooltip window will always appear directly below the tool and is centered regardless of the position of the mouse pointer.
  • TTF_RTLREADING You can forget about this flag if your program is not designed specifically for Arabic or Hebrew systems. This flag displays the tooltip text with right-to-left reading order. Doesn't work under other systems.
  • TTF_SUBCLASS If you use this flag, it means you tell the tooltip control to subclass the window that the tool is on so that the tooltip control can intercept mouse messages that are sent to the window. This flag is very handy. If you don't use this flag, you have to do more work to relay the mouse messages to the tooltip control.
hWnd Handle to the window that contains the tool. If you specify TTF_IDISHWND flag, this field is ignored since Windows will use the value in uId member as the window handle. You need to fill this field if:
  • You don't use TTF_IDISHWND flag (in other words, you use a rectangular tool)
  • You specify the value LPSTR_TEXTCALLBACK in lpszText member. This value tells the tooltip control that, when it needs to display the tooltip window, it must ask the window that contains the tool for the text to be displayed. This is a kind of dynamic realtime tooltip text update. If you want to change your tooltip text dynamically, you should specify LPSTR_TEXTCALLBACK value in lpszText member. The tooltip control will send TTN_NEEDTEXT notification message to the window identified by the handle in hWnd field.
uId The value in this field can have two meanings, depending on whether the uFlags member contains the flag TTF_IDISHWND.
  • Application-defined tool ID if the TTF_IDISHWND flag is not specified. Since this means you use a tool which covers only a part of the client area, it's logical that you can have many such tools on the same client area (without overlap). The tooltip control needs a way to differentiate between them. In this case, the window handle in hWnd member is not enough since all tools are on the same window. The application-defined IDs are thus necessary. The IDs can be any value so long as they are unique among themselves.
  • The handle to the window whose whole client area is used as the tool if the TTF_IDISHWND flag is specified. You may wonder why this field is used to store the window handle instead of the hWnd field above. The answer is: the hWnd member may already be filled if the value LPSTR_TEXTCALLBACK is specified in the lpszText member and the window that is responsible for supplying the tooltip text and the window that contains the tool may NOT be the same ( You can design your program so that a single window can serve both roles but this is too restrictive. In this case, Microsoft gives you more freedom. Cheers.)
rect | A RECT structure that specifies the dimension of the tool. This structure defines a rectangle relative to the upper left corner of the client area of the window specified by the hWnd member. In short, you must fill this structure if you want to specify a tool that covers only a part of the client area. The tooltip control will ignore this field if you specify TTF_IDISHWND flag (you choose to use a tool that covers the whole client area)
hInst | The handle of the instance that contains the string resource that will be used as the tooltip text if the value in the lpszText member specifies the string resource identifier. This may sound confusing. Read the explanation of the lpszText member first and you will understand what this field is used for. The tooltip control ignores this field if the lpszText field doesn't contain a string resource identifier.
lpszText This field can have several values:
  • If you specify the value LPSTR_TEXTCALLBACK in this field, the tooltip control will send TTN_NEEDTEXT notification message to the window identified by the handle in hWnd field for the text string to be displayed in the tooltip window. This is the most dynamic method of tooltip text update: you can change the tooltip text each time the tooltip window is displayed.
  • If you specify a string resource identifier in this field, when the tooltip control needs to display the tooltip text in the tooltip window, it searches for the string in the string table of the instance specified by hInst member. The tooltip control identifies a string resource identifier by checking the high word of this field. Since a string resource identifier is a 16-bit value, the high word of this field will always be zero. This method is useful if you plan to port your program to other languages. Since the string resource is defined in a resource script, you don't need to modify the source code.You only have to modify the string table and the tooltip texts will change without the risk of introducing bugs into your program.
  • If the value in this field is not LPSTR_TEXTCALLBACK and the high word is not zero, the tooltip control interprets the value as the pointer to a text string that will be used as the tooltip text. This method is the easiest to use but the least flexible.

To recapitulate, you need to fill the TOOLINFO structure prior to submitting it to the tooltip control. This structure describes the characteristics of the tool you desire.

Register the tool with the tooltip control

After you fill the TOOLINFO structure, you must submit it to tooltip control. A tooltip control can service many tools so it is usually unnecessary to create more than one tooltip control for a window. To register a tool with a tooltip control, you send the TTM_ADDTOOL message to the tooltip control. The wParam is not used and the lParam must contain the address of the TOOLINFO structure you want to register.
.data?
ti TOOLINFO <>
.......
.code
.......
<fill the TOOLINFO structure>
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti
SendMessage for this message will return TRUE if the tool is successfully registered with the tooltip control, FALSE otherwise.
You can unregister the tool by sending TTM_DELTOOL message to the tooltip control.

Relaying Mouse Messages to the Tooltip Control

When the above step is completed, the tooltip control knows which area it should monitor for mouse messages and what text it should display in the tooltip window. The only thing it lacks is the *trigger* for that action. Think about it: the area specified by the tool is on the client area of the other window. How can the tooltip control intercept the mouse messages destined for that window? It needs to do so in order that it can measure the amount of time the mouse pointer hovers over a point in the tool so that when the specified amount of time elapses, the tooltip control shows the tooltip window. There are two methods of accomplishing this goal, one that requires the cooperation of the window that contains the tool and the other without the cooperation on the part of the window.
  • The window that contains the tool must relay the mouse messages to the tooltip control by sending TTM_RELAYEVENT messages to the control. The lParam of this message must contain the address of a MSG structure that specifies the message to be relayed to the tooltip control. A tooltip control processes only the following mouse messages:
    • WM_LBUTTONDOWN
    • WM_MOUSEMOVE
    • WM_LBUTTONUP
    • WM_RBUTTONDOWN
    • WM_MBUTTONDOWN
    • WM_RBUTTONUP
    • WM_MBUTTONUP
    All other messages are ignored. Thus in the window procedure of the window that contains the tool, there must be a switch that does something like this:

    WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .......
    if uMsg==WM_CREATE
    .............
    elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || uMsg==WM_LBUTTONUP || uMsg==WM_RBUTTONDOWN || uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP || uMsg==WM_MBUTTONUP
    invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg
    ..........

  • You can specify TTF_SUBCLASS flag in the uFlags member of the TOOLINFO structure. This flag tells the tooltip control to subclass the window that contains the tool so it can intercept the mouse messages without the cooperation of the window. This method is easier to use since it doesn't require more coding than specifying TTF_SUBCLASS flag and the tooltip control handles all the message interception itself.
That's it. At this step, your tooltip control is fully functional. There are several useful tooltip-related messages you should know about.
  • TTM_ACTIVATE. If you want to disable/enable the tooltip control dynamically, this message is for you. If the wParam value is TRUE, the tooltip control is enabled. If the wParam value is FALSE, the tooltip control is disabled. A tooltip control is enabled when it first created so you don't need to send this message to activate it.
  • TTM_GETTOOLINFO and TTM_SETTOOLINFO. If you want to obtain/change the values in the TOOLINFO structure after it was submitted to the tooltip control, use these messages. You need to specify the tool you need to change with the correct uId and hWnd values. If you only want to change the rect member, use TTM_NEWTOOLRECT message. If you only want to change the tooltip text, use TTM_UPDATETIPTEXT.
  • TTM_SETDELAYTIME. With this message, you can specify the time delay the tooltip control uses when it's displaying the tooltip text and much more.

Example:

The following example is a simple dialog box with two buttons. The client area of the dialog box is divided into 4 areas: upper left, upper right, lower left and lower right. Each area is specified as a tool with its own tooltip text. The two buttons also has their own tooltip texts.
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
EnumChild proto :DWORD,:DWORD
SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.const
IDD_MAINDIALOG equ 101
.data
ToolTipsClassName db "Tooltips_class32",0
MainDialogText1 db "This is the upper left area of the dialog",0
MainDialogText2 db "This is the upper right area of the dialog",0
MainDialogText3 db "This is the lower left area of the dialog",0
MainDialogText4 db "This is the lower right area of the dialog",0
.data?
hwndTool dd ?
hInstance dd ?
.code
start:
    invoke GetModuleHandle,NULL
    mov hInstance,eax
    invoke DialogBoxParam,hInstance,IDD_MAINDIALOG,NULL,addr DlgProc,NULL
    invoke ExitProcess,eax

DlgProc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    LOCAL ti:TOOLINFO
    LOCAL id:DWORD
    LOCAL rect:RECT
    .if uMsg==WM_INITDIALOG
        invoke InitCommonControls
        invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
            TTS_ALWAYSTIP,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
            hInstance,NULL
        mov hwndTool,eax
        mov id,0
        mov ti.cbSize,sizeof TOOLINFO
        mov ti.uFlags,TTF_SUBCLASS
        push hDlg
        pop ti.hWnd
        invoke GetWindowRect,hDlg,addr rect
        invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect
        inc id
        invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText2,id,addr rect
        inc id
        invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText3,id,addr rect
        inc id
        invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText4,id,addr rect
        invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hDlg,NULL
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
DlgProc endp

EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
    LOCAL buffer[256]:BYTE
    mov edi,lParam
    assume edi:ptr TOOLINFO
    push hwndChild
    pop [edi].uId
    or [edi].uFlags,TTF_IDISHWND
    invoke GetWindowText,hwndChild,addr buffer,255
    lea eax,buffer
    mov [edi].lpszText,eax
    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
    assume edi:nothing
    ret
EnumChild endp

SetDlgToolArea proc uses edi esi hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
    mov edi,lpti
    mov esi,lprect
    assume esi:ptr RECT
    assume edi:ptr TOOLINFO
    .if id==0
        mov [edi].rect.left,0
        mov [edi].rect.top,0
        mov eax,[esi].right
        sub eax,[esi].left
        shr eax,1
        mov [edi].rect.right,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        shr eax,1
        mov [edi].rect.bottom,eax
    .elseif id==1
        mov eax,[esi].right
        sub eax,[esi].left
        shr eax,1
        inc eax
        mov [edi].rect.left,eax
        mov [edi].rect.top,0
        mov eax,[esi].right
        sub eax,[esi].left
        mov [edi].rect.right,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        mov [edi].rect.bottom,eax
    .elseif id==2
        mov [edi].rect.left,0
        mov eax,[esi].bottom
        sub eax,[esi].top
        shr eax,1
        inc eax
        mov [edi].rect.top,eax
        mov eax,[esi].right
        sub eax,[esi].left
        shr eax,1
        mov [edi].rect.right,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        mov [edi].rect.bottom,eax
    .else
        mov eax,[esi].right
        sub eax,[esi].left
        shr eax,1
        inc eax
        mov [edi].rect.left,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        shr eax,1
        inc eax
        mov [edi].rect.top,eax
        mov eax,[esi].right
        sub eax,[esi].left
        mov [edi].rect.right,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        mov [edi].rect.bottom,eax
    .endif
    push lpText
    pop [edi].lpszText
    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
    assume edi:nothing
    assume esi:nothing
    ret
SetDlgToolArea endp
end start

Analysis:

After the main dialog window is created, we create the tooltip control with CreateWindowEx.
invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
       TTS_ALWAYSTIP,CW_USEDEFAULT,\
       CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
       hInstance,NULL
mov hwndTool,eax
After that, we proceed to define four tools for each corner of the dialog box.

    mov id,0        ; used as the tool ID
    mov ti.cbSize,sizeof TOOLINFO
    mov ti.uFlags,TTF_SUBCLASS   ; tell the tooltip control to subclass the dialog window.
    push hDlg
    pop ti.hWnd    ; handle to the window that contains the tool
    invoke GetWindowRect,hDlg,addr rect   ; obtain the dimension of the client area
    invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect

We initialize the members of TOOLINFO structure. Note that we want to divide the client area into 4 tools so we need to know the dimension of the client area. That's why we call GetWindowRect. We don't want to relay mouse messages to the tooltip control ourselves so we specify TIF_SUBCLASS flag.
SetDlgToolArea is a function that calculates the bounding rectangle of each tool and registers the tool to the tooltip control. I won't go into gory detail on the calculation, suffice to say that it divides the client area into 4 areas with the same sizes. Then it sends TTM_ADDTOOL message to the tooltip control, passing the address of the TOOLINFO structure in the lParam parameter.

    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

After all 4 tools are registered, we can go on to the buttons on the dialog box. We can handle each button by its ID but this is tedious. Instead, we will use EnumChildWindows API call to enumerate all controls on the dialog box and then registers them to the tooltip control. EnumChildWindows has the following syntax:

EnumChildWindows proto hWnd:DWORD, lpEnumFunc:DWORD, lParam:DWORD
hWnd is the handle to the parent window. lpEnumFunc is the address of the EnumChildProc function that will be called for each control enumerated. lParam is the application-defined value that will be passed to the EnumChildProc function. The EnumChildProc function has the following definition:
EnumChildProc proto hwndChild:DWORD, lParam:DWORD
hwndChild is the handle to a control enumerated by EnumChildWindows. lParam is the same lParam value you pass to EnumChildWindows.
In our example, we callEnumChildWindows like this:
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
We pass the address of the TOOLINFO structure in the lParam parameter because we will register each child control to the tooltip control in the EnumChild function. If we don't use this method, we need to declare ti as a global variable which can introduce bugs.
When we call EnumChildWindows, Windows will enumerate the child controls on our dialog box and call the EnumChild function once for each control enumerated. Thus if our dialog box has two controls, EnumChild will be called twice.
The EnumChild function fills the relevant members of the TOOLINFO structure and then registers the tool with the tooltip control.
EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
    LOCAL buffer[256]:BYTE
    mov edi,lParam
    assume edi:ptr TOOLINFO
    push hwndChild
    pop [edi].uId    ; we use the whole client area of the control as the tool
    or [edi].uFlags,TTF_IDISHWND
    invoke GetWindowText,hwndChild,addr buffer,255
    lea eax,buffer   ; use the window text as the tooltip text
    mov [edi].lpszText,eax
    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
    assume edi:nothing
    ret
EnumChild endp
Note that in this case, we use a different type of tool: one that covers the whole client area of the window. We thus need to fill the uID field with the handle to the window that contains the tool. Also we must specify TTF_IDISHWND flag in the uFlags member.
[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 28: Win32 Debug API Part 1

Tutorial 28: Win32 Debug API Part 1

In this tutorial, you'll learn what Win32 offers to developers regarding debugging primitives. You'll know how to debug a process when you're finished with this tutorial.
Download the example.

Theory:

Win32 has several APIs that allow programmers to use some of the powers of a debugger. They are called Win32 Debug APIs or primitives. With them, you can:

  • Load a program or attach to a running program for debugging
  • Obtain low-level information about the program you're debugging, such as process ID, address of entrypoint, image base and so on.
  • Be notified of debugging-related events such as when a process/thread starts/exits, DLLs are loaded/unloaded etc.
  • Modify the process/thread being debugged

In short, you can code a simple debugger with those APIs. Since this subject is vast, I divide it into several managable parts: this tutorial being the first part. I'll explain the basic concepts and general framework for using Win32 Debug APIs in this tutorial.
The steps in using Win32 Debug APIs are:

  1. Create a process or attach your program to a running process. This is the first step in using Win32 Debug APIs. Since your program will act as a debugger, you need a program to debug. The program being debugged is called a debuggee. You can acquire a debuggee in two ways:
    • You can create the debuggee process yourself with CreateProcess. In order to create a process for debugging, you must specify the DEBUG_PROCESS flag. This flag tells Windows that we want to debug the process. Windows will send notifications of important debugging-related events (debug events) that occur in the debuggee to your program. The debuggee process will be immediately suspended until your program is ready. If the debuggee also creates child processes, Windows will also send debug events that occur in all those child processes to your program as well. This behavior is usually undesirable. You can disable this behavior by specifying DEBUG_ONLY_THIS_PROCESS flag in combination of DEBUG_PROCESS flag.
    • You can attach your program to a running process with DebugActiveProcess.
  2. Wait for debugging events. After your program acquired a debuggee, the debuggee's primary thread is suspended and will continue to be suspended until your program calls WaitForDebugEvent. This function works like other WaitForXXX functions, ie. it blocks the calling thread until the waited-for event occurs. In this case, it waits for debug events to be sent by Windows. Let's see its definition:

    WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

    lpDebugEvent is the address of a DEBUG_EVENT structure that will be filled with information about the debug event that occurs within the debuggee.

    dwMilliseconds is the length of time in milliseconds this function will wait for the debug event to occur. If this period elapses and no debug event occurs, WaitForDebugEvent returns to the caller. On the other hand, if you specify INFINITE constant in this argument, the function will not return until a debug event occurs.

    Now let's examine the DEBUG_EVENT structure in more detail.

    DEBUG_EVENT STRUCT
    dwDebugEventCode dd ?
    dwProcessId dd ?
    dwThreadId dd ?
    u DEBUGSTRUCT <>
    DEBUG_EVENT ENDS

    dwDebugEventCode contains the value that specifies what type of debug event occurs. In short, there can be many types of events, your program needs to check the value in this field so it knows what type of event occurs and responds appropriately. The possible values are:

    3. Value | Meanings
CREATE_PROCESS_DEBUG_EVENT | A process is created. This event will be sent when the debuggee process is just created (and not yet running) or when your program just attaches itself to a running process with DebugActiveProcess. This is the first event your program will receive.
EXIT_PROCESS_DEBUG_EVENT | A process exits.
CREATE_THEAD_DEBUG_EVENT | A new thread is created in the debuggee process or when your program first attaches itself to a running process. Note that you'll not receive this notification when the primary thread of the debuggee is created.
EXIT_THREAD_DEBUG_EVENT | A thread in the debuggee process exits. Your program will not receive this event for the primary thread. In short, you can think of the primary thread of the debuggee as the equivalent of the debuggee process itself. Thus, when your program sees CREATE_PROCESS_DEBUG_EVENT, it's actually the CREATE_THREAD_DEBUG_EVENT for the primary thread.
LOAD_DLL_DEBUG_EVENT | The debuggee loads a DLL. You'll receive this event when the PE loader first resolves the links to DLLs (you call CreateProcess to load the debuggee) and when the debuggee calls LoadLibrary.
UNLOAD_DLL_DEBUG_EVENT | A DLL is unloaded from the debuggee process.
EXCEPTION_DEBUG_EVENT | An exception occurs in the debuggee process. Important: This event will occur once just before the debuggee starts executing its first instruction. The exception is actually a debug break (int 3h). When you want to resume the debuggee, call ContinueDebugEvent with DBG_CONTINUE flag. Don't use DBG_EXCEPTION_NOT_HANDLED flag else the debuggee will refuse to run under NT (on Win98, it works fine).
OUTPUT_DEBUG_STRING_EVENT | This event is generated when the debuggee calls DebugOutputString function to send a message string to your program.
RIP_EVENT | System debugging error occurs

    dwProcessId and dwThreadId are the process and thread Ids of the process that the debug event occurs. You can use these values as identifiers of the process/thread you're interested in. Remember that if you use CreateProcess to load the debuggee, you also get the process and thread IDs of the debuggee in the PROCESS_INFO structure. You can use these values to differentiate between the debug events occurring in the debuggee and its child processes (in case you didn't specify DEBUG_ONLY_THIS_PROCESS flag).

    u is a union that contains more information about the debug event. It can be one of the following structures depending on the value of dwDebugEventCode above.

    value in dwDebugEventCode | Interpretation of u
CREATE_PROCESS_DEBUG_EVENT | A CREATE_PROCESS_DEBUG_INFO structure named CreateProcessInfo
EXIT_PROCESS_DEBUG_EVENT | An EXIT_PROCESS_DEBUG_INFO structure named ExitProcess
CREATE_THREAD_DEBUG_EVENT | A CREATE_THREAD_DEBUG_INFO structure named CreateThread
EXIT_THREAD_DEBUG_EVENT | An EXIT_THREAD_DEBUG_EVENT structure named ExitThread
LOAD_DLL_DEBUG_EVENT | A LOAD_DLL_DEBUG_INFO structure named LoadDll
UNLOAD_DLL_DEBUG_EVENT | An UNLOAD_DLL_DEBUG_INFO structure named UnloadDll
EXCEPTION_DEBUG_EVENT | An EXCEPTION_DEBUG_INFO structure named Exception
OUTPUT_DEBUG_STRING_EVENT | An OUTPUT_DEBUG_STRING_INFO structure named DebugString
RIP_EVENT | A RIP_INFO structure named RipInfo

    I won't go into detail about all those structures in this tutorial, only the CREATE_PROCESS_DEBUG_INFO structure will be covered here.
    Assuming that our program calls WaitForDebugEvent and it returns. The first thing we should do is to examine the value in dwDebugEventCode to see which type of debug event occured in the debuggee process. For example, if the value in dwDebugEventCode is CREATE_PROCESS_DEBUG_EVENT, you can interpret the member in u as CreateProcessInfo and access it with u.CreateProcessInfo.

  3. Do whatever your program want to do in response to the debug event. When WaitForDebugEvent returns, it means a debug event just occurred in the debuggee process or a timeout occurs. Your program needs to examine the value in dwDebugEventCode in order to react to the event appropriately. In this regard, it's like processing Windows messages: you choose to handle some and ignore some.
  4. Let the debuggee continues execution. When a debug event occurs, Windows suspends the debuggee. When you're finished with the event handling, you need to kick the debuggee into moving again. You do this by calling ContinueDebugEvent function.

    ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

    This function resumes the thread that was previously suspended because a debug event occurred.
    dwProcessId and dwThreadId are the process and thread IDs of the thread that will be resumed. You usually take these two values from the dwProcessId and dwThreadId members of the DEBUG_EVENT structure.
    dwContinueStatus specifies how to continue the thread that reported the debug event. There are two possible values: DBG_CONTINUE and DBG_EXCEPTION_NOT_HANDLED. For all other debug events, those two values do the same thing: resume the thread. The exception is the EXCEPTION_DEBUG_EVENT. If the thread reports an exception debug event, it means an exception occurred in the debuggee thread. If you specify DBG_CONTINUE, the thread will ignore its own exception handling and continue with the execution. In this scenario, your program must examine and resolve the exception itself before resuming the thread with DBG_CONTINUE else the exception will occur again and again and again.... If you specify DBG_EXCEPTION_NOT_HANDLED, your program is telling Windows that it didn't handle the exception: Windows should use the default exception handler of the debuggee to handle the exception.
    In conclusion, if the debug event refers to an exception in the debuggee process, you should call ContinueDebugEvent with DBG_CONTINUE flag if your program already removed the cause of exception. Otherwise, your program must call ContinueDebugEvent with DBG_EXCEPTION_NOT_HANDLED flag. Except in one case which you must always use DBG_CONTINUE flag: the first EXCEPTION_DEBUG_EVENT which has the value EXCEPTION_BREAKPOINT in the ExceptionCode member. When the debuggee is going to execute its very first instruction, your program will receive the exception debug event. It's actually a debug break (int 3h). If you respond by calling ContinueDebugEvent with DBG_EXCEPTION_NOT_HANDLED flag, Windows NT will refuse to run the debuggee (because no one cares for it). You must always use DBG_CONTINUE flag in this case to tell Windows that you want the thread to go on.

  5. Continue this cycle in an infinite loop until the debuggee process exits. Your program must be in an infinite loop much like a message loop until the debuggee exits. The loop looks like this:

    .while TRUE
    invoke WaitForDebugEvent, addr DebugEvent, INFINITE
    .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
    <Handle the debug events>
    invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
    .endw

    Here's the catch: Once you start debugging a program, you just can't detach from the debuggee until it exits.

Let's summarize the steps again:

  1. Create a process or attach your program to a running process.
  2. Wait for debugging events
  3. Do whatever your program want to do in response to the debug event.
  4. Let the debuggee continues execution.
  5. Continue this cycle in an infinite loop until the debuggee process exits

Example:

This example debugs a win32 program and shows important information such as the process handle, process Id, image base and so on.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
             db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
            db "Process Handle: %lx",0Dh,0Ah
            db "Thread Handle: %lx",0Dh,0Ah
            db "Image Base: %lx",0Dh,0Ah
            db "Start Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
   invoke WaitForDebugEvent, addr DBEvent, INFINITE
   .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
       .break
   .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
       invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
       invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
   .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
       .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
         .continue
       .endif
   .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif
   invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start

Analysis:

The program fills the OPENFILENAME structure and then calls GetOpenFileName to let the user choose a program to be debugged.

invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi

When the user chose one, it calls CreateProcess to load the program. It calls GetStartupInfo to fill the STARTUPINFO structure with its default values. Note that we use DEBUG_PROCESS combined with DEBUG_ONLY_THIS_PROCESS flags in order to debug only this program, not including its child processes.

.while TRUE
   invoke WaitForDebugEvent, addr DBEvent, INFINITE

When the debuggee is loaded, we enter the infinite debug loop, calling WaitForDebugEvent. WaitForDebugEvent will not return until a debug event occurs in the debuggee because we specify INFINITE as its second parameter. When a debug event occurred, WaitForDebugEvent returns and DBEvent is filled with information about the debug event.

   .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
       .break

We first check the value in dwDebugEventCode. If it's EXIT_PROCESS_DEBUG_EVENT, we display a message box saying "The debuggee exits" and then get out of the debug loop.

   .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
       invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
       invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION

If the value in dwDebugEventCode is CREATE_PROCESS_DEBUG_EVENT, then we display several interesting information about the debuggee in a message box. We obtain those information from u.CreateProcessInfo. CreateProcessInfo is a structure of type CREATE_PROCESS_DEBUG_INFO. You can get more info about this structure from Win32 API reference.

   .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
       .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
         .continue
       .endif

If the value in dwDebugEventCode is EXCEPTION_DEBUG_EVENT, we must check further for the exact type of exception. It's a long line of nested structure reference but you can obtain the kind of exception from ExceptionCode member. If the value in ExceptionCode is EXCEPTION_BREAKPOINT and it occurs for the first time (or if we are sure that the debuggee has no embedded int 3h), we can safely assume that this exception occured when the debuggee was going to execute its very first instruction. When we are done with the processing, we must call ContinueDebugEvent with DBG_CONTINUE flag to let the debuggee run. Then we go back to wait for the next debug event.

   .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif

If the value in dwDebugEventCode is CREATE_THREAD_DEBUG_EVENT or EXIT_THREAD_DEBUG_EVENT, we display a message box saying so.

   invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw

Except for the EXCEPTION_DEBUG_EVENT case above, we call ContinueDebugEvent with DBG_EXCEPTION_NOT_HANDLED flag to resume the debuggee.

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread

When the debuggee exits, we are out of the debug loop and must close both process and thread handles of the debuggee. Closing the handles doesn't mean we are killing the process/thread. It just means we don't want to use those handles to refer to the process/thread anymore.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 29: Win32 Debug API part 2

Tutorial 29: Win32 Debug API Part 2

We continue with the subject of win32 debug API. In this tutorial, we will learn how to modify the debuggee process.
Download the example

Theory:

In the previous tutorial, we know how to load the debuggee and handle debug events that occur in its process. In order to be useful, our program must be able to modify the debuggee process. There are several APIs just for this purpose.

  • ReadProcessMemory This function allows you to read memory in the specified process. The function prototype is as follows:

    ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD

    hProcess is the handle to the process you want to read.
    lpBaseAddress is the address in the target process you want to start reading. For example, if you want to read 4 bytes from the debuggee process starting at 401000h, the value in this parameter must be 401000h.
    lpBuffer is the address of the buffer to receive the bytes read from the process.
    nSize is the number of bytes you want to read
    lpNumberOfBytesRead is the address of the variable of dword size that receives the number of bytes actually read. If you don't care about it, you can use NULL.

  • WriteProcessMemory is the counterpart of ReadProcessMemory. It enables you to write memory of the target process. Its parameters are exactly the same as those of ReadProcessMemory

    The next two API functions need a little background on context. Under a multitasking OS like Windows, there can be several programs running at the same time. Windows gives each thread a timeslice. When that timeslice expires, Windows freezes the present thread and switches to the next thread that has the highest priority. Just before switching to the other thread, Windows saves values in registers of the present thread so that when the time comes to resume the thread, Windows can restore the last *environment* of that thread. The saved values of the registers are collectively called a context.
    Back to our subject. When a debug event occurs, Windows suspends the debuggee. The debuggee's context is saved. Since the debuggee is suspended, we can be sure that the values in the context will remain unchanged . We can get the values in the context with GetThreadContext and we can change them with SetThreadContext.
    These two APIs are very powerful. With them, you have at your fingertips the VxD-like power over the debuggee: you can alter the saved register values and just before the debuggee resumes execution, the values in the context will be written back into the registers. Any change you made to the context is reflected back to the debuggee. Think about it: you can even alter the value of the eip register and divert the flow of execution to anywhere you like! You won't be able to do that under normal circumstance.

    GetThreadContext proto hThread:DWORD, lpContext:DWORD

    hThread is the handle to the thread that you want to obtain the context from
    lpContext is the address of the CONTEXT structure that will be filled when the function returns successfully.

    SetThreadContext has exactly the same parameters. Let's see what a CONTEXT structure looks like:

  • CONTEXT STRUCT
  • ContextFlags dd ?
    ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_DEBUG_REGISTERS
  • ;-----------------------------------------------------------------------------------------------------------
    iDr0 dd ?
    iDr1 dd ?
    iDr2 dd ?
    iDr3 dd ?
    iDr6 dd ?
    iDr7 dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_FLOATING_POINT
  • ;-----------------------------------------------------------------------------------------------------------
  • FloatSave FLOATING_SAVE_AREA <>
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_SEGMENTS
  • ;-----------------------------------------------------------------------------------------------------------
  • regGs dd ?
    regFs dd ?
    regEs dd ?
    regDs dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_INTEGER
  • ;-----------------------------------------------------------------------------------------------------------
  • regEdi dd ?
    regEsi dd ?
    regEbx dd ?
    regEdx dd ?
    regEcx dd ?
    regEax dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_CONTROL
  • ;-----------------------------------------------------------------------------------------------------------
  • regEbp dd ?
    regEip dd ?
    regCs dd ?
    regFlag dd ?
    regEsp dd ?
    regSs dd ?
  • ;----------------------------------------------------------------------------------------------------------
    ; This section is returned if ContextFlags contains the value CONTEXT_EXTENDED_REGISTERS
  • ;-----------------------------------------------------------------------------------------------------------
  • ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS

    As you can observe, the members of this structures are mimics of the real processor's registers. Before you can use this structure, you need to specify which groups of registers you want to read/write in ContextFlags member. For example, if you want to read/write all registers, you must specify CONTEXT_FULL in ContextFlags. If you want only to read/write regEbp, regEip, regCs, regFlag, regEsp or regSs, you must specify CONTEXT_CONTROL in ContextFlags.

    One thing you must remember when using the CONTEXT structure: it must be aligned on dword boundary else you'd get strange results under NT. You must put "align dword" just above the line that declares it, like this:

    align dword
    MyContext CONTEXT <>

Example:

The first example demonstrates the use of DebugActiveProcess. First, you need to run a target named win.exe which goes in an infinite loop just before the window is shown on the screen. Then you run the example, it will attach itself to win.exe and modify the code of win.exe such that win.exe exits the infinite loop and shows its own window.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h

.data?
DBEvent DEBUG_EVENT <>
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT <>

.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax!=NULL
    invoke GetWindowThreadProcessId, eax, addr ProcessId
    mov ThreadId, eax
    invoke DebugActiveProcess, ProcessId
    .while TRUE
       invoke WaitForDebugEvent, addr DBEvent, INFINITE
       .break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
          mov context.ContextFlags, CONTEXT_CONTROL
          invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
          invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
          invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION
       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
          .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
             invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId, DBG_CONTINUE
             .continue
          .endif
       .endif
       invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
   .endw
.else
    invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR .endif
invoke ExitProcess, 0
end start

;--------------------------------------------------------------------
; The partial source code of win.asm, our debuggee. It's actually
; the simple window example in tutorial 2 with an infinite loop inserted
; just before it enters the message loop.
;----------------------------------------------------------------------

......
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax
jmp $ <---- Here's our infinite loop. It assembles to EB FE
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
   invoke GetMessage, ADDR msg,NULL,0,0
   .break .if (!eax)
   invoke TranslateMessage, ADDR msg
   invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

Analysis:

invoke FindWindow, addr ClassName, NULL

Our program needs to attach itself to the debuggee with DebugActiveProcess which requires the process Id of the debuggee. We can obtain the process Id by calling GetWindowThreadProcessId which in turn needs the window handle as its parameter. So we need to obtain the window handle first.
With FindWindow, we can specify the name of the window class we need. It returns the handle to the window created by that window class. If it returns NULL, no window of that class is present.

.if eax!=NULL
    invoke GetWindowThreadProcessId, eax, addr ProcessId
    mov ThreadId, eax
    invoke DebugActiveProcess, ProcessId

After we obtain the process Id, we can call DebugActiveProcess. Then we enter the debug loop waiting for the debug events.

       .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
          mov context.ContextFlags, CONTEXT_CONTROL
          invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context

When we get CREATE_PROCESS_DEBUG_INFO, it means the debuggee is suspended, ready for us to do surgery upon its process. In this example, we will overwrite the infinite loop instruction in the debuggee (0EBh 0FEh) with NOPs ( 90h 90h).
First, we need to obtain the address of the instruction. Since the debuggee is already in the loop by the time our program attached to it, eip will always point to the instruction. All we need to do is obtain the value of eip. We use GetThreadContext to achieve that goal. We set the ContextFlags member to CONTEXT_CONTROL so as to tell GetThreadContext that we want it to fill the "control" register members of the CONTEXT structure.

          invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL

Now that we get the value of eip, we can call WriteProcessMemory to overwrite the "jmp $" instruction with NOPs, thus effectively help the debuggee exit the infinite loop. After that we display the message to the user and then call ContinueDebugEvent to resume the debuggee. Since the "jmp $" instruction is overwritten by NOPs, the debuggee will be able to continue with showing its window and enter the message loop. The evidence is we will see its window on screen.

The other example uses a slightly different approach to break the debuggee out of the infinite loop.

.......
.......
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
   mov context.ContextFlags, CONTEXT_CONTROL
   invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
   add context.regEip,2
   invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
   invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
.......
.......

It still calls GetThreadContext to obtain the current value of eip but instead of overwriting the "jmp $" instruction, it increments the value of regEip by 2 to "skip over" the instruction. The result is that when the debuggee regains control , it resumes execution at the next instruction after "jmp $".

Now you can see the power of Get/SetThreadContext. You can also modify the other register images as well and their values will be reflected back to the debuggee. You can even insert int 3h instruction to put breakpoints in the debuggee process.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 30: Win32 Debug API part 3

Tutorial 30: Win32 Debug API part 3

In this tutorial, we continue the exploration of win32 debug api. Specifically, we will learn how to trace the debuggee.
Download the example.

Revision history:

12/2/2000: Forgot to dword-align the CONTEXT structure

Theory:

If you have used a debugger before, you would be familiar with tracing. When you "trace" a program, the program stops after executing each instruction, giving you the chance to examine the values of registers/memory. Single-stepping is the official name of tracing.
The single-step feature is provided by the CPU itself. The 8th bit of the flag register is called trap flag. If this flag(bit) is set, the CPU executes in single-step mode. The CPU will generate a debug exception after each instruction. After the debug exception is generated, the trap flag is cleared automatically.
We can also single-step the debuggee, using win32 debug api. The steps are as follows:

  1. Call GetThreadContext, specifying CONTEXT_CONTROL in ContextFlags, to obtain the value of the flag register.
  2. Set the trap bit in regFlag member of the CONTEXT structure
  3. call SetThreadContext
  4. Wait for the debug events as usual. The debuggee will execute in single-step mode. After it executes each instruction, we will get EXCEPTION_DEBUG_EVENT with EXCEPTION_SINGLE_STEP value in u.Exception.pExceptionRecord.ExceptionCode
  5. If you need to trace the next instruction, you need to set the trap bit again.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.4",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
             db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0Dh,0Ah
         db "Total Instructions executed : %lu",0
TotalInstruction dd 0

.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
align dword
context CONTEXT <>

.code
start:
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
    invoke GetStartupInfo,addr startinfo
    invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
    .while TRUE
       invoke WaitForDebugEvent, addr DBEvent, INFINITE
       .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
          invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction
          invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
          .break
       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT           .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
             mov context.ContextFlags, CONTEXT_CONTROL
             invoke GetThreadContext, pi.hThread, addr context
             or context.regFlag,100h
             invoke SetThreadContext,pi.hThread, addr context
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
             .continue
          .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
             inc TotalInstruction
             invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
             invoke SetThreadContext,pi.hThread, addr context
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
             .continue
          .endif
       .endif
       invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
    .endw
.endif
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
invoke ExitProcess, 0
end start

Analysis:

The program shows the openfile dialog box. When the user chooses an executable file, it executes the program in single-step mode, couting the number of instructions executed until the debuggee exits.

       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT           .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT

We take this opportunity to set the debuggee into single-step mode. Remember that Windows sends an EXCEPTION_BREAKPOINT just before it executes the first instruction of the debuggee.

             mov context.ContextFlags, CONTEXT_CONTROL
             invoke GetThreadContext, pi.hThread, addr context

We call GetThreadContext to fill the CONTEXT structure with the current values in the registers of the debuggee. More specifically, we need the current value of the flag register.

             or context.regFlag,100h

We set the trap bit (8th bit) in the flag register image.

             invoke SetThreadContext,pi.hThread, addr context
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
             .continue

Then we call SetThreadContext to overwrite the values in the CONTEXT structure with the new one(s) and call ContinueDebugEvent with DBG_CONTINUE flag to resume the debuggee.

          .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
             inc TotalInstruction

When an instruction is executed in the debuggee, we receive an EXCEPTION_DEBUG_EVENT. We must examine the value of u.Exception.pExceptionRecord.ExceptionCode. If the value is EXCEPTION_SINGLE_STEP, then this debug event is generated because of the single-step mode. In this case, we can increment the variable TotalInstruction by one because we know that exactly one instruction was executed in the debuggee.

             invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
             invoke SetThreadContext,pi.hThread, addr context
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
             .continue

Since the trap flag is cleared after the debug exception is generated, we must set the trap flag again if we want to continue in single-step mode.
Warning: Don't use the example in this tutorial with a large program: tracing is SLOW. You may have to wait for ten minutes before you can close the debuggee.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 31: ListView Control

Tutorial 31: Listview Control

We will learn how to create and use the listview control in this tutorial.

Download the example.

Theory:

A listview control is one of the common controls like treeview, richedit etc. You are familiar with it even if you may not know it by its name. For example, the right pane of Windows Explorer is a listview control. A listview control is good for displaying items. In this regard, it's like a listbox but with enhanced capabilities.
You can create a listview control in two ways. The first method is also the easiest one: create it with a resource editor. Just don't forget to call InitCommonControls within your asm source code. The other method is to call CreateWindowEx in your source code. You must specify the correct window class name for the control, ie. SysListView32. The window class "WC_LISTVIEW" is incorrect.
There are four methods of viewing data in a listview control: icon, small icon, list and report views. You can see examples of these views by selecting View->Large Icons (icon view), Small Icons (small icon view), List (list view) and Details (report view). Views are just data representation methods:they only affect the appearances of data. For example, you can have a lot of data in the listview control, but if you want, you can view only some of them. Report view is the most informative one while the remaining views give less info. You can specify the view you want when you create a listview control. You can later change the view by calling SetWindowLong, specifying GWL_STYLE flag.

Now that we know how to create a listview control, we will continue on how to use it. I'll focus on report view which can demonstrate many features of listview control. The steps in using a listview control are as follows:

  1. Create a listview control with CreateWindowEx, specifying SysListView32 as the class name. You can specify the initial view at this time.
  2. (if exists) Create and initialize image lists to be used with the listview items.
  3. Insert column(s) into the listview control. This step is necessary if the listview control will use report view.
  4. Insert items and subitems into the listview control.

Columns

In report view, there are one or more columns. You can think of the arrangement of data in the report view as a table: the data are arranged in rows and columns. You must have at least one column in your listview control (only in report view). In views other than report, you need not insert a column because there can be one and only one column in those views.
You can insert a column by sending LVM_INSERTCOLUMN to the listview control.

LVM_INSERTCOLUMN
wParam = iCol
lParam = pointer to a LV_COLUMN structure

iCol is the column number, starting from 0.
LV_COLUMN contains information about the column to be inserted. It has the following definition:

LV_COLUMN STRUCT
  imask dd ?
  fmt dd ?
  lx dd ?
  pszText dd ?
  cchTextMax dd ?
  iSubItem dd ?
  iImage dd ?
  iOrder dd ?
LV_COLUMN ENDS

Field name | Meanings
imask

A collection of flags that governs which members in this structure are valid. The reason behind this member is that not all members in this structure are used at the same time. Some members are used in some situations. And this structure is used both for input and output. Thus it's important that you *mark* the members that are used in this call to Windows so Windows knows which members are valid. The available flags are:

LVCF_FMT = The fmt member is valid.
LVCF_SUBITEM = The iSubItem member is valid.
LVCF_TEXT = The pszText member is valid.
LVCF_WIDTH = The lx member is valid.

You can combine the above flags. For example, if you want to specify the text label of the column, you must supply the pointer to the string in pszText member. And you must tell Windows that pszText member contains data by specifying LVCF_TEXT flag in this field else Windows will ignore the value in pszText.

fmt

Specify the alignment of items/subitems in the column. The available values are:

LVCFMT_CENTER = Text is centered.
LVCFMT_LEFT = Text is left-aligned.
LVCFMT_RIGHT = Text is right-aligned.

lx | The width of the column, in pixels. You can later change the width of the column with LVM_SETCOLUMNWIDTH.
pszText | Contains a pointer to the name of the column if this structure is used to set the column's properties. If this structure is used to receive the properties of a column, this field contains a pointer to a buffer large enough to receive the name of the column that will be returned. In that case, you must give the size of the buffer in cchTextMax below. You can ignore cchTextMax if you want to set the name of the column because the name must be an ASCIIZ string which Windows can determine the length.
cchTextMax | The size, in bytes, of the buffer specified in pszText above. This member is used only when you use this structure to receive info about a column. If you use this structure to set the properties of a column, this field is ignored.
iSubItem | Specify the index of subitem associated with this column. This value is used as a marker which subitem this column is associated with. If you want, you can specify an absurd number in this field and your listview control will still run like a breeze. The use of this field is best demonstrated when you have the column number and need to know with which subitem this column is associated. You can query the listview control by sending LVM_GETCOLUMN message to it, specifying LVCF_SUBITEM in the imask member. The listview control will fill the iSubItem member with whatever value you specify in this field when the column is inserted. In order for this method to work, you need to input the correct subitem index into this field.
iImage and iOrder | Used with Internet Explorer 3.0 upwards. I don't have info about them.

So after the listview control is created, you should insert one or more columns into it. Columns are not necessary if you don't plan to switch the listview control into report view. In order to insert a column, you need to create a LV_COLUMN structure, fill it with necessary information, specify the column number and then send the structure to the listview control with LVM_INSERTCOLUMN message.

   LOCAL lvc:LV_COLUMN
   mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
   mov lvc.pszText,offset Heading1
   mov lvc.lx,150
   invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc

The above code snippet demonstrates the process. It specifies the column header text and its width then send LVM_INSERTCOLUMN message to the listview control. It's that simple.

Items and subitems

Items are the main entries in the listview control. In views other than report view, you will only see items in the listview control. Subitems are details of the items. An item may have one or more associated subitems. For example, if the item is the name of a file, then you can have the file attributes, its size, the date of file creation as subitems. In report view, the leftmost column contains items and the remaining columns contain subitems. You can think of an item and its subitems as a database record. The item is the primary key of the record and the subitems are fields in the record.
At the bare minimum, you need some items in your listview control: subitems are not necessary. However, if you want to give the user more information about the items, you can associate items with subitems so the user can see the details in the report view.
You insert an item into a listview control by sending LVM_INSERTITEM message to it. You also need to pass the address of an LV_ITEM structure to it in lParam. LV_ITEM has the following definition:

LV_ITEM STRUCT
  imask dd ?
  iItem dd ?
  iSubItem dd ?
  state dd ?
  stateMask dd ?
  pszText dd ?
  cchTextMax dd ?
  iImage dd ?
  lParam dd ?
  iIndent dd ?
LV_ITEM ENDS

Field name | Meanings
imask | A collection of flags indicating which members in this structure are valid for this call. In general, this field is similar to imask member of LV_COLUMN above. Check your win32 api reference for more detail on the available flags.
iItem | The index of the item this structure refers to. The index is zero-based. You can think of this field as containing the "row" number of a table.
iSubItem | The index of the subitem associated with the item specified by iItem above. You can think of this field as containing the "column" of a table. For example, if you want to insert an item into a newly created listview control, the value in iItem would be 0 (because this item is the first one), and the value in iSubItem would also be 0 (we want to insert the item into the first column). If you want to specify a subitem associated with this item, the iItem would be the index of the item you want to associate with (in the above example, it's 0), the iSubItem would be 1 or greater, depending on which column you want to insert the subitem into. For example, if your listview control has 4 columns, the first column will contain the items. The remaining 3 columns are for subitems. If you want to insert a subitem into the 4th column, you need to specify the value 3 in iSubItem.
state | This member contains flags that reflect the status of the item. The state of an item can change because of the user's actions or it can be modified by our program. The state includes whether the item has the focus/is hilited/is selected for cut operation/is selected. In addition to the state flags, It can also contains one-based index into the overlay image/state image for use by the item.
stateMask | Since the state member above can contain the state flags, overlay image index , and state image index, we need to tell Windows which value we want to set or retrieve. The value in this field is for such use.
pszText | The address of an ASCIIZ string that will be used as the label of the item in the case we want to set/insert the item. In the case that we use this structure to retrieve the item's property, this member must contain the address of a buffer that will be filled with the label of the item.
cchTextMax | This field is used only when you use this structure to receive info about an item. In this case, this field contains the size in bytes of the buffer specified in pszText above.
iImage | The index into the imagelist containing the icons for the listview control. This index points to the icon to be used with this item.
lParam | A user-defined value that will be used when you sort items in the listview control. In short, when you tell the listview control to sort the items, the listview control will compare the items in pairs. It will send the lParam values of both items to you so you can decide which of the two should be listed first. If you're still hazy about this, don't worry. You'll learn more about sorting later.

Let's summarize the steps in inserting an item/subitem into a listview control.

  1. Create a variable of type LV_ITEM structure
  2. Fill it with necessary information
  3. Send LVM_INSERTITEM message to the listview control if you want to insert an item. Or if you want to *insert* a subitem, send LVM_SETITEM instead. This is rather confusing if you don't understand the relationship between an item and its subitems. Subitems are considered as properties of an item. Thus you can insert items but not subitems and you can't have a subitem without an associated item. That's why you need to send LVM_SETITEM message to add a subitem instead of LVM_INSERTITEM.

ListView Messages/Notifications

Now that you know how to create and populate a listview control, the next step is to communicate with it. A listview control communicates with the parent window via messages and notifications. The parent window can control the listview control by sending messages to it. The listview control notifies the parent of important/interesting events via WM_NOTIFY message, just like other common controls.

Sorting items/subitems

You can specify the default sorting order of a listview control by specifying LVS_SORTASCENDING or LVS_SORTDESCENDING styles in CreateWindowEx. These two styles order the items using their labels only. If you want to sort the items in other ways, you need to send LVM_SORTITEMS message to the listview control.

LVM_SORTITEMS
wParam = lParamSort
lParam = pCompareFunction

lParamSort is a user-defined value that will be passed to the compare function. You can use this value in any way you want.
pCompareFunction is the address of the user-defined function that will decide the outcome of the comparison of items in the listview control. The function has the following prototype:

CompareFunc proto lParam1:DWORD, lParam2:DWORD, lParamSort:DWORD

lParam1 and lParam2 are the values in lParam member of LV_ITEM that you specify when you insert the items into the listview control.
lParamSort is the value in wParam you sent with LVM_SORTITEMS

When the listview control receives LVM_SORTITEMS message, it calls the compare function specified in lParam of the message when it needs to ask us for the result of comparison between two items. In short, the comparison function will decide which of the two items sent to it will precede the other. The rule is simple: if the function returns a negative value, the first item (represented by lParam1) should precede the other. If the function returns a positive value, the second item (represented by lParam2) should precede the first one. If both items are equal, it must return zero.

What makes this method work is the value in lParam of LV_ITEM structure. If you need to sort the items (such as when the user clicks on a column header), you need to think of a sorting scheme that makes use of the values in lParam member. In the example, I put the index of the item in this field so I can obtain other information about the item by sending LVM_GETITEM message. Note that when the items are rearranged, their indexes also change. So when the sorting is done in my example, I need to update the values in lParam to reflect the new indexes. If you want to sort the items when the user clicks on a column header, you need to process LVN_COLUMNCLICK notification message in your window procedure. LVN_COLUMNCLICK is passed to your window proc via WM_NOTIFY message.

Example:

This example creates a listview control and fills it with the names and sizes of the files in the current folder. The default view is the report one. In the report view, you can click on the column heads and the items will be sorted in ascending/descending order. You can select the view you want via the menu. When you double-click on an item, a message box showing the label of the item is displayed.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

IDM_MAINMENU equ 10000
IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

RGB macro red,green,blue
  xor eax,eax
  mov ah,blue
  shl eax,8
  mov ah,green
  mov al,red
endm

.data
ClassName db "ListViewWinClass",0
AppName db "Testing a ListView Control",0
ListViewClassName db "SysListView32",0
Heading1 db "Filename",0
Heading2 db "Size",0
FileNamePattern db "*.*",0
FileNameSortOrder dd 0
SizeSortOrder dd 0
template db "%lu",0

.data?
hInstance HINSTANCE ?
hList dd ?
hMenu dd ?

.code
start:
  invoke GetModuleHandle, NULL
  mov hInstance,eax
  invoke WinMain, hInstance,NULL, NULL, SW_SHOWDEFAULT
  invoke ExitProcess,eax
  invoke InitCommonControls
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
  LOCAL wc:WNDCLASSEX
  LOCAL msg:MSG
  LOCAL hwnd:HWND

  mov wc.cbSize,SIZEOF WNDCLASSEX
  mov wc.style, NULL
  mov wc.lpfnWndProc, OFFSET WndProc
  mov wc.cbClsExtra,NULL
  mov wc.cbWndExtra,NULL
  push hInstance
  pop wc.hInstance
  mov wc.hbrBackground,COLOR_WINDOW+1
  mov wc.lpszMenuName,IDM_MAINMENU
  mov wc.lpszClassName,OFFSET ClassName
  invoke LoadIcon,NULL,IDI_APPLICATION
  mov wc.hIcon,eax
  mov wc.hIconSm,eax
  invoke LoadCursor,NULL,IDC_ARROW
  mov wc.hCursor,eax
  invoke RegisterClassEx, addr wc
  invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst,NULL
  mov hwnd,eax
  invoke ShowWindow, hwnd,SW_SHOWNORMAL
  invoke UpdateWindow, hwnd
  .while TRUE
    invoke GetMessage, ADDR msg,NULL,0,0
    .break .if (!eax)
      invoke TranslateMessage, ADDR msg
      invoke DispatchMessage, ADDR msg
  .endw
  mov eax,msg.wParam
  ret
WinMain endp

InsertColumn proc
  LOCAL lvc:LV_COLUMN

  mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
  mov lvc.pszText,offset Heading1
  mov lvc.lx,150
  invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc
  or lvc.imask,LVCF_FMT
  mov lvc.fmt,LVCFMT_RIGHT
  mov lvc.pszText,offset Heading2
  mov lvc.lx,100
  invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
  ret
InsertColumn endp

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
  LOCAL lvi:LV_ITEM
  LOCAL buffer[20]:BYTE
  mov edi,lpFind
  assume edi:ptr WIN32_FIND_DATA
  mov lvi.imask,LVIF_TEXT+LVIF_PARAM
  push row
  pop lvi.iItem
  mov lvi.iSubItem,0
  lea eax,[edi].cFileName
  mov lvi.pszText,eax
  push row
  pop lvi.lParam
  invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
  mov lvi.imask,LVIF_TEXT
  inc lvi.iSubItem
  invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
  lea eax,buffer
  mov lvi.pszText,eax
  invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
  assume edi:nothing
  ret
ShowFileInfo endp

FillFileInfo proc uses edi
  LOCAL finddata:WIN32_FIND_DATA
  LOCAL FHandle:DWORD

  invoke FindFirstFile,addr FileNamePattern,addr finddata
  .if eax!=INVALID_HANDLE_VALUE
    mov FHandle,eax
    xor edi,edi
    .while eax!=0
      test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
      .if ZERO?
         invoke ShowFileInfo,edi, addr finddata
         inc edi
      .endif
      invoke FindNextFile,FHandle,addr finddata
    .endw
    invoke FindClose,FHandle
  .endif
  ret
FillFileInfo endp

String2Dword proc uses ecx edi edx esi String:DWORD
  LOCAL Result:DWORD

  mov Result,0
  mov edi,String
  invoke lstrlen,String
  .while eax!=0
    xor edx,edx
    mov dl,byte ptr [edi]
    sub dl,"0"
    mov esi,eax
    dec esi
    push eax
    mov eax,edx
    push ebx
    mov ebx,10
    .while esi > 0
      mul ebx
      dec esi
    .endw
    pop ebx
    add Result,eax
    pop eax
    inc edi
    dec eax
  .endw
  mov eax,Result
  ret
String2Dword endp

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
  LOCAL buffer[256]:BYTE
  LOCAL buffer1[256]:BYTE
  LOCAL lvi:LV_ITEM

  mov lvi.imask,LVIF_TEXT
  lea eax,buffer
  mov lvi.pszText,eax
  mov lvi.cchTextMax,256
  .if SortType==1
    mov lvi.iSubItem,1
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke String2Dword,addr buffer
    mov edi,eax
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke String2Dword,addr buffer
    sub edi,eax
    mov eax,edi
  .elseif SortType==2
    mov lvi.iSubItem,1
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke String2Dword,addr buffer
    mov edi,eax
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke String2Dword,addr buffer
    sub eax,edi
  .elseif SortType==3
    mov lvi.iSubItem,0
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke lstrcpy,addr buffer1,addr buffer
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke lstrcmpi,addr buffer1,addr buffer
  .else
    mov lvi.iSubItem,0
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke lstrcpy,addr buffer1,addr buffer
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke lstrcmpi,addr buffer,addr buffer1
  .endif
  ret
CompareFunc endp

UpdatelParam proc uses edi
   LOCAL lvi:LV_ITEM

   invoke SendMessage,hList, LVM_GETITEMCOUNT,0,0
   mov edi,eax
   mov lvi.imask,LVIF_PARAM
   mov lvi.iSubItem,0
   mov lvi.iItem,0
   .while edi>0
     push lvi.iItem
     pop lvi.lParam
     invoke SendMessage,hList, LVM_SETITEM,0,addr lvi
     inc lvi.iItem
     dec edi
   .endw
   ret
UpdatelParam endp

ShowCurrentFocus proc
   LOCAL lvi:LV_ITEM
   LOCAL buffer[256]:BYTE

   invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED
   mov lvi.iItem,eax
   mov lvi.iSubItem,0
   mov lvi.imask,LVIF_TEXT
   lea eax,buffer
   mov lvi.pszText,eax
   mov lvi.cchTextMax,256
   invoke SendMessage,hList,LVM_GETITEM,0,addr lvi
   invoke MessageBox,0, addr buffer,addr AppName,MB_OK
   ret
ShowCurrentFocus endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
  .if uMsg==WM_CREATE
    invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
    mov hList, eax
    invoke InsertColumn
    invoke FillFileInfo
    RGB 255,255,255
    invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
    RGB 0,0,0
    invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
    RGB 0,0,0
    invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax
    invoke GetMenu,hWnd
    mov hMenu,eax
    invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED
  .elseif uMsg==WM_COMMAND
    .if lParam==0
      invoke GetWindowLong,hList,GWL_STYLE
      and eax,not LVS_TYPEMASK
      mov edx,wParam
      and edx,0FFFFh
      push edx
      or eax,edx
      invoke SetWindowLong,hList,GWL_STYLE,eax
      pop edx
      invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
    .endif
  .elseif uMsg==WM_NOTIFY
    push edi
    mov edi,lParam
    assume edi:ptr NMHDR
    mov eax,[edi].hwndFrom
    .if eax==hList
      .if [edi].code==LVN_COLUMNCLICK
        assume edi:ptr NM_LISTVIEW
        .if [edi].iSubItem==1
          .if SizeSortOrder==0 || SizeSortOrder==2
            invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc
            invoke UpdatelParam
            mov SizeSortOrder,1
          .else
            invoke SendMessage,hList,LVM_SORTITEMS,2,addr CompareFunc
            invoke UpdatelParam
            mov SizeSortOrder,2
          .endif
        .else
          .if FileNameSortOrder==0 || FileNameSortOrder==4
            invoke SendMessage,hList,LVM_SORTITEMS,3,addr CompareFunc
            invoke UpdatelParam
            mov FileNameSortOrder,3
          .else
            invoke SendMessage,hList,LVM_SORTITEMS,4,addr CompareFunc
            invoke UpdatelParam
            mov FileNameSortOrder,4
          .endif
        .endif
        assume edi:ptr NMHDR
      .elseif [edi].code==NM_DBLCLK
        invoke ShowCurrentFocus
      .endif
    .endif
    pop edi
  .elseif uMsg==WM_SIZE
    mov eax,lParam
    mov edx,eax
    and eax,0ffffh
    shr edx,16
    invoke MoveWindow,hList, 0, 0, eax,edx,TRUE
  .elseif uMsg==WM_DESTROY
    invoke PostQuitMessage,NULL
  .else
    invoke DefWindowProc,hWnd,uMsg,wParam,lParam
    ret
  .endif
  xor eax,eax
  ret
WndProc endp
end start

Analysis:

The first thing the program does when the main window is created is to create a listview control.

  .if uMsg==WM_CREATE
    invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
    mov hList, eax

We call CreateWindowEx, passing itthe name of the window class "SysListView32". The default view is the report view as specified by LVS_REPORT style.

    invoke InsertColumn

After the listview control is created, we insert columns into it.

  LOCAL lvc:LV_COLUMN

  mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
  mov lvc.pszText,offset Heading1
  mov lvc.lx,150
  invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc

We specify the label and the width of the first column, for storing the names of the files, in LV_COLUMN structure thus we need to set imask with LVCF_TEXT and LVCF_WIDTH flags. We fill pszText with the address of the label and lx with the width of the column, in pixels. When all is done, we send LVM_INSERTCOLUMN message to the listview control, passing the structure to it.

  or lvc.imask,LVCF_FMT
  mov lvc.fmt,LVCFMT_RIGHT

When we are done with the insertion of the first column, we insert another column for storing the sizes of the files. Since we need the sizes to right-align in the column, we need to specify a flag in fmt member, LVCFMT_RIGHT. We must also specify LVCF_FMT flag in imask, in addition to LVCF_TEXT and LVCF_WIDTH.

  mov lvc.pszText,offset Heading2
  mov lvc.lx,100
  invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc

The remaining code is simple. Put the address of the label in pszText and the width in lx. Then send LVM_INSERTCOLUMN message to the listview control, specifying the column number and the address of the structure.

When the columns are inserted, we can fill items in the listview control.

    invoke FillFileInfo

FillFileInfo has the following code.

FillFileInfo proc uses edi
  LOCAL finddata:WIN32_FIND_DATA
  LOCAL FHandle:DWORD

  invoke FindFirstFile,addr FileNamePattern,addr finddata

We call FindFirstFile to obtain the information of the first file that matches the search criteria. FindFirstFile has the following prototype:

FindFirstFile proto pFileName:DWORD, pWin32_Find_Data:DWORD

pFileName is the address of the filename to search for. This string can contain wildcards. In our example, we use *.*, which amounts to search for all the files in the current folder.
pWin32_Find_Data is the address of the WIN32_FIND_DATA structure that will be filled with information about the file (if found).

This function returns INVALID_HANDLE_VALUE in eax if no matching file is found. Otherwise it returns a search handle that will be used in subsequent FindNextFile calls.

  .if eax!=INVALID_HANDLE_VALUE
    mov FHandle,eax
    xor edi,edi

If a file is found, we store the search handle in a variable and then zero out edi which will be used as the index into the items (row number).

    .while eax!=0
      test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
      .if ZERO?

In this tutorial, I don't want to deal with the folders yet so I filter them out by checking dwFileAttributes for files which have FILE_ATTRIBUTE_DIRECTORY flag set. If they are found, I skip to call FindNextFile.

          invoke ShowFileInfo,edi, addr finddata
          inc edi
      .endif
      invoke FindNextFile,FHandle,addr finddata
    .endw

We insert the name and size of the file into the listview control by calling ShowFileInfo function. Then we increase the current row number in edi. Lastly we proceed to call FindNextFile to search for the next file in the current folder until FindNextFile returns 0 (meaning no more file is found).

    invoke FindClose,FHandle
  .endif
  ret
FillFileInfo endp

When all files in the current folder are enumerated, we must close the search handle.

Now let's look at the ShowFileInfo function. This function accepts two parameters, the index of the item (row number) and the address of WIN32_FIND_DATA structure.

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
  LOCAL lvi:LV_ITEM
  LOCAL buffer[20]:BYTE
  mov edi,lpFind
  assume edi:ptr WIN32_FIND_DATA

Store the address of WIN32_FIND_DATA structure in edi.

  mov lvi.imask,LVIF_TEXT+LVIF_PARAM
  push row
  pop lvi.iItem
  mov lvi.iSubItem,0

We will supply the label of the item and the value in lParam so we put LVIF_TEXT and LVIF_PARAM flags into imask. Next we set the iItem to the row number passed to the function and since this is the main item, we must filliSubItem with 0 (column 0).

  lea eax,[edi].cFileName
  mov lvi.pszText,eax
  push row
  pop lvi.lParam

Next we put the address of the label, in this case, the name of the file in WIN32_FIND_DATA structure, into pszText. Because we will implement sorting in the listview control, we must fill lParam with a value. I choose to put the row number into this member so I can retrieve the item info by its index.

  invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

When all necessary fields in LV_ITEM are filled, we send LVM_INSERTITEM message to the listview control to insert the item into it.

  mov lvi.imask,LVIF_TEXT
  inc lvi.iSubItem
  invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
  lea eax,buffer
  mov lvi.pszText,eax

We will set the subitem associated with the item just inserted into the second column. A subitem can only have a label. Thus we specify LVIF_TEXT in imask. Then we specify the column that the subitem should reside in iSubItem. In this case, we set it to 1 by incrementing iSubItem. The label we will use is the size of the file. However, we must convert it to a string first by calling wsprintf. Then we put the address of the string into pszText.

  invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
  assume edi:nothing
  ret
ShowFileInfo endp

When all necessary fields in LV_ITEM are filled, we send LVM_SETITEM message to the listview control, passing to it the address of the LV_ITEM structure. Note that we use LVM_SETITEM, not LVM_INSERTITEM because a subitem is considered as a property of an item. Thus we *set* the property of the item, not inserting a new item.

When all items are inserted into the listview control, we set the text and background colors of the listview control.

    RGB 255,255,255
    invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
    RGB 0,0,0
    invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
    RGB 0,0,0
    invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax

I use RGB macro to convert the red, green,blue values into eax and use it to specify the color we need. We set the foreground and background colors of the text with LVM_SETTEXTCOLOR and LVM_SETTEXTBKCOLOR messages. We set the background color of the listview control by sending LVM_SETBKCOLOR message to the listview control.

    invoke GetMenu,hWnd
    mov hMenu,eax
    invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED

We will let the user chooses the views he wants via the menu. Thus we must obtain the menu handle first. To help the user track the current view, we put a radio button system in our menu. The menu item that reflects the current view will be preceded by a radio button. That's why we call CheckMenuRadioItem. This function will put a radio button before a menu item.

Note that we create the listview control with width and height equal to 0. It will be resized later whenever the parent window is resized. This way, we can ensure that the size of the listview control will always match that of the parent window. In our example, we want the listview control to fill the whole client area of the parent window.

  .elseif uMsg==WM_SIZE
    mov eax,lParam
    mov edx,eax
    and eax,0ffffh
    shr edx,16
    invoke MoveWindow,hList, 0, 0, eax,edx,TRUE

When the parent window receives WM_SIZE message, the low word of lParam contains the new width of the client area and the high word the new height. Then we call MoveWindow to resize the listview control to cover the whole client area of the parent window.

When the user selects a view in the menu. We must change the view in the listview control accordingly. We accomplish this by setting a new style in the listview control with SetWindowLong.

  .elseif uMsg==WM_COMMAND
    .if lParam==0
      invoke GetWindowLong,hList,GWL_STYLE
      and eax,not LVS_TYPEMASK

The first thing we do is to obtain the current styles of the listview control. Then we clear the old view style from the returned style flags. LVS_TYPEMASK is a constant that is the combined value of all 4 view style constants (LVS_ICON+LVS_SMALLICON+LVS_LIST+LVS_REPORT). Thus when we perform and operation on the current style flags with the value "not LVS_TYPEMASK", it amounts to clearing away the current view style.

In designing the menu, I cheat a little. I use the view style constants as the menu IDs.

IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

Thus when the parent window receives WM_COMMAND message, the desired view style is in the low word of wParam as the menu ID.

      mov edx,wParam
      and edx,0FFFFh

We have the desired view style in the low word of wParam. All we have to do is to zero out the high word.

      push edx
      or eax,edx

And add the desired view style to the existing styles (minus the current view style) of the listview control.

      invoke SetWindowLong,hList,GWL_STYLE,eax

And set the new styles with SetWindowLong.

      pop edx
      invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
   .endif

We also need to put the radio button in front of the selected view menu item. Thus we call CheckMenuRadioItem, passing the current view style (double as menu ID) to it.

When the user clicks on the column headers in the report view, we want to sort the items in the listview control. We must respond to WM_NOTIFY message.

  .elseif uMsg==WM_NOTIFY
    push edi
    mov edi,lParam
    assume edi:ptr NMHDR
    mov eax,[edi].hwndFrom
    .if eax==hList

When we receive WM_NOTIFY message, lParam contains the pointer to an NMHDR structure. We can check if this message is from the listview control by comparing the hwndFrom member of NMHDR to the handle to the listview control. If they match, we can assume that the notification came from the listview control.

      .if [edi].code==LVN_COLUMNCLICK
        assume edi:ptr NM_LISTVIEW

If the notification is from the listview control, we check if the code is LVN_COLUMNCLICK. If it is, it means the user clicks on a column header. In the case that the code is LVN_COLUMNCLICK, we can assume that lParam contains the pointer to an NM_LISTVIEW structure which is a superset of the NMHDR structure. We then need to know on which column header the user clicks. Examination of iSubItem member reveals this info. The value in iSubItem can be treated as the column number, starting from 0.

        .if [edi].iSubItem==1
          .if SizeSortOrder==0 || SizeSortOrder==2

In the case iSubItem is 1, it means the user clicks on the second column, size. We use state variables to keep the current status of the sorting order. 0 means "no sorting yet", 1 means "sort ascending", 2 means "sort descending". If the items/subitems in the column are not sorted before, or sorted descending, we set the sorting order to ascending.

            invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc

We send LVM_SORTITEMS message to the listview control, passing 1 in wParam and the address of our comparison function in lParam. Note that the value in wParam is user-defined, you can use it in any way you like. I use it as the sorting method in this example. We will take a look at the comparison function first.

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
  LOCAL buffer[256]:BYTE
  LOCAL buffer1[256]:BYTE
  LOCAL lvi:LV_ITEM

  mov lvi.imask,LVIF_TEXT
  lea eax,buffer
  mov lvi.pszText,eax
  mov lvi.cchTextMax,256

In the comparison function, the listview control will pass lParams (in LV_ITEM) of the two items it needs to compare to us in lParam1 and lParam2. You'll recall that we put the index of the item in lParam. Thus we can obtain information about the items by querying the listview control using the indexes. The info we need is the labels of the items/subitems being sorted. Thus we prepare an LV_ITEM structure for such purpose, specifying LVIF_TEXT in imask and the address of the buffer in pszText and the size of the buffer in cchTextMax.

  .if SortType==1
    mov lvi.iSubItem,1
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi

If the value in SortType is 1 or 2, we know that the size column is clicked. 1 means sort the items according to their sizes in ascending order. 2 means the reverse. Thus we specify iSubItem as 1 ( to specify the size column) and send LVM_GETITEMTEXT message to the listview control to obtain the label (size string) of the subitem.

    invoke String2Dword,addr buffer
    mov edi,eax

Covert the size string into a dword value with String2Dword which is the function I wrote. It returns the dword value in eax. We store it in edi for comparison later.

    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke String2Dword,addr buffer
    sub edi,eax
    mov eax,edi

Do likewise with the value in lParam2. When we have the sizes of the two files, we can then compare them.
The rule of the comparison function is as follows:

  • If the first item should precede the other, you must return a negative value in eax
  • If the second item should precede the first one, you must return a positive value in eax
  • If both items are equal, you must return zero in eax.

In this case, we want to sort the items according to their sizes in ascending order. Thus we can simply subtract the size of the first item with that of the second one and return the result in eax.

  .elseif SortType==3
    mov lvi.iSubItem,0
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke lstrcpy,addr buffer1,addr buffer
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke lstrcmpi,addr buffer1,addr buffer

In case the user clicks the filename column, we must compare the names of the files. We first obtain the filenames and then compare them with lstrcmpi function. We can return the return value of lstrcmpi without any modification since it also uses the same rule of comparison, eg. negative value in eax if the first string is less than the second string.

When the items were sorted, we need to update the lParam values of all items to reflect the new indexes by calling UpdatelParam function.

            invoke UpdatelParam
            mov SizeSortOrder,1

This function simply enumerates all items in the listview control and updates the values in lParam with the new indexes. We need to do this else the next sort will not work as expected because our assumption is that the value in lParam is the index of the item.

      .elseif [edi].code==NM_DBLCLK
        invoke ShowCurrentFocus
      .endif

When the user double-clicks at an item, we want to display a message box with the label of the item on it. We must check if the code in NMHDR is NM_DBLCLK. If it is, we can proceed to obtain the label and display it in a message box.

ShowCurrentFocus proc
   LOCAL lvi:LV_ITEM
   LOCAL buffer[256]:BYTE

   invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED

How do we know which item is double-clicked? When an item is clicked or double-clicked, its state is set to "focused". Even if many items are hilited (selected), only one of them has got the focus. Our job than is to find the item that has the focus. We do this by sending LVM_GETNEXTITEM message to the listview control, specifying the desired state in lParam. -1 in wParam means search all items. The index of the item is returned in eax.

   mov lvi.iItem,eax
   mov lvi.iSubItem,0
   mov lvi.imask,LVIF_TEXT
   lea eax,buffer
   mov lvi.pszText,eax
   mov lvi.cchTextMax,256
   invoke SendMessage,hList,LVM_GETITEM,0,addr lvi

We then proceed to obtain the label by sending LVM_GETITEM message to the listview control.

   invoke MessageBox,0, addr buffer,addr AppName,MB_OK

Lastly, we display the label in a message box.

If you want to know how to use icons in the listview control, you can read about it in my treeview tutorial. The steps are just about the same.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 32: Multiple Document Interface (MDI)

Tutorial 32: Multiple Document Interface (MDI)

This tutorial shows you how to create MDI application. It's actually not too difficult to do. Download the example.

Theory:

Multiple Document Interface (MDI) is a specification for applications that handle multple documents at the same time. You are familiar with Notepad: It's an example of Single Document Interface (SDI). Notepad can handle only one document at a time. If you want to open another document, you have to close the previous one first. As you can imagine, it's rather cumbersome. Contrast it with Microsoft Word: Word can open arbitrary documents at the same time and let the user choose which document to use. Microsoft Word is an example of Multiple Document Interface (MDI).

MDI application has several characteristics that are distinctive. I'll list some of them:

  • Within the main window, there can be multiple child windows in the client area. All child windows are clipped to the client area.
  • When you minimize a child window, it minimizes to the lower left corner of the client area of the main window.
  • When you maximize achild window, its title merges with that of the main window.
  • You can close a child window by pressing Ctrl+F4 and switch the focus between the child windows by pressing Ctrl+Tab

The main window that contains the child windows is called a frame window. Its client area is where the child windows live, hence the name "frame". Its job is a little more elaborate than a usual window because it needs to handle some coordination for MDI.

To control an arbitrary number of child windows in your client area, you need a special window called client window. You can think of this client window as a transparent window that covers the whole client area of the frame window. It's this client window that is the actual parent of those MDI child windows. The client window is the real supervisor of the MDI child windows.

Frame Window
|
Client Window
|
|
|
|
|
|
MDI Child 1
MDI Child 2
MDI Child 3
MDI Child 4
MDI Child n

Figure 1. The hierachy of an MDI application

Creating the Frame Window

Now we can turn our attention to the detail. First of all you need to create a frame window. It's created the same way as the normal window: by calling CreateWindowEx. There are two major differences from a normal window.

The first difference is that you MUST call DefFrameProc instead of DefWindowProc to process the Windows messages your window don't want to handle. This is one way to let Windows do the dirty job of maintaining MDI application for you. If you forget to use DefFrameProc, your application won't get the MDI feature. Period. DefFrameProc has the following syntax:

DefFrameProc proc hwndFrame:DWORD,
                                   hwndClient:DWORD,
                                   uMsg:DWORD,
                                   wParam:DWORD,
                                   lParam:DWORD

If you compare DefFrameProc with DefWindowProc, you'll notice that the only difference between them is that DefFrameProc has 5 parameters while DefWindowProc has only 4. The extra parameter is the handle to the client window. This handle is necessary so Windows can send MDI-related messages to the client window.

The second difference is that, you must call TranslateMDISysAccel in the message loop of your frame window. This is necessary if you want Windows to handle MDI-related accelerator key strokes such as Ctrl+F4, Ctrl+Tab for you. It has the following syntax:

TranslateMDISysAccel proc hwndClient:DWORD,
                                                 lpMsg:DWORD

The first parameter is the handle to the client window. This should not come as a surprise to you because it's the client window that is the parent of all MDI child windows. The second parameter is the address of the MSG structure you filled by calling GetMessage. The idea is to pass the MSG structure to the client window so it could examine if the MSG structure contains the MDI-related keypresses. If so, it processes the message itself and returns a non-zero value, otherwise it returns FALSE.

The steps in creating the frame window can be summarized as follows:

  1. Fill in the WNDCLASSEX structure as usual
  2. Register the frame window class by calling RegisterClassEx
  3. Create the frame window by calling CreateWindowEx
  4. Within the message loop, call TranslateMDISysAccel.
  5. Within the window procedure, pass the unprocessed messages to DefFrameProc instead of DefWindowProc.

Creating the Client Window

Now that we have the frame window, we can create the client window. The client window class is pre-registered by Windows. The class name is "MDICLIENT". You also need to pass the address of a CLIENTCREATESTRUCT structure to CreateWindowEx. This structure has the following definition:

CLIENTCREATESTRUCT struct
        hWindowMenu    dd ?
        idFirstChild    dd ?
CLIENTCREATESTRUCT ends

hWindowMenu is the handle to the submenu that Windows will append the list of MDI child window names. This feature requires a little explanation. If you ever use an MDI application like Microsoft Word before, you'll notice that there is a submenu named "window" which, on activation, displays various menuitems related to window management and at the bottom, the list of the MDI child window currently opened. That list is internally maintained by Windows itself: you don't have to do anything special for it. Just pass the handle of the submenu you want the list to appear in hWindowMenu and Windows will handle the rest. Note that the submenu can be ANY submenu:it doesn't have to be the one that is named "window". The bottom line is that, you should pass the handle to the submenu you want the window list to appear. If you don't want the list, just put NULL in hWindowMenu. You get the handle to the submenu by calling GetSubMenu.

idFirstChild is the ID of the first MDI child window. Windows increments the ID for each new MDI child window the application created. For example, if you pass 100 to this field, the first MDI child window will have the ID of 100, the second one will have the ID of 101 and so on. This ID is sent to the frame window via WM_COMMAND when the MDI child window is selected from the window list. Normally you'll pass this "unhandled" WM_COMMAND messages to DefFrameProc. I use the word "unhandled" because the menuitems in the window list are not created by your application thus your application doesn't know their IDs and doesn't have the handler for them. This is another special case for the MDI frame window: if you have the window list, you must modify your WM_COMMAND handler a bit like this:

.elseif uMsg==WM_COMMAND
      .if lParam==0			; this message is generated from a menu
          mov eax,wParam
          .if ax==IDM_CASCADE
                 .....
          .elseif ax==IDM_TILEVERT
                .....
          .else
                invoke DefFrameProc, hwndFrame, hwndClient, uMsg,wParam, lParam
                ret
          .endif

Normally, you would just ignore the messages from unhandled cases. But In the MDI case, if you ignore them, when the user clicks on the name of an MDI child window in the window list, that window won't become active. You need to pass them to DefFrameProc so they can be handled properly.

A caution on the value of idFirstChild: you should not use 0. Your window list will not behave properly, ie. the check mark will not appear in front of the name of the first MDI child even though it's active. Choose a safe value such as 100 or above.

Having filled in the CLIENTCREATESTRUCT structure, you can create the client window by calling CreateWindowEx with the predefined class name,"MDICLIENT", and passing the address of the CLIENTCREATESTRUCT structure in lParam. You must also specify the handle to the frame window in the hWndParent parameter so Windows knows the parent-child relationship between the frame window and the client window. The window styles you should use are: WS_CHILD ,WS_VISIBLE and WS_CLIPCHILDREN. If you forget WS_VISIBLE, you won't see the MDI child windows even if they were created successfully.

The steps in creating the client window are as follows:

  1. Obtain the handle to the submenu that you want to append the window list to.
  2. Put the value of the menu handle along with the value you want to use as the ID of the first MDI child window in a CLIENTCREATESTRUCT structure
  3. call CreateWindowEx with the class name "MDICLIENT", passing the address of the CLIENTCREATESTRUCT structure you just filled in in lParam.

Creating the MDI Child Window

Now you have both the frame window and the client window. The stage is now ready for the creation of the MDI child window. There are two ways to do that.

  • You can send WM_MDICREATE message to the client window, passing the address of a structure of type MDICREATESTRUCT in wParam. This is the easiest and the usual method of MDI child window creation.
    .data?
        mdicreate MDICREATESTRUCT <>
        ....
    .code
        .....
        [fill the members of mdicreate]
        ......
    invoke SendMessage, hwndClient, WM_MDICREATE,addr mdicreate,0

    SendMessage will return the handle of the newly created MDI child window if successful. You don't need to save the handle though. You can obtain it by other means if you want to. MDICREATESTRUCT has the following definition.

    • MDICREATESTRUCT STRUCT
    szClass DWORD ?
    szTitle DWORD ?
    hOwner DWORD ?
    x DWORD ?
    y DWORD ?
    lx DWORD ?
    ly DWORD ?
    style DWORD ?
    lParam DWORD ?
    MDICREATESTRUCT ENDS
szClass | the address of the window class you want to use as the template for the MDI child window.
szTitle | the address of the text you want to appear in the title bar of the child window
hOwner | the instance handle of the application
x,y,lx,ly | the upper left coordinate and the width and height of the child window
style | child window style. If you create the client window with MDIS_ALLCHILDSTYLES, you can use any window style.
lParam | an application-defined 32-bit value. This is a way of sharing values among MDI windows. If you don't need to use it, set it to NULL
  • You can call CreateMDIWindow. This function has the following syntax:
    CreateMDIWindow proto lpClassName:DWORD
                                               lpWindowName:DWORD
                                               dwStyle:DWORD
                                           x:DWORD
                                               y:DWORD
                                               nWidth:DWORD
                                               nHeight:DWORD
                                               hWndParent:DWORD
                                               hInstance:DWORD
                                               lParam:DWORD

If you look closely at the parameters, you'll find that they are identical to the members of MDICREATESTRUCT structure, except for the hWndParent. Essentially it's the same number of parameters you pass with WM_MDICREATE. MDICREATESTRUCT doesn't have the hWndParent field because you must pass the whole structure to the correct client window with SendMessage anyway.

At this point, you may have some questions: which method should I use? What is the difference between the two? Here is the answer:

The WM_MDICREATE method can only create the MDI child window in the same thread as the calling code. For example, if your application has 2 threads, and the first thread creates the MDI frame window, if the second thread wants to create an MDI child, it must do so with CreateMDIChild: sending WM_MDICREATE message to the first thread won't work. If your application is single-threaded, you can use either method. (Thanks yap for the correction - 04/24/2002)

A little more detail needs to be covered about the window procedure of the MDI child. As with the frame window case, you must not call DefWindowProc to handle the unprocessed messages. Instead, you must use DefMDIChildProc. This function has exactly the same parameters as DefWindowProc.

In addition to WM_MDICREATE, there are other MDI-related window messages. I'll list them below:

WM_MDIACTIVATE | This message can be sent by the application to the client window to instruct the client window to activate the selected MDI child. When the client window receives the message, it activates the selected MDI child window and sends WM_MDIACTIVATE to the child being deactivated and activated. The use of this message is two-fold: it can be used by the application to activate the desired child window. And it can be used by the MDI child window itself as the indicator that it's being activated/deactivated. For example, if each MDI child window has different menu, it can use this opportunity to change the menu of the frame window when it's activated/deactivated.
WM_MDICASCADE WM_MDITILE WM_MDIICONARRANGE | These messages handle the arrangement of the MDI child windows. For example, if you want the MDI child windows to arrange themselves in cascading style, send WM_MDICASCADE to the client window.
WM_MDIDESTROY | Send this message to the client window to destroy an MDI child window. You should use this message instead of calling DestroyWindow because if the MDI child window is maxmized, this message will restore the tile of the frame window. If you use DestroyWindow, the title of the frame window will not be restored.
WM_MDIGETACTIVE | Send this message to retrieve the handle of the currently active MDI child window.
WM_MDIMAXIMIZE WM_MDIRESTORE | Send WM_MDIMAXIMIZE to maximize the MDI child window and WM_MDIRESTORE to restore it to previous state. Always use these messages for the operations. If you use ShowWindow with SW_MAXIMIZE, the MDI child window will maximize fine but it will have the problem when you try to restore it to previous size. You can minimize the MDI child window with ShowWindow without problem, however.
WM_MDINEXT | Send this message to the client window to activate the next or the previous MDI child window according to the values in wParam and lParam.
WM_MDIREFRESHMENU | Send this message to the client window to refresh the menu of the frame window. Note that you must call DrawMenuBar to update the menu bar after sending this message.
WM_MDISETMENU | Send this message to the client window to replace the whole menu of the frame window or just the window submenu. You must use this message instead of SetMenu. After sending this message, you must call DrawMenuBar to update the menu bar. Normally you will use this message when the active MDI child window has its own menu and you want it to replace the menu of the frame window while the MDI child window is active.

I'll review the steps in creating an MDI application for you again below.

  1. Register the window classes, both the frame window class and the MDI child window class
  2. Create the frame window with CreateWindowEx.
  3. Within the message loop, call TranslateMDISysAccel to process the MDI-related accelerator keys
  4. Within the window procedure of the frame window, call DefFrameProc to handle ALL messages unhandled by your code.
  5. Create the client window by calling CreateWindowEx using the name of the predefined window class, "MDICLIENT", passing the address of a CLIENTCREATESTRUCT structure in lParam. Normally, you would create the client window within the WM_CREATE handler of the frame window proc
  6. You can create an MDI child window by sending WM_MDICREATE to the client window or, alternatively, by calling CreateMDIWindow.
  7. Within the window proc of the MDI child window, pass all unhandled messages to DefMDIChildProc.
  8. Use MDI version of the messages if it exists. For example, use WM_MDIDESTROY instead of calling DestroyWindow

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU 	equ 101
IDR_CHILDMENU	equ 102
IDM_EXIT 		equ 40001
IDM_TILEHORZ	equ 40002
IDM_TILEVERT	equ 40003
IDM_CASCADE	equ 40004
IDM_NEW 		equ 40005
IDM_CLOSE	equ 40006

.data
ClassName 	db "MDIASMClass",0
MDIClientName	db "MDICLIENT",0
MDIChildClassName	db "Win32asmMDIChild",0
MDIChildTitle	db "MDI Child",0
AppName		db "Win32asm MDI Demo",0
ClosePromptMessage	db "Are you sure you want to close this window?",0

.data?
hInstance 	dd ?
hMainMenu 	dd ?
hwndClient 	dd ?
hChildMenu 	dd ?
mdicreate		MDICREATESTRUCT <>
hwndFrame 	dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    ;=============================================
    ; Register the frame window class
    ;=============================================
    mov wc.cbSize,SIZEOF WNDCLASSEX
    mov wc.style, CS_HREDRAW or CS_VREDRAW
    mov wc.lpfnWndProc,OFFSET WndProc
    mov wc.cbClsExtra,NULL
    mov wc.cbWndExtra,NULL
    push hInstance
    pop wc.hInstance
    mov wc.hbrBackground,COLOR_APPWORKSPACE
    mov wc.lpszMenuName,IDR_MAINMENU
    mov wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov wc.hIcon,eax
    mov wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    ;================================================
    ; Register the MDI child window class
    ;================================================
    mov wc.lpfnWndProc,offset ChildProc
    mov wc.hbrBackground,COLOR_WINDOW+1
    mov wc.lpszClassName,offset MDIChildClassName
    invoke RegisterClassEx,addr wc
    invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
            WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,0,\
            hInst,NULL
    mov hwndFrame,eax
    invoke LoadMenu,hInstance, IDR_CHILDMENU
    mov hChildMenu,eax
    invoke ShowWindow,hwndFrame,SW_SHOWNORMAL
    invoke UpdateWindow, hwndFrame
    .while TRUE
        invoke GetMessage,ADDR msg,NULL,0,0
        .break .if (!eax)
        invoke TranslateMDISysAccel,hwndClient,addr msg
        .if !eax
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
        .endif
    .endw
    invoke DestroyMenu, hChildMenu
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL ClientStruct:CLIENTCREATESTRUCT
    .if uMsg==WM_CREATE
        invoke GetMenu,hWnd
        mov hMainMenu,eax
        invoke GetSubMenu,hMainMenu,1
        mov ClientStruct.hWindowMenu,eax
        mov ClientStruct.idFirstChild,100
        INVOKE CreateWindowEx,NULL,ADDR MDIClientName,NULL,\
                WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAULT,\
                CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\
                hInstance,addr ClientStruct
        mov hwndClient,eax
        ;=======================================
        ; Initialize the MDICREATESTRUCT
        ;=======================================
        mov mdicreate.szClass,offset MDIChildClassName
        mov mdicreate.szTitle,offset MDIChildTitle
        push hInstance
        pop mdicreate.hOwner
        mov mdicreate.x,CW_USEDEFAULT
        mov mdicreate.y,CW_USEDEFAULT
        mov mdicreate.lx,CW_USEDEFAULT
        mov mdicreate.ly,CW_USEDEFAULT
    .elseif uMsg==WM_COMMAND
        .if lParam==0
            mov eax,wParam
            .if ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDM_TILEHORZ
                invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0
            .elseif ax==IDM_TILEVERT
                invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0
            .elseif ax==IDM_CASCADE
                invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISABLED,0
            .elseif ax==IDM_NEW
                invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate
            .elseif ax==IDM_CLOSE
                invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
                invoke SendMessage,eax,WM_CLOSE,0,0
            .else
                invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
                ret
            .endif
        .endif
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_MDIACTIVATE
        mov eax,lParam
        .if eax==hChild
            invoke GetSubMenu,hChildMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMenu,edx
        .else
            invoke GetSubMenu,hMainMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu,edx
        .endif
        invoke DrawMenuBar,hwndFrame
    .elseif uMsg==WM_CLOSE
        invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName,MB_YESNO
        .if eax==IDYES
            invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
        .endif
    .else
        invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
ChildProc endp
end start

Analysis:

The first thing the program does is to register the window classes of the frame window and the MDI child window. After that, it calls CreateWindowEx to create the frame window. Within the WM_CREATE handler of the frame window, we create the client window:

    LOCAL ClientStruct:CLIENTCREATESTRUCT
    .if uMsg==WM_CREATE
        invoke GetMenu,hWnd
        mov hMainMenu,eax
        invoke GetSubMenu,hMainMenu,1
        mov ClientStruct.hWindowMenu,eax
        mov ClientStruct.idFirstChild,100
        invoke CreateWindowEx,NULL,ADDR MDIClientName,NULL,\
            WS_CHILD or WS_VISIBLE or WS_CLIPCHILDREN,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,NULL,\
            hInstance,addr ClientStruct
        mov hwndClient,eax

It calls GetMenu to obtain the handle to the menu of the frame window, to be used in the GetSubMenu call. Note that we pass the value 1 to GetSubMenu because the submenu we want the window list to appear is the second submenu. Then we fill the members of the CLIENTCREATESTRUCT structure.
Next, we initialize the MDICLIENTSTRUCT structure. Note that we don't need to do it here. It's only convenient to do it in WM_CREATE.

    mov mdicreate.szClass,offset MDIChildClassName
    mov mdicreate.szTitle,offset MDIChildTitle
    push hInstance
    pop mdicreate.hOwner
    mov mdicreate.x,CW_USEDEFAULT
    mov mdicreate.y,CW_USEDEFAULT
    mov mdicreate.lx,CW_USEDEFAULT
    mov mdicreate.ly,CW_USEDEFAULT

After the frame window is created (and also the client window), we call LoadMenu to load the child window menu from the resource. We need to get this menu handle so we can replace the menu of the frame window with it when an MDI child window is present. Don't forget to call DestroyMenu on the handle before the application exits to Windows. Normally Windows will free the menu associated with a window automatically when the application exits but in this case, the child window menu is not associated with any window thus it will still occupy valuable memory even after the application exits.

    invoke LoadMenu,hInstance, IDR_CHILDMENU
    mov hChildMenu,eax
    ........
    invoke DestroyMenu, hChildMenu

Within the message loop, we call TranslateMDISysAccel.

    .while TRUE
        invoke GetMessage,ADDR msg,NULL,0,0
        .break .if (!eax)
        invoke TranslateMDISysAccel,hwndClient,addr msg
        .if !eax
            invoke TranslateMessage, ADDR msg
            invoke DispatchMessage, ADDR msg
        .endif
    .endw

If TranslateMDISysAccel returns a non-zero value, it means the message was already handled by Windows itself so you don't need to do anything to the message. If it returns 0, the message is not MDI-related and thus should be handled as usual.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .....
    .else
        invoke DefFrameProc,hWnd,hwndClient,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp

Note that within the window procedure of the frame window, we call DefFrameProc to handle the messages we are not interested in.

The bulk of the window procedure is the WM_COMMAND handler. When the user selects "New" from the File menu, we create a new MDI child window.

    .elseif ax==IDM_NEW
        invoke SendMessage,hwndClient,WM_MDICREATE,0,addr mdicreate

In our example, we create the MDI child window by sending WM_MDICREATE to the client window, passing the address of the MDICREATESTRUCT structure in lParam.

ChildProc proc hChild:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    .if uMsg==WM_MDIACTIVATE
        mov eax,lParam
        .if eax==hChild
            invoke GetSubMenu,hChildMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hChildMenu,edx
        .else
            invoke GetSubMenu,hMainMenu,1
            mov edx,eax
            invoke SendMessage,hwndClient,WM_MDISETMENU,hMainMenu,edx
        .endif
        invoke DrawMenuBar,hwndFrame

When the MDI child window is created, it monitors WM_MDIACTIVATE to see if it's the active window. It does this by comparing the value of the lParam which contains the handle of the active child window with its own handle. If they match, it's the active window and the next step is to replace the menu of the frame window to its own. Since the original menu will be replaced, you have to tell Windows again in which submenu the window list should appear. That's why we must call GetSubMenu again to retrieve the handle to the submenu. We send WM_MDISETMENU message to the client window to achieve the desired result. wParam of WM_MDISETMENU contains the handle of the menu you would like to replace the original menu. lParam contains the handle of the submenu you want the window list to appear. Right after sending WM_MDISETMENU, we call DrawMenuBar to refresh the menu else your menu will be a mess.

    .else
        invoke DefMDIChildProc,hChild,uMsg,wParam,lParam
        ret
    .endif
Within the window procedure of the MDI    child window, you must pass all unhandled messages to DefMDIChildProc    instead of DefWindowProc.
    .elseif ax==IDM_TILEHORZ
        invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_HORIZONTAL,0
    .elseif ax==IDM_TILEVERT
        invoke SendMessage,hwndClient,WM_MDITILE,MDITILE_VERTICAL,0
    .elseif ax==IDM_CASCADE
        invoke SendMessage,hwndClient,WM_MDICASCADE,MDITILE_SKIPDISABLED,0

When the user selects one of the menuitems in the window submenu, we send the corresponding message to the client window. If the user chooses to tile the windows, we send WM_MDITILE to the client window, specifying in wParam what kind of tiling we want. WM_CASCADE is similar.

    .elseif ax==IDM_CLOSE
        invoke SendMessage,hwndClient,WM_MDIGETACTIVE,0,0
        invoke SendMessage,eax,WM_CLOSE,0,0
If the user chooses "Close"    menuitem, we must obtain the handle of the currently active MDI child window    first by sending WM_MDIGETACTIVE to the client window. The return value in eax    is the handle of the currently active MDI child window. After that, we send    WM_CLOSE to that window.
    .elseif uMsg==WM_CLOSE
        invoke MessageBox,hChild,addr ClosePromptMessage,addr AppName,MB_YESNO
        .if eax==IDYES
            invoke SendMessage,hwndClient,WM_MDIDESTROY,hChild,0
        .endif

Within the window procedure of the MDI child, when WM_CLOSE is received, it displays a message box asking the user if he really wants to close the window. If the answer is yes, we send WM_MDIDESTROY to the client window. WM_MDIDESTROY closes the MDI child window and restores the title of the frame window.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 33: RichEdit Control: Basics

Tutorial 33: RichEdit Control: Basics

There are lots of request on tutorials about RichEdit controls. Finally I have played with it enough to think I can write tutorials about it. So here it is: the first RichEdit tutorial. The tutorials will describe nearly everything there is to know about RichEdit control or at least as much as I know it. The amount of information is rather large so I divide it into several parts, this tutorial being the first part. In this tutorial, you'll learn what a RichEdit control is, how to create it and how to load/save data to/from it.

Download the example.

Theory

A richedit control can be thought of as a souped-up edit control. It provides many desirable features that are lacking from the plain simple edit control, for example, the ability to use multiple font face/size, multiple-level undo/redo, search-for-text operation, OLE-embedded objects, drag-and-drop editing support, etc. Since the richedit control has so many features, it's stored in a separate DLL. This also means that, to use it, you can't just call InitCommonControls like other common controls. You have to call LoadLibrary to load the richedit DLL.

The problem is that there are three versions of richedit control up till now. Version 1,2, and 3. The table below shows you the name of the DLL for each version.

DLL Name | RichEdit version | Richedit Class Name
Riched32.dll | 1.0 | RICHEDIT
RichEd20.dll | 2.0 | RICHEDIT20A
RichEd20.dll | 3.0 | RICHEDIT20A

You can notice that richedit version 2 and 3 use the same DLL name. They also use the same class name! This can pose a problem if you want to use specific features of richedit 3.0. Up to now, I haven't found an official method of differentiating between version 2.0 and 3.0. However, there is a workaround which works ok, I'll show you later.

.data
   RichEditDLL db "RichEd20.dll",0
    .....
.data?
    hRichEditDLL dd ?
.code
    invoke LoadLibrary,addr RichEditDLL
    mov hRichEditDLL,eax
    ......
    invoke FreeLibrary,hRichEditDLL

When the richedit dll is loaded, it registers the RichEdit window class. Thus it's imperative that you load the DLL before you create the control. The names of the richedit control classes are also different. Now you may have a question: how do I know which version of richedit control should I use? Using the latest version is not always appropriate if you don't require the extra features. So below is the table that shows the features provided by each version of richedit control.

Feature | Version 1.0 | Version 2.0 | Version 3.0
selection bar | x | x | x
unicode editing |  | x | x
character/paragraph formatting | x | x | x
search for text | forward | forward/backward | forward/backward
OLE embedding | x | x | x
Drag and drop editing | x | x | x
Undo/Redo | single-level | multi-level | multi-level
automatic URL recognition |  | x | x
Accelerator key support |  | x | x
Windowless operation |  | x | x
Line break | CRLF | CR only | CR only (can emulate version 1.0)
Zoom |  |  | x
Paragraph numbering |  |  | x
simple table |  |  | x
normal and heading styles |  |  | x
underline coloring |  |  | x
hidden text |  |  | x
font binding |  |  | x

The above table is by no means comprehensive: I only list the important features.

Creating the richedit control

After loading the richedit dll, you can call CreateWindowEx to create the control. You can use edit control styles and common window styles in CreateWindowEx except ES_LOWERCASE, ES_UPPERCASE and ES_OEMCONVERT.

.const
    RichEditID equ 300
.data
    RichEditDLL db "RichEd20.dll",0
    RichEditClass db "RichEdit20A",0
    ...
.data?
    hRichEditDLL dd ?
    hwndRichEdit dd ?
.code
    .....
    invoke LoadLibrary,addr RichEditDLL
    mov hRichEditDLL,eax
    invoke CreateWindowEx,0,addr RichEditClass,WS_VISIBLE or ES_MULTILINE or WS_CHILD or WS_VSCROLL or WS_HSCROLL, \
                                        CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
    mov hwndRichEdit,eax

Setting default text and background color

You may have the problem with setting text color and the background color of the edit control. But this problem has been remedy in richedit control. To set the background color of the richedit control, you send EM_SETBKGNDCOLOR to the richedit control. This message has the following syntax.

wParam == color option. The value of 0 in this parameter specifies that Windows uses the color value in lParam as the background color. If this value is nonzero, Windows uses the Windows system background color. Since we send this message to change the background color, we must pass 0 in wParam.
lParam == specifies the COLORREF structure of the color you want to set if wParam is 0.

For example, if I want to set the background color to pure blue, I would issue this following line:

        invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,0FF0000h

To set the text color, richedit control provides another new message, EM_SETCHARFORMAT, for the job. This message controls the text formatting of a range to characters in selection or all text in the control. This message has the following syntax:

wParam == formatting options:

SCF_ALL | The operation affects all text in the control.
SCF_SELECTION | The operation affects only the text in selection
SCF_WORD or SCF_SELECTION | Affects the word in selection. If the selection is empy, ie, only the caret is in the word, the operation affects that word. SCF_WORD flag must be used with SCF_SELECTION.

lParam == pointer to a CHARFORMAT or CHARFORMAT2 structure that specifies the text formatting to be applied. CHARFORMAT2 is available for richedit 2.0 and above only. This doesn't mean that you must use CHARFORMAT2 with RichEdit 2.0 or above. You can still use CHARFORMAT if the added features in CHARFORMAT2 are not necessary for your need.

CHARFORMATA STRUCT
    cbSize DWORD ?
    dwMask DWORD ?
    dwEffects DWORD    ?
    yHeight DWORD ?
    yOffset DWORD ?
    crTextColor COLORREF ?
    bCharSet BYTE ?
    bPitchAndFamily    BYTE ?
    szFaceName BYTE LF_FACESIZE dup(?)
    _wPad2 WORD ?
CHARFORMATA ENDS
Field Name | Description
cbSize | The size of the structure. RichEdit control uses this field to determine the version of the structure whether it is CHARFORMAT or CHARFORMAT2
dwMask

Bit flags that determine which of the following members are valid.

CFM_BOLD | The CFE_BOLD value of the dwEffects member is valid
CFM_CHARSET | The bCharSet member is valid.
CFM_COLOR | The crTextColor member and the CFE_AUTOCOLOR value of the dwEffects member are valid
CFM_FACE | The szFaceName member is valid.
CFM_ITALIC | The CFE_ITALIC value of the dwEffects member is valid
CFM_OFFSET | The yOffset member is valid
CFM_PROTECTED | The CFE_PROTECTED value of the dwEffects member is valid
CFM_SIZE | The yHeight member is valid
CFM_STRIKEOUT | The CFE_STRIKEOUT value of the dwEffects member is valid.
CFM_UNDERLINE | The CFE_UNDERLINE value of the dwEffects member is valid
dwEffects

The character effects. Can be the combination of the following values

CFE_AUTOCOLOR | Use the system text color
CFE_BOLD | Characters are bold
CFE_ITALIC | Characters are italic
CFE_STRIKEOUT | Characters are struck.
CFE_UNDERLINE | Characters are underlined
CFE_PROTECTED | Characters are protected; an attempt to modify them will cause an EN_PROTECTED notification message.
yHeight | Character height, in twips (1/1440 of an inch or 1/20 of a printer's point).
yOffset | Character offset, in twips, from the baseline. If the value of this member is positive, the character is a superscript; if it is negative, the character is a subscript.
crTextColor | Text color. This member is ignored if the CFE_AUTOCOLOR character effect is specified.
bCharSet | Character set value
bPitchAndFamily | Font family and pitch.
szFaceName | Null-terminated character array specifying the font name
_wPad2 | Padding

From examination of the structure, you'll see that we can change the text effects (bold,italic, strikeout,underline), text color (crTextColor) and font face/size/character set. A notable flag is CFE_RPOTECTED. The text with this flag is marked as protected which means that when the user tries to modify it, EN_PROTECTED notification message will be sent to the parent window. And you can allow the change to happen or not.

CHARFORMAT2 adds more text styles such as font weight, spacing,text background color, kerning, etc. If you don't need these extra features, simply use CHARFORMAT.

To set text formatting, you have to think about the range of text you want to apply to. Richedit control introduces the notion of character text range. Richedit control gives each character a number starting from 0: the first characterin the control has Id of 0, the second character 1 and so on. To specify a text range, you must give the richedit control two numbers: the IDs of the first and the last character of the range. To apply the text formatting with EM_SETCHARFORMAT, you have at most three choices:

  1. Apply to all text in the control (SCF_ALL)
  2. Apply to the text currently in selection (SCF_SELECTION)
  3. Apply to the whole word currently in selection (SCF_WORD or SCF_SELECTION)

The first and the second choices are straightforward. The last choice requires a little explanation. If the current selection only covers one or more of the characters in the word but not the whole word, specifying the flag SCF_WORD+SCF_SELECTION applies the text formatting to the whole word. Even if there is no current selection, ie, only the caret is positioned in the word, the third choice also applies the text formatting to the whole word.

To use EM_SETCHARFORMAT, you need to fill several members of CHARFORMAT (or CHARFORMAT2) structure. For example, if we want to set the text color, we will fill the CHARFORMAT structure as follows:

.data?
    cf CHARFORMAT <>
....
.code
    mov cf.cbSize,sizeof cf
    mov cf.dwMask,CFM_COLOR
    mov cf.crTextColor,0FF0000h
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cf

The above code snippet sets the text color of the richedit control to pure blue. Note that if there is no text in the richedit control when EM_SETCHARFORMAT is issued, the text entered into the richedit control following the message will use the text formatting specified by the EM_SETCHARFORMAT message.

Setting the text/saving the text

For those of you who are used to edit control, you'll surely be familiar with WM_GETTEXT/WM_SETTEXT as the means to set the text/get the text to/from the control. This method still works with richedit control but may not be efficient if the file is large. Edit control limits the text that can be entered into it to 64K but richedit control can accept text much larger than that. It would be very cumbersome to allocate a very large block of memory (such as 10 MB or so) to receive the text from WM_GETTEXT. Richedit control offers a new approach to this method, ie. text streaming.

To put it simply, you provide the address of a callback function to the richedit control. And richedit control will call that callback, passing the address of the buffer to it, when it's ready. The callback will fill the buffer with the data it wants to send to the control or read the data from the buffer and then waits for the next call until the operation is finished. This paradigm is used for both streaming in (setting the text) and streaming out (getting the text out of the control). You'll see that this method is more efficient: the buffer is provided by the richedit control itself so the data are divided into chunks. The operations involve two messages: EM_STREAMIN and EM_STREAMOUT

Both EM_STREAMIN and EM_STREAMOUT use the same syntax:

wParam == formatting options.

SF_RTF | The data is in the rich-text format (RTF)
SF_TEXT | The data is in the plain text format
SFF_PLAINRTF | Only the keywords common to all languages are streamed in.
SFF_SELECTION | If specified, the target of the operation is the text currently in selection. If you stream the text in, the text replaces the current selection. If you stream the text out, only the text currently in selection is streamed out. If this flag is not specified, the operation affects the whole text in the control.
SF_UNICODE | (Available on RichEdit 2.0 or later) Specify the unicode text.

lParam == point to an EDITSTREAM structure which has the following definition:

 EDITSTREAM STRUCT
    dwCookie DWORD    ?
    dwError DWORD ?
    pfnCallback DWORD ?
EDITSTREAM ENDS
dwCookie | application-defined value that will be passed to the callback function speficied in pfnCallback member below. We normally pass some important value to the callback function such as the file handle to use in the stream-in/out procedure.
dwError | Indicates the results of the stream-in (read) or stream-out (write) operation. A value of zero indicates no error. A nonzero value can be the return value of the EditStreamCallback function or a code indicating that the control encountered an error.
pfnCallback | Pointer to an EditStreamCallback function, which is an application-defined function that the control calls to transfer data. The control calls the callback function repeatedly, transferring a portion of the data with each call

The editstream callback function has the following definition:

    EditStreamCallback proto dwCookie:DWORD,
                 pBuffer:DWORD,
                 NumBytes:DWORD,
                 pBytesTransferred:DWORD

You have to create a function with the above prototype in your program. And then pass its address to EM_STREAMIN or EM_STREAMOUT via EDITSTREAM structure.

For stream-in operation (settting the text in the richedit control):

    dwCookie: the application-defined value you pass to EM_STREAMIN via EDITSTREAM structure. We almost always
        pass the file handle of the file we want to set its content to the control here.
    pBuffer: points to the buffer provided by the richedit control that will receive the text from your callback function.
    NumBytes: the maximum number of bytes you can write the the buffer (pBuffer) in this call. You MUST always obey this limit, ie, you can send
        less data than the value in NumBytes but must not send more data than this value. You can think of this value as the size
        of the buffer in pBuffer.
   pBytesTransferred: points to a dword that you must set the value indicating the number of bytes you actually transferred to the buffer.
        This value is usually identical to the value in NumBytes. The exception is when the data is to send is less than
        the size of the buffer provided such as when the end of file is reached.

For stream-out operation (getting the text out of the richedit control):

    dwCookie: Same as the stream-in operation. We usually pass the file handle we want to write the data to in this parameter.
    pBuffer: points to the buffer provided by the richedit control that is filled with the data from the richedit control.
        To obtain its size, you must examine the value of NumBytes.
    NumBytes: the size of the data in the buffer pointed to by pBuffer.
    pBytesTransferred: points to a dword that you must set the value indicating the number of bytes you actually read from the buffer.

The callback function returns 0 to indicate success and richedit control will continue calling the callback function if there is still data left to read/write. If some error occurs during the process and you want to stop the operation, returns a non-zero value and the richedit control will discard the data pointed to by pBuffer. The error/success value will be filled in the dwError field of EDITSTREAM so you can examine the error/success status of the stream operation after SendMessage returns.

Example:

The example below is a simple editor which you can open an asm source code file, edit and save it. It uses RichEdit control version 2.0 or above.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\gdi32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU                   equ 101
IDM_OPEN                      equ  40001
IDM_SAVE                       equ 40002
IDM_CLOSE                      equ 40003
IDM_SAVEAS                     equ 40004
IDM_EXIT                       equ 40005
IDM_COPY                      equ  40006
IDM_CUT                       equ  40007
IDM_PASTE                      equ 40008
IDM_DELETE                     equ 40009
IDM_SELECTALL                  equ 40010
IDM_OPTION 			equ 40011
IDM_UNDO			equ 40012
IDM_REDO			equ 40013
IDD_OPTIONDLG                  equ 101
IDC_BACKCOLORBOX               equ 1000
IDC_TEXTCOLORBOX               equ 1001

RichEditID 			equ 300

.data
ClassName db "IczEditClass",0
AppName  db "IczEdit version 1.0",0
RichEditDLL db "riched20.dll",0
RichEditClass db "RichEdit20A",0
NoRichEdit db "Cannot find riched20.dll",0
ASMFilterString 		db "ASM Source code (*.asm)",0,"*.asm",0
                db "All Files (*.*)",0,"*.*",0,0
OpenFileFail db "Cannot open the file",0
WannaSave db "The data in the control is modified. Want to save it?",0
FileOpened dd FALSE
BackgroundColor dd 0FFFFFFh		; default to white
TextColor dd 0		; default to black

.data?
hInstance dd ?
hRichEdit dd ?
hwndRichEdit dd ?
FileName db 256 dup(?)
AlternateFileName db 256 dup(?)
CustomColors dd 16 dup(?)

.code
start:
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke LoadLibrary,addr RichEditDLL
    .if eax!=0
        mov hRichEdit,eax
        invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT
        invoke FreeLibrary,hRichEdit
    .else
        invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
    .endif
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:DWORD
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,IDR_MAINMENU
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    .while TRUE
        invoke GetMessage, ADDR msg,0,0,0
        .break .if (!eax)
        invoke TranslateMessage, ADDR msg
        invoke DispatchMessage, ADDR msg
    .endw
    mov   eax,msg.wParam
    ret
WinMain endp

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD
    invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0
    xor eax,1
    ret
StreamOutProc endp

CheckModifyState proc hWnd:DWORD
    invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
    .if eax!=0
        invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL
        .if eax==IDYES
            invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0
        .elseif eax==IDCANCEL
            mov eax,FALSE
            ret
        .endif
    .endif
    mov eax,TRUE
    ret
CheckModifyState endp

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor
    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL clr:CHOOSECOLOR
    .if uMsg==WM_INITDIALOG
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDC_TEXTCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push TextColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop TextColor
                    invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDOK
                ;==================================================================================
                ; Save the modify state of the richedit control because changing the text color changes the
                ; modify state of the richedit control.
                ;==================================================================================
                invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
                push eax
                invoke SetColor
                pop eax
                invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0
                invoke EndDialog,hWnd,0
            .endif
        .endif
    .elseif uMsg==WM_CTLCOLORSTATIC
        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret
        .else
            invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
            .if eax==lParam
                invoke CreateSolidBrush,TextColor
                ret
            .endif
        .endif
        mov eax,FALSE
        ret
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
OptionProc endp

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL chrg:CHARRANGE
    LOCAL ofn:OPENFILENAME
    LOCAL buffer[256]:BYTE
    LOCAL editstream:EDITSTREAM
    LOCAL hFile:DWORD
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax
        ;=============================================================
        ; Set the text limit. The default is 64K
        ;=============================================================
        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0
        ;=============================================================
        ; Set the default text/background color
        ;=============================================================
        invoke SetColor
        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
        invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0
    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif
        .elseif ax==1	; edit menu
            ;=============================================================================
            ; Check whether there is some text in the clipboard. If so, we enable the paste menuitem
            ;=============================================================================
            invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
            .if eax==0		; no text in the clipboard
                invoke EnableMenuItem,wParam,IDM_PASTE,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_PASTE,MF_ENABLED
            .endif
            ;==========================================================
            ; check whether the undo queue is empty
            ;==========================================================
            invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
            .if eax==0
                invoke EnableMenuItem,wParam,IDM_UNDO,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_UNDO,MF_ENABLED
            .endif
            ;=========================================================
            ; check whether the redo queue is empty
            ;=========================================================
            invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
            .if eax==0
                invoke EnableMenuItem,wParam,IDM_REDO,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_REDO,MF_ENABLED
            .endif
            ;=========================================================
            ; check whether there is a current selection in the richedit control.
            ; If there is, we enable the cut/copy/delete menuitem
            ;=========================================================
            invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
            mov eax,chrg.cpMin
            .if eax==chrg.cpMax		; no current selection
                invoke EnableMenuItem,wParam,IDM_COPY,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CUT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_DELETE,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_COPY,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CUT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_DELETE,MF_ENABLED
            .endif
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam==0		; menu commands
            mov eax,wParam
            .if ax==IDM_OPEN
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset FileName
                mov byte ptr [FileName],0
                mov ofn.nMaxFile,sizeof FileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetOpenFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax
                        ;================================================================
                        ; stream the text into the richedit control
                        ;================================================================
                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream
                        ;==========================================================
                        ; Initialize the modify state to false
                        ;==========================================================
                        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                        invoke CloseHandle,hFile
                        mov FileOpened,TRUE
                    .else
                        invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                    .endif
                .endif
            .elseif ax==IDM_CLOSE
                invoke CheckModifyState,hWnd
                .if eax==TRUE
                    invoke SetWindowText,hwndRichEdit,0
                    mov FileOpened,FALSE
                .endif
            .elseif ax==IDM_SAVE
                invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                .if eax!=INVALID_HANDLE_VALUE
@@:
                    mov hFile,eax
                    ;================================================================
                    ; stream the text to the file
                    ;================================================================
                    mov editstream.dwCookie,eax
                    mov editstream.pfnCallback,offset StreamOutProc
                    invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream
                    ;==========================================================
                    ; Initialize the modify state to false
                    ;==========================================================
                    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                    invoke CloseHandle,hFile
                .else
                    invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                .endif
            .elseif ax==IDM_COPY
                invoke SendMessage,hwndRichEdit,WM_COPY,0,0
            .elseif ax==IDM_CUT
                invoke SendMessage,hwndRichEdit,WM_CUT,0,0
            .elseif ax==IDM_PASTE
                invoke SendMessage,hwndRichEdit,WM_PASTE,0,0
            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
            .elseif ax==IDM_UNDO
                invoke SendMessage,hwndRichEdit,EM_UNDO,0,0
            .elseif ax==IDM_REDO
                invoke SendMessage,hwndRichEdit,EM_REDO,0,0
            .elseif ax==IDM_OPTION
                invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0
            .elseif ax==IDM_SAVEAS
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset AlternateFileName
                mov byte ptr [AlternateFileName],0
                mov ofn.nMaxFile,sizeof AlternateFileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetSaveFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        jmp @B
                    .endif
                .endif
            .elseif ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        invoke CheckModifyState,hWnd
        .if eax==TRUE
            invoke DestroyWindow,hWnd
        .endif
    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

;===================================================================
; The resource file
;===================================================================
#include "resource.h"
#define IDR_MAINMENU                    101
#define IDD_OPTIONDLG                   101
#define IDC_BACKCOLORBOX                1000
#define IDC_TEXTCOLORBOX                1001
#define IDM_OPEN                        40001
#define IDM_SAVE                        40002
#define IDM_CLOSE                       40003
#define IDM_SAVEAS                      40004
#define IDM_EXIT                        40005
#define IDM_COPY                        40006
#define IDM_CUT                         40007
#define IDM_PASTE                       40008
#define IDM_DELETE                      40009
#define IDM_SELECTALL                   40010
#define IDM_OPTION                      40011
#define IDM_UNDO                        40012
#define IDM_REDO                        40013

IDR_MAINMENU MENU DISCARDABLE
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&Open",                       IDM_OPEN
        MENUITEM "&Close",                      IDM_CLOSE
        MENUITEM "&Save",                       IDM_SAVE
        MENUITEM "Save &As",                    IDM_SAVEAS
        MENUITEM SEPARATOR
        MENUITEM "E&xit",                       IDM_EXIT
    END
    POPUP "&Edit"
    BEGIN
        MENUITEM "&Undo",                       IDM_UNDO
        MENUITEM "&Redo",                       IDM_REDO
        MENUITEM "&Copy",                       IDM_COPY
        MENUITEM "C&ut",                        IDM_CUT
        MENUITEM "&Paste",                      IDM_PASTE
        MENUITEM SEPARATOR
        MENUITEM "&Delete",                     IDM_DELETE
        MENUITEM SEPARATOR
        MENUITEM "Select &All",                 IDM_SELECTALL
    END
    MENUITEM "Options",                     IDM_OPTION
END

IDD_OPTIONDLG DIALOG DISCARDABLE  0, 0, 183, 54
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU | DS_CENTER
CAPTION "Options"
FONT 8, "MS Sans Serif"
BEGIN
    DEFPUSHBUTTON   "OK",IDOK,137,7,39,14
    PUSHBUTTON      "Cancel",IDCANCEL,137,25,39,14
    GROUPBOX        "",IDC_STATIC,5,0,124,49
    LTEXT           "Background Color:",IDC_STATIC,20,14,60,8
    LTEXT           "",IDC_BACKCOLORBOX,85,11,28,14,SS_NOTIFY | WS_BORDER
    LTEXT           "Text Color:",IDC_STATIC,20,33,35,8
    LTEXT           "",IDC_TEXTCOLORBOX,85,29,28,14,SS_NOTIFY | WS_BORDER
END

Analysis:

The program first loads the richedit dll, which in this case is riched20.dll. If the dll cannot be loaded, it exits to Windows.

invoke LoadLibrary,addr RichEditDLL
.if eax!=0
    mov hRichEdit,eax
    invoke WinMain,hInstance,0,0, SW_SHOWDEFAULT
    invoke FreeLibrary,hRichEdit
.else
    invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
.endif
invoke ExitProcess,eax

After the dll is loaded successfully, we proceed to create a normal window which will be the parent of the richedit control. Within the WM_CREATE handler, we create the richedit control:

        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax

Note that we specify ES_MULTILINE style else the control will be a single-lined one.

        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0

After the richedit control is created, we must set the new text limit on it. By default, the richedit control has 64K text limit, the same as a simple multi-line edit control. We must extend this limit to allow it to operate with larger files. In the above line, I specify -1 which amounts to 0FFFFFFFFh, a very large value.

       invoke SetColor

Next, we set the text/background color. Since this operation can be performed in other part of the program, I put the code in a function named SetColor.

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor

Setting the background color of the richedit control is a straightforward operation: just send EM_SETBKGNDCOLOR message to the richedit control. (If you use a multi-line edit control, you have to process WM_CTLCOLOREDIT). The default background color is white.

    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor

After the background color is set, we fill in the members of CHARFORMAT in order to set the text color. Note that we fill cbSize with the size of the structure so the richedit control knows we are sending it CHARFORMAT, not CHARFORMAT2. dwMask has only one flag, CFM_COLOR, because we only want to set the text color and crTextColor is filled with the value of the desired text color.

    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

After settting the color, you have to empty undo buffer simply because the act of changing text/background color is undo-able. We send EM_EMPTYUNDOBUFFER message to achieve this.

    invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0

After filling the CHARFORMAT structure, we send EM_SETCHARFORMAT to the richedit control, specifying SCF_ALL flag in wParam to indicate that we want the text formatting to be applied to all text in the control.

Note that when we first created the richedit control, we didn't specify its size/position at that time. That's because we want it to cover the whole client area of the parent window. We resize it whenever the size of the parent window changes.

    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE

In the above code snippet, we use the new dimension of the client area passed in lParam to resize the richedit control with MoveWindow.

When the user clicks on the File/Edit menu bar, we process WM_INITPOPUPMENU so that we can prepare the states of the menuitems in the submenu before displaying it to the user. For example, if a file is already opened in the richedit control, we want to disable the open menuitem and enable all the remaining menuitems.

In the case of the File menu bar, we use the variable FileOpened as the flag to determine whether a file is already opened. If the value in this variable is TRUE, we know that a file is already opened.

    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif

As you can see, if a file is already opened, we gray out the open menuitem and enable the remaining menuitems. The reverse is true of FileOpened is false.

In the case of the edit menu bar, we need to check the state of the richedit control/clipboard first.

            invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
            .if eax==0		; no text in the clipboard
                invoke EnableMenuItem,wParam,IDM_PASTE,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_PASTE,MF_ENABLED
            .endif

We first check whether some text is available in the clipboard by sending EM_CANPASTE message. If some text is available, SendMessage returns TRUE and we enable the paste menuitem. If not, we gray out the menuitem.

            invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
            .if eax==0
                invoke EnableMenuItem,wParam,IDM_UNDO,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_UNDO,MF_ENABLED
            .endif

Next, we check whether the undo buffer is empty by sending EM_CANUNDO message. If it's not empty, SendMessage returns TRUE and we enable the undo menuitem.

            invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
            .if eax==0
                invoke EnableMenuItem,wParam,IDM_REDO,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_REDO,MF_ENABLED
            .endif

We check the redo buffer by sending EM_CANREDO message to the richedit control. If it's not empty, SendMessage returns TRUE and we enable the redo menuitem.

            invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
            mov eax,chrg.cpMin
            .if eax==chrg.cpMax		; no current selection
                invoke EnableMenuItem,wParam,IDM_COPY,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CUT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_DELETE,MF_GRAYED
            .else
                invoke EnableMenuItem,wParam,IDM_COPY,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CUT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_DELETE,MF_ENABLED
            .endif

Lastly, we check whether a current selection exists by sending EM_EXGETSEL message. This message uses a CHARRANGE structure which is defined as follows:

CHARRANGE STRUCT
  cpMin  DWORD      ?
  cpMax  DWORD      ?
CHARRANGE ENDS

cpMin contains the character position index immediately preceding the first character in the range.
cpMax contains the character position immediately following the last character in the range.

After EM_EXGETSEL returns, the CHARRANGE structure is filled with the starting-ending character position indices of the selection range. If there is no current selection, cpMin and cpMax are identical and we gray out the cut/copy/delete menuitems.

When the user clicks the Open menuitem, we display an open file dialog box and if the user selects a file, we open the file and stream its content to the richedit control.

                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax

                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream

After the file is successfully opened with CreateFile, we fill the EDITSTREAM structure in preparation for EM_STREAMIN message. We choose to send the handle to the opened file via dwCookie member and pass the address of the stream callback function in pfnCallback.

The stream callback procedure itself is the essence of simplicity.

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

You can see that all parameters of the stream callback procedure fit perfectly with ReadFile. And the return value of ReadFile is xor-ed with 1 so that if it returns 1 (success), the actual value returned in eax is 0 and vice versa.

    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
    invoke CloseHandle,hFile
    mov FileOpened,TRUE

After EM_STREAMIN returns, it means the stream operation is completed. In reality, we must check the value of dwError member of the EDITSTREAM structure.

Richedit (and edit) control supports a flag to indicate whether its content is modified. We can obtain the value of this flag by sending EM_GETMODIFY message to the control. SendMessage returns TRUE if the content of the control was modified. Since we stream the text into the control, it's a kind of a modification. We must set the modify flag to FALSE by sending EM_SETMODIFY with wParam==FALSE to the control to start anew after the stream-in opertion is finished. We immediately close the file and set FileOpened to TRUE to indicate that a file was opened.

When the user clicks on save/saveas menuitem, we use EM_STREAMOUT message to output the content of the richedit control to a file. As with the streamin callback function, the stream-out callback function is simplicity in itself. It fits perfectly with WriteFile.

The text operations such as cut/copy/paste/redo/undo are easily implemented by sending single message to the richedit control, WM_CUT/WM_COPY/WM_PASTE/WM_REDO/WM_UNDO respectively.

The delete/select all operations are done as follows:

            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg

The delete operation affects the currently selection. I send EM_REPLACESEL message with NULL string so the richedit control will replace the currently selected text with the null string.

The select-all operation is done by sending EM_EXSETSEL message, specifying cpMin==0 and cpMax==-1 which amounts to selecting all the text.

When the user selects Option menu bar, we display a dialog box presenting the current background/text colors.

When the user clicks on one of the color boxes, it displays the choose-color dialog box. The "color box" is in fact a static control with SS_NOTIFY and WS_BORDER flag. A static control with SS_NOTIFY flag will notify its parent window with mouse actions on it, such as BN_CLICKED (STN_CLICKED). That's the trick.

            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif

When the user clicks on one of the color box, we fill the members of the CHOOSECOLOR structure and call ChooseColor to display the choose-color dialog box. If the user selects a color, the colorref value is returned in rgbResult member and we store that value in BackgroundColor variable. After that, we force a repaint on the color box by calling InvalidateRect on the handle to the color box. The color box sends WM_CTLCOLORSTATIC message to its parent window.

        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret

Within the WM_CTLCOLORSTATIC handler, we compare the handle of the static control passed in lParam to that of both the color boxes. If the values match, we create a new brush using the color in the variable and immediately return. The static control will use the newly created brush to paint its background.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial No.34: RichEdit Control: More Text Operations

Tutorial 34: RichEdit Control: More Text Operations

You'll learn more about text operations under RichEdit control. Specifically, you'll know how to search for/replace text, jumping to specific line number.

Download the example.

Theory

Searching for Text

There are several text operations under RichEdit control. Searching for text is one such operation. Searching for text is done by sending EM_FINDTEXT or EM_FINDTEXTEX. These messages has a small difference.

EM_FINDTEXT
wParam == Search options. Can be any combination of the values in the table below.These options are identical for both
            EM_FINDTEXT and EM_FINDTEXTEX
FR_DOWN If this flag is specified, the search starts from the end of the current selection to the end of the text in the control (downward). This flag has effect only for RichEdit 2.0 or later: This behavior is the default for RichEdit 1.0. The default behavior of RichEdit 2.0 or later is to search from the end of the current selection to the beginning of the text (upward).
In summary, if you use RichEdit 1.0, you can't do anything about the search direction: it always searches downward. If you use RichEdit 2.0 and you want to search downward, you must specify this flag else the search would be upward.
FR_MATCHCASE | If this flag is specified, the search is case-sensitive.
FR_WHOLEWORD | If this flag is set, the search finds the whole word that matches the specified search string.
Actually, there are a few more flags but they are relevant to non-English languages.
lParam == pointer to the FINDTEXT structure.

            FINDTEXT STRUCT
              chrg          CHARRANGE  <>
              lpstrText     DWORD      ?
            FINDTEXT ENDS

chrg is a CHARRANGE structure which is defined as follows:

            CHARRANGE STRUCT
              cpMin  DWORD      ?
              cpMax  DWORD      ?
            CHARRANGE ENDS

cpMin contains the character index of the first character in the character array (range).
cpMax contains the character index of the character immediately following the last character in the character array.

In essence, to search for a text string, you have to specify the character range in which to search. The meaning of cpMin
and cpMax differ according to whether the search is downward or upward. If the search is downward, cpMin
specifies the starting character index to search in and cpMax the ending character index. If the search is upward, the
reverse is true, ie. cpMin contains the ending character index while cpMax the starting character index.

lpstrText is the pointer to the text string to search for.

EM_FINDTEXT returns the character index of the first character in the matching text string in the richedit control. It returns -1 if
no match is found.

EM_FINDTEXTEX
wParam == the search options. Same as those of EM_FINDTEXT.
lParam == pointer to the FINDTEXTEX structure.

            FINDTEXTEX STRUCT
              chrg          CHARRANGE  <>
              lpstrText     DWORD      ?
              chrgText	CHARRANGE <>
            FINDTEXTEX ENDS

The first two members of FINDTEXTEX are identical to those of FINDTEXT structure. chrgText is a CHARRANGE structure that will
be filled with the starting/ending characterindices if a match is found.

The return value of EM_FINDTEXTEX is the same as that of EM_FINDTEXT.

The difference between EM_FINDTEXT and EM_FINDTEXTEX is that the FINDTEXTEX structure has an additional member,
chrgText, which will be filled with the starting/ending character indices if a match is found. This is convenient if we want to do
more text operations on the string.

Replace/Insert Text

RichEdit control provides EM_SETTEXTEX for replacing/inserting text. This message combines the functionality of WM_SETTEXT and EM_REPLACESEL. It has the following syntax:

    EM_SETTEXTEX
    wParam == pointer to SETTEXTEX structure.

            SETTEXTEX STRUCT
              flags          DWORD      ?
              codepage       DWORD      ?
            SETTEXTEX ENDS

    flags can be the combination of the following values:
ST_DEFAULT | Deletes the undo stack, discards rich-text formatting, replaces all text.
ST_KEEPUNDO | Keeps the undo stack
ST_SELECTION | Replaces selection and keeps rich-text formatting
    codepage is the constant that specifies the code page you want to text to be. Usually, we simply use CP_ACP.

Text Selection

We can select the text programmatically with EM_SETSEL or EM_EXSETSEL. Either one works fine. Choosing which message to use depends on the available format of the character indices. If they are already stored in a CHARRANGE structure, it's easier to use EM_EXSETSEL.

    EM_EXSETSEL
    wParam == not used. Must be 0
    lParam == pointer to a CHARRANGE structure that contains the character range to be selected.

Event Notification

In the case of a multiline edit control, you have to subclass it in order to obtain the input messages such as mouse/keyboard events. RichEdit control provides a better scheme that will notify the parent window of such events. In order to register for notifications, the parent window sends EM_SETEVENTMASK message to the RichEdit control, specifying which events it's interested in. EM_SETEVENTMASK has the following syntax:

    EM_SETEVENTMASK
    wParam == not used. Must be 0
    lParam == event mask value. It can be the combination of the flags in the table below.
ENM_CHANGE | Sends EN_CHANGE notifications
ENM_CORRECTTEXT | Sends EN_CORRECTTEXT notifications
ENM_DRAGDROPDONE | Sends EN_DRAGDROPDONE notifications
ENM_DROPFILES | Sends EN_DROPFILES notifications.
ENM_KEYEVENTS | Sends EN_MSGFILTER notifications for keyboard events
ENM_LINK | Rich Edit 2.0 and later: Sends EN_LINK notifications when the mouse pointer is over text that has the CFE_LINK and one of several mouse actions is performed.
ENM_MOUSEEVENTS | Sends EN_MSGFILTER notifications for mouse events
ENM_OBJECTPOSITIONS | Sends EN_OBJECTPOSITIONS notifications
ENM_PROTECTED | Sends EN_PROTECTED notifications
ENM_REQUESTRESIZE | Sends EN_REQUESTRESIZE notifications
ENM_SCROLL | Sends EN_HSCROLL and EN_VSCROLL notifications
ENM_SCROLLEVENTS | Sends EN_MSGFILTER notifications for mouse wheel events
ENM_SELCHANGE | Sends EN_SELCHANGE notifications
ENM_UPDATE | Sends EN_UPDATE notifications. Rich Edit 2.0 and later: this flag is ignored and the EN_UPDATE notifications are always sent. However, if Rich Edit 3.0 emulates Rich Edit 1.0, you must use this flag to send EN_UPDATE notifications

All the above notifications will be sent as WM_NOTIFY message: you have to check the code member of NMHDR structure for the notification message. For example, if you want to register for mouse events (eg. you want to provide a context sensitive popup menu), you must do something like this:

    invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
    .....
    .....
    WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .....
    ....
        .elseif uMsg==WM_NOTIFY
            push esi
            mov esi,lParam
            assume esi:ptr NMHDR
            .if [esi].code==EN_MSGFILTER
                ....
                [ do something here]
                ....
            .endif
            pop esi

Example:

The following example is the update of IczEdit in tutorial no. 33. It adds search/replace functionality and accelerator keys to the program. It also processes the mouse events and provides a popup menu on right mouse click.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\gdi32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU                   equ 101
IDM_OPEN                      equ  40001
IDM_SAVE                       equ 40002
IDM_CLOSE                      equ 40003
IDM_SAVEAS                     equ 40004
IDM_EXIT                       equ 40005
IDM_COPY                      equ  40006
IDM_CUT                       equ  40007
IDM_PASTE                      equ 40008
IDM_DELETE                     equ 40009
IDM_SELECTALL                  equ 40010
IDM_OPTION 			equ 40011
IDM_UNDO			equ 40012
IDM_REDO			equ 40013
IDD_OPTIONDLG                  equ 101
IDC_BACKCOLORBOX               equ 1000
IDC_TEXTCOLORBOX               equ 1001
IDR_MAINACCEL                 equ  105
IDD_FINDDLG                    equ 102
IDD_GOTODLG                    equ 103
IDD_REPLACEDLG                 equ 104
IDC_FINDEDIT                  equ  1000
IDC_MATCHCASE                  equ 1001
IDC_REPLACEEDIT                 equ 1001
IDC_WHOLEWORD                  equ 1002
IDC_DOWN                       equ 1003
IDC_UP                       equ   1004
IDC_LINENO                   equ   1005
IDM_FIND                       equ 40014
IDM_FINDNEXT                  equ  40015
IDM_REPLACE                     equ 40016
IDM_GOTOLINE                   equ 40017
IDM_FINDPREV                  equ  40018
RichEditID 			equ 300

.data
ClassName db "IczEditClass",0
AppName  db "IczEdit version 2.0",0
RichEditDLL db "riched20.dll",0
RichEditClass db "RichEdit20A",0
NoRichEdit db "Cannot find riched20.dll",0
ASMFilterString 		db "ASM Source code (*.asm)",0,"*.asm",0
                db "All Files (*.*)",0,"*.*",0,0
OpenFileFail db "Cannot open the file",0
WannaSave db "The data in the control is modified. Want to save it?",0
FileOpened dd FALSE
BackgroundColor dd 0FFFFFFh		; default to white
TextColor dd 0		; default to black
hSearch dd ?		; handle to the search/replace dialog box
hAccel dd ?

.data?
hInstance dd ?
hRichEdit dd ?
hwndRichEdit dd ?
FileName db 256 dup(?)
AlternateFileName db 256 dup(?)
CustomColors dd 16 dup(?)
FindBuffer db 256 dup(?)
ReplaceBuffer db 256 dup(?)
uFlags dd ?
findtext FINDTEXTEX <>

.code
start:
    mov byte ptr [FindBuffer],0
    mov byte ptr [ReplaceBuffer],0
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke LoadLibrary,addr RichEditDLL
    .if eax!=0
        mov hRichEdit,eax
        invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT
        invoke FreeLibrary,hRichEdit
    .else
        invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
    .endif
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:DWORD
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,IDR_MAINMENU
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    invoke LoadAccelerators,hInstance,IDR_MAINACCEL
    mov hAccel,eax
    .while TRUE
        invoke GetMessage, ADDR msg,0,0,0
        .break .if (!eax)
        invoke IsDialogMessage,hSearch,addr msg
        .if eax==FALSE
            invoke TranslateAccelerator,hwnd,hAccel,addr msg
            .if eax==0
                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
            .endif
        .endif
    .endw
    mov   eax,msg.wParam
    ret
WinMain endp

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD
    invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0
    xor eax,1
    ret
StreamOutProc endp

CheckModifyState proc hWnd:DWORD
    invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
    .if eax!=0
        invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL
        .if eax==IDYES
            invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0
        .elseif eax==IDCANCEL
            mov eax,FALSE
            ret
        .endif
    .endif
    mov eax,TRUE
    ret
CheckModifyState endp

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor
    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL clr:CHOOSECOLOR
    .if uMsg==WM_INITDIALOG
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDC_TEXTCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push TextColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop TextColor
                    invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDOK
                invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
                push eax
                invoke SetColor
                pop eax
                invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0
                invoke EndDialog,hWnd,0
            .endif
        .endif
    .elseif uMsg==WM_CTLCOLORSTATIC
        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret
        .else
            invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
            .if eax==lParam
                invoke CreateSolidBrush,TextColor
                ret
            .endif
        .endif
        mov eax,FALSE
        ret
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
OptionProc endp

SearchProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke CheckRadioButton,hWnd,IDC_DOWN,IDC_UP,IDC_DOWN
        invoke SendDlgItemMessage,hWnd,IDC_FINDEDIT,WM_SETTEXT,0,addr FindBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDOK
                mov uFlags,0
                invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                .if eax!=0
                    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
                    .if eax==BST_CHECKED
                        or uFlags,FR_DOWN
                        mov eax,findtext.chrg.cpMin
                        .if eax!=findtext.chrg.cpMax
                            push findtext.chrg.cpMax
                            pop findtext.chrg.cpMin
                        .endif
                        mov findtext.chrg.cpMax,-1
                    .else
                        mov findtext.chrg.cpMax,0
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
                    .if eax==BST_CHECKED
                        or uFlags,FR_MATCHCASE
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
                    .if eax==BST_CHECKED
                        or uFlags,FR_WHOLEWORD
                    .endif
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .else
                mov eax,FALSE
                ret
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
SearchProc endp

ReplaceProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL settext:SETTEXTEX
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke SetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer
        invoke SetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer
                mov findtext.chrg.cpMin,0
                mov findtext.chrg.cpMax,-1
                mov findtext.lpstrText,offset FindBuffer
                mov settext.flags,ST_SELECTION
                mov settext.codepage,CP_ACP
                .while TRUE
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax==-1
                        .break
                    .else
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                        invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
                    .endif
                .endw
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
ReplaceProc endp

GoToProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL LineNo:DWORD
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
                mov LineNo,eax
                invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
                .if eax>LineNo
                    invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0
                    mov chrg.cpMin,eax
                    mov chrg.cpMax,eax
                    invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
                    invoke SetFocus,hwndRichEdit
                .endif
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
GoToProc endp

PrepareEditMenu proc hSubMenu:DWORD
    LOCAL chrg:CHARRANGE
    invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
    .if eax==0		; no text in the clipboard
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
    mov eax,chrg.cpMin
    .if eax==chrg.cpMax		; no current selection
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_ENABLED
    .endif
    ret
PrepareEditMenu endp

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL ofn:OPENFILENAME
    LOCAL buffer[256]:BYTE
    LOCAL editstream:EDITSTREAM
    LOCAL hFile:DWORD
    LOCAL hPopup:DWORD
    LOCAL pt:POINT
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax
        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0
        invoke SetColor
        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
        invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
        invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0
    .elseif uMsg==WM_NOTIFY
        push esi
        mov esi,lParam
        assume esi:ptr NMHDR
        .if [esi].code==EN_MSGFILTER
            assume esi:ptr MSGFILTER
            .if [esi].msg==WM_RBUTTONDOWN
                invoke GetMenu,hWnd
                invoke GetSubMenu,eax,1
                mov hPopup,eax
                invoke PrepareEditMenu,hPopup
                mov edx,[esi].lParam
                mov ecx,edx
                and edx,0FFFFh
                shr ecx,16
                mov pt.x,edx
                mov pt.y,ecx
                invoke ClientToScreen,hWnd,addr pt
                invoke TrackPopupMenu,hPopup,TPM_LEFTALIGN or TPM_BOTTOMALIGN,pt.x,pt.y,NULL,hWnd,NULL
            .endif
        .endif
        pop esi
    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif
        .elseif ax==1	; edit menu
            invoke PrepareEditMenu,wParam
        .elseif ax==2		; search menu bar
            .if FileOpened==TRUE
                invoke EnableMenuItem,wParam,IDM_FIND,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_FIND,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_GRAYED
            .endif
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam==0		; menu commands
            mov eax,wParam
            .if ax==IDM_OPEN
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset FileName
                mov byte ptr [FileName],0
                mov ofn.nMaxFile,sizeof FileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetOpenFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax
                        ;================================================================
                        ; stream the text into the richedit control
                        ;================================================================
                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream
                        ;==========================================================
                        ; Initialize the modify state to false
                        ;==========================================================
                        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                        invoke CloseHandle,hFile
                        mov FileOpened,TRUE
                    .else
                        invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                    .endif
                .endif
            .elseif ax==IDM_CLOSE
                invoke CheckModifyState,hWnd
                .if eax==TRUE
                    invoke SetWindowText,hwndRichEdit,0
                    mov FileOpened,FALSE
                .endif
            .elseif ax==IDM_SAVE
                invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                .if eax!=INVALID_HANDLE_VALUE
@@:
                    mov hFile,eax
                    ;================================================================
                    ; stream the text to the file
                    ;================================================================
                    mov editstream.dwCookie,eax
                    mov editstream.pfnCallback,offset StreamOutProc
                    invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream
                    ;==========================================================
                    ; Initialize the modify state to false
                    ;==========================================================
                    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                    invoke CloseHandle,hFile
                .else
                    invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                .endif
            .elseif ax==IDM_COPY
                invoke SendMessage,hwndRichEdit,WM_COPY,0,0
            .elseif ax==IDM_CUT
                invoke SendMessage,hwndRichEdit,WM_CUT,0,0
            .elseif ax==IDM_PASTE
                invoke SendMessage,hwndRichEdit,WM_PASTE,0,0
            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
            .elseif ax==IDM_UNDO
                invoke SendMessage,hwndRichEdit,EM_UNDO,0,0
            .elseif ax==IDM_REDO
                invoke SendMessage,hwndRichEdit,EM_REDO,0,0
            .elseif ax==IDM_OPTION
                invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0
            .elseif ax==IDM_SAVEAS
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset AlternateFileName
                mov byte ptr [AlternateFileName],0
                mov ofn.nMaxFile,sizeof AlternateFileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetSaveFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        jmp @B
                    .endif
                .endif
            .elseif ax==IDM_FIND
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_FINDDLG,hWnd,addr SearchProc,0
                .endif
            .elseif ax==IDM_REPLACE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_REPLACEDLG,hWnd,addr ReplaceProc,0
                .endif
            .elseif ax==IDM_GOTOLINE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_GOTODLG,hWnd,addr GoToProc,0
                .endif
            .elseif ax==IDM_FINDNEXT
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov eax,findtext.chrg.cpMin
                    .if eax!=findtext.chrg.cpMax
                        push findtext.chrg.cpMax
                        pop findtext.chrg.cpMin
                    .endif
                    mov findtext.chrg.cpMax,-1
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_FINDPREV
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov findtext.chrg.cpMax,0
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,0,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        invoke CheckModifyState,hWnd
        .if eax==TRUE
            invoke DestroyWindow,hWnd
        .endif
    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

Analysis

The search-for-text capability is implemented with EM_FINDTEXTEX. When the user clicks on Find menuitem, IDM_FIND message is sent and the Find dialog box is displayed.

    invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
    .if eax!=0

When the user types the text to search for and then press OK button, we get the text to be searched for into FindBuffer.

        mov uFlags,0
        invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg

If the text string is not null, we continue to initialize uFlags variable to 0.This variable is used to store the search flags used withEM_FINDTEXTEX. After that, we obtain the current selection with EM_EXGETSEL because we need to know the starting point of the search operation.

    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
        .if eax==BST_CHECKED
            or uFlags,FR_DOWN
            mov eax,findtext.chrg.cpMin
            .if eax!=findtext.chrg.cpMax
                push findtext.chrg.cpMax
                pop findtext.chrg.cpMin
            .endif
            mov findtext.chrg.cpMax,-1
        .else
            mov findtext.chrg.cpMax,0
        .endif

The next part is a little tricky. We check the direction radio button to ascertain which direction the search should go. If the downward search is indicated, we set FR_DOWN flag to uFlags. After that, we check whether a selection is currently in effect by comparing the values of cpMin and cpMax. If both values are not equal, it means there is a current selection and we must continue the search from the end of that selection to the end of text in the control. Thus we need to replace the value of cpMax with that of cpMin and change the value of cpMax to -1 (0FFFFFFFFh). If there is no current selection, the range to search is from the current caret position to the end of text.

If the user chooses to search upward, we use the range from the start of the selection to the beginning of the text in the control. That's why we only modify the value of cpMax to 0. In the case of upward search, cpMin contains the character index of the last character in the search range and cpMax the character index of the first char in the search range. It's the inverse of the downward search.

        invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
        .if eax==BST_CHECKED
            or uFlags,FR_MATCHCASE
        .endif
        invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
        .if eax==BST_CHECKED
            or uFlags,FR_WHOLEWORD
        .endif
        mov findtext.lpstrText,offset FindBuffer

We continue to check the checkboxes for the search flags, ie, FR_MATCHCASE and FR_WHOLEWORD. Lastly, we put the offset of the text to search for in lpstrText member.

        invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
        .if eax!=-1
            invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
        .endif
    .endif

We are now ready to issue EM_FINDTEXTEX. After that, we examine the search result returned by SendMessage. If the return value is -1, no match is found in the search range. Otherwise, chrgText member of FINDTEXTEX structure is filled with the character indices of the matching text. We thus proceed to select it with EM_EXSETSEL.

The replace operation is done in much the same manner.

    invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
    invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer

We retrieve the text to search for and the text used to replace.

    mov findtext.chrg.cpMin,0
    mov findtext.chrg.cpMax,-1
    mov findtext.lpstrText,offset FindBuffer

To make it easy, the replace operation affects all the text in the control. Thus the starting index is 0 and the ending index is -1.

    mov settext.flags,ST_SELECTION
    mov settext.codepage,CP_ACP

We initialize SETTEXTEX structure to indicate that we want to replace the current selection and use the default system code page.

    .while TRUE
        invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
        .if eax==-1
            .break
        .else
            invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
            invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
        .endif
    .endw

We enter an infinite loop, searching for the matching text. If one is found, we select it with EM_EXSETSEL and replace it with EM_SETTEXTEX. When no more match is found, we exit the loop.

Find Next and Find Prev. features use EM_FINDTEXTEX message in the similar manner to the find operation.

We will examine the Go to Line feature next. When the user clicks Go To Line menuitem, we display a dialog box below:

When the user types a line number and presses Ok button, we begin the operation.

    invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
    mov LineNo,eax

Obtain the line number from the edit control

    invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
    .if eax>LineNo

Obtain the number of lines in the control. Check whether the user specifies the line number that is out of the range.

        invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0

If the line number is valid, we want to move the caret to the first character of that line. So we send EM_LINEINDEX message to the richedit control. This message returns the character index of the first character in the specified line. We send the line number in wParam and in return, we has the character index.

        invoke SendMessage,hwndRichEdit,EM_SETSEL,eax,eax

To set the current selection, this time we use EM_SETSEL because the character indices are not already in a CHARRANGE structure thus it saves us two instructions (to put those indices into a CHARRANGE structure).

        invoke SetFocus,hwndRichEdit
    .endif

The caret will not be displayed unless the richedit control has the focus. So we call SetFocus on it.

[Iczelion's Win32 Assembly Homepage]


Iczelion's Win32 Assembly Tutorial 35: RichEdit Control: Syntax Hilighting

Tutorial 35: RichEdit Control: Syntax Hilighting

Before reading this tutorial, let me warn you that it's a complicated subject: not suited for a beginner. This is the last in the richedit control tutorials.

Download the example.

Theory

Syntax hilighting is a subject of hot debate for those writing text editors. The best method (in my opinion) is to code a custom edit control and this is the approach taken by lots of commercial softwares. However, for those of us who don't have time for coding such control, the next best thing is to adapt the existing control to make it suit our need.

Let us take a look at what RichEdit control provides to help us in implementing syntax hilighting. I should state at this moment that the following method is not the "correct" path: I just want to show you the pitfall that many fall for. RichEdit control provides EM_SETCHARFORMAT message that you can use to change the color of the text. At first glance, this message seems to be the perfect solution (I know because I was one of the victim). However, a closer examination will show you several things that are undesirable:

  • EM_SETCHARFORMAT only works for a text currently in selection or all text in the control. If you want to change the text color (hilight) a certain word, you must first select it.
  • EM_SETCHARFORMAT is very slow
  • It has a problem with the caret position in the richedit control

With the above discussion, you can see that using EM_SETCHARFORMAT is a wrong choice. I'll show you the "relatively correct" choice.

The method I currently use is "syntax hilighting just-in-time". I'll hilight only the visible portion of text. Thus the speed of the hilighting will not be related to the size of the file at all. No matter how large the file, only a small portion of it is visible at one time.

How to do that? The answer is simple:

  1. subclass the richedit control and handle WM_PAINT message within your own window procedure
  2. When it receives WM_PAINT message, it calls the original window procedure of the richedit control to let it update the screen as usual.
  3. After that, we overwrite the words to be hilighted with different color

Of course, the road is not that easy: there are still a couple of minor things to fix but the above method works quite nicely. The display speed is very satisfactory.

Now let's concentrate on the detail. The subclassing process is simple and doesn't require much attention. The really complicated part is when we have to find a fast way of searching for the words to be hilighted. This is further complicated by the need not to hilight any word within a comment block.

The method I use may not be the best but it works ok. I'm sure you can find a faster way. Anyway, here it is:

  • I create a 256 dword array, initialized to 0. Each dword corresponds to a possible ASCII character,named ASMSyntaxArray. For example, the 21th dword represents the ascii 20h (space). I use them as a fast lookup table: For example, if I have the word "include", I'll extract the first character (i) from the word and look up the dword at the corresponding index. If that dword is 0, I know immediately that there is no words to be hilighted starting with "i". If the dword is non-zero, it contains the pointer to the linked list of the WORDINFO structure which contains the information about the word to be hilighted.
  • I read the words to be hilighted and create a WORDINFO structure for each of them.
            WORDINFO struct
                WordLen dd ? 		; the length of the word: used as a quick comparison
                pszWord dd ? 	; pointer to the word
                pColor dd ? 		; point to the dword that contains the color used to hilite the word
                NextLink dd ? 		; point to the next WORDINFO structure
            WORDINFO ends

As you can see, I use the length of the word as the second quick comparison. If the first character of the word matches, we next compare its length to the available words. Each dword in ASMSyntaxArray contains a pointer to the head of the associated WORDINFO array. For example, the dword that represents the character "i" will contain the pointer to the linked list of the words that begin with "i". pColor member points to the dword that contains the color value used to hilight the word. pszWord points to the word to be hilighted, in lowercase.

  • The memory for the linked list is allocated from the default heap so it's fast and easy to clean up, ie, no cleaning up required at all.

The word list is stored in a file named "wordfile.txt" and I access it with GetPrivateProfileString APIs. I provide as many as 10 different syntax coloring, starting from C1 to C10. The color array is named ASMColorArray. pColor member of each WORDINFO structure points to one of the dwords in ASMColorArray. Thus it is easy to change the syntax coloring on the fly: you just change the dword in ASMColorArray and all words using that color will use the new color immediately.

Example

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\gdi32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

WORDINFO struct
    WordLen dd ?		; the length of the word: used as a quick comparison
    pszWord dd ?		; pointer to the word
    pColor dd ?		; point to the dword that contains the color used to hilite the word
    NextLink dd ?		; point to the next WORDINFO structure
WORDINFO ends

.const
IDR_MAINMENU                   equ 101
IDM_OPEN                      equ  40001
IDM_SAVE                       equ 40002
IDM_CLOSE                      equ 40003
IDM_SAVEAS                     equ 40004
IDM_EXIT                       equ 40005
IDM_COPY                      equ  40006
IDM_CUT                       equ  40007
IDM_PASTE                      equ 40008
IDM_DELETE                     equ 40009
IDM_SELECTALL                  equ 40010
IDM_OPTION 			equ 40011
IDM_UNDO			equ 40012
IDM_REDO			equ 40013
IDD_OPTIONDLG                  equ 101
IDC_BACKCOLORBOX               equ 1000
IDC_TEXTCOLORBOX               equ 1001
IDR_MAINACCEL                 equ  105
IDD_FINDDLG                    equ 102
IDD_GOTODLG                    equ 103
IDD_REPLACEDLG                 equ 104
IDC_FINDEDIT                  equ  1000
IDC_MATCHCASE                  equ 1001
IDC_REPLACEEDIT                 equ 1001
IDC_WHOLEWORD                  equ 1002
IDC_DOWN                       equ 1003
IDC_UP                       equ   1004
IDC_LINENO                   equ   1005
IDM_FIND                       equ 40014
IDM_FINDNEXT                  equ  40015
IDM_REPLACE                     equ 40016
IDM_GOTOLINE                   equ 40017
IDM_FINDPREV                  equ  40018
RichEditID 			equ 300

.data
ClassName db "IczEditClass",0
AppName  db "IczEdit version 3.0",0
RichEditDLL db "riched20.dll",0
RichEditClass db "RichEdit20A",0
NoRichEdit db "Cannot find riched20.dll",0
ASMFilterString 		db "ASM Source code (*.asm)",0,"*.asm",0
                db "All Files (*.*)",0,"*.*",0,0
OpenFileFail db "Cannot open the file",0
WannaSave db "The data in the control is modified. Want to save it?",0
FileOpened dd FALSE
BackgroundColor dd 0FFFFFFh		; default to white
TextColor dd 0		; default to black
WordFileName db "\wordfile.txt",0
ASMSection db "ASSEMBLY",0
C1Key db "C1",0
C2Key db "C2",0
C3Key db "C3",0
C4Key db "C4",0
C5Key db "C5",0
C6Key db "C6",0
C7Key db "C7",0
C8Key db "C8",0
C9Key db "C9",0
C10Key db "C10",0
ZeroString db 0
ASMColorArray dd 0FF0000h,0805F50h,0FFh,666F00h,44F0h,5F8754h,4 dup(0FF0000h)
CommentColor dd 808000h

.data?
hInstance dd ?
hRichEdit dd ?
hwndRichEdit dd ?
FileName db 256 dup(?)
AlternateFileName db 256 dup(?)
CustomColors dd 16 dup(?)
FindBuffer db 256 dup(?)
ReplaceBuffer db 256 dup(?)
uFlags dd ?
findtext FINDTEXTEX <>
ASMSyntaxArray dd 256 dup(?)
hSearch dd ?		; handle to the search/replace dialog box
hAccel dd ?
hMainHeap dd ?		; heap handle
OldWndProc dd ?
RichEditVersion dd ?

.code
start:
    mov byte ptr [FindBuffer],0
    mov byte ptr [ReplaceBuffer],0
    invoke GetModuleHandle, NULL
    mov    hInstance,eax
    invoke LoadLibrary,addr RichEditDLL
    .if eax!=0
        mov hRichEdit,eax
        invoke GetProcessHeap
        mov hMainHeap,eax
        call FillHiliteInfo
        invoke WinMain, hInstance,0,0, SW_SHOWDEFAULT
        invoke FreeLibrary,hRichEdit
    .else
        invoke MessageBox,0,addr NoRichEdit,addr AppName,MB_OK or MB_ICONERROR
    .endif
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:DWORD
    mov   wc.cbSize,SIZEOF WNDCLASSEX
    mov   wc.style, CS_HREDRAW or CS_VREDRAW
    mov   wc.lpfnWndProc, OFFSET WndProc
    mov   wc.cbClsExtra,NULL
    mov   wc.cbWndExtra,NULL
    push  hInst
    pop   wc.hInstance
    mov   wc.hbrBackground,COLOR_WINDOW+1
    mov   wc.lpszMenuName,IDR_MAINMENU
    mov   wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov   wc.hIcon,eax
    mov   wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov   wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
    mov   hwnd,eax
    invoke ShowWindow, hwnd,SW_SHOWNORMAL
    invoke UpdateWindow, hwnd
    invoke LoadAccelerators,hInstance,IDR_MAINACCEL
    mov hAccel,eax
    .while TRUE
        invoke GetMessage, ADDR msg,0,0,0
        .break .if (!eax)
        invoke IsDialogMessage,hSearch,addr msg
        .if eax==FALSE
            invoke TranslateAccelerator,hwnd,hAccel,addr msg
            .if eax==0
                invoke TranslateMessage, ADDR msg
                invoke DispatchMessage, ADDR msg
            .endif
        .endif
    .endw
    mov   eax,msg.wParam
    ret
WinMain endp

StreamInProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesRead:DWORD
    invoke ReadFile,hFile,pBuffer,NumBytes,pBytesRead,0
    xor eax,1
    ret
StreamInProc endp

StreamOutProc proc hFile:DWORD,pBuffer:DWORD, NumBytes:DWORD, pBytesWritten:DWORD
    invoke WriteFile,hFile,pBuffer,NumBytes,pBytesWritten,0
    xor eax,1
    ret
StreamOutProc endp

CheckModifyState proc hWnd:DWORD
    invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
    .if eax!=0
        invoke MessageBox,hWnd,addr WannaSave,addr AppName,MB_YESNOCANCEL
        .if eax==IDYES
            invoke SendMessage,hWnd,WM_COMMAND,IDM_SAVE,0
        .elseif eax==IDCANCEL
            mov eax,FALSE
            ret
        .endif
    .endif
    mov eax,TRUE
    ret
CheckModifyState endp

SetColor proc
    LOCAL cfm:CHARFORMAT
    invoke SendMessage,hwndRichEdit,EM_SETBKGNDCOLOR,0,BackgroundColor
    invoke RtlZeroMemory,addr cfm,sizeof cfm
    mov cfm.cbSize,sizeof cfm
    mov cfm.dwMask,CFM_COLOR
    push TextColor
    pop cfm.crTextColor
    invoke SendMessage,hwndRichEdit,EM_SETCHARFORMAT,SCF_ALL,addr cfm
    ret
SetColor endp

OptionProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL clr:CHOOSECOLOR
    .if uMsg==WM_INITDIALOG
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDC_BACKCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push BackgroundColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop BackgroundColor
                    invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDC_TEXTCOLORBOX
                invoke RtlZeroMemory,addr clr,sizeof clr
                mov clr.lStructSize,sizeof clr
                push hWnd
                pop clr.hwndOwner
                push hInstance
                pop clr.hInstance
                push TextColor
                pop clr.rgbResult
                mov clr.lpCustColors,offset CustomColors
                mov clr.Flags,CC_ANYCOLOR or CC_RGBINIT
                invoke ChooseColor,addr clr
                .if eax!=0
                    push clr.rgbResult
                    pop TextColor
                    invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
                    invoke InvalidateRect,eax,0,TRUE
                .endif
            .elseif ax==IDOK
                invoke SendMessage,hwndRichEdit,EM_GETMODIFY,0,0
                push eax
                invoke SetColor
                pop eax
                invoke SendMessage,hwndRichEdit,EM_SETMODIFY,eax,0
                invoke EndDialog,hWnd,0
            .endif
        .endif
    .elseif uMsg==WM_CTLCOLORSTATIC
        invoke GetDlgItem,hWnd,IDC_BACKCOLORBOX
        .if eax==lParam
            invoke CreateSolidBrush,BackgroundColor
            ret
        .else
            invoke GetDlgItem,hWnd,IDC_TEXTCOLORBOX
            .if eax==lParam
                invoke CreateSolidBrush,TextColor
                ret
            .endif
        .endif
        mov eax,FALSE
        ret
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
OptionProc endp

SearchProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke CheckRadioButton,hWnd,IDC_DOWN,IDC_UP,IDC_DOWN
        invoke SendDlgItemMessage,hWnd,IDC_FINDEDIT,WM_SETTEXT,0,addr FindBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDOK
                mov uFlags,0
                invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                .if eax!=0
                    invoke IsDlgButtonChecked,hWnd,IDC_DOWN
                    .if eax==BST_CHECKED
                        or uFlags,FR_DOWN
                        mov eax,findtext.chrg.cpMin
                        .if eax!=findtext.chrg.cpMax
                            push findtext.chrg.cpMax
                            pop findtext.chrg.cpMin
                        .endif
                        mov findtext.chrg.cpMax,-1
                    .else
                        mov findtext.chrg.cpMax,0
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_MATCHCASE
                    .if eax==BST_CHECKED
                        or uFlags,FR_MATCHCASE
                    .endif
                    invoke IsDlgButtonChecked,hWnd,IDC_WHOLEWORD
                    .if eax==BST_CHECKED
                        or uFlags,FR_WHOLEWORD
                    .endif
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,uFlags,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .else
                mov eax,FALSE
                ret
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
SearchProc endp

ReplaceProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL settext:SETTEXTEX
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
        invoke SetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer
        invoke SetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemText,hWnd,IDC_FINDEDIT,addr FindBuffer,sizeof FindBuffer
                invoke GetDlgItemText,hWnd,IDC_REPLACEEDIT,addr ReplaceBuffer,sizeof ReplaceBuffer
                mov findtext.chrg.cpMin,0
                mov findtext.chrg.cpMax,-1
                mov findtext.lpstrText,offset FindBuffer
                mov settext.flags,ST_SELECTION
                mov settext.codepage,CP_ACP
                .while TRUE
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax==-1
                        .break
                    .else
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                        invoke SendMessage,hwndRichEdit,EM_SETTEXTEX,addr settext,addr ReplaceBuffer
                    .endif
                .endw
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
ReplaceProc endp

GoToProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL LineNo:DWORD
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_INITDIALOG
        push hWnd
        pop hSearch
    .elseif uMsg==WM_COMMAND
        mov eax,wParam
        shr eax,16
        .if ax==BN_CLICKED
            mov eax,wParam
            .if ax==IDCANCEL
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .elseif ax==IDOK
                invoke GetDlgItemInt,hWnd,IDC_LINENO,NULL,FALSE
                mov LineNo,eax
                invoke SendMessage,hwndRichEdit,EM_GETLINECOUNT,0,0
                .if eax>LineNo
                    invoke SendMessage,hwndRichEdit,EM_LINEINDEX,LineNo,0
                    invoke SendMessage,hwndRichEdit,EM_SETSEL,eax,eax
                    invoke SetFocus,hwndRichEdit
                .endif
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        mov hSearch,0
        invoke EndDialog,hWnd,0
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
GoToProc endp

PrepareEditMenu proc hSubMenu:DWORD
    LOCAL chrg:CHARRANGE
    invoke SendMessage,hwndRichEdit,EM_CANPASTE,CF_TEXT,0
    .if eax==0		; no text in the clipboard
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_PASTE,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANUNDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_UNDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_CANREDO,0,0
    .if eax==0
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_REDO,MF_ENABLED
    .endif
    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr chrg
    mov eax,chrg.cpMin
    .if eax==chrg.cpMax		; no current selection
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_GRAYED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_GRAYED
    .else
        invoke EnableMenuItem,hSubMenu,IDM_COPY,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_CUT,MF_ENABLED
        invoke EnableMenuItem,hSubMenu,IDM_DELETE,MF_ENABLED
    .endif
    ret
PrepareEditMenu endp

ParseBuffer proc uses edi esi hHeap:DWORD,pBuffer:DWORD, nSize:DWORD, ArrayOffset:DWORD,pArray:DWORD
    LOCAL buffer[128]:BYTE
    LOCAL InProgress:DWORD
    mov InProgress,FALSE
    lea esi,buffer
    mov edi,pBuffer
    invoke CharLower,edi
    mov ecx,nSize
SearchLoop:
    or ecx,ecx
    jz Finished
    cmp byte ptr [edi]," "
    je EndOfWord
    cmp byte ptr [edi],9 	; tab
    je EndOfWord
    mov InProgress,TRUE
    mov al,byte ptr [edi]
    mov byte ptr [esi],al
    inc esi
SkipIt:
    inc edi
    dec ecx
    jmp SearchLoop
EndOfWord:
    cmp InProgress,TRUE
    je WordFound
    jmp SkipIt
WordFound:
    mov byte ptr [esi],0
    push ecx
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
    push esi
    mov esi,eax
    assume esi:ptr WORDINFO
    invoke lstrlen,addr buffer
    mov [esi].WordLen,eax
    push ArrayOffset
    pop [esi].pColor
    inc eax
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
    mov [esi].pszWord,eax
    mov edx,eax
    invoke lstrcpy,edx,addr buffer
    mov eax,pArray
    movzx edx,byte ptr [buffer]
    shl edx,2		; multiply by 4
    add eax,edx
    .if dword ptr [eax]==0
        mov dword ptr [eax],esi
    .else
        push dword ptr [eax]
        pop [esi].NextLink
        mov dword ptr [eax],esi
    .endif
    pop esi
    pop ecx
    lea esi,buffer
    mov InProgress,FALSE
    jmp SkipIt
Finished:
    .if InProgress==TRUE
        invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO
        push esi
        mov esi,eax
        assume esi:ptr WORDINFO
        invoke lstrlen,addr buffer
        mov [esi].WordLen,eax
        push ArrayOffset
        pop [esi].pColor
        inc eax
        invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
        mov [esi].pszWord,eax
        mov edx,eax
        invoke lstrcpy,edx,addr buffer
        mov eax,pArray
        movzx edx,byte ptr [buffer]
        shl edx,2		; multiply by 4
        add eax,edx
        .if dword ptr [eax]==0
            mov dword ptr [eax],esi
        .else
            push dword ptr [eax]
            pop [esi].NextLink
            mov dword ptr [eax],esi
        .endif
        pop esi
    .endif
    ret
ParseBuffer endp

FillHiliteInfo proc uses edi
    LOCAL buffer[1024]:BYTE
    LOCAL pTemp:DWORD
    LOCAL BlockSize:DWORD
    invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray
    invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer
    invoke lstrlen,addr buffer
    mov ecx,eax
    dec ecx
    lea edi,buffer
    add edi,ecx
    std
    mov al,"\"
    repne scasb
    cld
    inc edi
    mov byte ptr [edi],0
    invoke lstrcat,addr buffer,addr WordFileName
    invoke GetFileAttributes,addr buffer
    .if eax!=-1
        mov BlockSize,1024*10
        invoke HeapAlloc,hMainHeap,0,BlockSize
        mov pTemp,eax
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C1Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C2Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,4
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C3Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,8
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C4Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,12
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C5Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,16
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C6Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,20
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C7Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,24
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C8Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,28
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C9Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,32
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C10Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0
            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif
            mov edx,offset ASMColorArray
            add edx,36
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray
        .endif
        invoke HeapFree,hMainHeap,0,pTemp
    .endif
    ret
FillHiliteInfo endp

NewRichEditProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL hdc:DWORD
    LOCAL hOldFont:DWORD
    LOCAL FirstChar:DWORD
    LOCAL rect:RECT
    LOCAL txtrange:TEXTRANGE
    LOCAL buffer[1024*10]:BYTE
    LOCAL hRgn:DWORD
    LOCAL hOldRgn:DWORD
    LOCAL RealRect:RECT
    LOCAL pString:DWORD
    LOCAL BufferSize:DWORD
    LOCAL pt:POINT
    .if uMsg==WM_PAINT
        push edi
        push esi
        invoke HideCaret,hWnd
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        push eax
        mov edi,offset ASMSyntaxArray
        invoke GetDC,hWnd
        mov hdc,eax
        invoke SetBkMode,hdc,TRANSPARENT
        invoke SendMessage,hWnd,EM_GETRECT,0,addr rect
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect
        invoke SendMessage,hWnd,EM_LINEFROMCHAR,eax,0
        invoke SendMessage,hWnd,EM_LINEINDEX,eax,0
        mov txtrange.chrg.cpMin,eax
        mov FirstChar,eax
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect.right
        mov txtrange.chrg.cpMax,eax
        push rect.left
        pop RealRect.left
        push rect.top
        pop RealRect.top
        push rect.right
        pop RealRect.right
        push rect.bottom
        pop RealRect.bottom
        invoke CreateRectRgn,RealRect.left,RealRect.top,RealRect.right,RealRect.bottom
        mov hRgn,eax
        invoke SelectObject,hdc,hRgn
        mov hOldRgn,eax
        invoke SetTextColor,hdc,CommentColor
        lea eax,buffer
        mov txtrange.lpstrText,eax
        invoke SendMessage,hWnd,EM_GETTEXTRANGE,0,addr txtrange
        .if eax>0
            mov esi,eax		; esi == size of the text
            mov BufferSize,eax
            push edi
            push ebx
            lea edi,buffer
            mov edx,edi		; used as the reference point
            mov ecx,esi
            mov al,";"
ScanMore:
            repne scasb
            je NextSkip
            jmp NoMoreHit
NextSkip:
            dec edi
            inc ecx
            mov pString,edi
            mov ebx,edi
            sub ebx,edx
            add ebx,FirstChar
            mov txtrange.chrg.cpMin,ebx
            push eax
            mov al,0Dh
            repne scasb
            pop eax
HiliteTheComment:
            .if ecx>0
                mov byte ptr [edi-1],0
            .endif
            mov ebx,edi
            sub ebx,edx
            add ebx,FirstChar
            mov txtrange.chrg.cpMax,ebx
            pushad
            mov edi,pString
            mov esi,txtrange.chrg.cpMax
            sub esi,txtrange.chrg.cpMin		; esi contains the length of the buffer
            mov eax,esi
            push edi
            .while eax>0
                .if byte ptr [edi]==9
                    mov byte ptr [edi],0
                .endif
                inc edi
                dec eax
            .endw
            pop edi
            .while esi>0
                .if byte ptr [edi]!=0
                    invoke lstrlen,edi
                    push eax
                    mov ecx,edi
                    lea edx,buffer
                    sub ecx,edx
                    add ecx,FirstChar
                    .if RichEditVersion==3
                        invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                    .else
                        invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                        mov ecx,eax
                        and ecx,0FFFFh
                        mov rect.left,ecx
                        shr eax,16
                        mov rect.top,eax
                    .endif
                    invoke DrawText,hdc,edi,-1,addr rect,0
                    pop eax
                    add edi,eax
                    sub esi,eax
                .else
                    inc edi
                    dec esi
                .endif
            .endw
            mov ecx,txtrange.chrg.cpMax
            sub ecx,txtrange.chrg.cpMin
            invoke RtlZeroMemory,pString,ecx
            popad
            .if ecx>0
                jmp ScanMore
            .endif
NoMoreHit:
            pop ebx
            pop edi
            mov ecx,BufferSize
            lea esi,buffer
            .while ecx>0
                mov al,byte ptr [esi]
                .if al==" " || al==0Dh || al=="/" || al=="," || al=="|" || al=="+" || al=="-" || al=="*" || al=="&" || al=="<" || al==">" || al=="=" || al=="(" || al==")" || al=="{" || al=="}" || al=="[" || al=="]" || al=="^" || al==":" || al==9
                    mov byte ptr [esi],0
                .endif
                dec ecx
                inc esi
            .endw
            lea esi,buffer
            mov ecx,BufferSize
            .while ecx>0
                mov al,byte ptr [esi]
                .if al!=0
                    push ecx
                    invoke lstrlen,esi
                    push eax
                    mov edx,eax		; edx contains the length of the string
                    movzx eax,byte ptr [esi]
                    .if al>="A" && al<="Z"
                        sub al,"A"
                        add al,"a"
                    .endif
                    shl eax,2
                    add eax,edi		; edi contains the pointer to the WORDINFO pointer array
                    .if dword ptr [eax]!=0
                        mov eax,dword ptr [eax]
                        assume eax:ptr WORDINFO
                        .while eax!=0
                            .if edx==[eax].WordLen
                                pushad
                                invoke lstrcmpi,[eax].pszWord,esi
                                .if eax==0
                                    popad
                                    mov ecx,esi
                                    lea edx,buffer
                                    sub ecx,edx
                                    add ecx,FirstChar
                                    pushad
                                    .if RichEditVersion==3
                                        invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                                    .else
                                        invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                                        mov ecx,eax
                                        and ecx,0FFFFh
                                        mov rect.left,ecx
                                        shr eax,16
                                        mov rect.top,eax
                                    .endif
                                    popad
                                    mov edx,[eax].pColor
                                    invoke SetTextColor,hdc,dword ptr [edx]
                                    invoke DrawText,hdc,esi,-1,addr rect,0
                                    .break
                                .endif
                                popad
                            .endif
                            push [eax].NextLink
                            pop eax
                        .endw
                    .endif
                    pop eax
                    pop ecx
                    add esi,eax
                    sub ecx,eax
                .else
                    inc esi
                    dec ecx
                .endif
            .endw
        .endif
        invoke SelectObject,hdc,hOldRgn
        invoke DeleteObject,hRgn
        invoke SelectObject,hdc,hOldFont
        invoke ReleaseDC,hWnd,hdc
        invoke ShowCaret,hWnd
        pop eax
        pop esi
        pop edi
        ret
    .elseif uMsg==WM_CLOSE
        invoke SetWindowLong,hWnd,GWL_WNDPROC,OldWndProc
    .else
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
NewRichEditProc endp

WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    LOCAL ofn:OPENFILENAME
    LOCAL buffer[256]:BYTE
    LOCAL editstream:EDITSTREAM
    LOCAL hFile:DWORD
    LOCAL hPopup:DWORD
    LOCAL pt:POINT
    LOCAL chrg:CHARRANGE
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,WS_EX_CLIENTEDGE,addr RichEditClass,0,WS_CHILD or WS_VISIBLE or ES_MULTILINE or WS_VSCROLL or WS_HSCROLL or ES_NOHIDESEL,\
                CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,hWnd,RichEditID,hInstance,0
        mov hwndRichEdit,eax
        invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
        invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
        .if eax==0		; means this message is not processed
            mov RichEditVersion,2
        .else
            mov RichEditVersion,3
            invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
        .endif
        invoke SetWindowLong,hwndRichEdit,GWL_WNDPROC, addr NewRichEditProc
        mov OldWndProc,eax
        invoke SendMessage,hwndRichEdit,EM_LIMITTEXT,-1,0
        invoke SetColor
        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
        invoke SendMessage,hwndRichEdit,EM_SETEVENTMASK,0,ENM_MOUSEEVENTS
        invoke SendMessage,hwndRichEdit,EM_EMPTYUNDOBUFFER,0,0
    .elseif uMsg==WM_NOTIFY
        push esi
        mov esi,lParam
        assume esi:ptr NMHDR
        .if [esi].code==EN_MSGFILTER
            assume esi:ptr MSGFILTER
            .if [esi].msg==WM_RBUTTONDOWN
                invoke GetMenu,hWnd
                invoke GetSubMenu,eax,1
                mov hPopup,eax
                invoke PrepareEditMenu,hPopup
                mov edx,[esi].lParam
                mov ecx,edx
                and edx,0FFFFh
                shr ecx,16
                mov pt.x,edx
                mov pt.y,ecx
                invoke ClientToScreen,hWnd,addr pt
                invoke TrackPopupMenu,hPopup,TPM_LEFTALIGN or TPM_BOTTOMALIGN,pt.x,pt.y,NULL,hWnd,NULL
            .endif
        .endif
        pop esi
    .elseif uMsg==WM_INITMENUPOPUP
        mov eax,lParam
        .if ax==0		; file menu
            .if FileOpened==TRUE	; a file is already opened
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_OPEN,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_CLOSE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_SAVEAS,MF_GRAYED
            .endif
        .elseif ax==1	; edit menu
            invoke PrepareEditMenu,wParam
        .elseif ax==2		; search menu bar
            .if FileOpened==TRUE
                invoke EnableMenuItem,wParam,IDM_FIND,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_ENABLED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_ENABLED
            .else
                invoke EnableMenuItem,wParam,IDM_FIND,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDNEXT,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_FINDPREV,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_REPLACE,MF_GRAYED
                invoke EnableMenuItem,wParam,IDM_GOTOLINE,MF_GRAYED
            .endif
        .endif
    .elseif uMsg==WM_COMMAND
        .if lParam==0		; menu commands
            mov eax,wParam
            .if ax==IDM_OPEN
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset FileName
                mov byte ptr [FileName],0
                mov ofn.nMaxFile,sizeof FileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetOpenFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr FileName,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        mov hFile,eax
                        mov editstream.dwCookie,eax
                        mov editstream.pfnCallback,offset StreamInProc
                        invoke SendMessage,hwndRichEdit,EM_STREAMIN,SF_TEXT,addr editstream
                        invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                        invoke CloseHandle,hFile
                        mov FileOpened,TRUE
                    .else
                        invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                    .endif
                .endif
            .elseif ax==IDM_CLOSE
                invoke CheckModifyState,hWnd
                .if eax==TRUE
                    invoke SetWindowText,hwndRichEdit,0
                    mov FileOpened,FALSE
                .endif
            .elseif ax==IDM_SAVE
                invoke CreateFile,addr FileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                .if eax!=INVALID_HANDLE_VALUE
@@:
                    mov hFile,eax
                    mov editstream.dwCookie,eax
                    mov editstream.pfnCallback,offset StreamOutProc
                    invoke SendMessage,hwndRichEdit,EM_STREAMOUT,SF_TEXT,addr editstream
                    invoke SendMessage,hwndRichEdit,EM_SETMODIFY,FALSE,0
                    invoke CloseHandle,hFile
                .else
                    invoke MessageBox,hWnd,addr OpenFileFail,addr AppName,MB_OK or MB_ICONERROR
                .endif
            .elseif ax==IDM_COPY
                invoke SendMessage,hwndRichEdit,WM_COPY,0,0
            .elseif ax==IDM_CUT
                invoke SendMessage,hwndRichEdit,WM_CUT,0,0
            .elseif ax==IDM_PASTE
                invoke SendMessage,hwndRichEdit,WM_PASTE,0,0
            .elseif ax==IDM_DELETE
                invoke SendMessage,hwndRichEdit,EM_REPLACESEL,TRUE,0
            .elseif ax==IDM_SELECTALL
                mov chrg.cpMin,0
                mov chrg.cpMax,-1
                invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr chrg
            .elseif ax==IDM_UNDO
                invoke SendMessage,hwndRichEdit,EM_UNDO,0,0
            .elseif ax==IDM_REDO
                invoke SendMessage,hwndRichEdit,EM_REDO,0,0
            .elseif ax==IDM_OPTION
                invoke DialogBoxParam,hInstance,IDD_OPTIONDLG,hWnd,addr OptionProc,0
            .elseif ax==IDM_SAVEAS
                invoke RtlZeroMemory,addr ofn,sizeof ofn
                mov ofn.lStructSize,sizeof ofn
                push hWnd
                pop ofn.hwndOwner
                push hInstance
                pop ofn.hInstance
                mov ofn.lpstrFilter,offset ASMFilterString
                mov ofn.lpstrFile,offset AlternateFileName
                mov byte ptr [AlternateFileName],0
                mov ofn.nMaxFile,sizeof AlternateFileName
                mov ofn.Flags,OFN_FILEMUSTEXIST or OFN_HIDEREADONLY or OFN_PATHMUSTEXIST
                invoke GetSaveFileName,addr ofn
                .if eax!=0
                    invoke CreateFile,addr AlternateFileName,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0
                    .if eax!=INVALID_HANDLE_VALUE
                        jmp @B
                    .endif
                .endif
            .elseif ax==IDM_FIND
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_FINDDLG,hWnd,addr SearchProc,0
                .endif
            .elseif ax==IDM_REPLACE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_REPLACEDLG,hWnd,addr ReplaceProc,0
                .endif
            .elseif ax==IDM_GOTOLINE
                .if hSearch==0
                    invoke CreateDialogParam,hInstance,IDD_GOTODLG,hWnd,addr GoToProc,0
                .endif
            .elseif ax==IDM_FINDNEXT
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov eax,findtext.chrg.cpMin
                    .if eax!=findtext.chrg.cpMax
                        push findtext.chrg.cpMax
                        pop findtext.chrg.cpMin
                    .endif
                    mov findtext.chrg.cpMax,-1
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,FR_DOWN,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_FINDPREV
                invoke lstrlen,addr FindBuffer
                .if eax!=0
                    invoke SendMessage,hwndRichEdit,EM_EXGETSEL,0,addr findtext.chrg
                    mov findtext.chrg.cpMax,0
                    mov findtext.lpstrText,offset FindBuffer
                    invoke SendMessage,hwndRichEdit,EM_FINDTEXTEX,0,addr findtext
                    .if eax!=-1
                        invoke SendMessage,hwndRichEdit,EM_EXSETSEL,0,addr findtext.chrgText
                    .endif
                .endif
            .elseif ax==IDM_EXIT
                invoke SendMessage,hWnd,WM_CLOSE,0,0
            .endif
        .endif
    .elseif uMsg==WM_CLOSE
        invoke CheckModifyState,hWnd
        .if eax==TRUE
            invoke DestroyWindow,hWnd
        .endif
    .elseif uMsg==WM_SIZE
        mov eax,lParam
        mov edx,eax
        and eax,0FFFFh
        shr edx,16
        invoke MoveWindow,hwndRichEdit,0,0,eax,edx,TRUE
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

Analysis:

The first action before calling WinMain to to call FillHiliteInfo. This function reads the content of wordfile.txt and parses the content.

FillHiliteInfo proc uses edi
    LOCAL buffer[1024]:BYTE
    LOCAL pTemp:DWORD
    LOCAL BlockSize:DWORD
    invoke RtlZeroMemory,addr ASMSyntaxArray,sizeof ASMSyntaxArray

Initialize ASMSyntaxArray to zero.

    invoke GetModuleFileName,hInstance,addr buffer,sizeof buffer
    invoke lstrlen,addr buffer
    mov ecx,eax
    dec ecx
    lea edi,buffer
    add edi,ecx
    std
    mov al,"\"
    repne scasb
    cld
    inc edi
    mov byte ptr [edi],0
    invoke lstrcat,addr buffer,addr WordFileName

Construct the full path name of wordfile.txt: I assume that it's always in the same folder as the program.

    invoke GetFileAttributes,addr buffer
    .if eax!=-1

I use this method as a quick way of checking whether a file exists.

        mov BlockSize,1024*10
        invoke HeapAlloc,hMainHeap,0,BlockSize
        mov pTemp,eax

Allocate the memory block to store the words. Default to 10K. The memory is allocated from the default heap.

@@:
        invoke GetPrivateProfileString,addr ASMSection,addr C1Key,addr ZeroString,pTemp,BlockSize,addr buffer
        .if eax!=0

I use GetPrivateProfileString to retrieve the content of each key in wordfile.txt. The key starts from C1 to C10.

            inc eax
            .if eax==BlockSize	; the buffer is too small
                add BlockSize,1024*10
                invoke HeapReAlloc,hMainHeap,0,pTemp,BlockSize
                mov pTemp,eax
                jmp @B
            .endif

Checking whether the memory block is large enough. If it is not, we increment the size by 10K until the block is large enough.

            mov edx,offset ASMColorArray
            invoke ParseBuffer,hMainHeap,pTemp,eax,edx,addr ASMSyntaxArray

Pass the words, the memory block handle, the size of the data read from wordfile.txt, the address of the color dword that will be used to hilight the words and the address of ASMSyntaxArray.

Now, let's examine what ParseBuffer does. In essence, this function accepts the buffer containing the words to be hilighted ,parses them to individual words and stores each of them in a WORDINFO structure array that can be accessed quickly from ASMSyntaxArray.

ParseBuffer proc uses edi esi hHeap:DWORD,pBuffer:DWORD, nSize:DWORD, ArrayOffset:DWORD,pArray:DWORD
    LOCAL buffer[128]:BYTE
    LOCAL InProgress:DWORD
    mov InProgress,FALSE

InProgress is the flag I use to indicate whether the scanning process has begun. If the value is FALSE, we haven't encountered a non-white space character yet.

    lea esi,buffer
    mov edi,pBuffer
    invoke CharLower,edi

esi points to our local buffer that will contain the word we have parsed from the word list. edi points to the word list string. To simplify the search later, we convert all characters to lowercase.

    mov ecx,nSize
SearchLoop:
    or ecx,ecx
    jz Finished
    cmp byte ptr [edi]," "
    je EndOfWord
    cmp byte ptr [edi],9 	; tab
    je EndOfWord

Scan the whole word list in the buffer, looking for the white spaces. If a white space is found, we have to determine whether it marks the end or the beginning of a word.

    mov InProgress,TRUE
    mov al,byte ptr [edi]
    mov byte ptr [esi],al
    inc esi
SkipIt:
    inc edi
    dec ecx
    jmp SearchLoop

If the byte under scrutiny is not a white space, we copy it to the buffer to construct a word and then continue the scan.

EndOfWord:
    cmp InProgress,TRUE
    je WordFound
    jmp SkipIt

If a white space is found, we check the value in InProgress. If the value is TRUE, we can assume that the white space marks the end of a word and we may proceed to put the word currently in the local buffer (pointed to by esi) into a WORDINFO structure. If the value is FALSE, we continue the scan until a non-white space character is found.

WordFound:
    mov byte ptr [esi],0
    push ecx
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,sizeof WORDINFO

When the end of a word is found, we append 0 to the buffer to make the word an ASCIIZ string. We then allocate a block of memory from the heap the size of WORDINFO for this word.

    push esi
    mov esi,eax
    assume esi:ptr WORDINFO
    invoke lstrlen,addr buffer
    mov [esi].WordLen,eax

We obtain the length of the word in the local buffer and store it in the WordLen member of the WORDINFO structure, to be used as a quick comparison.

    push ArrayOffset
    pop [esi].pColor

Store the address of the dword that contains the color to be used to hilight the word in pColor member.

    inc eax
    invoke HeapAlloc,hHeap,HEAP_ZERO_MEMORY,eax
    mov [esi].pszWord,eax
    mov edx,eax
    invoke lstrcpy,edx,addr buffer

Allocate memory from the heap to store the word itself. Right now, the WORDINFO structure is ready to be inserted into the appropriate linked list.

    mov eax,pArray
    movzx edx,byte ptr [buffer]
    shl edx,2		; multiply by 4
    add eax,edx

pArray contains the address of ASMSyntaxArray. We want to move to the dword that has the same index as the value of the first character of the word. So we put the first character of the word in edx then multiply edx by 4 (because each element in ASMSyntaxArray is 4 bytes in size) and then add the offset to the address of ASMSyntaxArray. We have the address of the corresponding dword in eax.

    .if dword ptr [eax]==0
        mov dword ptr [eax],esi
    .else
        push dword ptr [eax]
        pop [esi].NextLink
        mov dword ptr [eax],esi
    .endif

Check the value of the dword. If it's 0, it means there is currently no word that begins with this character in the list. We thus put the address of the current WORDINFO structure in that dword.

If the value in the dword is not 0, it means there is at least one word that begins with this character in the array. We thus insert this WORDINFO structure to the head of the linked list and update its NextLink member to point to the next WORDINFO structure.

    pop esi
    pop ecx
    lea esi,buffer
    mov InProgress,FALSE
    jmp SkipIt

After the operation is complete, we begin the next scan cycle until the end of buffer is reached.

        invoke SendMessage,hwndRichEdit,EM_SETTYPOGRAPHYOPTIONS,TO_SIMPLELINEBREAK,TO_SIMPLELINEBREAK
        invoke SendMessage,hwndRichEdit,EM_GETTYPOGRAPHYOPTIONS,1,1
        .if eax==0		; means this message is not processed
            mov RichEditVersion,2
        .else
            mov RichEditVersion,3
            invoke SendMessage,hwndRichEdit,EM_SETEDITSTYLE,SES_EMULATESYSEDIT,SES_EMULATESYSEDIT
        .endif

After the richedit control is created, we need to determine the its version. This step is necessary since EM_POSFROMCHAR behaves differently for RichEdit 2.0 and 3.0 and EM_POSFROMCHAR is crucial to our syntax hilighting routine. I have never seen a documented way of checking the version of richedit control thus I have to use a workaround. In this case, I set an option that is specific to version 3.0 and immediately retrieve its value. If I can retrieve the value, I assume that the control version is 3.0.

If you use RichEdit control version 3.0, you will notice that updating the font color for a large file takes quite a long time. This problem seems to be specific to version 3.0. I found a workaround: making the control emulate the behavior of the system edit control by sending EM_SETEDITSTYLE message.

After we can obtain the version information, we proceed to subclass the richedit control. We will now examine the new window procedure for the richedit control.

NewRichEditProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    ........
    .......
    .if uMsg==WM_PAINT
        push edi
        push esi
        invoke HideCaret,hWnd
        invoke CallWindowProc,OldWndProc,hWnd,uMsg,wParam,lParam
        push eax

We handle WM_PAINT message. First, we hide the caret so as to avoid some ugly gfx after the hilighting. After that we pass the message to the original richedit procedure to let it update the window. When CallWindowProc returns, the text is updated with its usual color/background. Now is our opportunity to do syntax hilighting.

        mov edi,offset ASMSyntaxArray
        invoke GetDC,hWnd
        mov hdc,eax
        invoke SetBkMode,hdc,TRANSPARENT

Store the address of ASMSyntaxArray in edi. Then we obtain the handle to the device context and set the text background mode to transparent so the text that we will write will use the default background color.

        invoke SendMessage,hWnd,EM_GETRECT,0,addr rect
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect
        invoke SendMessage,hWnd,EM_LINEFROMCHAR,eax,0
        invoke SendMessage,hWnd,EM_LINEINDEX,eax,0

We want to obtain the visible text so we first have to obtain the formatting rectangle by sending EM_GETRECT message to the richedit control. Now that we have the bounding rectangle, we obtain the nearest character index to the upper left corner of the rectangle with EM_CHARFROMPOS. Once we have the character index (the first visible character in the control), we can start to do syntax hilighting starting from that position. But the effect might not be as good as when we start from the first character of the line that the character is in. That's why I need to obtain the line number of that the first visible character is in by sending EM_LINEFROMCHAR message. To obtain the first character of that line, I send EM_LINEINDEX message.

        mov txtrange.chrg.cpMin,eax
        mov FirstChar,eax
        invoke SendMessage,hWnd,EM_CHARFROMPOS,0,addr rect.right
        mov txtrange.chrg.cpMax,eax

Once we have the first character index, store it for future reference in FirstChar variable. Next we obtain the last visible character index by sending EM_CHARFROMPOS, passing the lower-right corner of the formatting rectangle in lParam.

        push rect.left
        pop RealRect.left
        push rect.top
        pop RealRect.top
        push rect.right
        pop RealRect.right
        push rect.bottom
        pop RealRect.bottom
        invoke CreateRectRgn,RealRect.left,RealRect.top,RealRect.right,RealRect.bottom
        mov hRgn,eax
        invoke SelectObject,hdc,hRgn
        mov hOldRgn,eax

While doing syntax hilighting, I noticed an unsightly side-effect of this method: if the richedit control has a margin (you can specify margin by sending EM_SETMARGINS message to the richedit control), DrawText writes over the margin. Thus I need to create a clipping region, the size of the formatting rectangle, by calling CreateRectRgn. The output of GDI functions will be clipped to the "writable" area.

Next, we need to hilight the comments first and get them out of our way. My method is to search for ";" and hilight the text with the comment color until the carriage return is found. I will not analyze the routine here: it's fairly long and complicated. Suffice here to say that, when all the comments are hilighted, we replace them with 0s in the buffer so that the words in the comments will not be processed/hilighted later.

        mov ecx,BufferSize
        lea esi,buffer
        .while ecx>0
            mov al,byte ptr [esi]
            .if al==" " || al==0Dh || al=="/" || al=="," || al=="|" || al=="+" || al=="-" || al=="*" || al=="&" || al=="<" || al==">" || al=="=" || al=="(" || al==")" || al=="{" || al=="}" || al=="[" || al=="]" || al=="^" || al==":" || al==9
                mov byte ptr [esi],0
            .endif
            dec ecx
            inc esi
        .endw

Once the comments are out of our way, we separate the words in the buffer by replacing the "separator" characters with 0s. With this method, we need not concern about the separator characters while processing the words in the buffer anymore: there is only one separator character, NULL.

        lea esi,buffer
        mov ecx,BufferSize
        .while ecx>0
            mov al,byte ptr [esi]
            .if al!=0

Search the buffer for the first character that is not null,ie, the first character of a word.

                push ecx
                invoke lstrlen,esi
                push eax
                mov edx,eax

Obtain the length of the word and put it in edx

                movzx eax,byte ptr [esi]
                .if al>="A" && al<="Z"
                    sub al,"A"
                    add al,"a"
                .endif

Convert the character to lowercase (if it's an uppercase character)

                shl eax,2
                add eax,edi		; edi contains the pointer to the WORDINFO pointer array
                .if dword ptr [eax]!=0

After that, we skip to the corresponding dword in ASMSyntaxArray and check whether the value in that dword is 0. If it is, we can skip to the next word.

                    mov eax,dword ptr [eax]
                    assume eax:ptr WORDINFO
                    .while eax!=0
                        .if edx==[eax].WordLen

If the value in the dword is non-zero, it points to the linked list of WORDINFO structures. We process to walk the linked list, comparing the length of the word in our local buffer with the length of the word in the WORDINFO structure. This is a quick test before we compare the words. Should save some clock cycles.

                            pushad
                            invoke lstrcmpi,[eax].pszWord,esi
                            .if eax==0

If the lengths of both words are equal, we proceed to compare them with lstrcmpi.

                                popad
                                mov ecx,esi
                                lea edx,buffer
                                sub ecx,edx
                                add ecx,FirstChar

We construct the character index from the address of the first character of the matching word in the buffer. We first obtain its relative offset from the starting address of the buffer then add the character index of the first visible character to it.

                                pushad
                                .if RichEditVersion==3
                                    invoke SendMessage,hWnd,EM_POSFROMCHAR,addr rect,ecx
                                .else
                                    invoke SendMessage,hWnd,EM_POSFROMCHAR,ecx,0
                                    mov ecx,eax
                                    and ecx,0FFFFh
                                    mov rect.left,ecx
                                    shr eax,16
                                    mov rect.top,eax
                                .endif
                                popad

Once we know the character index of the first character of the word to be hilighted, we proceed to obtain the coordinate of it by sending EM_POSFROMCHAR message. However, this message is interpreted differently by richedit 2.0 and 3.0. For richedit 2.0, wParam contains the character index and lParam is not used. It returns the coordinate in eax. For richedit 3.0, wParam is the pointer to a POINT structure that will be filled with the coordinate and lParam contains the character index.

As you can see, passing the wrong arguments to EM_POSFROMCHAR can wreak havoc to your system. That's why I have to differentiate between RichEdit control versions.

                                mov edx,[eax].pColor
                                invoke SetTextColor,hdc,dword ptr [edx]
                                invoke DrawText,hdc,esi,-1,addr rect,0

Once we got the coordinate to start, we set the text color with the one specified in the WORDINFO structure. And then proceed to overwrite the word with the new color.

As the final words, this method can be improved in several ways. For example, I obtain all the text starting from the first to the last visible line. If the lines are very long, the performance may hurt by processing the words that are not visible. You can optimize this by obtaining the really visible text line by line. Also the searching algorithm can be improved by using a more efficient method. Don't take me wrong: the syntax hilighting method used in this example is FAST but it can be FASTER. :)

[Iczelion's Win32 Assembly Homepage]


Version 1.2
(Décembre 2000)

Index

A lire absolument : Le Principe Général

Assembler une DLL

AIDE

Voici le contenu de chacun des Tutoials.

Tut1 : Les Bases
Tut2 : Les boîtes de message. (MessageBox)
Tut3 : Les boîtes de dialogue. (DialogBox)
Tut4 : Dessiner du texte. (Painting with Text)
Tut5 : Encore plus à propos des textes.(Taille, police...)
Tut6 : Les entrées clavier. (Saisie de texte)
Tut7 : Le contrôle de la Souris.
Tut8 : Le menu.
Tut9 : Child Window Controls.
Tut10 : Boîte de dialogue comme Fenêtre Principale.
Tut11 : Encore plus à propos des boîtes de dialogue.
Tut12 : Gestion de la mémoire et des fichiers d'entrée-sortie.
Tut13 : Fichiers à Mémoire Configurée. (File Mapping)
Tut14 : Process.
Tut15 : Programmation Multi-liens.
Tut16 : Event Object.
Tut17 : Les DLL ou Bibliothèques de Liaison, Dynamiques.
Tut18 : Les Controls Communs (Common Controls).
Tut19 : Dossiers organisés sous la forme d'un arbre.
Tut20 : Les Sousclassifications de Fenêtre.
Tut21 : Le Pipe.
Tut22 : Superclassification.
Tut23 : Icône de Barre de Taches.
Tut24 : Windows Hooks.
Tut25 : Un Simple Bitmap.
Tut26 : Splash Screen.
Tut27 : Les Bulles d'Information (Tooltip Control).
Tut28 : Win32 Debug API Partie 1/3.
Tut29 : Win32 Debug API Partie 2/3.
Tut30 : Win32 Debug API Partie 3/3.
Tut31 : Fichiers organisés sous la forme d'une liste.

REMARQUE:

Dans cette nouvelle version j'ai supprimé la partie concernant les APIs. En fait je les ai dissociés de ce ZIP pour les placer dans un autre ZIP qui leur est spécialement dédiés. Bon nombre d'APIs ont été rajoutées. La principale nouveauté, c'est une petite partie concernant la création de DLL, avec un petit programme pour les assembler (Assembles_DLL.bat)

Voilà les Tuts 28, 29 et 30 à propos des debuggers ont étés mis à jour dans la version précédente. Si c'est seulement l'ASM qui vous intéresse, ça n'aura pas beaucoup d'intérêt pour vous, mais par contre si votre truc c'est de cracker et franchement on y prend plaisir, alors ces trois là sont vraiment le but ultime de cette série. Faites un tour aussi sur l'excellent site d'ACZELION (l'adresse est sur chacun ces tuts) vous y en retrouverez encore d'autres toujours sur Masm mais à propos de sujets bien plus pointus. Genre: les VxD, et détection de debuggers en tous genres. (en anglais, s'il vous plait)

MESSAGE POUR TOUS : rien ne change, toujours dans le but de faire évoluer cette doc, je permet à chacun qui le souhaite de la retoucher et faire évoluer les versions pour corriger mes erreurs, mes imprécisions et autres.

Morgatte@citeweb.net


Iczelion's Win32asm tutorial part 1: The Basic

Tutorial 1 : les bases

Ce tutorial suppose que le lecteur sache comment employer MASM. Si vous n'êtes pas familiers avec MASM, téléchargez win32asm.exe et étudiez le texte à l'intérieur du ZIP avant la suite des Tutoials. Bon. Vous êtes maintenant prêts. On y va!

Théorie:

Les programmes Win32 fonctionnent en mode protégé depuis le 80286. Mais le 80286 c'est maintenant de l'histoire ancienne. Donc nous devons seulement nous intéresser au 80386 et à ses descendants. Windows dirigent chaque programme Win32 séparément. Ceci signifie que chaque programme Win32 aura à sa disposition ses propres espaces de mémoire. Cependant, cela ne signifie pas que chaque programme win32 a 4GB de mémoire physique, mais seulement que le programme peut adresser n'importe quelle adresse dans cette gamme. Windows fera tout le nécessaire pour adresser les vraies références (physiques) du programme aux adresses (mémoires) valables 00401000… qu'il utilise. Bien sûr, le programme doit respecter les règles de Windows, autrement il causera une Erreur de Protection Générale tant redoutée. Chaque programme est seul dans son espace d'adresse. C'est la différence avec les programmes Win16. Tous les programmes Win16 peuvent *malheureusement plus ou moins se chevaucher* les uns les l'autres. Mais pas sous Win32. Ces gardes fous réduisent la chance d'une écriture du code d'un programme par dessus les données d'un autre.
Le modèle de mémoire est aussi résolument différent des vieux jours du monde du 16 bits. Désormais, sous Win32, nous ne sommes plus concernés par le modèle de mémoire en plusieurs segments! (0001.xxxx puis 0002.xxxx puis 0003.xxxx ….)Il y a seulement un seul modèle de mémoire : le modèle de mémoire uniforme (Flat). Il n'y a plus de segments de plus 64Ko (différence *.com / *.exe). La mémoire est un seul espace continu. Ça veut dire aussi que vous n'avez plus besoin de jouer avec les différents registres concernant les segments [ES et SS]. Vous pouvez employer n'importe quel registre de segment pour adresser n'importe quel point dans l'espace de mémoire (ouf !). C'est une GRANDE aide pour les programmeurs. C'est ce qui fait de l'assembleur Win32 une programmation aussi facile que le C.
Quand vous programmez sous Win32, vous devez respecter quelques règles importantes. Une règle{*Majeure*} est que Windows emploie lui-même ESI, EDI, EBP et EBX et les valeurs de ses registres changent, en même temps qu'une autre application tourne en parallèle avec Windows. Rappelez-vous donc cette règle {*Primordiale*} d'abord : si vous employez chacun de ces quatre registres à l'intérieur d'une procédure, n'oubliez jamais de les reconstituer{*rétablir*} avant le contrôle de retour à Windows. L'exemple évident sont les Call qui appellent une API de Windows. Cela ne signifie pas que vous ne pouvez pas employer ces quatre registres, vous le pouvez. Mais seulement que vous devez rétablir ces registres après un call.

Contenu:

Voici le squelette de n'importe quel programme. Si vous ne comprenez pas quelques-uns des codes, ne paniquez pas. J'expliquerai chacun d'entre eux plus tard.

.386
.MODEL Flat, STDCALL
.DATA
<Vous initialisez les data>
......
.DATA?
<Vous initialisez les data>
......
.CONST
<Vous initialisez vos constantes>
......
.CODE
<label>
<Vous initialisez votre code(début du segment Code)>
.....
end <label>

C'est tout! On va analyser le squelette de ce programme.

.386
C'est une directive d'assembleur, disant à l'assembleur d'employer le jeu d'instructions du 80386. Vous pouvez aussi employer .486 .586 mais l'important c'est d'utiliser au minimum les fonctions à partir du .386. Il y a en réalité deux formes presque identiques pour chaque modèle de directive. .386/.386p.486/.486p. Ces versions "p" sont nécessaires seulement quand votre programme emploie des instructions privilégiées. Les instructions privilégiées sont des instructions réservées par le système du CPU / FONCTIONNEMENT en mode protégé. Elles peuvent seulement être employés par le code privilégié, comme les conducteurs de dispositif virtuels. La plupart du temps, votre programme travaillera dans le mode non-privilégié donc c'est presque sûr que vous n'emploierez pas les versions -p.

.MODEL FLAT, STDCALL
.MODEL est une directive d'assembleur qui spécifie le type de mémoire de votre programme. Sous Win32, il y a seulement un type, le modèle FLAT (mémoire un en seul bloque).
STDCALL dit à MASM quel est l'ordre de passage des paramètres de la pile vers un Call. De " gauche à droite " ou " le droit à gauche " et aussi ça équilibrera l'encadrement de la pile après l'appel de la fonction.
Sous Win16, il y a deux types de convention d'appel, la convention C et le PASCAL
La convention C passe les paramètres de la pile de droit à gauche, c'est-à-dire le paramètre extrême droit est poussé d'abord. L'interlocuteur est responsable du balancement de l'encadrement de pile après l'appel. Par exemple, pour appeler une fonction nommée foo (int first_param, int second_param, int third_param) dans la 'convention d'appel C', les codes d'asm ressembleront à cela :

push [3e_paramètre] ; Pousse le 3e paramètre sur la pile
push [2e_paramètre] ; Suivi par le second
push [1er_paramètre] ; Et du 1er
call Procédure_foo
add sp, 12 ; La fonction appelante équilibre le remplissage de la pile
la convention d'appel PASCAL est le contraire de la convention C. elle passe les paramètres de la pile de gauche à droite et le Call est responsable de équilibre (de l'état) de la pile après l'appel.
Win16 adopte la convention PASCAL parce qu'il produit des codes plus petits. La convention C, est utile quand vous ne savez pas combien de paramètres on passera à la fonction comme dans le cas de wsprintf (). Dans le cas de wsprintf (), la fonction n'a aucune façon de déterminer à l'avance combien de paramètres seront préservés sur la pile, donc on ne peut pas déterminer l'état de la pile.
STDCALL est l'hybride de la Convention de Pascal et C. Il passe les paramètres de droit à gauche. La plate-forme Win32 emploie exclusivement STDCALL . Sauf dans un cas : wsprintf (). Vous devez employer la convention C avec wsprintf ()

.DATA
.DATA?
.CONST
.CODE
Ces quatre directives servent a appeler la section (00401000……). Vous n'avez pas de segments (0001.xxxx, 0002.xxxx,…) dans Win32, vous vous souvenez ? Mais vous pouvez diviser votre espace d'adresse (00401000 à 00620000 par exemple) en sections logiques, exemple : " le code de 00401000 à 005F0000 " et "les DATA de 005F0000 à 00620000 ". Le début d'une section dénote la fin de la section précédente. Il existe deux groupes de section : données et code. Les sections de données sont divisées en 3 catégories :

  • .DATA Cette section contient les données initialisées de votre programme.
  • .DATA? Cette section contient les données non initialisées de votre programme. Parfois vous voulez juste prévoir un espace mémoire pour contenir de futurs données, mais ne voulez pas l'initialiser (lui donner une valeur initiale). c'est le but de cette section. L'avantage des données non initialisées est : qu'il ne prend pas d'espace dans le fichier exécutable. Par exemple, si vous allouez(prévoyez) 10,000 octets dans votre .DATA ? La section de votre exécutable ne sera pas gonflée de 10,000 octets. Sa taille reste pratiquement la même. Vous dites seulement à l'assembleur de combien de place votre programme aura besoin une fois qu'il sera chargé en mémoire, c'est tout.
  • .CONST Cette section contient la déclaration des constantes employées par votre programme. Les constantes qui sont dans cette section ne seront jamais modifiées par votre programme. elles resteront seulement des *constantes*.

Vous n'êtes pas forcément obligé d'employer ces trois sections dans votre programme. Déclarez seulement la (ou les) section (s) que vous voulez utiliser.

Il n'y a seulement qu'une section pour le code : .CODE. C'est celle où vos lignes de code résident.
<label>
end <label>
Où <label> représente n'importe quelle étiquette arbitraire, et est employé pour indiquer le début et la fin de votre code. Les deux 'label' doivent être identiques. Toutes vos lignes de code doivent se trouver entre <label> et end <label>

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 ASM tutorial 2: MessageBox

Tutorial 2: MessageBox

Dans ce tutorial, nous créerons un programme Windows entièrement fonctionnel qui montre une MessageBox (boîte de message) disant "Win32 assembly is great!".

Téléchargez le fichier d'exemple ici.

Théorie:

Windows met à notre disposition de multiples ressources pour les programmes Win (programmes fonctionnant sous windows). La plus importante c'est l'API (Applications Programmant l'Interface). Les API représentent une collection énorme de fonctions très utiles qui résident dans Windows lui-même, prêtes pour l'utilisation de n'importe quels autres programmes Win (programmes tournant sous Windows contrairement à ceux sous Dos). Ces fonctions sont stockées dans plusieurs bibliothèques dynamiquement liées (les DLLs) comme kernel32.dll, user32.dll et gdi32.dll. Kernel32.dll Contient les fonctions API qui traitent avec la mémoire et sa gestion. User32.dll contrôle les interfaces dont se sert votre programme. Gdi32.dll est responsable d'opérations graphiques. "Les trois principales " sont là, d'autre DLLs peuvent être employées pour votre programme, pourvu que vous ayez assez d'informations sur les fonctions API désirées.
Les programmes Win se lient dynamiquement avec ces DLLs, c'est-à-dire : Les codes des fonctions API ne sont pas inclus dans le programme Win exécutable. Dans votre programme, pendant son exécution, pour accéder à l'API désirée, vous devez déclarer cette information dans le fichier exécutable. L'information est dans une bibliothèque d'importation. Vous devez lier vos programmes avec les bibliothèques d'importation correctes ou ils ne seront pas capables de retrouver la fonction de l'API souhaitée.
Quand un programme Win est chargé dans la mémoire, Windows lit l'information stockée dans le programme. Cette information inclut les noms des fonctions et les DLLs où résident ces fonctions. Ex : la DLL User32.dll contient la fonction MessageBox (ainsi qu'une multitude d'autres fonctions). L'information que cherchera Windows dans votre exécutable ce sera quelque chose comme " Call USER32 ! MessageBox ". Dès que Windows trombera sur un tel renseignement dans votre programme, il chargera la DLL et exécutera la fonction.
Il y a deux catégories de fonctions API : les uns pour ANSI et les autres pour Unicode. Les noms des fonctions API pour ANSI sont suivis du suffixe "A", ex : MessageBoxA. Ceux pour Unicode ont le suffixe "W". Windows 95 traite naturellement ANSI, et Windows NT Unicode.
D'habitude on se sert de la convention ANSI, qui pour l'ensemble des caractères proposés par votre ordinateur est terminés par le caractère NULL. ANSI représente chaque caractère sur 1 octet. Il est suffisant pour les langues européennes, mais il ne peut pas manipuler la plupart des langues orientales qui ont plusieurs milliers de caractères uniques. C'est pourquoi UNICODE entre en jeu. Un caractère UNICODE a une taille de 2 octets, ce qui lui permet d'avoir une série de code de 65536 caractères uniques.
Mais la plupart du temps, vous emploierez un fichier INCLUDE (*.inc) qui peut déterminer et choisir les fonctions API appropriées à votre plate-forme suivant ce que doit faire votre programme. Référez-vous juste aux noms de fonction API sans le suffixe.

Example:

Je vous ai présenté ci-dessous le squelette nu d'un programme. Nous l'enrichirons plus tard.

.386
.model flat, stdcall
.data
.code
start:
end start

L'exécution commence à la première instruction immédiatement au-dessous de l'étiquette Start: Le programme s'exécutera instruction par l'instruction jusqu'à ce qu'une instruction de contrôle de direction comme jmp, jne, je ,ret Etc..soit trouvée. Ces instructions dirigent l'exécution vers d'autres lignes d'instructions. Quand le programme a besoin de sortir de Windows pour se terminer, il doit appeler la fonction ExitProcess qui est une API de la DLL Kernel32.dll.

ExitProcess proto uExitCode:DWORD

Ci dessus, ce type d'expression est appelé un prototype de fonction. Un prototype de fonction définit les attributs d'une fonction pour ensuite transformer cette expression (pseudo-assembler) en véritable assembler. Le format d'un prototype de fonction est le suivant :

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...

En résumé, le mot-clé PROTO est précédé du nom de la fonction puis suivi de la liste des paramètres, séparés par des virgules. Dans l'exemple ExitProcess ci-dessus, il définit ExitProcess comme une fonction qui prend seulement un seul paramètre de type DWORD. Les prototypes de fonctions sont très utiles quand vous employez la syntaxe d'appel de niveau haut :INVOKE. En plus, le Linker vérifiera si vous avez correctement utilisé la fonction que vous invokez. En outre, il vérifiie que ses paramètres ne sont pas oubliés.
Par exemple, si vous faites :

call ExitProcess

Sans pousser un dword sur la pile (Push 0, avant d'appeler la fonction ExitProcess), l'assembler ne sera pas capable de comprendre cette erreur à votre place. Vous vous en apecevrez plus tard quand votre programme plantera. Mais si vous employez :

invoke ExitProcess

Le linker vous informera que vous avez oublié de pousser un dword sur la pile évitant ainsi l'erreur. Je vous recommande donc d'employer. invoke au lieu d'un simple 'Call'. La syntaxe d''Invoke' est la suivante :

INVOKE expression [,arguments]

L'expression peut être le nom d'une fonction ou bien un pointeur de fonction. Les paramètres sont séparés par des virgules.

La plupart des prototypes de fonction pour des fonctions API sont tenus dans les fichiers nommés : INCLUDE. Si vous employez MASM32, ils seront dans le dossier MASM32/INCLUDE. Les fichiers INCLUDE ont l'extension .inc et les prototypes de fonction pour une DLL sont stockés dans le fichier .inc avec le même nom que la DLL. Par exemple, ExitProcess est exporté par kernel32.lib donc le prototype de fonction pour ExitProcess est stocké dans kernel32.inc.
Vous pouvez aussi créer des prototypes de fonction pour vos propres fonctions. (HéHé ! ! !)
Partout dans mes exemples, j'emploierai windows.inc que vous pouvez télécharger à http://win32asm.cjb.net

Maintenant revenons à ExitProcess, uExitCode est la valeur du paramètre que vous voulez que votre programme renvoie à Windows après que celui-ci se termine. Vous pouvez appeler ExitProcess comme cela :

invoke ExitProcess, 0

Si vous placez cette ligne immédiatement après l' étiquette de commencement (ici on l'a appelée arbitrairement 'Start :', vous obtiendrez un programme win32 qui sortira immédiatement de Windows (pour revenir à ce que vous aviez à l'écran juste avant d'exécuter votre programme), mais c'est néanmoins un programme valable.

.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
start:
invoke ExitProcess,0
end start

Pour en savoir plus sur ExitProcess, lisez ExitProcess.html

L'option casemap:none indique à MASM de ne pas faire de différence entre les expressions ExitProcess et exiteprocess par exemple. Notez la nouvelle directive, include. Cette directive est suivie par le nom du fichier que vous voulez insérer à sa place. Dans cet exemple, quand MASM traite la ligne include \masm32\include\windows.inc, Il ouvrira windows.inc qui est dans le dossier \MASM32\include et fera en sorte que son contenu (celui de windows.inc) soit collé dans votre programme win32. Il ne contient pas de prototype de fonction. windows.inc n'est en aucun cas complet(c'est compréhensif car on peut toujours y rajouter de nouvelles choses). Ce fichier 'windows.inc' qui regroupe tous mes fichiers.inc je l'appelle le fichier 'HUTCH'. Il sera constamment remis à jour. Vérifiez donc HUTCH de temps en temps sur ma page d'accueil pour des mises à jour.

Dans notre exemple ci-dessus, nous appelons une fonction exportée par kernel32.dll, donc nous avons besoin d'inclure le prototype de fonction de kernel32.dll. Ce fichier est kernel32.inc. Si vous l'ouvrez avec un éditeur de texte, vous verrez que c'est plein de prototypes de fonction pour kernel32.dll. Si vous n'incluez pas kernel32.inc, vous pouvez toujours appeler ExitProcess, mais seulement avec la syntaxe d'appel simple (Push 0 puis Call ExitProcess). Vous ne serez pas capables d'invoker la function (ExitProcess).

*** Faisons le point : pour invoquer une fonction, vous devez mettre son prototype de fonction (qui est le contenu d'un des fichier *.inc) quelque part dans le code source. Dans notre exemple, si vous n'incluez pas kernel32.inc, vous pouvez définir le prototype de fonction pour ExitProcess n'importe où dans le code source au-dessus de la commande invokée et ça marchera. Les fichiers invokés doivent ici vous faciliter le travail en vous épargnant de taper les prototypes vous-même, donc employez-les chaque fois que vous le pouvez.
Maintenant nous rencontrons une nouvelle directive, includelib. includelib ne fonctionne pas comme include. C'est seulement une façon de dire à l'assembleur quelles bibliothèques d'importation sont employées par vos programmes. Quand l'assembleur voit une directive includelib, il met une commande de linker dans le fichier d'objet pour que le linker sache avec quelles bibliothèques d'importation votre programme a besoin de se lier. Vous n'êtes pas forcé d'employer includelib quoique. Vous pouvez spécifier les noms des bibliothèques d'importation dans la ligne de commande du linker, mais croyez-moi, c'est ennuyeux et la ligne de commande ne peut contenir que 128 caractères.

Sauvegardez maintenant cet exemple sous le nom de msgbox.asm. Vérifiez le chemin d'accès du fichier ml.exe, et assemblez msgbox.asm avec:

    ml /c /coff /Cp msgbox.asm
  • /c dit à MASM de seulement assembler. ça n'appelle pas link.exe. La plupart du temps, on ne veut pas appeler link.exe automatiquement puisque on doit exécuter quelques autres tâches avant l'appel de link.exe.

    • /coff dit à MASM de créer le fichier .obj dans le format COFF. MASM emploie une variante du format COFF (le Format de Fichier d'Objet Commun) qui est employé sous Unix comme son propre objet et le format de fichier exécutable.
    /Cp Dit à MASM de préserver les identificateurs d'utilisateur. Si vous employez les fichier du HUTCH (Les fichiers .inc), vous pouvez mettre l'"option casemap:none" en entête de votre source code. La directive .model réalise le même effet.
Après que vous ayez assemblé avec succès msgbox.asm, vous obtiendrez msgbox.obj. msgbox.obj est un fichier objet. Un fichier objet n'est pas loin d'un fichier exécutable. Il contient les instructions et les données dans la forme binaire. Ce qui manque sont chacunes des adresses fixes devant les instructions. Et ça, c'est le linker qui va faire le boulot.

Alors allons y avec link:

    link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib msgbox.obj
/SUBSYSTEM:WINDOWS Ça informe 'Link' de quel sorte d'exécutable est votre programme.
/LIBPATH:<path to import library> Dit à Link où sont les bibliothèques d'importation. Si vous utilisez MASM32, ils seront dans le dossier MASM32\LIB.
Link lit dans le fichier *.obj et pose les adresses des bibliothèques d'importation. Quand le processus est fini vous obtenez msgbox.exe.

Maintenant vous arrivez à msgbox.exe. Continuez, exécutez-le. Vous constaterez qu'il ne fait rien. Bien, nous n'y avons rien mis d'intéressant encore. Mais c'est un programme de Windows néanmoins. Et regardez sa taille! Dans mon PC, il prend 1,536 octets.

Maintenant, nous allons créer une boîte de message. Son prototype de fonction est :

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd est le 'handle' ou la poignée de la fenêtre parente. Vous pouvez vous imaginer qu'un 'Handle' est un numéro qui représente la fenêtre à laquelle vous faîtes référence. Sa valeur n'a pas d'importante pour vous. Vous vous rappelez seulement qu'il représente la fenêtre. Quand vous voulez faire quelque chose avec la fenêtre, vous devez vous y référer par son 'handle'.
lpText est un pointeur sur le texte que vous voulez montrer en tant que Contenu de la MessageBox (boîte de message). Cet pointeur représente réellement l'adresse de quelque chose. Cette adresse sera quelque chose du genre 00602154 et elle sinifie que le contenu qui sert à votre boite de message est en fait écrit à partir de la mémoire 00602154 (Cette adresse fait toujours parti des DATA) = Le Contenu De La MessageBox
lpCaption est l'pointeur de texte qui sert cette fois à accèder à l'adresse du texte qui est le titre de la message box. = Le Titre De La MessageBox
uType spécifie le type de message box utilisée. Par exemple quels sont les boutons quelle doit avoir, est-ce une MessageBox simple ou bien une avec un signe particulier comme le Point d'Interrogation...
Pour plus d'informations concernant les flags de MessageBox, regardez le fichier suivant : msgbox.html

Modifions msgbox.asm pour inclure une MessageBox.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0

.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start

Assemblez le fichier msgbox.asm et exécutez le nouveau prog msgbox.exe. Vous verrez une boîte de message montrant le texte"Win32 Assembly is Great!".

Regardons de nouveau le code source.
Nous avons définis deux données terminées par un zéro dans la section .data. Rappelez-vous que chaque données ANSI dans Windows doit être terminée par le caractère NULL (0 hexadécimal).
Nous avons aussi employé deux constantes, le NULL et MB_OK. Ces constantes sont définies dans windows.inc. Donc vous pouvez faire référence à leurs noms à la place de leurs valeurs. Cela améliore la lisibilité De votre code source.
L'expression ADDR est employée pour passer l'adresse d'une données ou d'un label à la fonction. C'est valable seulement dans le contexte de la directive 'invoke'. Vous ne pouvez pas l'employer pour assigner l'adresse d'une donnée à un registre / variable, par exemple. Vous devez employer offset au lieu de 'ADDR' dans ce cas. Cependant, il y a quelques différences entre les deux :

  1. addr ne peut pas manipuler à l'avance la référence tandis qu' offset le peut. Par exemple, si la ligne où est rendu le programme, invoke un label qui est défini quelque part plus loin dans le code source, ADDR ne marchera pas.
    2. invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
    ......
    MsgBoxCaption db "Iczelion Tutorial No.2",0
    MsgBoxText db "Win32 Assembly is Great!",0
    MASM indiquera une erreur. Si vous utilisez offset au lieu de addr dans ce petit bout de code, alors MASM l'assemblera avec succès.
  2. addr peut manipuler des variables locales tandis qu' offset ne le peut pas. . Une variable locale est seulement un petit espace réservé sur la pile. Vous ne connaîtrez seulement son adresse que le temps de l'exécution. offset est interprété pendant le déroulement du programme par l'assembleur. Donc il est naturel qu' offset ne travaille pas avec des variables locales. addr est capable de manipuler des variables locales grâce au fait que l'assembleur vérifie d'abord si la variable mentionnée par addr est globale ou locale. Si c'est une variable globale, il met l'adresse de cette variable dans le fichier d'objet. À cet égard, il travaille comme offset. Si c'est une variable locale, il produit un ordre d'instruction qu'il appelle en réalité avant la fonction :
    3. lea eax, LocalVar
    push eax

    Puisque 'lea' peut déterminer l'adresse d'un label pendant l'exécution, ça fonctionne tès bien.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 ASM tutorial 3: Une Simple Fenêtre

Tutorial 3: Une Simple Fenêtre

Dans ce tutorial, nous construirons un programme Win qui ouvre une fenêtre entièrement fonctionnelle sur le bureau. (Un programme Win, est un programme qui tourne sous Windows et non pas comme certains sous DOS).
Downloadez le fichier en exemple ici

Théorie:

Les programmes Win font fréquemment appels aux fonctions API pour leur GUI (NdT : Graphic User Interface). Le GUI c'est l'ensemble des objets tels qu'une boîte de saisie de texte, les boutons... En fait, pour fabriquer un programme Win32 on crée d'abord une fenêtre principale vide(c'est ce qu'on va faire dans ce tutorial) puis on place par dessus des objets du GUI suivant ce qu'on veut. Il faut bien un lien entre la fenêtre principale et ses objets, car il peut aussi y avoir plusieurs fenêtres principales d'ouvertes, ce lien c'est le n°'Handle' de la fenêtre principale qui est donnée comme paramètre à chaque objet pour dire que ces objets font parti de cette fenêtre-ci et non d'une autre. Ceci est à la fois bénéfique pour les utilisateurs et pour les programmeurs. Ainsi les utilisateurs n'ont plus besoin de comprendre comment le GUI de chaques nouveaux programmes fonctionne, le GUI de chaque programme Win est semblable. Pour les programmeurs, les codes du GUI sont déjà à sa disposition, évalués et prêts à être utilisés. Le revers pour les programmeurs c'est la complexité accrue. Pour créer ou manipuler n'importe quels objets du GUI comme des fenêtres, un menu ou des icônes, les programmeurs doivent suivre une recette stricte. Mais ceci peut être surmonté par la programmation modulaire ou le paradigme OOP.
Ci-dessous, je vous montre les étapes exigées pour créer une fenêtre sur le bureau:
  1. Déclarez l'"instance handle" de votre programme (obligatoire)

    2. l'instance handle du prog est l'adresse mémoire de base où windows à chargé l'exe dans le processeur, en général 400000 pour un exe et 100000 pour une DLL. Cette valeur sert à des fonctions win32 pour charger des ressources.
  2. Déclarez la 'command line' (Non exigé à moins que votre programme ne veuille traiter une ligne de commande)

    3. 'GetCommandLine' sert à connaître la ligne de commande lors du lancement de l'appli, des paramètres. exemple -->monprog.exe cmd1,cmd2 ,et tout ce qui est après exe fait partie de la ligne de commande.
  3. Définissez la 'window class' (Obligatoire, à moins que vous utilisiez un type de fenêtre prédéfini telle qu': une MessageBox ou une DialogBox)
  4. Créez la fenêtre (ben oui Obligé)
  5. Affichez la fenêtre sur le bureau (Obligé à moins que vous ne vouliez pas montrer la fenêtre immédiatement)
  6. Rafraichir l'intérieur de la (Client Area) zone de travail de notre fenêtre
  7. Créer un bout de code qui tourne sur lui-même à l'infini, capable de vérifier les messages de Windows (comme l'attente d'une action)
  8. Si des messages sont reçus, ils sont traités par une fonction spécifique
  9. Sortie du programme si l'utilisateur referme la fenêtre
Comme vous pouvez le voir, la structure d'un programme Win est plutôt complexe comparée à un programme fonctionnant sous DOS. Mais le monde de Windows diffère résolument du monde de DOS. Les programmes Win doivent être capables de coexister paisiblement les uns avec les autres. Ils doivent suivre des règles très strictes. Chaque programmeur doit aussi être très stricte avec son style de programmation et d'habitudes.

Contenu:

Ci-dessous voici le code source de notre programme ouvrant une simple fenêtre. Avant une plongée dans les entrailles de la programmation Win32 ASM, regardons quelques points importants qui soulageront votre programmation.
  • Vous devez mettre tous les 'Constants Windows', les structures et les prototypes de fonction dans un fichier INCLUDE (*.inc) et l'inclure au début de votre fichier .asm. Cela vous évitera de gros efforts de réécriture par vous même. Actuellement, la plupart des fichiers INCLUDE ,tel que windows.inc, sont complets. La totalité des fichiers INCLUDE forment le répertoire HUTCH que vous pouvez télécharger sur ma page. Vous pouvez aussi définir vos propres constants et structures mais vous devez les mettre dans un fichier INCLUDE (*.inc) différent de 'windows.inc'.
    Les 'constants Windows', les 'structures' et les 'Prototypes de Fonction' sont toutes des instructions assembleur. Donc si vous n'avez pas compris, on écrit seulement des bouts de programme en ASM dans des fichiers *.inc et quand vous en avez besoin pour un de vos programmes, vos y faites référence grâce à une instruction spéciale :'INCLUDE…'
  • Utilisez la directive includelib pour indiquer quelle bibliothèque d'importation est employée par votre programme. Par exemple, si votre programme appelle la fonction MessageBox, vous devez mettre la ligne :
      • includelib user32.lib
    au début de votre fichier .asm. Cette directive informe MASM que votre programme utilisera certaines fonctions dans cette bibliothèque d'importation. Si vos fonctions d'appels de programme ne font pas parti de la même bibliothèque, ajoutez juste un IncludeLib pour chaque bibliothèque que vous employez. En employant la directive IncludeLib, vous n'avez plus à vous inquiétez des bibliothèques d'importations. Vous pouvez employer /LIBPATH linker comme commutateur pour dire où se trouvent tous les libs et y faire ainsi référence.
  • En déclarant des prototypes de fonction API, des structures, ou des constants pour les inclure dans vos propres fichiers, essayez de respecter les noms d'origines employés par les fichiers INCLUDE de Windows. Ça vous évitera bien des maux de tête à chercher quelque article dans la référence Win32 API.
  • Employez le fichier makefile.bat pour automatiser votre processus d'assemblage. Cela vous évitera encore de tapez pleins de lignes de code à chaque fois.
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib ; Fait appel à des fonctions dans user32.lib et kernel32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.DATA ; Initialise les DATA (ceux qui font déjà parti du programme)
ClassName db "SimpleWinClass",0 ; le nom de Class de votre fenêtre
AppName db "Our First Window",0 ; Le nom de votre fenêtre (c'est son titre)

.DATA? ; Initialise les DATA (ceux qui ne font pas encores parti du programme. EX: les futures saisies au clavier).
hInstance HINSTANCE ? ; l'Instance handle de votre programme
CommandLine LPSTR ?
.CODE ; C'est ici que commence votre code.
start:
invoke GetModuleHandle, NULL ; Récupère l'Instance handle de votre programme.
; Sous Win32, hmodule==hinstance mov hInstance,eax
mov hInstance,eax
invoke GetCommandLine ; Obtient la command line. vous n'avez besoin d'appeler cette fonction QUE SI
; votre programme n'a pas encore de command line.
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT ; Appelle la fonction principale
invoke ExitProcess, eax ; Sort de votre programme. Le code de sortie est retourné dans eax.

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX ; Créez des variables locales sur la pile
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX ; Place les valeurs dans les membres de wc
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc ; Enregistre notre 'Window Class', ainsi elle est définit
invoke CreateWindowEx,NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL
mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow ; Affiche notre fenêtre sur le bureau
invoke UpdateWindow, hwnd ; Régénérez le client area

.WHILE TRUE ; Création d'une boucle pour traiter les messages qui arrivent.
invoke GetMessage, ADDR msg,NULL,0,0 (comme détection du déplacement de la souris)
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam ; Retoune le code de sortie dans eax
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY ; si l'utilisateur ferme notre fenêtre (parce qu'on vient de recevoir un message WM_DESTROY)...alors
invoke PostQuitMessage,NULL ; on quitte notre application
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ; Traitement par défaut de tous les autres messages qui ne nous intéressent pas.
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

Analyse:

Vous pouvez être déconcertés par le fait qu'un programme ouvrant une simple fenêtre exige tant de codes. Mais la plupart de ces codes sont juste *une ossature commune pour tous les prog*. Ces codes vous pouvez les copier d'un fichier de code source à un autre. Ou si vous préférez, vous pourriez assembler quelques-uns de ces codes dans une bibliothèque pour être employés comme une sorte d''introduction' dans votre programme. Ensuite vous pourrez vous contenter d'écrire les lignes de codes dans la fonction WinMain. Où 'WinMain' est en fait un peu comme la procédure principale dans laquelle vous écrirez votre programme. Sachez que c'est ce que font les compilateurs C. Ils vous laissent écrire uniquement les codes de WinMain (ou dans WinMain) sans s'inquiéter du reste. Le seul inconvénient c'est que vous devez avoir une fonction nommée WinMain sinon les compilateurs C ne seront pas capables de combiner vos codes avec ceux de votre INTRODUCTION. Vous n'aurez pas de telles restrictions avec le langage Assembleur. Vous pouvez employer d'autres noms de fonction que 'WinMain' ou bien même aucune fonction du tout
Soyez prévenus. Ça va être un long tutorial. On va analyser ce programme à mort!.
    .386
    .model flat,stdcall
    option casemap:none

    WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

    include \masm32\include\windows.inc
    include \masm32\include\user32.inc
    include \masm32\include\kernel32.inc
    includelib \masm32\lib\user32.lib
    includelib \masm32\lib\kernel32.lib

Les trois premières lignes sont "NECESSAIRES". .386 dit à MASM que nous avons l'intention d'employer le jeu d'instruction de processeur 80386 dans ce programme. .model flat,stdcall dit à MASM que notre programme emploie la mémoire plate adressant le modèle. C'est pourquoi nous employions le paramètre stdcall prenant par défaut cette convention dans notre programme.
Ensuite c'est au tour du prototype de fonction de WinMain. Puisque nous appellerons WinMain plus tard, nous devons définir son prototype de fonction d'abord, pour que nous puissions l'invoquer par la suite.
Nous devons inclure windows.inc au début du code source. Il contient les structures importantes et les constants qui sont employés par notre programme. Le fichier INCLUDE, windows.inc, est simplement un fichier texte. Vous pouvez l'ouvrir avec n'importe quel éditeur de texte. Notez s'il vous plaît que windows.inc ne contient pas toutes les structures et constants (encore). Pour ce qui est du HUTCH (le groupe de fichiers *.inc) j'y travaille.
Notre programme appelle les fonctions API qui résident dans user32.dll (CreateWindowEx, RegisterWindowClassEx, par exemple) et kernel32.dll (ExitProcess), donc nous devons lier notre programme avec ces deux bibliothèques d'importation. La question suivante : comment puis-je savoir qui importe la bibliothèque qui doit être liée avec mon programme ? La réponse : Vous devez savoir où résident les fonctions API appelées par votre programme. Par exemple, si vous appelez une fonction API dans gdi32.dll, vous devez vous lier avec gdi32.lib.
C'est l'approche spécifique de MASM. Pour TASM, la bibliothèque d'importation qui est liée à votre programme est uniquement : import32.lib. C'est beaucoup , beaucoup plus simple.
    .DATA
    ClassName db "SimpleWinClass",0
    AppName db "Our First Window",0

    .DATA?
    hInstance HINSTANCE ?
    CommandLine LPSTR ?

Ci-dessus on trouve les 2 sections pour les "DATA".
Dans .DATA, on déclare deux données terminées chacune par zéro : ClassName est le nom de notre classe de fenêtre (de notre Window Class).
'AppName' est le nom de notre fenêtre. Notez que les deux variables sont initialisées.
Dans .DATA?, Deux variables sont déclarées : HINSTANCE (l'Instance Handle de notre programme) et CommandLine (ligne de commande de notre programme). Ces deux données sont extrêmement courrantes, HINSTANCE et LPSTR, sont vraiment de nouveaux noms (de type: DWORD). Vous pouvez les voir dans windows.inc. Notez que chacunes des variables se trouvant dans la section .DATA ? ne sont pas initialisées, c'est-à-dire qu'elles ne doivent pas contenir de valeur spécifique en démarrage, mais nous réservons de l'espace pour notre future utilisation.
    .CODE
    start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke GetCommandLine
    mov CommandLine,eax
    invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
    invoke ExitProcess,eax
    .....
    end start
.CODE contient toutes vos instructions ASM. Vos codes doivent résider entre le label " Start : " et le label " End Start ". Le nom du label est arbitraire. Vous pouvez le nommer comme ça vous chante tant qu'il est unique et ne viole pas la convention de nom de MASM. Par exemple vous ne pouvez pas prendre le mot 'Mov' comme nom de label.
Notre première instruction est l'appel GetModuleHandle pour aller chercher l'Instance handle de notre programme. Sous Win32, l' Instance Handle et Module Handle sont les mêmes. Vous pouvez vous représenter l'Instance Handle comme le n° d'identification de votre programme. Il est employé comme paramètre par plusieurs des fonctions API que notre programme doit appeler, donc c'est généralement une bonne IDée de le déclarer dès le début de notre programme.
Notez : En réalité sous win32, l'Instance Handle est l'adresse linéaire de votre programme dans la mémoire.
Après avoir fait appel à une fonction de Win32 (une API), la valeur de retour de cette fonction, est placée dans eax. Toutes les autres valeurs sont renvoyées par des variables que vous avez vous-même défini avant le Call qui appel cette fonction.
Une fonction Win32 (ou une procédure) que vous appelez(Call) souvent préservera les Registres de segment ebx, edi, esi et le Registre ebp. Au contraire, ecx et edx sont considérés comme des Registres de travail et sont toujours indéfinis après la sortie d'une fonction de Win32 (sortie d'une procédure).
Notez : n'espérez pas que les valeurs d'eax, ecx, edx soient préservés après un appel de fonction API.
La ligne qui suit un appel (un call) à une fonction d'API, attend en retour une valeur dans eax. Dès que vous appelez une fonction API de Windows, vous devez utiliser la règle suivante : préservez puis rétablissez les valeurs des Registres de segment ebx, edi, esi et ebp après le retour de la fonction sinon votre programme plantera peu de temps après, ceci est valable pour vos procédures Windows et pour les fonctions de rappel de service Windows.
L'appel 'GetCommandLine'est inutile si votre programme ne traite pas de ligne de commande. Dans cet exemple, je vous montre comment l'appeler dans le cas où vous en auriez besoin dans votre programme.
Vient ensuite l'appel de WinMain. Ici il reçoit quatre paramètres : l'Instance Handle de notre programme, l'Instance Handle du précédent Instance de notre programme (un programme de plus haut niveau qui aurait appeler le notre), la Command Line et l'état de la fenêtre à sa première apparition. Sous Win32, il n'y a AUCUN Instance précédent(premier). Chaque programme est seul dans son espace d'adresse, donc la valeur d'hPrevInst est toujours 0. C'est un des restes de Win16 quand tous les programmes étaient encore exécutés dans un même espace d'adresses pour savoir si c'était le premier Instance ou non. Donc sous win16, si hPrevInst est NUL, alors c'est que c'est le premier Instance.
Notez : Vous n'êtes pas obligé de déclarer le nom de fonction en tant que : 'WinMain'. En fait, vous avez une totale liberté à cet égard. Et même, vous pouvez ne pas employer de fonction WinMain-équivalent du tout. Le but est de placer votre code (votre véritable programme) à l'intérieur de la fonction WinMain à côté de GetCommandLine et votre programme sera toujours capable de fonctionner parfaitement.
A la sortie de WinMain, eax est rempli du code de sortie. Nous passons ce code de sortie comme paramètre à ExitProcess qui sert à terminer notre programme.

WinMain proc Inst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

La susdite ligne est la déclaration de la fonction WinMain. Notez l'ensemble des paramètres qui suivent la directive PROC. Ce sont les paramètres que WinMain reçoit de la fonction qui l'appelle. Vous pouvez utiliser les noms de ces paramètres au lieu de vous servir de la pile pour pousser la valeurs de vos paramètres.

LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

La directive LOCAL alloue de la mémoire sur la pile pour des variables locales employées par la fonction. La totalité des directives LOCALES doivent être placées juste au-dessous de la directive PROC. La directive LOCAL est immédiatement suivie par " le nom de la variable locale " ; " le type de variable ". Et donc 'LOCAL wc:WNDCLASSEX' dit à MASM de réserver de la mémoire sur la pile. Nous pourrons ensuite nous référer à wc dans notre code sans aucune difficulté engendrée par la manipulation de la pile. C'est vraiment une aubaine, je pense. le revers est le fait que les variables locales ne peuvent pas être employées à l'extérieur de la fonction, elles sont créés et seront automatiquement détruites quand la fonction retourne à l'appel. Un autre inconvénient est que vous ne pouvez pas initialiser des variables locales automatiquement parce qu'elles sont uniquement allouées dans la mémoire de la pile au moment seulement où la fonction est entrée. Vous devez les assigner manuellement avec les valeurs désirées après les directives 'LOCAL'.

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInstance
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc

Les lignes ci-dessus sont vraiment simples par leur concept. Il s'agit juste de plusieurs lignes d'instruction regroupées. Le concept qui se cache derrière toutes ces lignes c'est d'initialiser la Windows class. Une 'Windows Class' n'est rien de plus que la première ébauche d'une fenêtre. On définit donc plusieurs caractéristiques importantes d'une fenêtre comme son icône, son curseur, la fonction qui la contrôle, sa couleur etc. Vous définisez la fenêtre grâce à sa 'Windows Class'. C'est en quelque sorte le but de ce concept. Si vous voulez créer plusieurs fenêtres avec les mêmes caractéristiques, il serait temps de penser à stocker toutes ces caractéristiques dans un unique endroit (donc dans une procédure par exemple) et de s'y référer quand c'est nécessaire. Cet arrangement économisera pas mal de mémoire en évitant la duplication d'informations. Windows doit être très efficaces dans l'utilisation des ressources mémoires, qui restent encore rares.
Faisons le point : Dès que vous définissez votre propre fenêtre, vous devez définir ses caractéristiques grâce à l'instruction WNDCLASS ou WNDCLASSEX et ensuite appeler RegisterClass ou RegisterClassEx avant que vous ne soyez capables de créer votre fenêtre. Vous devrez donner une 'Windows Class' pour chaque type de fenêtre que vous voulez créer.
Windows possède plusieurs 'Windows Class' déjà prédéterminées, comme les boutons et la boîte d'édition (pour entrer un texte). Pour ces fenêtres (ou ces Controls), vous n'avez pas besoin de déclarer une 'Windows Class', appelez juste CreateWindowEx avec le nom de classe prédéterminée.
L'élément le plus important de WNDCLASSEX est lpfnWndProc. Lpfn attend un 'Pointeur Long' pour fonctionner. Sous Win32, il n'y a pas de Pointeur(proche) ou de Pointeur(lointain), mais un simple Pointeur grâce au nouveau modèle de mémoire Flat(PLAT). Mais c'est encore un reste des vieux jours de Win16. Chaque 'Windows Class' doit être associée à une procédure appelée par Windows. La procédure qui sert à créer vos fenêtres sera responsable du traitement de chaque message associée la à Windows Class. Les fenêtres enverront des messages vers la procédure qui les gère pour lui notifier quels événements importants sont utilisés par ces fenêtres, comme l'utilisation du clavier ou de la souris. Il ne reste plus à la procédure (qui s'occupe de la gestion des fenêtres) qu'à répondre intelligemment à chaque message qu'elle reçoit (Ex le fait de bouger la souris envoie un message dans cette procédure pour qu'on puisse en faire qque chose d'intéressant etc..). La plupart du temps vous écrirez des enchaînements d'événements dans cette procédure.
Ci-dessous, Je vous détaille la construction de la fonction WNDCLASSEX:

WNDCLASSEX STRUCT DWORD
cbSize DWORD ?
style DWORD ?
lpfnWndProc DWORD ?
cbClsExtra DWORD ?
cbWndExtra DWORD ?
hInstance DWORD ?
hIcon DWORD ?
hCursor DWORD ?
hbrBackground DWORD ?
lpszMenuName DWORD ?
lpszClassName DWORD ?
hIconSm DWORD ?
WNDCLASSEX ENDS

cbSize: La taille de la structure WNDCLASSEX en octets. Nous pouvons employer l'opérateur SIZEOF pour obtenir (initialiser) cette valeur.
style: Le style (ou le type) de fenêtre créée pour cette 'Class'. Vous pouvez combiner plusieurs types ensembles en employant l'opérateur "or".
lpfnWndProc: L'adresse de la procédure responsable de la gestion des fenêtres créées dans cette 'Class'.
cbClsExtra: Indique le nombre d'octets supplémentaires à allouer en fonction de la structure(ses éléments) de la Windows Class choisie. Le système d'exploitation initialise les octets à zéro. Vous pouvez stocker des données spécifiques à la Windows Class ici.
cbWndExtra: Indique le nombre d'octets supplémentaires à allouer en fonction de la 'Windows Instance'. Le système d'exploitation initialise les octets à zéro.
hInstance: l'Instance Handle de ce module (de votre prog).
hIcon: Handle de l'icône. Obtenez-le après l'appel de 'LoadIcon' (ou en retour de l'appel de 'LoadIcon').
hCursor: Handle du curseur. Obtenez-le près l'appel de 'LoadCursor'.
hbrBackground: Couleur du fond de toutes les fenêtres créées dans cette 'Class'.
lpszMenuName: L'Handle du menu des fenêtres créées par cette 'Class'. C'est une valeur par défaut.
lpszClassName: Le nom de cette 'Class'.
hIconSm: L'Handle d'une petite icône associée à cette 'Class' de fenêtres. Si cette valeur est un NULL, le système recherche dans la 'ressource d'icône' indiquée par le membre hIcon, une icône de la taille appropriée sera employée en tant que petite icône.

invoke CreateWindowEx, NULL,\
ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
CW_USEDEFAULT,\
NULL,\
NULL,\
hInst,\
NULL

Après l'enregistrement de la Window Class, nous pouvons appeler CreateWindowEx pour créer notre fenêtre basée sur la Window Class ainsi définie. Remarquez qu'il y a 12 paramètres à cette fonction, beuurr ! ! !

CreateWindowExA proto dwExStyle:DWORD,\
lpClassName:DWORD,\
lpWindowName:DWORD,\
dwStyle:DWORD,\
X:DWORD,\
Y:DWORD,\
nWidth:DWORD,\
nHeight:DWORD,\
hWndParent:DWORD ,\
hMenu:DWORD,\
hInstance:DWORD,\
lpParam:DWORD

On va voir la description détaillée de chaque paramètre :
dwExStyle: Styles de fenêtre supplémentaires. C'est le nouveau paramètre qui est ajouté à la vieille fonction 'CreateWindow'. Vous pouvez mettre des nouveaux styles de fenêtre pour Windows 95 & NT ici. En temps normal, vous pouvez spécifier votre style de fenêtre ordinaire dans dwStyle, mais si vous voulez certains styles spéciaux pour une fenêtre 'tip top', vous devez les spécifier ici. Vous devez employer un NULL(un 0) si vous ne voulez pas de styles de fenêtre supplémentaires.
lpClassName: (obligatoire).L'adresse de la suite de caractères ASCII contenant le nom de la Window Class que vous voulez employer en tant que gabarit pour cette fenêtre. La Class peut être votre propre Class que vous venez de créer ou bien une Window Class prédéterminée. Comme exposé ci-dessus, chaque fenêtre que vous avez créée doit être basée sur une Window Class. (Normal ! , puisque la Window Class sert justement à définir les propriétés de la fenêtre à créer)
lpWindowName: Adresse de la suite de caractères ASCII contenant le nom de la fenêtre. Sera en fait la titre de la fenêtre (en haut à gauche). Si ce paramètre est NULL, la barre de titre de la fenêtre sera vide.
dwStyle: C'est le style de la fenêtre. Vous pouvez spécifier ici l'apparition de la fenêtre. Mettez un NULL pour ce paramètre dwSTYLE est possible mais la fenêtre n'aurait aucun menu, n'aurait pas de boutons de réduction-agrandissement ni aucun autre bouton. La fenêtre n'aurait pas beaucoup d'intérêt. Vous devrez même appuyer sur Alt+F4 pour la refermer. Le style de fenêtre le plus commun est le paramètre 'WS_OVERLAPPEDWINDOW' (et non pas le paramètre NULL). Ainsi vous pouvez combiner plusieurs styles de fenêtre grâce à l'opérateur "OR" pour faire apparaître la fenêtre que vous désirez. Le style WS_OVERLAPPEDWINDOW est en réalité une combinaison des styles de fenêtres les plus communs.
X,Y: C'est la coordonnée du coin supérieur gauche de la fenêtre. Normalement on prend le paramètre CW_USEDEFAULT comme valeur, comme ça Windows décidera pour vous où placer la fenêtre sur le bureau.
nWidth, nHeight: Ce sont les largeur et hauteur de la fenêtre en pixels. Vous pouvez aussi employer CW_USEDEFAULT pour laisser Windows choisir la largeur appropriée et la hauteur à votre place.
hWndParent: C'est l'Handle de la fenêtre parente de votre fenêtre (si celle-ci existe). Ce paramètre indique à Windows si cette fenêtre est un enfant (Window Child) d'une autre fenêtre et, si elle l'est, qui est la fenêtre parente. Notez que ce n'est pas la même relation Parent-Enfant que les 'multiple document interface' ou (MDI). Les 'Window Child' n'ont pas forcément les mêmes caractéristiques que leur 'Window Parent'. Ces caractéristiques sont uniquement spécifiques à l'utilisation interne de chaque fenêtre. Si la fenêtre parente est détruite, toutes ses fenêtres enfant seront détruites automatiquement. C'est vraiment très simple. Dans notre exemple, il y a seulement une fenêtre, donc ce paramètre reste NULL.
hMenu: C'est l'Handle du menu de la fenêtre. Il est NULL si la Class Menu est utilisée. Revenez en arrière à un des membres de l'instruction WNDCLASSEX, soit 'lpszMenuName'. lpszMenuName indique le menu par *default* pour la 'Window Class'. Chaque fenêtre de cette Window Class aura le même menu par défaut. À moins que vous ne spécifiiez un menu *overriding* pour une fenêtre spécifique via son paramètre hMenu. hMenu est en réalité un paramètre à double usage. Si la fenêtre que vous souhaitez créer utilise un 'Type de Fenêtre' (Style) prédéterminé (c'est-à-dire un Control), un tel Control ne peut pas posséder de menu. hMenu est employé comme ID de ce Control au lieu de cela. Windows peut décider si hMenu est vraiment un Handle de menu ou un Control ID en regardant le paramètre lpClassName. Si c'est le nom d'une Window Class prédéterminée, hMenu est un Control ID. Si ce n'est pas, ce sera alors l' Handle du menu de votre fenêtre.
hInstance: l'Instance Handle pour le module (la partie) du programme créant la fenêtre.
lpParam: Le pointeur facultatif pour des données a passer à la fenêtre. Il est employé par la fenêtre MDI pour passer les données CLIENTCREATESTRUCT. Normalement, cette valeur est mise au NULL, signifiant que l'on ne passe aucunes données via CreateWindow (). La fenêtre peut prendre la valeur de ce paramètre après l'appel à la fonction GetWindowLong.

mov hwnd,eax
invoke ShowWindow, hwnd,CmdShow
invoke UpdateWindow, hwnd

En retour couronné de succès de CreateWindowEx, l'Handle de la fenêtre est placée dans eax. Nous devons garder cette valeur pour une utilisation future. La fenêtre que nous venons de créer n'est pas automatiquement affichée. Vous devez appeler 'ShowWindow' avec l'Handle de fenêtre et la *résolution* désirée de la fenêtre pour la faire apparaître à l'écran. Ensuite vous pourrez appeler 'UpdateWindow' pour ordonner à votre fenêtre de repeindre son secteur de client (de réactualiser son contenu). Cette fonction est utile à chaque fois que vous voulez mettre à jour le contenu du secteur client. Vous pouvez omettre cet appel quoique.

.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW

Cette fois-ci, notre fenêtre est sur l'écran. Mais elle ne peut pas recevoir d'informations extérieur (comme une saisie de texte ou bien la détection de la pression d'un bouton). Donc nous devons l'informer de ce qui se passe. Nous faisons ça avec une boucle de message. Il y a seulement une boucle de message pour chaque module. Cette boucle de message vérifie continuellement les messages de Windows grâce à un 'Call GetMessage'. GetMessage passe une donnée à une structure de MESSAGE de Windows. Cette structure de MESSAGE sera remplie de l'information du message que Windows veut envoyer à une fenêtre dans le module. La fonction GetMessage ne retournera pas (ne reviendra pas à votre programme mais au contraire restera dans Kernel ou User) tant qu'un message (ex : appui sur un bouton) ne sera pas transmis pour une fenêtre dans le module. Pendant ce temps-là, Windows peut donner le contrôle à d'autres programmes. C'est en quelque sorte le fonctionnement en Multigestion de la plate-forme Win16. GetMessage renverra une erreur si le Message WM_QUIT est reçu dans la boucle de message, et ainsi nous termineront la boucle et irons vers la sortie du programme.
TranslateMessage est une fonction utile qui saisie les entrées de clavier (à la volée) et produit un nouveau Message WM_CHAR qui est placé sur la file d'attente des messages. Le message avec WM_CHAR contient la valeur ASCII pour la touche pressée, c'est bien plus facile de procéder ainsi. Vous pouvez omettre cet appel si votre programme ne traite pas de frappes.
'DispatchMessage' redirige les données d'un message( ex : envoi : on vient d'appuyer sur le bouton) à la procédure responsable de la fenêtre spécifique.

mov eax,msg.wParam
ret
WinMain endp

Si la boucle de message se termine, le code de sortie est stocké dans le membre wParam de la structure de MESSAGE. Vous pouvez stocker ce code de sortie dans eax pour le rendre à Windows. Actuellement, Windows ne se sert pas de la valeur de retour, mais c'est mieux de respecter les règles.

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

C'est notre procédure Win. Vous ne devez pas la nommer WndProc. Le premier paramètre, hWnd, est l' Handle de la fenêtre pour laquelle le message est destiné. uMsg est le message. Notez qu'uMsg n'est pas une structure de MESSAGE. C'est juste un numéro(nombre). Windows définit des centaines de messages, dont la plupart ne seront pas intéressants pour vos programmes. Windows enverra un message approprié à une fenêtre seulement si quelque chose d'intéressant arrive à cette fenêtre. La procédure qui traite vos fenêtres reçoit le message et y réagit intelligemment. wParam et lParam sont juste des paramètres supplémentaires pour l'utilisation de certains messages. Certains messages envoient des données d'accompagnement en plus du message lui-même. On passe ces données à la procédure de fenêtre au moyen de lParam et wParam.

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

Vient ici la partie cruciale. C'est l'endroit où la plupart de l'intelligence de votre programme réside. Les codes qui répondent à chaque message de Windows sont dans la procédure de fenêtre (la procédure qui s'occupe du contrôle de votre fenêtre). Votre code doit vérifier le message de Windows pour voir si c'est un message intéressant. S'il l'est, faites tout ce que vous voulez faire en réponse à ce message et retourner ensuite la valeur zéro dans eax. S'il ne l'est pas, vous DEVEZ appeler l'API 'DefWindowProc', pour lui passer tous les paramètres que vous y avez reçu pour les traiter par défaut .'DefWindowProc' est une fonction API qui traite les messages qui ne sont pas intéressant pour votre programme.
Le seul message que vous pouvez et DEVEZ envoyer vers votre procédure qui traite les messages est WM_DESTROY. Ce message est envoyé à votre procédure de fenêtre pour la refermée. Au moment où votre procédure de fenêtre reçoit ce message, votre fenêtre est déjà enlevée de l'écran. C'est juste un renseignement comme quoi votre fenêtre a bien été détruite, vous devez vous préparer à retourner à Windows. Si vous utilisez cela, vous n'avez d'autres choix que de quitter. Si vous voulez avoir une chance d'arrêter la fermeture de votre fenêtre, vous devez utiliser le message WM_CLOSE. Maintenant revenons à WM_DESTROY, après cette fermeture radicale, vous devez appeler PostQuitMessage qui postera WM_QUIT en retour à votre module(traitement des message). WM_QUIT fera le retour de GetMessage avec la valeur zéro dans eax, qui à son tour, terminera la boucle de message et quittera Windows. Vous pouvez envoyer le message WM_DESTROY à votre propre procédure de fenêtre en appelant la fonction DestroyWindow.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 4: Painting with Text

Tutorial 4: Dessiner du texte
(Painting with Text)

Dans ce tutorial, nous apprendrons comment "peindre" le texte dans 'le secteur client' (la zone de travail) d'une fenêtre. Nous verrons aussi tout le contexte autour de cette fonction. Vous pouvez télécharger le code source ici.

Théorie:

Le texte dans Windows est un type d'objet du GUI (NdT : Graphic User Interface). Chaque caractère est composé de plusieurs pixels (les points) qui sont regroupés dans un ordre particulier. C'est pourquoi on peut appeler "painting" au lieu de writting". Normalement, vous peignez le texte dans votre propre secteur client (en réalité, vous pouvez peindre le secteur client extérieur mais c'est une autre histoire). L'affichage du texte sur l'écran sous Windows diffère résolument du DOS. Sous DOS, la dimension de l'écran est 80x25. Mais sous Windows, l'écran est partagé par plusieurs programmes. Des règles doivent être prises pour éviter que certains programmes s'affichent par dessus l'application d'autres programmes. Windows contrôle ceci en limitant la peinture du secteur de chaque fenêtre à son propre secteur client seulement. La taille du secteur client d'une fenêtre n'est pas aussi rigide. L'utilisateur peut changer sa taille n'importe quand. Donc vous devez déterminer les dimensions de votre propre secteur client.
Avant que vous ne puissiez peindre quelque chose sur le secteur client, vous devez demander la permission à Windows. C'est ça, vous n'avez pas le contrôle absolu de l'écran comme si vous étiez sous DOS. Vous devez demander à Windows la permission de peindre votre propre secteur client. Windows déterminera la taille de votre secteur client, la taille des caractères, les couleurs et les autres GDI attribués et ensuite renvoie à votre programme l'Handle du 'contexte de ce dispositif' (de tout ça). Vous pourrez alors employer le 'contexte de dispositif' comme un passeport à la peinture sur votre 'secteur client'.
C'est quoi le 'contexte de dispositif' ? C'est juste une structure de données conçue à intérieure de Windows (pas accessible pour nous). Un 'contexte de dispositif' est associé à un dispositif particulier, comme une imprimante ou l'affichage vidéo. Pour une exposition vidéo, un contexte de dispositif est d'habitude associé à une fenêtre particulière en exposition.
Quelques-unes des valeurs dans le contexte de dispositif sont des attributs graphiques comme des couleurs, la font etc. Ce sont des valeurs par défaut que vous pouvez changer à volonté. Il est possible pour réduire la charge de travail de spécifier ces attributs dans chaques appels de fonction GDI.
Vous pouvez vous représenter un 'contexte de dispositif' comme un 'environnement par défaut' mis à votre disposition par Windows. Vous pouvez ne pas tenir compte de certains éléments par défaut puis y revenir plus tard si vous le souhaitez.
Quand un programme a besoin de peindre, il doit obtenir un 'Handle de contexte de dispositif'. Normalement, il y a plusieurs façons de l'obtenir.
    Appelez BeginPaint en réponse au message WM_PAINT.
    Appelez GetDC en réponse à d'autres messages.
    Appelez CreateDC pour créer votre propre contexte de dispositif.
Rappeler vous une chose, après avoir obtenu un 'Handle de contexte de dispositif', vous devez le sortir pendant le traitement d'un simple message. Ne récupérez pas un Handle en réponse à un message, transférez-le en réponse à un autre message.
Windows envoie des messages WM_PAINT à une fenêtre pour lui indiquer qu'il est maintenant temps de repeindre son secteur client. Windows ne sauve pas le contenu du secteur client de la fenêtre, mais au lieu de cela, quand une situation arrive et garantit une nouvelle peinture du secteur client (comme quand une fenêtre a été couverte par une autre et vient la recouvrir), Windows met le message WM_PAINT dans la file d'attente des messages de cette fenêtre. C'est à cette fenêtre de repeindre son propre secteur client. Vous devez réunir toute l'information qui sert à repeindre votre secteur client dans la section WM_PAINT de votre procédure de fenêtre, ainsi la procédure de fenêtre (la procédure qui gère l'affichage et le comportement de votre fenêtre) pourra repeindre le secteur client quand le message WM_PAINT arrivera.
Un autre concept que vous devez prendre en compte est une petite zone rectangle. Windows définit un rectangle invalide comme le plus petit secteur rectangulaire dans le secteur client qui a besoin d'être repeint (rafraîchi) . Quand Windows détecte un rectangle invalide dans le secteur client d'une fenêtre, il envoie le message WM_PAINT à cette fenêtre. En réponse à ce message WM_PAINT, la fenêtre peut obtenir une structure 'Paintstruct' qui contient, entre autre, les coordonnées du rectangle invalide. Vous appelez 'BeginPaint' en réponse au message WM_PAINT pour valider le rectangle invalide. Si vous ne traitez pas le message WM_PAINT, alors vous devez appeler DefWindowProc ou ValidateRect pour valider le rectangle invalide sinon à plusieurs reprises Windows vous enverra le message WM_PAINT. Laissez tombé ça, c'est trop confus.
Voici ci-dessous les étapes que vous devez exécuter en réponse à un message WM_PAINT :
    Obtenez l' 'handle du contexte de dispositif' avec BeginPaint.
    Repeignez le secteur client (la zone de travail de votre fenêtre).
    Libérez l' 'handle du contexte de dispositif' avec EndPaint.
Notez que vous ne devez pas explicitement valider le rectangle invalide. C'est le Call BeginPaint qui s'en occupe automatiquement. Entre la paire de BeginPaint-Endpaint, vous pouvez appeler n'importe quelles fonctions du GDI pour peindre votre secteur client. Presque tous exigent l' 'handle du contexte de dispositif' en tant que paramètre.

Contenu:

Nous allons écrire un programme qui fait apparaître le texte " Win32 assembly is great and easy!" au le centre du secteur client.
.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib

.DATA
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
OurText db "Win32 assembly is great and easy!",0

.DATA?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.CODE
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax, eax
ret
WndProc endp
end start

Analyse:

Le code dans sa grande majorité est le même que celui de l'exemple du Tutorial 3. J'expliquerai donc seulement les changements importants.

LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL rect:RECT

Voici les variables locales qui sont employées par des fonctions du GDI dans notre section WM_PAINT. 'Hdc' est employé pour stocker l'handle du contexte de dispositif renvoyé après l'appel de la fonction BeginPaint. 'ps' est une structure de PAINTSTRUCT. Normalement ce n'est pas vous qui donnez une valeur à ps. On le passe à la fonction BeginPaint et Windows lui donne la valeur appropriée. De la même façon, vous passez 'ps' à la fonction EndPaint quand vous finissez de repeindre le secteur client. 'rect' est une structure rect définie comme suit :

RECT Struct
left LONG ?
top LONG ?
right LONG ?
bottom LONG ?
RECT ends
Left et top sont les coordonnées du coin supérieur gauche d'un rectangle Right and bottom sont les coordonnées du coin inférieur droit. Une chose à se souvenir : l'origine des axes x-y est au coin supérieur gauche du secteur client. Donc le point y=10 est AU-DESSOUS du point y=0.

invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke GetClientRect,hWnd, ADDR rect
invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \
DT_SINGLELINE or DT_CENTER or DT_VCENTER
invoke EndPaint,hWnd, ADDR ps

En réponse au message WM_PAINT, vous appelez BeginPaint avec la structure PAINTSTRUCT non initialisée et l'handle de la fenêtre que vous souhaitez peindre, comme paramètres. Après un appel couronné de succès, eax contient l'Handle du contexte de dispositif. Ensuite vous appelez GetClientRect pour retrouver la dimension du secteur client. La dimension est renvoyée dans la variable rect que vous passez à DrawText comme un de ses paramètres. La syntaxe de DrawText est :

DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD, uFormat:DWORD

DrawText est une production de texte de haut niveau créé par une fonction API. Il manipule quelques renseignements peu coutumiers tels que le 'word wrap' (l'enveloppe autour des mots), ou le 'centering' (l'allignement au centre) etc… que vous pourriez appliquez sur le texte que vous souhaitez peindre. Son équivalent de bas niveau, TextOut, sera examiné dans le Tutorial suivant. DrawText définit un texte pour qu'il loge dans les limites d'un rectangle. Il emploie la fonte (la taille, la police) actuellement choisie, la couleur et le fond (dans le contexte de dispositif) pour dessiner le texte. Les lignes sont encadrées pour rester dans les limites du rectangle. Il rend la hauteur du texte produit dans les unités du dispositif, qui dans notre cas sont en pixels. Regardons de plus près ces paramètres :

    hdc est l'handle du contexte de dispositif
    lpString est le pointer de la chaîne de caractères que vous voulez dessiner dans le rectangle. Cette chaîne de caractères doit être terminée par un NULL (0) sinon avec l'autre méthode vous devez spécifier sa longueur dans le paramètre suivant, nCount.
    nCount est le nombre de caractères à afficher. Si la chaîne de caractères est terminée par un NULL (cas précédent), nCount doit être mis à -1. Autrement nCount doit contenir le nombre de caractères de la chaîne que vous voulez dessiner.
    lpRect est le pointer du rectangle (de l'objet de type RECT) que vous voulez dessiner contenant la chaîne de caractères. Notez que ce rectangle est aussi un rectangle de coupure, c'est-à-dire que vous ne pouvez pas dessiner le texte à l'extérieur de ce rectangle.
    uFormat est la valeur qui indique comment la chaîne de caractère est insérée dans le rectangle. Nous employons trois valeurs combinées par l'opérateur "or" :
    • DT_SINGLELINE indique une simple (une unique)ligne de texte.
    • DT_CENTER centre le texte horizontallement.
    • DT_VCENTER centre le texte verticalement. Il doit être employé avec DT_SINGLELINE.
Après que vous ayez fini de repeindre le secteur client, vous devez appeler la fonction EndPaint pour libérer l'handle du contexte de dispositif.
Voilà. Nous pouvons récapituler les points importants:
  • On appelle la paire BeginPaint - EndPaint en réponse au message WM_PAINT.
  • Faites tout ce que vous voulez dans le secteur client entre les appels BeginPaint et EndPaint.
  • Si vous voulez repeindre votre secteur client en réponse à d'autres messages, vous avez deux choix :
    (Si il y a une communication entre l'ordi et vous et que vous n'êtes pas en mode texte mais en mode Painting, vous avez deux choix)
    • Employez la paire GetDC - ReleaseDC et faîtes votre 'painting' entre ces 2 appels.
    • Appelez InvalidateRect ou UpdateWindow; pour préserver le secteur client entier de toute écriture, forçant ainsi Windows à renvoyer le message WM_PAINT dans la file d'attente de message de votre fenêtre et faites votre peinture dans la section WM_PAINT.
[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 5: More about Text

Tutorial 5: Encore plus
A propos des Textes

Allons plus loin avec les attributs de texte, c'est-à-dire. La fonte (Fonte = Assortissement complet de caractères de même type) et la couleur.

Downloadez le fichier en exemple ici.

Théorie:

Le système de couleur de Windows est basé sur le système RGB, R=red, G=Green, B=Blue. Si vous souhaitez définir une couleur dans Windows, vous devez exprimer votre couleur recherchée en combinant ces trois couleurs principales. Chaque plage de couleur (R,G, ou B) a une gamme de 0 à 255 (valeur exprimée en octet). Par exemple, si vous voulez la couleur rouge pur, vous devez employer 255,0,0. Ou si vous voulez la couleur blanc pur, vous devez employer 255,255,255. Vous verrez en expérimentant, qu'obtenir la couleur dont vous avez besoin est très difficile avec ce système puisque vous devez avoir un bon 'felling' pour mélanger les couleurs correspondantes.
Pour la couleur du texte et du fond, vous employez SetTextColor et SetBkColor, tous deux exigent l'handle du contexte de dispositif et une valeur RGB sur 32 bits. La structure de la valeur 32 bits RGB est définie ainsi :

RGB_value struct
unused db 0
blue db ?
green db ?
red db ?
RGB_value ends

Notez que le premier octet n'est pas utilisé et doit être à zéro. L'ordre des trois octets restant est inversé, il s'agit du Bleu, vert, rouge (et non pas RGB). Cependant, nous n'emploierons pas cette structure puisque elle est encombrante à initialiser et à employer. Nous créerons une macro à la place. La macro recevra trois paramètres : les valeurs rouges, vertes et bleues. Ceci produira la valeur RGB désirée, et le stockera sur 32 bits dans eax. La macro est la suivante :

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

Vous pouvez mettre cette macro dans le fichier INCLUDE pour une utilisation future.
Vous pouvez "créer" une fonte en appelant CreateFont ou CreateFontIndirect. La différence entre ces deux fonctions est que CreateFontIndirect reçoit seulement un paramètre : 'un indicateur logique sur la structure de la fonte', à savoir : LOGFONT. CreateFontIndirect est le plus flexible de deux en particulier si vos programmes ont besoin de changer fréquemment de fontes. Cependant, dans notre exemple, nous "créerons" seulement une fonte pour la démonstration, nous échapperons donc à CreateFont. Après l'appel de CreateFont, il renverra l'handle d'une fonte que vous devez choisir dans le contexte de dispositif. Après cela, chaque texte affiché par la fonction API (Paint) emploiera la fonte que nous avons choisie dans le contexte de dispositif.

Contenu:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

RGB macro red,green,blue
xor eax,eax
mov ah,blue
shl eax,8
mov ah,green
mov al,red
endm

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
TestString db "Win32 assembly is great and easy!",0
FontName db "script",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
nbsp; WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT
LOCAL hfont:HFONT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName
invoke SelectObject, hdc, eax
mov hfont,eax
RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax
invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString
invoke SelectObject,hdc, hfont
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

end start

Analyse:

invoke CreateFont,24,16,0,0,400,0,0,0,OEM_CHARSET,\
OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,\
DEFAULT_QUALITY,DEFAULT_PITCH or FF_SCRIPT,\
ADDR FontName

CreateFont crée une fonte logique(mélangée RGB) qui est la couleur la plus proche des paramètres sélectionnés et des données de fonte disponibles. Cette fonction a plus de paramètres qu'aucune autre fonction de Windows. Il renvoie l'handle de la fonte logique qui doit être utilisé par la fonction SelectObject. Nous allons examiner ces paramètres en détail.

CreateFont proto nHeight:DWORD,\
nWidth:DWORD,\
nEscapement:DWORD,\
nOrientation:DWORD,\
nWeight:DWORD,\
cItalic:DWORD,\
cUnderline:DWORD,\
cStrikeOut:DWORD,\
cCharSet:DWORD,\
cOutputPrecision:DWORD,\
cClipPrecision:DWORD,\
cQuality:DWORD,\
cPitchAndFamily:DWORD,\
lpFacename:DWORD

nHeight est la hauteur désirée des caractères. On prend 0 par défaut pour utiliser la taille moyenne.
nWidth est la largeur désirée des caractères. Normalement cette valeur doit être 0,ce qui permet à Windows de faire correspondre la largeur à la hauteur. Cependant, dans notre exemple, la largeur par défaut fait que les caractères sembles difficile à lire, donc on emploiera la largeur 16 à la place de cela.
nEscapement Sert à spécifier l'inclinaison des caractères qui suivent cette instruction. On compte en dixième de degré. Normalement, cette valeur est à 0. On la met à 900 pour avoir tous les caractères va vers le haut, à 1800 pour écrire vers l'arrière, ou 2700 pour écrire chaque caractère du haut vers le bas.
nOrientation Indique de combien doivent être tournés les caractères (en dixième de degré). 900 pour avoir tous les caractères couchés tournés vers le haut, 1800 pour le contraire, etc.
nWeight Donne l'épaisseur de chaque caractère. Windows définit les tailles suivantes :

    FW_DONTCARE equ 0
    FW_THIN equ 100
    FW_EXTRALIGHT equ 200
    FW_ULTRALIGHT equ 200
    FW_LIGHT equ 300
    FW_NORMAL equ 400
    FW_REGULAR equ 400
    FW_MEDIUM equ 500
    FW_SEMIBOLD equ 600
    FW_DEMIBOLD equ 600
    FW_BOLD equ 700
    FW_EXTRABOLD equ 800
    FW_ULTRABOLD equ 800
    FW_HEAVY equ 900
    FW_BLACK equ 900
cItalic 0 pour normal, une autre valeur pour caractères italiques.
cUnderline 0 pour normal, une autre valeur pour les caractères soulignés.
cStrikeOut 0 pour normal, une autre valeur pour des caractères (barrés) avec une ligne par le centre.
cCharSet C'est le jeu de caractères de la fonte (ex : ARIAL). Ce doit normalement être OEM_CHARSET, ce qui permet à Windows de choisir la fonte.
cOutputPrecision Indique à quel point la fonte choisie doit être proche des caractéristiques que nous voulons. Doit normalement on prend OUT_DEFAULT_PRECIS par défaut.
cClipPrecision Indique la précision de la coupure. La coupure c'est ce qui définit les bords de la zone d'écriture. Le texte sera coupé en arrivant près des bords et cette zone de coupure peut être plus ou moins étendue dans l'intérieur du secteur client. Vous pouvez utiliser la valeur CLIP_DEFAULT_PRECIS qui définit le comportement de coupure par défaut.
cQuality Indique la qualité de production. La qualité de production définit précisément comment le GDI doit essayer de calquer au mieux les attributs de la fonte logique à ceux d'une fonte physique réelle. Il y a trois choix : DEFAULT_QUALITY, PROOF_QUALITY et DRAFT_QUALITY.
cPitchAndFamily Spécifie la hauteur et la type de fonte. Vous devez combiner ces 2 valeurs avec l'opérateur "or".
lpFacename C'est un NULL qui indique la police de caractères de la fonte.

La description ci-dessus n'est en aucun cas complète. Vous devez consulter votre référence API Win32 pour plus de détails.

invoke SelectObject, hdc, eax
mov hfont,eax

Après avoir obtenu l' handle de la fonte logique, nous devons l'employer pour choisir la fonte dans le contexte de dispositif en appelant SelectObject. SelectObject met les nouveaux objets du GDI tels que les stylos, les brosses et les fontes dans le contexte de dispositif pour être employés par des fonctions du GDI. Il renvoie l' handle (de l'objet à replacé) que nous devons sauvegarder pour de futures appels de la fonction SelectObject. Après l'appel SelectObject, n'importe quelle fonction de production de texte emploiera la fonte que nous venons juste de choisir dans le contexte de dispositif.

RGB 200,200,50
invoke SetTextColor,hdc,eax
RGB 0,0,255
invoke SetBkColor,hdc,eax

Employez la macro RGB pour créer une valeur RGB sur 32 bits pour être employée par SetColorText et SetBkColor.

invoke TextOut,hdc,0,0,ADDR TestString,SIZEOF TestString

Appelez la fonction TextOut pour dessiner le texte sur le secteur client. Le texte sera dans la fonte et la couleur que nous avons spécifié précédemment.

invoke SelectObject,hdc, hfont

Quand on as fini de se servir de cette fonte, on doit rétablir l'ancienne fonte dans le contexte de dispositif. Vous devez toujours rétablir l'objet que vous avez remplacé dans le contexte de dispositif.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 6: Keyboard Input

Tutorial 6: Les Entrées Clavier

Nous allons voir comment un programme Windows traite les entrées clavier.

Downloadez le fichier en example ici.

Théorie:

Puisque normalement il y a seulement un clavier pour chaque PC, tous les programmes Windows en action doivent le partager entre eux. Windows est responsable de l'envoie des frappes de touches vers la bonne fenêtre qui attend ces entrées.
Bien qu'il puisse y avoir plusieurs fenêtres sur l'écran, seule une d'entre elles a temporairement la possibilité de traiter les entrées clavier. La fenêtre qui traite les entrées est la seule qui peut recevoir des frappes de touches. Vous pouvez différencier la fenêtre qui traite les entrées des autres fenêtres en regardant la barre de titre. La barre de titre de cette fenêtre est activée.
En réalité, il y a deux principaux types de messages pour le clavier, selon votre vision du clavier. Vous pouvez voir un clavier comme un ensemble de touches. Dans ce cas, si vous pressez une touche, Windows envoie le message WM_KEYDOWN à la fenêtre qui traite les entrées clavier, signifiant ainsi qu'une touche a été enfoncée. Quand vous relâchez cette touche, Windows envoie le message WM_KEYUP. Vous traitez une touche de la même manière qu'un bouton. L'autre façon de voir le clavier consiste en ce que c'est un dispositif d'entrée de caractères. Quand vous appuyez sur la touche "a", Windows envoie un message WM_CHAR à la fenêtre qui traite les entrées, disant ainsi que l'utilisateur envoie le caractère "a". En fait, Windows envoie les messages WM_KEYDOWN et WM_KEYUP à la fenêtre qui traite les entrées et ces messages seront traduis en messages WM_CHAR par l'appel de TranslateMessage. La procédure qui s'occupe du fonctionnement de votre fenêtre peut décider de traiter les trois messages ou bien seulement les messages auxquels vous vous intéressez. La plupart du temps, vous pouvez ignorer WM_KEYDOWN et WM_KEYUP puisque la fonction TranslateMessage appelle la boucle de message qui traduit les messages WM_KEYDOWN et WM_KEYUP en message WM_CHAR. Nous nous concentrerons donc uniquement sur WM_CHAR dans ce Tutorial.

Example:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
char WPARAM 20h ; Le caractère que le programme reçoit du clavier.
; 20h est la valeur pour obtenir un espace.
; Ainsi rien ne sera affiché à l'écran au moment de son ouverture.

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
invoke TextOut,hdc,0,0,ADDR char,1
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analyse:

char WPARAM 20h ; Le caractère que le programme reçoit du clavier

C'est la variable qui stockera le caractère envoyé par le clavier. Puisque le caractère est envoyé dans le paramètre WPARAM (appartenant à la procédure de fenêtre). La valeur initiale de WPARAM est 20h pour afficher un 'espace' au moment où notre fenêtre régénère son secteur client la toute première fois, quand il n'y a aucune entrée clavier. Donc au commencement, nous voulons afficher 'espace'.

.ELSEIF uMsg==WM_CHAR
push wParam
pop char
invoke InvalidateRect, hWnd,NULL,TRUE

Ceci a été rajouté à la procédure de fenêtre pour manipuler le message WM_CHAR. Il met seulement le caractère dans la variable nommée "char" et appelle ensuite InvalidateRect. InvalidateRect fait apparaître le 'rectangle invalide' dans le secteur client et force ainsi Windows à envoyer le message WM_PAINT à la procédure de fenêtre. Sa syntaxe est la suivante :

InvalidateRect proto hWnd:HWND,\
lpRect:DWORD,\
bErase:DWORD

lpRect est un indicateur sur le rectangle qui se trouve dans le secteur client que nous voulons déclarer comme étant invalide. Si ce paramètre est nul, le secteur client entier deviendra invalide.
bErase est un drapeau (un flag) indiquant à Windows s'il a besoin d'effacer le fond (background). Si ce drapeau est VRAI, alors Windows effacera le background du rectangle invalide dès que BeginPaint sera appelé.

Ainsi la stratégie que nous avons employé ici est que : nous stockons toute l'information nécessaire touchant à la peinture du secteur client et fabriquons ensuite le message WM_PAINT pour repeindre le secteur client. Bien sûr, les codes dans la section WM_PAINT doivent savoir à l'avance ce que l'on attend d'eux. On pourrait croire qu'on tourne en rond mais c'est la méthode utilisée par Windows.
En réalité nous pouvons repeindre (réactualiser) le secteur client pendant le traitement du message WM_CHAR en appelant la paire GetDC - ReleaseDC. A ce niveau, il n'y a aucun problème. Mais ça commence à être intéressant quand notre fenêtre a besoin de repeindre son secteur client. Puisque les codes qui peignent (affichent) le caractère sont dans la section WM_CHAR, la procédure de fenêtre ne sera plus capable de repeindre notre caractère sur le secteur client. Donc la ligne suivante sert à mettre toutes les données et les codes nécessaires à peinture dans WM_PAINT. Vous pouvez envoyer le message WM_PAINT n'importe où dans votre code dès que vous voulez repeindre le secteur client.

invoke TextOut,hdc,0,0,ADDR char,1

Quand InvalidateRect est appelé, il envoie un message WM_PAINT à la procédure de fenêtre en retour. Donc les codes dans la section WM_PAINT sont appelés. Il appelle BeginPaint comme d'habitude pour obtenir l'handle du contexte de dispositif et appelle ensuite TextOut qui dessine notre caractère dans le secteur de client à x=0, y=0. Quand vous exécutez le programme et appuyez sur une touche, vous verrez que le caractère se répercute dans le coin supérieur gauche de la fenêtre. Et quand la fenêtre est rétrécie au maximum puis réagrandie de nouveau, le caractère est toujours présent puisque tous les codes et toutes les données essentielles pour repeindre sont tous réunis dans la section WM_PAINT.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 7: Mouse Input

Tutorial 7: Les entrées Souris

Nous allons voir comment recevoir et répondre aux entrées souris dans notre procédure de fenêtre. Le programme d'exemple contrôlera les cliques gauche de la souris et affichera un texte sur le secteur client aux coordonnées même de la souris.

Downloadez le fichier en example ici.

Théorie:

Comme pour les entrées clavier, Windows détecte les activités de la souris dépendantes de chaque fenêtre. Ici notre activité portera uniquement sur le clique gauche et sur le mouvement du curseur de la souris dans la fenêtre. À la différence des entrées clavier qui sont adressés uniquement à la fenêtre qui est activée, les messages de la souris sont envoyés à n'importe quelle fenêtre (active ou non) du moment que la souris soit placée dessus. En plus, il existe aussi des 'messages souris' adressés au secteur non-client. Mais la plupart du temps, nous pouvons les ignorer sans s'en soucier. On peut se concentrer pleinement sur les messages touchant au secteur client.
Il y a deux messages pour chaque bouton de la souris : WM_LBUTTONDOWN, WM_RBUTTONDOWN et WM_LBUTTONUP, WM_RBUTTONUP. Pour une souris avec trois boutons, il y a aussi WM_MBUTTONDOWN et WM_MBUTTONUP. Pour le déplacement du curseur de souris sur le secteur client, Windows envoie le message WM_MOUSEMOVE à la fenêtre qui se trouve sous le curseur.
Une fenêtre peut recevoir des messages de double-clic, WM_LBUTTONDBCLK ou WM_RBUTTONDBCLK, si et seulement si sa classe de fenêtre obtient le drapeau (flag) de style CS_DBLCLKS, sinon la fenêtre ne recevra seulement qu'une série de messages disant que les boutons de souris on étés pressés (Button Up, Button Down).
Pour tous ces messages, la valeur de lParam contient (sera) la position de la souris. On travaille en 32 Bit donc sur des doubles mots. Le mot de poids faible sera la coordonnée X et le mot de poids fort sera la coordonnée Y à partir du coin supérieur gauche du secteur client de la fenêtre. WParam indique l'état des boutons de la souris, et des touches Shift et Ctrl.

Example:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MouseClick db 0 ; 0=pas encore de click

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hitpoint POINT <>

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL hdc:HDC
LOCAL ps:PAINTSTRUCT

.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE
.ELSEIF uMsg==WM_PAINT
invoke BeginPaint,hWnd, ADDR ps
mov hdc,eax
.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF
invoke EndPaint,hWnd, ADDR ps
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analyse:

.ELSEIF uMsg==WM_LBUTTONDOWN
mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax
mov eax,lParam
shr eax,16
mov hitpoint.y,eax
mov MouseClick,TRUE
invoke InvalidateRect,hWnd,NULL,TRUE

La procédure de fenêtre attend qu'on clique sur le bouton gauche de la souris. Quand il reçoit WM_LBUTTONDOWN, lParam prend la valeur de la coordonnée du curseur de la souris dans le secteur client. Il sauvegarde la coordonnée dans une variable de type POINT qui est défini comme ceci:

POINT STRUCT
x dd ?
y dd ?
POINT ENDS

et le 'flag' qui contrôle l'état de MouseClick, est mis à TRUE (VRAI), signifiant ainsi qu'il y a au moins le bouton gauche de la souris qu'on peut cliquer sur le secteur client.

mov eax,lParam
and eax,0FFFFh
mov hitpoint.x,eax

Puisque la coordonnée X est le mot de poids faible du double mot lParam et que les 2 membres qui forment la structure du POINT sont d'une taille de 32 bits, nous devons mettre la partie haute de 'eax' à 0 avant le stockage de celle-ci.

shr eax,16
mov hitpoint.y,eax

Puisque la coordonnée Y est le mot de poids fort du double mot lParam, nous devons mettre la partie basse de 'eax' à 0 avant le stockage de celle-ci. Nous pouvons faire ça en déplaçant 16bits de eax vers la droit.
Après le stockage de la position de la souris, nous mettons le flag contrôlant l'état de MouseClick à TRUE (VRAI) pour que le code de la section WM_PAINT sache qu'il y a au moins un clique effectué sur le secteur client. Donc il peut dessiner la chaîne de caractère à la position de la souris. Ensuite nous appelons la fonction InvalidateRect pour forcer la fenêtre à repeindre la totalité de son secteur client.

.IF MouseClick
invoke lstrlen,ADDR AppName
invoke TextOut,hdc,hitpoint.x,hitpoint.y,ADDR AppName,eax
.ENDIF

Le code de la section WM_PAINT (celui qui sert à repeindre le secteur client de votre fenêtre) doit vérifier si MouseClick est vrai, dès que la fenêtre a été créée. Au tout début il a reçu un message WM_PAINT, pour lequel il n'y avait aucun clique de souris, ainsi il ne doit pas dessiner la chaîne de caractère dans le secteur client. Nous initialisons donc MouseClick à FALSE (FAUX), mais changeons sa valeur à TRUE (VRAI) uniquement quand advient un réel clique de souris.
Si au moins un clique de souris arrive, il dessine la chaîne de caractère sur le secteur client à la position de la souris. Notez qu'il appelle 'lstrlen' pour obtenir la longueur de la chaîne à afficher et renvoie la longueur comme dernier paramètre de la fonction TextOut.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 8: Menu

Tutorial 8: Le Menu

Dans ce tutorial, nous allons apprendre comment incorporer un menu à notre fenêtre.
Downloadez l'example 1 et l'example2.

Theorie:

Le menu est un des composants le plus important d'une votre fenêtre. Le menu présente la liste des services qu'un programme peut offrir à l'utilisateur. L'utilisateur ne doit pas lire le manuel inclus avec le programme pour être capable de l'employer, il lui suffit de lire attentivement le menu pour obtenir une vue d'ensemble de la capacité d'un programme particulier et commencer à jouer avec lui immédiatement. Puisque un menu est un outil pour que l'utilisateur puisse rapidement se diriger, vous devez suivre la norme. Succinctement, les deux premiers articles du menu doivent être : le fichier et d'Edition et le fichier d'Aide. Vous pouvez insérer vos propres articles de menu entre celui d'Edition et celui de l'Aide. Si un article du menu invoque une boîte de dialogue, vous devez ajouter un ellipsis (...) à la chaîne de caractère du menu.
Le menu est une sorte de ressource. Il y a plusieurs sortes de ressources tels que la boîte de dialogue, les tableaux de caractères, l'icône, bitmap, le menu etc. Les ressources sont décrites dans un fichier séparé, appelé fichier de ressources et qui normalement a l'extension .rc. Vous combinez alors les ressources avec le code source pendant l'étape de liaison. Le produit final est un fichier exécutable qui contient, à la fois les instructions et les ressources.
Vous pouvez écrire des scénarios de ressources en employant n'importe quel éditeur de texte. Ces fichiers ressources sont composés des expressions qui décrivent les convenances ainsi que d'autres attributs des ressources employées dans un programme particulier. Bien que vous puissiez écrire des scénarios de ressource avec un éditeur de texte, c'est plutôt encombrant. Une meilleure alternative est d'employer un éditeur de ressources qui vous laisse concevoir visuellement des ressources avec un bien meilleurs confort. Les éditeurs de ressources sont habituellement inclus dans le pack avec le compilateur comme C Visuel +, Borland C +, etc.
Nous pouvons décrire une ressource de menu comme ceci :
    MyMenu MENU
    {
    [Ici, la liste du menu]
    }
Les programmeurs sous C peuvent se rendre compte que c'est semblable à la déclaration de la structure MyMenu d'un nom de menu suivi par le mot clef MENU et le menu est inscrit entre parenthèses[]. Alternativement, vous pouvez employer 'BEGIN' et 'END' au lieu des parenthèses si vous préférez. Cette syntaxe est plus acceptable pour les programmeurs en Pascal.
La liste du menu peut être soit MENUITEM soit la déclaration POPUP.
La déclaration MENUITEM définit une barre(bar) de menu qui n'invoque pas de menu popup lorsqu'il est choisi. La syntaxe est la suivante :
    MENUITEM "&text", ID [,options]
Il commence par le mot-clé MENUITEM suivi du texte que vous voulez employer en tant que chaîne de caractères pour la barre de menu. Notez l'esperluète. Il sert à souligner le caractère qui le suit. Après la chaîne de caractère formant le texte on trouve le n°ID de l'article du menu. L'ID est le numéro qui sera employé pour identifier l'article du menu dans le message envoyé à la procédure de fenêtre lorsqu'on le sélectionne. Chaque n°ID du menu doit être unique vis à vis des autres.
'Options' justement en option. Les options disponibles sont les suivantes :
  • GRAYED L'article du menu est inactif et il ne produit pas de message WM_COMMAND. Le texte est grisé (grayed).
  • INACTIVE L'article du menu est inactif et il ne produit pas de message WM_COMMAND. Le texte est quand même affiché normalement.
  • MENUBREAK Cet article et les articles suivants apparaissent sur une nouvelle ligne du menu.
  • HELP Cet article et les articles suivants sont alignés à droite.
Vous pouvez employer une de ces options ou bien les combiner entre elles avec l'opérateur "or". Prenez garde à ce que INACTIVE et GRAYED ne soient pas utilisés ensembles.
La syntaxe de la déclaration POPUP est la suivante :
POPUP "&text" [,options]
{
[menu list]
}
La déclaration POPUP définit une barre de menu qui, quand elle est choisi, abaisse une liste d'articles de menu dans une petite fenêtre POPUP. La liste du menu peut être un MENUTIEM ou la déclaration POPUP. Il y a une sorte de déclaration spéciale qui est MENUITEM, MENUITEM est le SÉPARATEUR, qui dessinera une ligne horizontale à l'intérieur de la fenêtre POPUP.
L'étape suivante après que vous en ayez finis avec le scénario de ressources est que votre programme doit faire référence à votre menu.
Vous pouvez faire ceci à deux endroits différents dans votre programme.
  • Dans le membre lpszMenuName de la structure de WNDCLASSEX. Déclarez, que vous avez un menu nommé "FirstMenu", vous pouvez assigné le menu à votre fenêtre comme cela :
        • .DATA
          MenuName db "FirstMenu",0
        ...........................
        ...........................
        .CODE
          ...........................
          mov wc.lpszMenuName, OFFSET MenuName
          ...........................
  • Ou bien dans les paramètres de CreateWindowEx au niveau de l' handle du menu (Hmenu) comme cela :
        • .DATA
          MenuName db "FirstMenu",0
          hMenu HMENU ?
        ...........................
        ...........................
        .CODE
          ...........................
          invoke LoadMenu, hInst, OFFSET MenuName
          mov hMenu, eax
          invoke CreateWindowEx,NULL,OFFSET ClsName,\
          OFFSET Caption, WS_OVERLAPPEDWINDOW,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          CW_USEDEFAULT,CW_USEDEFAULT,\
          NULL,\
          hMenu,\
          hInst,\
          NULL\
          ...........................
Donc vous pouvez vous demander, quelle est la différence entre ces deux méthodes ?
Quand vous faites référence au menu dans la structure de WNDCLASSEX, le menu devient le menu "par défaut" pour la classe de fenêtre. Chaque fenêtre de cette classe aura le même menu.
Si vous souhaitez que chaque fenêtre ait la même classe mais qu'elles aient des menus différents, vous devez choisir la deuxième forme. Dans ce cas, n'importe quelle fenêtre dont on passe l'handle de son menu à la fonction CreateWindowEx aura un menu qui "ne tient pas compte" du menu par défaut, défini dans la structure WNDCLASSEX.
Ensuite nous devons examiner comment un menu fait pour indiquer à la procédure de fenêtre quand l'utilisateur choisit un des article du menu.
Quand l'utilisateur choisit un article du menu, la procédure de fenêtre reçoit le message WM_COMMAND. Le mot de poids faible de wParam contient le n°ID de l'article choisi dans le menu. (Le mot de poids faible de wParam = si c'est du contenu de eax (qui est sur 32Bit) dont on parle alors on parle plus précisément ici des 16 dernier bits de eax).
Maintenant nous avons les informations suffisantes pour créer et employer un menu. On va le faire.

Example:

Le premier exemple montre comment créer et employer un menu en spécifiant le nom de menu dans la classe de fenêtre.

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0 ; Le nom de notre menu dans le fichier de ressources.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.const
IDM_TEST equ 1 ; (Menu IDs) ou: Les n°Identification des éléments constituants le Menu
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName ; Place le nom de notre menu ici
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start
**************************************************************************************************************************

Menu.rc
**************************************************************************************************************************

#define IDM_TEST 1
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4

FirstMenu MENU
{
POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}
MENUITEM "&Test", IDM_TEST
}

Analyse:

On va analyser le fichier de ressources d'abord.
#define IDM_TEST 1 /* cette ligne revient au même que : IDM_TEST equ 1*/
#define IDM_HELLO 2
#define IDM_GOODBYE 3
#define IDM_EXIT 4
Les susdites lignes définissent les n°ID des éléments constituants le menu. Vous pouvez assigner n'importe quelle valeur à l'ID tant que cette valeur est unique dans le menu.

FirstMenu MENU

Déclarez votre menu avec le mot clef 'MENU'

POPUP "&PopUp"
{
MENUITEM "&Say Hello",IDM_HELLO
MENUITEM "Say &GoodBye", IDM_GOODBYE
MENUITEM SEPARATOR
MENUITEM "E&xit",IDM_EXIT
}

Définit un menu popup avec quatre articles de menu, le troisième est un séparateur de menu.

MENUITEM "&Test", IDM_TEST

Définit une barre de menu dans le menu principal.
Ensuite nous examinons la source du code.

MenuName db "FirstMenu",0 ; C'est le nom de notre menu dans le fichier de ressources.
Test_string db "You selected Test menu item",0
Hello_string db "Hello, my friend",0
Goodbye_string db "See you again, bye",0
MenuName est le nom du menu dans le fichier de ressources. Remarquez que vous pouvez définir plus qu'un menu dans le fichier de ressource. Donc vous devez spécifier quel menu vous voulez employer. Ces trois lignes définissent les chaînes de caractères à afficher dans les boîtes de message (Message Box) qui sont invoquées plus loin quand l'article approprié du menu est choisi par l'utilisateur.
IDM_TEST equ 1 ; (Menu IDs) Les n° d'identités des éléments constituants le Menu.
IDM_HELLO equ 2
IDM_GOODBYE equ 3
IDM_EXIT equ 4
Définit les 'menu IDs' pour pouvoir les utiliser dans la procédure de fenêtre. Ces valeurs DOIVENT ÊTRE identiques à celles définies dans le fichier de ressources.

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF ax==IDM_TEST
invoke MessageBox,NULL,ADDR Test_string,OFFSET AppName,MB_OK
.ELSEIF ax==IDM_HELLO
invoke MessageBox, NULL,ADDR Hello_string, OFFSET AppName,MB_OK
.ELSEIF ax==IDM_GOODBYE
invoke MessageBox,NULL,ADDR Goodbye_string, OFFSET AppName, MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF

Dans la procédure de fenêtre, nous traitons des messages WM_COMMAND. Quand l'utilisateur choisit un article du menu, le menu ID de cet article est envoyé à la procédure de fenêtre dans le mot de poids faible de wParam grâce au message WM_COMMAND. Ainsi quand nous stockons la valeur de wParam dans eax, on compare sa valeur avec eax au niveau des 'menu IDs' que nous avons définis précédemment et le programme agit en conséquence. Dans les trois premiers cas, quand l'utilisateur sélectionne les articles Test, Say Hello, and Say GoodBye du menu, uniquement une seule chaîne de caractères s'affiche dans une boîte de message.
Si l'utilisateur choisit l'article 'Exit' dans le menu, nous appelons DestroyWindow avec l'handle de notre fenêtre comme paramètre pour qu'il puisse fermer notre fenêtre.
Comme vous pouvez le voir, spécifier le nom du menu dans une classe de fenêtre est très facile. Cependant vous pouvez aussi employer une méthode supplémentaire pour charger un menu dans votre fenêtre. Je ne vous montrerai pas le code source entier ici. Le fichier de ressources est le même pour les deux méthodes. Il y a seulement quelques changements mineurs du fichier source que je vais vous montrer ci-dessous.

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HMENU ? ; handle of our menu
Définit une variable de type HMENU pour stocker notre menu handle.

invoke LoadMenu, hInst, OFFSET MenuName
mov hMenu,eax
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,hMenu,\
hInst,NULL

Pour plus d'informations sur LoadMenu, référez-vous à LoadMenu.html

Avant l'appel CreateWindowEx, nous appelons LoadMenu avec l'Instance Handle et un pointeur sur le nom de notre menu. LoadMenu renvoie l'handle de notre menu dans le fichier de ressources que nous passons à CreateWindowEx.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 9: Child Window Controls

Tutorial 9: Child Window Controls

Dans ce tutorial, nous allons voir les 'Child Window Controls' qui sont, pour nos programmes, des mécanismes d'entrée - sortie très importants.

Downloadez le fichier en example ici.

Theorie:

Windows fournit plusieurs classes de fenêtre prédéterminées que nous pouvons aisément employer dans nos propres programmes. La plupart du temps nous les employons, comme par exemple les composants d'une boîte de dialogue. Donc on les appelle généralement les 'Child Window Controls'. Les 'Child Window Controls' traitent leurs propres messages de souris et de clavier et en informent la fenêtre parente quand leurs états ont étés changés. Ils soulagent les programmeurs d'un fardeau énorme, donc vous devez essayer de les employer autant que possible. Dans ce tutorial, je les ai mis sur une fenêtre normale juste pour voir comment vous pouvez les créer et les employer mais en réalité vous devez les mettre dans une boîte de dialogue.
Les exemples de classes de fenêtre prédéterminées sont le bouton, la liste, la boîte à cocher, le bouton radio, d'édition etc.
Pour utiliser un 'Child Window Control', vous devez d'abord le créer avec 'CreateWindow' ou 'CreateWindowEx'. Notez que vous ne devez pas enregistrer la classe de fenêtre puisque Windows l'enregistrera pour vous. Le paramètre 'class name' DOIT ÊTRE le nom de la classe prédéterminée. Quand vous voulez créer un bouton, vous devez mettre "BUTTON" comme nom de classe dans CreateWindowEx. Les autres paramètres que vous devez remplir sont le handle de sa fenêtre parente' et le n°ID pour identifier ce Control. Le n°ID du Control doit être unique parmi les différents Controls. Le n°ID du Control est le numéro qui servira à identifier ce Control. Vous l'employez pour le différencier des autres Controls.
Après que ce Control ait été créé, des messages sont envoyés pour avertir que l'état de la fenêtre parente a changé. Normalement, vous créez les child window grâce au message WM_CREATE de la fenêtre parente. Chaque Control (exemple: les boutons, la zone de saisie de texte=Control d'édition, les barres de défilement en bas et sur le côté des fenêtres,...) est un objet qu'on dépose par dessus la fenêtre principale. Ce sont des objets enfants des la fenêtre dite parente. Il y a donc la fenêtre parente, et ses objets, ses Controls sont ses 'Fenêtres Enfants' (ou Child Window Controls). Une fenêtre enfant (child window) envoie le message WM_COMMAND à sa fenêtre parente, dont son n°ID (celui de ce Control) est dans le mot de poids faible du paramètre wParam, son code dans le mot de poids fort de wParam et son 'Window Handle' (son Handle de Fenêtre (de Control)) dans lParam. Chaque 'Child Window Control' a des codes différents, pour en savoir plus à propos de l'instruction (le code) faîtes appel à votre référence API Win32.
De façon inverse, la fenêtre parente peut également envoyer des messages à ses Controls (fenêtres Enfants), en faisant appel à la fonction SendMessage. La fonction SendMessage envoie donc le message en question en l'accompagnant en plus des valeurs de wParam et lParam vers le child windows ciblée par son 'Window Handle'. C'est une fonction extrêmement utile puisque elle peut envoyer des messages à n'importe quelle fenêtre pourvu que vous sachiez son 'Window Handle' (Window Handle = le n° qui identifie une fenêtre ou bien un objet quelconque comme par exemple un 'champs de saisie de texte' ou bien un 'bouton').
Ainsi, après la création des child windows, la fenêtre parente doit possèder une boucle de traitement de messages pour s'occuper des messages WM_COMMAND pour être capable de recevoir les codes de notification des fenêtres enfant (child windows).

Example:

Nous allons créer une fenêtre qui contient une boîte d'édition (boîte de saisie de texte) et un bouton. Quand vous cliquez sur le bouton, une (MessageBox) boîte de message apparaîtra affichant le texte que vous avez tapé dans la boîte d'édition. Il y a aussi un menu avec 4 Items (4 sous-menu) :
  1. Say Hello -- Met une chaîne de caractères dans la boîte d'édition
  2. Clear Edit Box -- Efface le contenu de la boîte d'édition
  3. Get Text -- Affiche une MessageBox avec le texte de la boîte d'édition
  4. Exit -- Ferme le programme.
.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
MenuName db "FirstMenu",0
ButtonClassName db "button",0
ButtonText db "My First Button",0
EditClassName db "edit",0
TestString db "Wow! I'm in an edit box now",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndButton HWND ?
hwndEdit HWND ?
buffer db 512 dup(?) ; Buffer pour stocker le texte qu'on inscrit dans la boîte d'édition.

.const
ButtonID equ 1 ; le n°ID du Control de type Button
EditID equ 2 ; le n°ID du Control de type 'Zone d'édition' (de saisie de texte)
IDM_HELLO equ 1
IDM_CLEAR equ 2
IDM_GETTEXT equ 3 ; n°IDs des différents Items contituants le menu
IDM_EXIT equ 4

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName, \
ADDR AppName, WS_OVERLAPPEDWINDOW,\
CW_USEDEFAULT, CW_USEDEFAULT,\
300,200,NULL,NULL, hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE, ADDR EditClassName,NULL,\
WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT or\
ES_AUTOHSCROLL,\
50,35,200,25,hWnd,8,hInstance,NULL
mov hwndEdit,eax
invoke SetFocus, hwndEdit
invoke CreateWindowEx,NULL, ADDR ButtonClassName,ADDR ButtonText,\
WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
75,70,140,25,hWnd,ButtonID,hInstance,NULL
mov hwndButton,eax
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analyse:

On va analyser le programme.
    .ELSEIF uMsg==WM_CREATE
    invoke CreateWindowEx,WS_EX_CLIENTEDGE, \
    ADDR EditClassName,NULL,\
    WS_CHILD or WS_VISIBLE or WS_BORDER or ES_LEFT\
    or ES_AUTOHSCROLL,\
    50,35,200,25,hWnd,EditID,hInstance,NULL
    mov hwndEdit,eax
    invoke SetFocus, hwndEdit
    invoke CreateWindowEx,NULL, ADDR ButtonClassName,\
    ADDR ButtonText,\
    WS_CHILD or WS_VISIBLE or BS_DEFPUSHBUTTON,\
    75,70,140,25,hWnd,ButtonID,hInstance,NULL
    mov hwndButton,eax
Nous créons les différents Controls pendant le traitement du message WM_CREATE. Nous appelons CreateWindowEx avec un style de fenêtre supplémentaire, WS_EX_CLIENTEDGE. Le nom de chaque contrôle est prédéterminé. On a "EDIT" pour le Control de type 'zone d'édition' (ou boîte de saisie de texte), "BUTTON" pour le Control de type 'bouton'. Ensuite nous spécifions le style de chaque 'Child Window' (le style de chacun des objets qui constituent notre fenêtre, de chaque Control. Comme le style du bouton par exemple). Chaque Control possède des styles supplémentaires en plus des styles normaux. Par exemple, le style pour le bouton a le préfixe BS_" pour "Button Style", le style de la zone d'édition a pour préfixe "ES _" pour "Edit Style". Notez que vous mettez un n°ID de Control à la place du Handle du menu. Ceci ne provoquera aucun problème puisque un (Child Window Control) un Control Enfant ne peut pas avoir de menu. (Ex: une fenêtre principale peut avoir un menu mais un simple bouton, sûrment pas).
Après la création de chaque Control, nous obtenons en retour son handle dans une variable pour une future utilisation.
La fonction 'SetFocus' est appelée pour que la boîte d'édition prenne la main (pour que se soit elle qui soit active), comme ça l'utilisateur pourra taper son texte tout de suite après l'appel de cette fonction.
Vient maintenant la partie vraiment passionnante. Chaque 'Child Window Control' envoie ses notifications (Messages allant d'un 'Child Control' vers sa 'Fenêtre Parente', le contraire c'est un Message simplement) à sa 'Parent Window' (sa fenêtre parente) avec WM_COMMAND.

.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0

Rappelez-vous bien qu'un menu envoie lui aussi des messages WM_COMMAND vers la fenêtre parente pour l'informer de son propre état. Comment pouvez-vous faire la différence entre des messages WM_COMMAND, émis par un menu (Child Window) ou par un contrôle (effectué par la Parent Window) ? Voici ci-dessous la réponse :

 | Mot de poids faible de wParam | Mot de poids fort de wParam | lParam
Menu | Menu ID | 0 | 0
Control | Control ID | Notification code | Child Window Handle

Vous pouvez voir que vous devez vérifier lParam. Si c'est zéro, l'actuel message WM_COMMAND provient d'un menu. Vous ne pouvez pas employer wParam pour faire la différence entre un menu et un Control, puisque le 'menu ID' et 'control ID' peuvent être identiques et que le code de notification peut être zéro.

.IF ax==IDM_HELLO
invoke SetWindowText,hwndEdit,ADDR TestString
.ELSEIF ax==IDM_CLEAR
invoke SetWindowText,hwndEdit,NULL
.ELSEIF ax==IDM_GETTEXT
invoke GetWindowText,hwndEdit,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK

Vous pouvez écrire une chaîne de caractère (du texte) dans la boîte d'édition en appelant la fonction SetWindowText. Vous pouvez aussi effacez le contenu de cette boîte d'édition en appelant SetWindowText avec le NULL. SetWindowText est une fonction API d'usage flexible. Vous pouvez employer SetWindowText pour modifier du titre dans une fenêtre ou pour changer le texte inscrit sur un bouton.
Pour importer du texte dans une boîte d'édition, vous employez GetWindowText. (Comprenez seulement que, dans ce cas on écrit pas manuellement le texte dans la boîte d'édition (de saisie), mais on le fait s'inscrire dans la zone de saisie comme on aurait pu faire s'inscrire un texte dans une MessageBox).

.IF ax==ButtonID
shr eax,16
.IF ax==BN_CLICKED
invoke SendMessage,hWnd,WM_COMMAND,IDM_GETTEXT,0
.ENDIF
.ENDIF

Ce petit bout de code réagit à la seule condition que l'utilisateur presse le bouton. D'abord, il vérifie le mot bas (ou de poids faible) de wParam pour voir si le 'control ID' (le n°ID du Control) correspond à celui du bouton. S'il l'est, il vérifie le haut mot (ou de poids fort) de wParam pour savoir si on a bien l'indication BN_CLICKED, qui est envoyée quand le bouton est cliqué.
La partie qui est intéressante, c'est après qu'on soit certain que le code de notification soit BN_CLICKED. Nous voulons récupérer le texte qu'on vient d'écrire dans la boîte d'édition et le montrer dans une boîte de message (une MessageBox). Nous pourrions faire un double du code de la section IDM_GETTEXT ci-dessus mais ça n'a pas de sens. Si d'une façon ou d'une autre nous pouvions envoyer un message WM_COMMAND avec le mot bas de wParam, alors la valeur contenue dans IDM_GETTEXT serait transmise à notre propre procédure de fenêtre, nous pouvons donc éviter la duplication de ce code et simplifier notre programme. La solution, c'est la fonction SendMessage. Cette fonction envoie n'importe quel message à n'importe quelle fenêtre avec n'importe quel wParam et lParam. Ainsi au lieu de recopier du code, nous appelons SendMessage avec :
1- l'Handle de la fenêtre parente.
2- WM_COMMAND.
3- IDM_GETTEXT.
4- 0.
Ceci a le même effet que de sélectionner le menu Item "Get Text" (un de nos sous-menu) du menu. La procédure de fenêtre ne voit aucune différence entre les deux. (La procédure de fenêtre = la procédure qui s'occupe du fonctionnement de notre fenêtre, comme la détection d'un appui sur un bouton ou bien une inscription de texte dans une zone de saisie)
Vous devez essayer d'employer cette technique le plus possible pour organiser votre code au mieux.
Dernier point mais non le moindre, n'oubliez pas d'utiliser la fonction TranslateMessage à l'intérieur de la boucle de message. Puisque vous devez taper du texte dans la boîte d'édition, votre programme doit traduire des entrées clavier brutes en un texte lisible. Si vous omettez cette fonction, vous serez incapable de taper quoique ce soit dans votre boîte d'édition.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 10: Boîte de Dialogue comme Fenêtre Principale

Tutorial 10: Boîte de Dialogue
Comme Fenêtre Principale

Vient maintenant un objet vraiment intéressante du GUI (NdT : Graphic User Interface), à savoir : la boîte de dialogue. Dans ce Tutorial (et le suivant), nous apprendrons comment se servir d'une boîte de dialogue comme fenêtre principale.

Downloadez le premier exemple ici, le second Là.

Théorie:

Si vous avez joué assez longtemps avec les exemples des Tutorials précédants, vous avez dû découvrir que la touche TAB ne permettait pas de changer de centre d'entrée d'un 'Child Window Control' à un autre (EX : si vous avez un bouton et une zone de saisie, la touche TAB sert à activer puis à passer de l'un à l'autre. Essayez sur les exemples de ce TUT) . Dans les Tutorials précédents, le seul moyen d'activer le centre d'entrée d'un des 'Child Wondow Control' était de simple-cliquer sur l'un d'eux. C'est une situation un peu lourde. Une nouvelle chose à remarquer, c'est que j'ai changé la couleur du fond de la fenêtre parente, elle est grise maintenant au lieu de blanche comme dans des exemples précédents. C'est fait pour que la couleur des 'child window controls' puisse se mélanger sans différence avec la couleur du 'secteur client' de la fenêtre parente. Il y a une autre façon de contourner ce problème mais ce n'est pas facile. Vous devez sous-classer tous les 'child window controls' de votre fenêtre parente.
La raison pour laquelle un tel inconvénient existe, c'est qu'à l'origine les 'Child Window Controls' ont étés conçus pour travailler avec une boîte de dialogue, et non pas une simple fenêtre. La couleur par défaut des 'Child Window Controls' tel qu'un bouton est grise parce que le 'secteur client' d'une boîte de dialogue est normalement gris. Donc ils se marient l'un avec l'autre sans que le programmeur ne se casse encore plus la tête sur un détail comme ça.
Avant que nous n'allions plus profondément dans les détails, nous devons d'abord comprendre ce qu'est une boîte de dialogue. Une boîte de dialogue n'est rien de plus d'une fenêtre normale qui est conçue pour travailler avec des'child window controls'. Windows possède aussi son propre ''administrateur de boîte de dialog'' lequel est responsable de la majeure partie de la logique du clavier, comme par exemple le changement du centre d'entrée quand l'utilisateur presse 'TAB' (= activer un autre Child Window Control avec TAB), ou bien en 'Validant' la touche dont son centre d'entrée est sélectionné si la touche ENTER est pressée (=quand vous avez 2 boutons dans une fenêtre, un OK l'autre NON, si c'est OK qui est sélectionné en cadre pointillé, en appuyant sur ENTER c'est comme si on avait cliqué sur OK ), etc… donc avec ça, les programmeurs peuvent traiter des tâches de plus haut niveau. Les boîtes de dialogue sont principalement employées comme des dispositifs d'Entrée / Sorties. Puisqu'une telle boîte de dialogue peut être considérée comme une "boîte noire" d' Entrée / Sortie, vous n'avez pas besoin de savoir comment travaille intérieurement cette boîte de dialogue pour pouvoir l'employer. Vous devez seulement savoir comment elle agir. C'est le principe d'OBJET ORIENTÉ PROGRAMMÉ (OOP) qui fait en sorte que l'information est dissimulée (tout n'est pas apparent dans son fonctionnement). Si la boîte noire était *parfaitement* conçue, l'utilisateur devrait pouvoir s'en servir sans aucune connaissance de son fonctionne. Le hic c'est que la boîte noire doit être parfaite, et en réalité c'est difficile à réaliser car les API Win32 sont aussi conçues comme des boîtes noire.
Bien, il semble que nous dévions de notre chemin. On va revenir à notre sujet. Les boîtes de dialogue sont conçues pour réduire le travail du programmeur. Normalement si vous utilisez des 'Child Window Controls' sur une fenêtre normale, vous devez les sous-classer et écrire (définir) la logique du clavier vous-même. Mais si vous les mettez sur une boîte de dialogue, tout ceci sera manipulé pour vous. Vous devez seulement savoir comment s'utilisent vos Child Window dans la boîte de dialogue ou comment envoyer des commandes.
Une boîte de dialogue est définie comme une ressource (un objet qu'on défini pour pouvoir l'utiliser parmi d'autres) un peu de la même façon qu'un menu. Vous décrivez les caractéristiques de votre boîte de dialogue et de ses commandes, puis ensuite vous compilez le scénario de ressources avec un éditeur de ressources.
Remarquez que toutes les ressources sont réunies dans le même fichier de scénario de ressource. Vous pouvez employer n'importe quel éditeur de texte pour écrire un calibre de boîte de dialogue mais je ne le recommande pas. Vous vaut mieux employer un éditeur de ressources pour faire le travail visuellement puisque l'arrangement des 'child window controls' sur une boîte de dialogue est difficile à faire manuellement. Plusieurs éditeurs de ressources excellents sont disponibles. La plupart des Packs de compilateurs incluent aussi leurs propres éditeurs de ressources. Vous pouvez les employer pour créer un scénario de ressources pour votre programme et couper ensuite les lignes sans rapport comme celles liés à MFC.
Il y a deux principaux types de boîte de dialogue : 'modale' et 'sans modèle'. Une boîte de dialogue sans modèle vous laisse changer son centre d'entrée pour une autre fenêtre. Le meilleur exemple c'est la boîte de dialogue 'RECHERCHER' de WORD. Il y a deux sous-types de boîte de dialogue modale : l'application modale et le système modal. Une boîte de dialogue d'application modale ne vous laisse pas changer son centre d'entrée pour aller vers une autre fenêtre dans la même application. Par contre vous pouvez changer le centre d'entrée de la fenêtre pour aller vers d'AUTRES applications. Le système de la boîte de dialogue modale ne vous permet pas de changer son centre d'entrée pour une autre fenêtre avant que vous ne répondiez d'abord.
On créé une 'boîte de dialogue sans modèle' en appelant la fonction API CreateDialogParam. On créé une boîte de dialogue modale en appelant DialogBoxParam. La seule différence entre une boîte de dialogue d'application modale et un système modal est le style DS_SYSMODAL. Si vous incluez le style DS_SYSMODAL dans un calibre de boîte de dialogue, cette boîte de dialogue sera un système modale.
Vous pouvez communiquer avec n'importe quel 'child window control' dans une boîte de dialogue en employant la fonction SendDlgItemMessage. Sa syntaxe est la suivante :
SendDlgItemMessage proto hwndDlg:DWORD,\
idControl:DWORD,\
uMsg:DWORD,\
wParam:DWORD,\
lParam:DWORD
Cet appel d'API est super utile pour l'action réciproque avec les 'child window control'. Par exemple, si vous voulez récupérer le texte d'un contrôle d'édition, vous pouvez faire ceci :
call SendDlgItemMessage, hDlg, ID_EDITBOX, WM_GETTEXT, 256, ADDR text_buffer
Pour savoir quel message (quelle commande) vous devez utiliser, vous devez consulter votre référence Win32 API.
Windows fournit aussi plusieurs fonctions API de contrôle-spécifiques pour récupérer et transmettre des données rapidement, par exemple, GetDlgItemText, CheckDlgButton etc. Ces fonctions de contrôle-spécifiques sont mis à la disposition du programmeur. Donc il ne doit pas rechercher les significations de wParam et lParam pour chaque message. Normalement, vous devez utiliser les appels d'API de contrôle-spécifiques quand ils sont disponibles puisque ils servent à vous faciliter l'écriture de votre code source. On recours seulement à SendDlgItemMessage si aucun des appels d'API de contrôle-spécifiques n'est disponible.
L'administrateur de boîte de dialogue de Windows envoie quelques messages à une fonction de rappel de service spécialisée, laquelle est appelée 'procédure de boîte de dialogue' et qui a le format suivant :
DlgProc proto hDlg:DWORD ,\
iMsg:DWORD ,\
wParam:DWORD ,\
lParam:DWORD
La 'procédure de boîte de dialogue' est très semblable à une 'procédure de fenêtre' à part le type de valeur de retour qui est TRUE / FAULSE au lieu de LRESULT. L'administrateur de boîte de dialogue interne à Windows EST la vraie procédure de fenêtre pour la boîte de dialogue. Il appelle notre procédure de boîte de dialogue avec les messages qu'il a reçu. Donc la règle générale en vigueur consiste en ce que : si notre procédure de boîte de dialogue traite un message, il DOIT retourner la valeur TRUE (VRAIE) dans eax et s'il ne traite pas le message, il doit retourner FAULSE (FAUX) dans eax. Remarquez qu'une procédure de boîte de dialogue ne transmet pas les messages et elle ne traite pas l'appel de DefWindowProc puisque ce n'est pas une procédure de fenêtre réelle (normale).
Il y a deux utilisations distinctes d'une boîte de dialogue. Vous pouvez l'employer en tant que fenêtre principale de votre application ou l'employer comme un dispositif d'entrée. Nous allons examiner la première approche dans ce tutorial.
""L'utilisation d'une boîte de dialogue comme fenêtre principale" peut être interprétée dans deux sens différents.
  1. Vous pouvez calibrer une boîte de dialogue comme un calibre de classe que vous déclarez avec l'appel de RegisterClassEx. Dans ce cas, la boîte de dialogue se comporte comme une fenêtre "normale" : elle reçoit des messages via une procédure de fenêtre mentionnée par le membre lpfnWndProc de la classe de fenêtre, et non pas par une procédure de boîte de dialogue. L'avantage de cette approche est que vous n'avez pas à créer vous-même de 'child window controls'. Windows les crée pour vous lorsque la boîte de dialogue est créée. Ainsi Windows gère la logique du clavier pour vous. De plus vous pouvez choisir le curseur et l'icône de votre fenêtre dans la structure de classe de fenêtre.

    2. Votre programme crée seulement une boîte de dialogue sans créer aucune fenêtre parente. Cette approche fait une boucle de message inutile puisque les messages sont envoyés directement à la procédure de boîte de dialogue. Vous n'avez même pas besoin d'enregistrer une classe de fenêtre!
Ce Tutorial va être long. Je présenterai la première approche suivie par le deuxième.

Exemples:

dialog.asm

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
ClassName db "DLGCLASS",0
MenuName db "MyMenu",0
DlgName db "MyDialog",0
AppName db "Our First Dialog Box",0
TestString db "Wow! Je suis dans une boîte d'édition maintenant",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hDlg:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,DLGWINDOWEXTRA
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_BTNFACE+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
mov hDlg,eax
invoke ShowWindow, hDlg,SW_SHOWNORMAL
invoke UpdateWindow, hDlg
invoke GetDlgItem,hDlg,IDC_EDIT
invoke SetFocus,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSE
invoke DestroyWindow,hWnd
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.IF dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Dialog.rc

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our First Dialog Box"
CLASS "DLGCLASS"
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Dis Bonjour", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13, WS_GROUP
END

MyMenu MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

Analyse:

On va analyser ce premier exemple.
Cet exemple montre comment créer un calibre de dialogue comme une classe de fenêtre, et créer "une fenêtre" avec cette classe. ça simplifie notre programme puisque on n'a pas besoin de créer de 'child window controls' soi-même.
On va d'abord analyser le calibre de dialogue.

MyDialog DIALOG 10, 10, 205, 60

On déclare le nom d'un dialogue, dans ce cas, "MyDialog" suivi du mot-clé "DIALOG". Les quatre nombres suivants sont : x, y, largeur et hauteur de la boîte de dialogue, ce sont des unités propres aux DialogBox (C'est pas des pixels).

STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK

Déclare les styles de la boîte de dialogue.

CAPTION "Our First Dialog Box"

C'est le texte qui apparaîtra dans la barre de titre de la boîte de dialogue.

CLASS "DLGCLASS"

Cette ligne est cruciale. C'est ce mot-clé CLASS qui nous permet d'employer le calibre de boîte de dialogue comme une 'Window Class' (classe de fenêtre). Puis, suivant le mot-clé on trouve le nom de la "Window Class"

BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Dis Bonjour", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

Le susdit bloc définit les 'child window controls' de la boîte de dialogue. Ils sont définis entre les mots-clés BEGIN et END. Généralement la syntaxe est la suivante :

    control-type "text" ,controlID, x, y, width, height [,styles]
Les 'types' de contrôle sont propre au compilateur de ressource, donc vous devez consulter le manuel.
Maintenant nous voyons le code de source assembleur. La partie intéressante est dans la structure de la 'Window Class ' :
    mov wc.cbWndExtra,DLGWINDOWEXTRA
    mov wc.lpszClassName,OFFSET ClassName
Normalement, ce membre est le NULL, mais si nous voulons enregistrer un calibre de boîte de dialogue comme une Window Class (classe de fenêtre), nous devons mettre ce membre à la valeur DLGWINDOWEXTRA. Notez que le nom de la classe doit être identique à celui qui est après le mot-clé CLASS pour le calibre de boîte de dialogue. Les membres restants sont initialisés comme d'habitude. Après que vous ayez remplis la structure de classe de fenêtre, déclarez-la avec RegisterClassEx. Ça vous semble familier ? C'est la même routine que pour enregistrer une classe de fenêtre normale.
    invoke CreateDialogParam,hInstance,ADDR DlgName,NULL,NULL,NULL
Après avoir définit notre "Window Class", nous créons notre boîte de dialogue. Dans cet exemple, je la crée comme une boîte de dialogue sans modèle grâce à la fonction CreateDialogParam. Cette fonction prend 5 paramètres, mais en réalité, vous n'avez besoin que des deux premier : l'instance handle et le pointeur qui justement pointe sur 'le nom du calibre de DialogBox'. Notez que le 2ème paramètre n'est pas un pointeur sur le nom de class.
À ce point, la boîte de dialogue et ses 'child window controls' sont créés par Windows. Votre procédure de fenêtre recevra comme d'habitude le message WM_CREATE.
    invoke GetDlgItem,hDlg,IDC_EDIT
    invoke SetFocus,eax
Maintenant que la boîte de dialogue est créée, j'aimerai mettre le centre d'entrée au contrôle d'édition (= on veut que la zone de saisie de texte soit active). Si je met ces codes dans la section WM_CREATE, l'appel GetDlgItem échouera jusqu'à ce que les 'child window controls' ne soient créés. La seule façon pour faire cela est de l'appeler après que la boîte de dialogue et tous ses 'child window controls' soient créés. C'est pour ça que j'ai mis ces deux lignes après l'appel à UpdateWindow. la fonction GetDlgItem obtient le contrôle ID et renvoie le contrôle du 'Window Handle' associé. C'est de cette manière que vous pouvez récupérer un 'Window Handle' lorsque vous connaissez son 'contrôle ID'.

invoke IsDialogMessage, hDlg, ADDR msg
.IF eax ==FALSE
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDIF

Le programme entre dans la boucle de message et avant que nous traduisions et expédiions des messages, nous appelons la fonction IsDialogMessage pour que l'administrateur de boîte de dialogue puisse s'occuper de la logique du clavier à notre place. Si ces retours de fonction sont TRUES(VRAIS), alors ça signifie que le message est destiné à la boîte de dialogue et est traité par l'administrateur de la boîte de dialogue. Remarquez une autre différence par rapport Tutorial précédent. Quand la procédure de fenêtre veut récupérer le texte qui est inscrit dans la zone d'édition, elle appelle la fonction GetDlgItemText au lieu de GetWindowText. GetDlgItemText accepte un contrôle ID au lieu d'un 'Window Handle'. Ceci rend l'appel plus facile dans le cas vous employez une boîte de dialogue.

Maintenant on va voir à la deuxième approche, à savoir : l'utilisation d'une boîte de dialogue comme fenêtre principale. Dans l'exemple suivant, je vais créer une boîte de dialogue d'application modale. Vous ne trouverez pas de boucle de message ou de procédure de fenêtre parce qu'elles ne sont pas nécessaires!
dialog.asm (part 2)

.386
.model flat,stdcall
option casemap:none

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
DlgName db "MyDialog",0
AppName db "Our Second Dialog Box",0
TestString db "Wow! Je suis dans une zone d'édition",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
buffer db 512 dup(?)

.const
IDC_EDIT equ 3000
IDC_BUTTON equ 3001
IDC_EXIT equ 3002
IDM_GETTEXT equ 32000
IDM_CLEAR equ 32001
IDM_EXIT equ 32002

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL
invoke ExitProcess,eax

DlgProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.IF lParam==0
.IF ax==IDM_GETTEXT
invoke GetDlgItemText,hWnd,IDC_EDIT,ADDR buffer,512
invoke MessageBox,NULL,ADDR buffer,ADDR AppName,MB_OK
.ELSEIF ax==IDM_CLEAR
invoke SetDlgItemText,hWnd,IDC_EDIT,NULL
.ELSEIF ax==IDM_EXIT
invoke EndDialog, hWnd,NULL
.ENDIF
.ELSE
mov edx,wParam
shr edx,16
.if dx==BN_CLICKED
.IF ax==IDC_BUTTON
invoke SetDlgItemText,hWnd,IDC_EDIT,ADDR TestString
.ELSEIF ax==IDC_EXIT
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0
.ENDIF
.ENDIF
.ENDIF
.ELSE
mov eax,FALSE
ret
.ENDIF
mov eax,TRUE
ret
DlgProc endp
end start

dialog.rc (partie 2)

#include "resource.h"

#define IDC_EDIT 3000
#define IDC_BUTTON 3001
#define IDC_EXIT 3002

#define IDR_MENU1 3003

#define IDM_GETTEXT 32000
#define IDM_CLEAR 32001
#define IDM_EXIT 32003

MyDialog DIALOG 10, 10, 205, 60
STYLE 0x0004 | DS_CENTER | WS_CAPTION | WS_MINIMIZEBOX |
WS_SYSMENU | WS_VISIBLE | WS_OVERLAPPED | DS_MODALFRAME | DS_3DLOOK
CAPTION "Our Second Dialog Box"
MENU IDR_MENU1
BEGIN
EDITTEXT IDC_EDIT, 15,17,111,13, ES_AUTOHSCROLL | ES_LEFT
DEFPUSHBUTTON "Dis Bonjour", IDC_BUTTON, 141,10,52,13
PUSHBUTTON "E&xit", IDC_EXIT, 141,26,52,13
END

IDR_MENU1 MENU
BEGIN
POPUP "Test Controls"
BEGIN
MENUITEM "Get Text", IDM_GETTEXT
MENUITEM "Clear Text", IDM_CLEAR
MENUITEM "", , 0x0800 /*MFT_SEPARATOR*/
MENUITEM "E&xit", IDM_EXIT
END
END

The analysis follows:

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD

Nous déclarons le prototype de fonction de DlgProc ,ainsi nous pouvons lui attribuer l'opérateur addr à la ligne ci-dessous :

invoke DialogBoxParam, hInstance, ADDR DlgName,NULL, addr DlgProc, NULL

la susdite ligne appelle la fonction DialogBoxParam qui prend 5 paramètres : l'Instance handle', le 'nom du calibre de boîte de dialogue', l'handle de la fenêtre parente', l'adresse de la procédure de la de boîte de dialogue et des données dialogue-spécifiques. DialogBoxParam crée une boîte de dialogue modale. Rien ne sera retourné tant que la boîte de dialogue ne sera pas détruite.

.IF uMsg==WM_INITDIALOG
invoke GetDlgItem, hWnd,IDC_EDIT
invoke SetFocus,eax
.ELSEIF uMsg==WM_CLOSE
invoke SendMessage,hWnd,WM_COMMAND,IDM_EXIT,0

La procédure de boîte de dialogue ressemble à une procédure de fenêtre sauf qu'elle ne reçoit pas le message WM_CREATE. Le premier message qu'elle reçoit est WM_INITDIALOG. Normalement vous pouvez mettre le code d'initialisation ici. Remarquez que vous devez renvoier la valeur TRUE (VRAIE) dans eax si vous traitez le message.
L'administrateur interne de boîte de dialogue n'envoie pas à notre procédure de boîte de dialogue par défaut le message WM_DESTROY quand WM_CLOSE est envoyé à notre boîte de dialogue. Ainsi si nous voulons réagir quand l'utilisateur presse le bouton proche sur notre boîte de dialogue, nous devons traiter le message WM_CLOSE. Dans notre exemple, nous envoyons le message WM_COMMAND avec la valeur IDM_EXIT dans wParam. Ça a le même effet que lorsque l'utilisateur choisit l'article de menu de Sortie (le sous-menu : EXIT). EndDialog est appelé dans la réponse à IDM_EXIT.
Le traitement du messages WM_COMMAND reste le même.
Quand vous voulez détruire la boîte de dialogue, il faut appeler la fonction EndDialog. N'essayez pas DestroyWindow! EndDialog ne détruit pas la boîte de dialogue immédiatement. Il met seulement un drapeau(Flag) pour l'administrateur de la boîte de dialogue et continue à exécuter les instructions suivantes.
Maintenant on va examiner le fichier de ressources. Un changement notable est qu'au lieu de l'utilisation d'une chaîne de texte pour le nom de menu nous employons une valeur, IDR_MENU1. C'est nécessaire si vous voulez attacher un menu à une boîte de dialogue créée avec DialogBoxParam. Notez que dans le calibre de boîte de dialogue, vous devez ajouter le mot-clé MENU suivi de la ressource du menu ID.
La différence entre les deux exemples de ce Tutorial est le manque d'une icône dans le dernier exemple. Cependant, vous pouvez mettre l'icône en envoyant le message WM_SETICON à la boîte de dialogue pendant WM_INITDIALOG.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 11: More about Dialog Box

Tutorial 11: Encore Plus à Propos
Des Boîtes de Dialogue

Dans ce Tutorial, nous allons en apprendre encore plus sur la boîte de dialogue. Plus spécialement, nous allons voir comment employer les boîtes de dialogue en tant que dispositifs d'entrée-sortie. Si vous avez lu le tutorial précédent, celui-ci ne sera qu'une brise. Il s'agit ici d'une modification mineure pour pouvoir utiliser les boîtes de dialogue comme des adjonctions à notre fenêtre principale. Aussi dans ce tutorial, nous allons apprendre à utiliser les boîtes de dialogue comme elles sont utilisées normalement.

Downloadez les exemples de DialogBox ici et ici. Downloadez l'exemple de DialogBox couramment utilisée ici.

Théorie:

Vite fait, voici ce qui doit être dit à propos de l'emploi des boîtes de dialogue en tant que dispositifs d'entrée-sortie de notre programme. Comme d'habitude, notre programme crée la fenêtre principale puis quand vous voulez montrer la DialogBox, appelez juste CreateDialogParam ou bien DialogBoxParam. Avec l'appel de DialogBoxParam, vous n'avez rien de plus à faire, traitez seulement les messages dans la procédure de la boîte de dialogue. Avec CreateDialogParam, vous devez insérer un 'Call IsDialogMessage' dans la boucle de message pour permettre à l'administrateur de la boîte de dialogue de gérer la navigation du clavier dans votre boîte de dialogue à votre place. Puisque ces deux cas sont rares, je ne mettrai pas le code source ici. Vous pouvez télécharger les exemples et les examiner vous-même, ici et ici.
Il existe des objets prédéfinits et tout près à être utilisés, ce sont les 'Common Controls' voir le Tutorial 18. Il en existe beaucoup, mais ici nous nous limiterons seulement aux objets de type : DialogBox.
Continuons avec ces 'Common' boîtes de dialogue (DialogBox prédéfinies). Windows possède des DialogBox prédéterminées pour que vos applications puissent les utiliser. Ces boîtes de dialogue existent pour servir d'interfaces utilisateur standardisées. Il s'agit de boîtes comme celle de l'imprimante, la couleur, la fonte ou les boîtes de dialogue de recherche...etc. Vous devez les utiliser dès que c'est possible. Ces boîtes de dialogue prédéfinies résident dans comdlg32.dll. Pour les employer, vous devez vous lier avec comdlg32.lib. Vous créez ces boîtes de dialogue en appelant les fonctions appropriées dans la bibliothèque des 'Common' DialogBox . Pour sélectionner et ouvrir des fichiers, on se sert d'une boite de dialogue déjà à notre disposition, c'est la spéciale boîte d'ouverture de fichier, la boîte GetOpenFileName. Pour sauvegarder des fichier on se sert de sa soeur la DialogBox GetSaveFileName, pour les impressions c'est la boîte de dialogue de l'imprimante, qui est PrintDlg et cetera. Chacune de ces fonctions utilise un pointeur comme paramètre dans sa structure. Vous devez les regarder dans la référence Win32 API. Dans ce Tutorial, on va justement voir comment créer et employer cette DialogBox servant à ouvrir des fichiers.
Voici ci-dessous le prototype de fonction de GetOpenFileName :
GetOpenFileName proto lpofn:DWORD
Vous pouvez voir qu'il ne reçoit qu'un seul paramètre, un pointeur sur la structure OPENFILENAME. La valeur de retour TRUE(VRAI) signifie que l'utilisateur vient de choisi un fichier pour l'ouvrir, sinon la valeur en retour est FALSE(FAUX). Voyons la structure OPENFILENAME maintenant.
OPENFILENAME STRUCT
lStructSize DWORD ?
hwndOwner HWND ?
hInstance HINSTANCE ?
lpstrFilter LPCSTR ?
lpstrCustomFilter LPSTR ?
nMaxCustFilter DWORD ?
nFilterIndex DWORD ?
lpstrFile LPSTR ?
nMaxFile DWORD ?
lpstrFileTitle LPSTR ?
nMaxFileTitle DWORD ?
lpstrInitialDir LPCSTR ?
lpstrTitle LPCSTR ?
Flags DWORD ?
nFileOffset WORD ?
nFileExtension WORD ?
lpstrDefExt LPCSTR ?
lCustData LPARAM ?
lpfnHook DWORD ?
lpTemplateName LPCSTR ?
OPENFILENAME ENDS
On va voir la signification des membres fréquemment employés.
lStructSize | Taille de la structure OPENFILENAME, en octets.
hwndOwner | handle de la boîte de dialogue qui sert à ouvrir des fichiers.
hInstance | Instance Handle de l'application qui crée la 'boîte de dialogue qui sert à ouvrir des fichiers'.
lpstrFilter | Sert à filtrer le type de chaînes de caractères à ouvrir. Il faut 2 NULL pour terminer cette instruction. La première chaîne de caractères est la description (donc le texte). La deuxième c'est le type de filtre. Par exemple, dans un répertoire on peut sélectionner tous les dossiers ou bien que les .exe ou bien seulement les .zip……etc: FilterString db "All Files (*.*)",0, "*.*",0 db "Text Files (*.txt)",0,"*.txt",0,0 Remarquez bien que c'est la deuxième chaîne de caractères (pour chaque ligne) qui en réalité sert à Windows à filtrer les fichiers. Donc vous êtes obligés de mettre un 0 supplémentaire à la fin de la chaîne de caractères du filtre pour en indiquer sa fin.
nFilterIndex | Indique quelle [paire de chaîne de caractères de] filtre sera utilisée par défaut dans la DialogBox qui sert à ouvrir les fichiers. Ce sera le 1, pour la première paire "All Files (*.*)", le 2 pour la deuxième paire "Text Files (*.txt)" etc. Ainsi dans notre exemple, si nous spécifions nFilterIndex en tant que 2, le deuxième modèle, "*.txt" sera employé.
lpstrFile | Le pointeur du buffer qui contient le nom de fichier qu'on a l'habitude d'initialiser dans la boîte d'édition de notre DialogBox (ici c'est le pointeur qui pointe sur le texte " ALL FILES ". Le pointeur doit avoir une longueur d'au moins 260 octets. Après que l'utilisateur ait choisit un fichier pour l'ouvrir, le nom de ce fichier ainsi que le chemin (Patch) complète où il est stocké, sont gardés dans ce buffer pstrFile. Vous pourrez en extraire l'information plus tard.
nMaxFile | La taille de buffer lpstrFile.
lpstrTitle | Pointeur du titre de la DialogBox qui sert à ouvrir les fichiers. (donc c'est le titre).
Flags | Détermine le style et les caractéristiques de la boîte de dialogue.
nFileOffset | Après que l'utilisateur ait choisi un fichier pour l'ouvrir, ce membre contient le n° du premier caractère du nom du fichier réel. Par exemple, si le nom complète avec son chemin est "c:\Windows\system\lz32.dll", ce membre contiendra la valeur 18.
nFileExtension | Après que l'utilisateur ait choisi un fichier pour l'ouvrir, ce membre contient le numéro du premier caractère de l'extension du fichier. Ici c'est 23

Exemple:

Le programme suivant montre une boîte de dialogue servant à ouvrir des fichiers. Pour en ouvrir un, l'utilisateur doit choisir le Fichier avec le sous-menu 'OPEN'. Quand l'utilisateur choisit un fichier dans la DialogBox, le programme affiche une MessageBox montrant le nom complet, à savoir le nom du fichier avec l'extension de ce fichier.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_EXIT equ 2
MAXSIZE equ 260
OUTPUTSIZE equ 512

.data
ClassName db "SimpleWinClass",0
AppName db "Our Main Window",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
OurTitle db "-=Our First Open File Dialog Box=-: Choose the file to open",0
FullPathName db "The Full Filename with Path is: ",0
FullName db "The Filename is: ",0
ExtensionName db "The Extension is: ",0
OutputString db OUTPUTSIZE dup(0)
CrLf db 0Dh,0Ah,0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if ax==IDM_OPEN
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
mov ofn.lpstrTitle, OFFSET OurTitle
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset ExtensionName
mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileExtension
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax
invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK
invoke RtlZeroMemory,offset OutputString,OUTPUTSIZE
.endif
.else
invoke DestroyWindow, hWnd
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analyse:

mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance

Nous remplissons notre structure OPENFILENAME (que nous avons ici appelée 'ofn') avec les membres courants (de tout programme). Exemple 2e et 3e ligne: on place temporairement le Handle de notre programme sur la pile pour ensuite le reprendre. Et c'est le membre hwndOwner de notre structure 'ofn' qui le reçoit.

mov ofn.lpstrFilter, OFFSET FilterString

Ce FilterString est le filtre du nom de fichier que nous définissons comme suit :

FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
Remarquez que les quatre chaînes de caractères sont terminées par un Zéro. La première chaîne est la description de la chaîne suivante (EX : ''ALL FILES est la description de *.*''). Le modèle réel est la 2e chaîne de caractères, dans notre cas ce sont *. * et *.txt. En réalité nous pouvons prendre n'importe quel modèle d'extension. exemple : *.wavwav . Nous DEVONS mettre un zéro supplémentaire après la dernière chaîne de caractères pour à montrer que c'est la fin de la chaîne du filtre. N'oubliez surtout pas ça, sinon votre DialogBox se comportera étrangement.

mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

Nous indiquons où la boîte de dialogue mettra le nom du fichier que l'utilisateur vient de choisir. Notez que nous devons spécifier sa taille dans le membre nMaxFile. Plus tard, nous pourrons extraire le nom du fichier contenu dans ce buffer.

mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

Flags indique les caractéristiques de la boîte de dialogue.
Les 'flags' (les drapeaux) OFN_FILEMUSTEXIST et OFN_PATHMUSTEXIST exigent que le nom du fichier et le chemin que l'utilisateur tape dans la zone d'édition DOIVENT exister.
le 'flag' OFN_LONGNAMES dit à la boîte de dialogue de montrer des noms de fichier longs. (et non pas des trucs du genre 'nomtropl~1.exe')
Le flag OFN_EXPLORER indique que l'apparition de la boîte de dialogue doit être semblable à celle de l'Explorer Windows.
Le flag OFN_HIDEREADONLY cache la 'case à cocher : read-only ' de la boîte de dialogue.
Il y a encore beaucoup d'autres flags que vous pouvez employer. Consultez votre référence Win32 API.

mov ofn.lpstrTitle, OFFSET OurTitle

Ce sera le titre de votre boîte de dialogue qui sert à ouvrir des fichiers. Si vous effacez totalement cette ligne, son titre par défaut sera 'Ouverture'

invoke GetOpenFileName, ADDR ofn

Appelle la fonction GetOpenFileName. On passage le pointer de la structure 'ofn' en tant que paramètre pour cette fonction.
À partir d'ici, la boîte de dialogue qui ouvre les fichiers, est affichée à l'écran. La fonction ne retournera pas avant que l'utilisateur ne choisisse un fichier pour l'ouvrir ou bien qu'il ne presse le bouton ANNULER ou FERMER de la boîte de dialogue.
Il renverra la valeur TRUE(VRAI) dans eax si l'utilisateur choisit un fichier pour l'ouvrir. Sinon, il retournera FALSE(FAUX).

.if eax==TRUE
invoke lstrcat,offset OutputString,OFFSET FullPathName
invoke lstrcat,offset OutputString,ofn.lpstrFile
invoke lstrcat,offset OutputString,offset CrLf
invoke lstrcat,offset OutputString,offset FullName

Dans le cas où l'utilisateur choisit un fichier pour l'ouvrir, nous préparons une chaîne de caractère pour qu'elle soit affichée dans une MessageBox. Nous réservons un bloc de mémoire dans la variable OutputString puis ensuite nous employons la fonction API, lstrcat, pour enchaîner ensembles les chaîne de caractères . Pour faire apparaître les chaînes sur plusieurs lignes, nous devons séparer chaque ligne avec un retour chariot.

mov eax,ofn.lpstrFile
push ebx
xor ebx,ebx
mov bx,ofn.nFileOffset
add eax,ebx
pop ebx
invoke lstrcat,offset OutputString,eax

Les susdites lignes exigent quelques explications. nFileOffset contient le n° dans l'ofn.lpstrFile (ce n° c'est son positionnement). Mais vous ne pouvez pas les ajouter ensemble directement puisque nFileOffset est une variable de TAILLE 'WORD' et lpstrFile est de TAILLE 'DWORD'. Il faut donc mettre la valeur de nFileOffset dans le mot bas (le mot de poids faible) de ebx et l'ajouter à la valeur de lpstrFile.

invoke MessageBox,hWnd,OFFSET OutputString,ADDR AppName,MB_OK

Nous affichons la chaîne de caractères dans la MessageBox (boîte de message).

invoke RtlZerolMemory,offset OutputString,OUTPUTSIZE

Nous devons *effacer* l'OutputString avant que nous ne puissions remplir une autre chaîne de caractères. Donc nous employons la fonction RtlZeroMemory pour faire le travail.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 12: Memory Management and File I/O

Tutorial 12 : Gestion de la Mémoire
Et des Fichiers d'entrée-sortie

Dans ce Tutorial, nous allons étudier de façon rudimentaire la gestion de la mémoire et les opérations sur les fichiers i/o (i/o=input/output=entrée sortie). En plus nous utiliserons le Common Control de type 'boîtes de dialogue d'overture de fichiers' (OpenFileName) en tant que dispositifs d'entrée-sortie.

Downloadez l'exemple ici.

Théorie:

La gestion de la mémoire sous Win32 du point de vue de l'application est très simple et directe. Chaque process possède 4 GB d'espace d'adresses mémoire. Le modèle de mémoire employé, est appelé le modèle de mémoire 'FLAT'(plat). Dans ce modèle, tous les registres de segment pointent sur la même adresse de départ, un offset sur 32 bits. Ainsi, une application peut accéder à n'importe quel endroit de la mémoire dans son propre espace d'adresses sans avoir besoin de changer la valeur des registres. Ça simplifie beaucoup la gestion de la mémoire. Il n'y a plus de pointers ou registres "Near" ou "FAR" désormais (pointers proches ou lointains).
Sous Win16, il y a deux principales sortes de fonctions API de mémoire : Global et Local. Le type d'API Global peut utiliser des Call qui vont vers des parties de mémoire se trouvant dans d'autres segments. Ainsi ce type d'API Global sont des fonctions de mémoires "lointaines". Le type d'API Local utilise des Call qui traitent uniquement avec la partie locale où le process en est . Donc ce sont des fonctions de mémoire "proches". Sous Win32, ces deux types sont identiques. Si vous appelez GlobalAlloc ou LocalAlloc, vous obtenez exactement le même résultat.
Voici les étapes pour allouer et utiliser la mémoire :
  1. Réservez un bloc de mémoire en appelant GlobalAlloc. Cette fonction renvoie l'handle du bloc de mémoire demandé.
  2. "Lockez(Fermez)" le bloc de mémoire en appelant GlobalLock. Cette fonction reçoit l'handle du bloc de mémoire et renvoie le pointer du bloc de mémoire.
  3. Vous pouvez employer le pointer pour lire ou écrire dans cette mémoire.
  4. "Ouvrez" le bloc de mémoire en appelant GlobalUnlock. Cette fonction rend nul et sans effet le pointer du bloc de mémoire.
  5. Libérez le bloc de mémoire en appelant GlobalFree. Cette fonction reçoit l'handle du bloc de mémoire.
Vous pouvez aussi remplacer "Global" par "Local" comme LocalAlloc, LocalLock, etc.
Cette méthode sera davantage simplifiée plus loin en employant le flag 'GMEM_FIXED', dans l'appel de GlobalAlloc. Si vous utilisez ce flag, la valeur de retour de Global/LocalAlloc sera le pointer du bloc de mémoire alloué, et non pas l'Handle du bloque de mémoire. Vous ne devez pas appeler Global/LocalLock puis passer le pointer à Global/LocalFree sans appeler d'abord Global/LocalUnlock. Mais dans ce Tutorial, j'emploierai l'approche "traditionnelle".

Les fichiers d'entrée-sortie sous Win32 ressemblent étonnement à ceux sous DOS. Les étapes nécessaires sont les mêmes. Vous avez seulement besoin de changer les appels aux API, c'est tout. Les étapes exigées sont les suivantes :

  1. Ouvrez ou créez le fichier en appelant la fonction CreateFile. Cette fonction est bourrée de ressources : en plus des fichiers, elle est capable d'ouvrir des ports de communication, des lecteurs de disques ou le clavier. En cas de succès (quand tout ce passe bien), elle renvoie l'handle du fichier ou celui du Device = l'extension matérielle (ex: le clavier). Vous pouvez alors employer cet handle pour exécuter des opérations sur le fichier ou le Device.

    2. Déplacez le pointer dans le fichier à l'emplacement désiré en appelant SetFilePointer.
  2. Exécutez, lisez ou écrivez l'opération en appelant ReadFile ou WriteFile. Ces fonctions transfèrent des données d'un bloc de mémoire vers un fichier et inversement. Donc vous devez allouer(réserver) un bloc de mémoire assez grand pour qu'il puisse contenir les données.
  3. Fermez le fichier en appelant CloseHandle. Cette fonction reçoit l'Handle du fichier.

Contenu:

Le programme inscrit ci-dessous, affiche une boîte de dialogue (OpenFile) qui sert à ouvrir des fichiers. Il laisse l'utilisateur choisir un fichier texte quelconque pour l'ouvrir puis affiche le contenu de ce fichier dans une fenêtre d'édition à l'intérieur de son secteur client. L'utilisateur a la possibilité de modifier le texte dans la zone d'édition à sa guise puis ensuite il peut de retour sauvegarder le contenu dans un fichier.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260
MEMSIZE equ 65535

EditID equ 1 ; n° ID de la zone d'édition.

.data
ClassName db "Win32ASMEditClass",0
AppName db "Win32 ASM Edit",0
EditClass db "edit",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndEdit HWND ? ; L'Handle de la zone d'édition.
hFile HANDLE ? ; Handle du fichier.
hMemory HANDLE ? ; Handle du bloc de mémoire réservé.
pMemory DWORD ? ; Pointer du bloc de mémoire réservé
SizeReadWrite DWORD ? ; Nombre d'octets réellement lus ou écrits. (dans le bloque mémoire on peut très bien ne pas tout utiliser).

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,hwndEdit
;====================================================
; Initialisation des members de la structure OPENFILENAME
;====================================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax
invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL
invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif
invoke SetFocus,hwndEdit
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analyse:

invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
0,0,0,hWnd,EditID,\
hInstance,NULL
mov hwndEdit,eax

Dans la section WM_CREATE, nous créons un contrôle d'édition (ou une zone d'édition, de saisie). Notez que les paramètres x, y, width, height qui sont les dimensions du contrôle sont tous mis à zéros puisque nous redéfinirons leurs tailles plus tard pour que son secteur client soit contenu en entier dans la fenêtre parente.
Remarquez que dans ce cas, nous ne devons pas appeler ShowWindow pour faire apparaître la zone d'édition à l'écran, parce que nous incluons le style WS_VISIBLE. Vous pouvez aussi utiliser ce tour de passe passe pour la fenêtre parente.

;=====================================================
; Initialisation des membres de la structure OPENFILENAME.
;=====================================================
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE

Après la création du contrôle d'édition, prenons le temps d'initialiser les membres d'ofn. Parce que nous souhaitons aussi réutiliser ofn pour la boîte de dialogue de sauvegarde, nous remplissons uniquement les membres *communs* employés par GetOpenFileName et GetSaveFileName.
La section WM_CREATE est un formidable endroit pour pouvoir faire vite fait l'initialisation.

.ELSEIF uMsg==WM_SIZE
mov eax,lParam
mov edx,eax
shr edx,16
and eax,0ffffh
invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE

Nous recevons des messages WM_SIZE lorsque la taille du secteur client de notre fenêtre principale change. Nous recevons aussi ce message la toute première fois que la fenêtre est créée. Pour pouvoir recevoir ce message, les styles de classe de fenêtre doivent inclure les styles CS_VREDRAW et CS_HREDRAW. Nous profitons de cette occasion pour redimensionner notre contrôle d'édition à la taille du le secteur client de notre fenêtre parente. D'abord nous devons connaître la largeur actuelle et la hauteur du secteur client de la fenêtre parente. Nous obtenons ces renseignements de lParam. Le mot de poids fort d'lParam contient la hauteur alors que le mot de poids faible d'lParam contient la largeur du secteur client. Nous employons alors ces deux informations pour redimentionner la zone d'édition en appelant la fonction MoveWindow. En plus du changement de la taille de la fenêtre, nous pouvons aussi changer sa position.

.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn

Quand l'utilisateur choisit le sous-menu File/Open, nous remplaçons les Flags de la structure ofn et appelons la fonction GetOpenFileName pour afficher la boîte de dialogue qui sert à ouvrir des fichiers.

.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFile,eax

Après que l'utilisateur ait choisi un fichier pour l'ouvrir, nous appelons CreateFile pour ouvrir ce fichier. Nous indiquons à cette fonction qu'elle doit essayer d'ouvrir le fichier pour le lire ou bien écrire dedans. Après que le fichier soit ouvert, la fonction renvoie l'handle de ce fichier que nous stockons dans une variable Globale pour une future utilisation. Cette fonction a la syntaxe suivante :

CreateFile proto lpFileName:DWORD,\
dwDesiredAccess:DWORD,\
dwShareMode:DWORD,\
lpSecurityAttributes:DWORD,\
dwCreationDistribution:DWORD\,
dwFlagsAndAttributes:DWORD\,
hTemplateFile:DWORD

dwDesiredAccess spécifie quelle est l'opération que vous voulez exécuter sur le fichier.

  • 0 Ouvre le fichier pour interroger ses attributs. Vous avez le droit de lire ou écrire les données.
  • GENERIC_READ Ouvre le fichier pour le lire.
  • GENERIC_WRITE Ouvre le fichier pour pouvoir écrire dedans.
dwShareMode indique quelle opération, sur le fichier ouvert, vous voulez permettre à d'autres process d'effectuer.
  • 0 Empêche le partage du fichier avec d'autres process.
  • FILE_SHARE_READ permet à d'autres process de lire les données, du fichier qui est ouvert.
  • FILE_SHARE_WRITE permet à d'autres process d'écrire des données dans le fichier qui est ouvert.
lpSecurityAttributes n'a aucune signification sous Windows 95.
dwCreationDistribution indique que l'action CreateFile s'exécutera, que le fichier indiqué dans lpFileName existe ou non.
  • CREATE_NEW Crée un nouveau fichier. La fonction échoue si le fichier indiqué existe déjà.
  • CREATE_ALWAYS Crée un nouveau fichier. La fonction recopie le fichier s'il existe.
  • OPEN_EXISTING Ouvre le fichier. La fonction échoue si le fichier n'existe pas.
  • OPEN_ALWAYS Ouvre le fichier, s'il existe. Si le fichier n'existe pas, la fonction crée le fichier comme si dwCreationDistribution était CREATE_NEW.
  • TRUNCATE_EXISTING Ouvre le fichier. Une fois ouvert, le fichier est tronqué(écourté) pour que sa taille finisse par des octets zéro (ex taille de 400 octets). Le process d'appel doit ouvrir le fichier avec au moins l'accès GENERIC_WRITE. La fonction échoue si le fichier n'existe pas.
dwFlagsAndAttributes indique les attribus du fichier
  • FILE_ATTRIBUTE_ARCHIVE dit que le fichier est un fichier d'archives. Les applications emploient cet attribut pour marquer des fichiers pour des Backup(sauvegardes) ou des déplacements.
  • FILE_ATTRIBUTE_COMPRESSED indique que le fichier ou le dossier est comprimé. Pour un fichier, ça signifie que toutes les données dans le fichier sont comprimées. Pour un dossier, ça signifie que la compression est par défaut pour tous les fichiers et ses sous-répertoires nouvellement créés.
  • FILE_ATTRIBUTE_NORMAL indique que le fichier n'a aucun autre jeu d'attributs. Cet attribut est valable seulement s'il est employé seul.
  • FILE_ATTRIBUTE_HIDDEN indique que c'est un fichier caché. Il ne doit pas figurer dans une inscription de renseignements ordinaire.
  • FILE_ATTRIBUTE_READONLY le fichier ne peut être qu'uniquement lu. Les applications peuvent lire le fichier, mais ne peuvent pas y écrire ni le supprimer.
  • FILE_ATTRIBUTE_SYSTEM le fichier fait parti ou bien est exclusivement employé par le système d'exploitation. c'est donc un fichier 'system'.
invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax

Quand le fichier est ouvert, nous allouons(réservons) un bloc de mémoire pour l'utiliser par les fonctions WriteFile ou ReadFile. Nous spécifions le flag GMEM_MOVEABLE pour que ce soit Windows qui s'occupe de déplacer le bloc de mémoire dans le but de consolider la mémoire (défragmentation locale). Le flag GMEM_ZEROINIT dit à GlobalAlloc de remplir le bloc de mémoire nouvellement réservé avec des zéros.
Quand les retours de GlobalAlloc sont menés avec succès, eax contiennent l'handle du bloc de mémoire alloué. Nous passons cet handle à la fonction GlobalLock qui renvoie ensuite le pointer au bloc de mémoire.

invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory

Quand le bloc de mémoire est prêt pour l'utilisation, nous appelons la fonction ReadFile pour lire les données du fichier. Quand un fichier est d'abord ouvert ou créé, le fichier n'est pas. Ainsi dans ce cas, nous commençons à lire le premier octet dans le fichier. Le premier paramètre de ReadFile est l'handle du fichier à lire, le deuxième est le pointer du bloc de mémoire qui content les données, ensuite c'est le nombre d'octets à lire dans le fichier, le quatrième paramètre est l'adresse de la variable (de taille DWORD) qui contiendra le nombre d'octets réellement lus dans le fichier.
Après que nous ayons remplis le bloc de mémoire des données, nous transvasons ces données vers le contrôle d'édition (la zone de saisie) en envoyant le message WM_SETTEXT au contrôle d'édition, avec lParam qui contient le pointer du bloc de mémoire. Après cet appel, le contrôle d'édition affiche les données dans son secteur client.

invoke CloseHandle,hFile
invoke GlobalUnlock,pMemory
invoke GlobalFree,hMemory
.endif

À ce point, nous n'avons aucun besoin de maintenir le fichier ouvert plus longtemps puisque notre but est d'écrire dans un autre fichier (que le fichier original)les données visibles dans contrôle d'édition. Donc nous fermons le fichier en appelant CloseHandle avec l'Handle du fichier comme son paramètre. Ensuite nous ouvrons le bloc de mémoire et le libérons. En réalité vous ne devez pas libérer la mémoire dès maintenant, vous pourrez réutiliser plus tard le bloc de mémoire pendant l'opération de sauvegarde. Mais dans le but de notre démonstration, je tiens à la libérer ici.

invoke SetFocus,hwndEdit

Quand la DialogBox (qui sert à ouvrir des fichiers) s'affiche à l'écran, le centre d'entrée passe de la fenêtre principale à la DialogBox en question pour que ce soit elle qui soit active. Ainsi quand on referme un fichier de la DialogBox (déjà ouvert), nous devons rendre le centre d'entrée à la zone de saisie (au contrôle d'édition), qui se trouve en arrière.
Ceci fini l'opération de lecture sur le fichier. À ce point, l'utilisateur peut écrire sur le contrôle d'édition. Et quand il veut sauvegarder les données dans un autre fichier, il doit choisir le sous-menu File/Save qui affiche une DialogBox pour les sauvegardes. La création de la boîte de dialogue de sauvegarde n'est pas très différente de la boîte de dialogue d'ouverture de fichiers. En fait, elles diffèrent seulement entre le nom des fonctions, GetOpenFileName et GetSaveFileName. Vous pouvez aussi réutiliser la plupart des membres de la structure ofn, sauf le membre Flag.

mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY

Dans notre cas, nous voulons créer un nouveau fichier, donc OFN_FILEMUSTEXIST et OFN_PATHMUSTEXIST doivent être omis sinon la boîte de dialogue ne nous laissera pas créer un fichier qui n'existe pas déjà.
Le paramètre dwCreationDistribution de la fonction CreateFile doit être changé en CREATE_NEW puisque nous voulons créer un nouveau fichier.
Le reste du code est identique à celui dans la partie 'fichiers à ouvrir' sauf la chose suivante :

invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL

Nous envoyons le message WM_GETTEXT au contrôle d'édition pour que celui-ci puisse recopier les données (le texte) qu'il contient dans le bloc de mémoire que nous avions alloués, la valeur de retour dans eax est la longueur des données à l'intérieur du buffer. Après que les données soient dans le bloc de mémoire, nous les écrivons dans un nouveau fichier.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 13: Memory Mapped Files

Tutorial 13 : Fichiers à Mémoire Configurée

(Memory Mapped Files)

Je vais vous montrer ce que sont les fichiers à mémoires configurées et comment les employer à votre avantage. L'utilisation d'un fichier à mémoire configurée est très facile comme vous le verrez dans ce Tutorial.

Downloadez l'exemple ici.

Aparté : Le procédé de File Mapping ou en français 'Mapper un Fichier en mémoire' c'est en faite faire une copie en mémoire d'un fichier qui était jusque là bien physique. Par exemple un fichier *.txt (physique) peut être entièrement copié quelque part en mémoire (il est Mappé) ce qui présente de gros avantages.

Théorie:

Si vous examinez l'exemple du Tutorial précédent, vous constaterez qu'il a un défaut sérieux : que se passe-t-il si la taille du fichier vous voulez lire est plus grande que le bloc de mémoire allouée ? Ou que la chaîne de caractère que vous voulez chercher est coupée en deux à la fin du bloc de mémoire ? La réponse traditionnelle pour la première question c'est que vous devez à plusieurs reprises lire dans les données du fichier avant que l'on ne rencontre la fin du fichier. La réponse à la deuxième question c'est que vous devez vous préparer à ce cas spécial de fin de bloc de mémoire. ça s'appelle un problème de valeur aux frontières. C'est un vrai casse tête pour les programmeurs car ça engendre des bogues (des défauts) innombrables.
il n'y aurait plus aucun problème si nous pouvions réserver un très grand bloc de mémoire, assez grand pour stocker le fichier en entier. Cependant notre programme s'accaparerait trop de ressources. Ce sont les (Files Mapping) configurations de fichiers qui vont nous sauver. Pour utiliser la configuration de fichier, vous pouvez vous imaginer que c'est un fichier entier, étant déjà chargé en mémoire et vous pouvez employer un pointer de mémoire pour lire ou écrire des données dans ce fichier. C'est aussi simple que cela. Nul besoin d'employer des fonctions API de mémoire et pas besoin de faire de différence entre les fonctions API de fichiers d'entrée et de sortie non plus, ce sont les mêmes dans la configuration de fichiers (Files Mapping). La configuration de fichier est aussi employée comme moyen de partage des données entre plusieurs process (entre plusieurs programmes). En employant la configuration de fichiers de cette façon, il n'y aucun fichier réellement impliqué. C'est plutôt dans le bloc de mémoire réservé que chaque process peut *lire ou écrire*. Mais le partage de ces données entre plusieurs process est un point délicat, ça ne doit pas être traité à la légère. Vous devez exécuter ces process par ordre (les uns après les autres) en les synchronisant, sinon vos applications planteront très vite.
Dans ce Tutorial, nous ne verrons pas le sujet de 'fichier configuré' (File Mapping) en tant que moyen pour créer une zone de mémoire partagée. Nous allons plutôt voir comment utiliser le 'File Mapping' comme moyen de "Mapper" un fichier en mémoire. En fait, le 'PE Loader' emploie la configuration de fichier (File Mapping) pour charger des fichiers exécutables en mémoire. C'est très commode uniquement pour les parties du fichier nécessaires, qui doivent être sélectivement lues sur le disque. Sous Win32, vous devez employer le File Mapping autant que possible.
Il y a quelques limitations à la configuration de fichier, quoique. Une fois que vous créez un fichier à mémoire configurée, sa taille ne peut pas être changée pendant cette session. Donc le 'File Mapping' est important pour les fichiers 'Read-Only' (une des propriété d'un fichier) ou pour des opérations sur des fichiers qui ne doivent en aucun cas affecter la taille du fichier. Ça ne signifie pas que vous ne pouvez pas employer le File Mapping lorsque vous voulez augmenter la taille du fichier. Vous pouvez estimer sa nouvelle taille et prendre comme mémoire cette taille pour votre fichier configuré, ainsi le fichier s'agrandira à cette taille. C'est pas très pratique, c'est tous.
Assez d'explications. On va plonger dans l'exécution de configuration de fichier (File Mapping). Pour employer le (File Mapping), on doit suivre ces étapes :
  1. Appelez CreateFile pour ouvrir le fichier que vous voulez Mapper.
  2. Appelez CreateFileMapping avec l'Handle du fichier renvoyé par CreateFile comme un de ses paramètres. Cette fonction crée un objet 'File Mappinf' du fichier ouvert par CreateFile.
  3. Appelez MapViewOfFile pour Mapper une région du fichier choisie ou bien du fichier entier en mémoire. Cette fonction renvoie le pointer du premier octet de la zone où commence le fichier configuré (Mappé).
  4. Employez le pointer pour lire ou écrire dans le fichier.
  5. Appelez UnmapViewOfFile pour Unmapper le fichier.
  6. Appelez CloseHandle avec l'Handle du fichier configuré (l'Handle du File Mapping) comme paramètre pour fermer le fichier configuré.
  7. Appelez CloseHandle une fois de plus, mais cette fois-ci avec l'Handle renvoyé par CreateFile pour fermer le fichier réel.

Exemple:

Le programme ci-dessous vous laisse ouvrir un fichier grâce à une DialogBox. Il ouvre le fichier en employant le principe du 'File Mapping', si le fichier est ouvert avec succès, le titre de fenêtre principale est changé par nom du fichier qui vient d'être ouvert. Vous pouvez ensuite sauvegarder le fichier sous un autre nom par le biais du sous-menu File/save. Le programme recopiera le contenu total du fichier ouvert, vers un nouveau fichier. Notez que vous ne devez pas appeler GlobalAlloc pour réserver un bloc de mémoire dans ce programme.

.386
.model flat,stdcall
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260

.data
ClassName db "Win32ASMFileMappingClass",0
AppName db "Win32 ASM File Mapping Example",0
MenuName db "FirstMenu",0
ofn OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)
hMapFile HANDLE 0 ; Handle du 'memory mapped file', doit être
; initialisé avec un 0 parce qu'on doit aussi l'utiliser comme
; un flag dans la section WM_DESTROY

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hFileRead HANDLE ? ; Handle du fichier source (fichier réel)
hFileWrite HANDLE ? ; Handle du fichier de 'sortie'.
hMenu HANDLE ?
pMemory DWORD ? ; Pointer sur les données dans le fichier source.
SizeWritten DWORD ? ; Nombre d'octes actuellement écrit par WriteFile. (écrit dans la Box
; où on peut mettre du texte)

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke GetMenu,hWnd ; Obtient l'handle du menu
mov hMenu,eax
mov ofn.lStructSize,SIZEOF ofn
push hWnd
pop ofn.hWndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,MAXSIZE
.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_OPEN
mov ofn.Flags, OFN_FILEMUSTEXIST or \
OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileRead,eax
invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL
mov hMapFile,eax
mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED
.endif
.elseif ax==IDM_SAVE
mov ofn.Flags,OFN_LONGNAMES or\
OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetSaveFileName, ADDR ofn
.if eax==TRUE
invoke CreateFile,ADDR buffer,\
GENERIC_READ or GENERIC_WRITE ,\
FILE_SHARE_READ or FILE_SHARE_WRITE,\
NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
NULL
mov hFileWrite,eax
invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax
invoke GetFileSize,hFileRead,NULL
invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL
invoke UnmapViewOfFile,pMemory
call CloseMapFile
invoke CloseHandle,hFileWrite
invoke SetWindowText,hWnd,ADDR AppName
invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED
.endif
.else
invoke DestroyWindow, hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

end start

Analyse:

invoke CreateFile,ADDR buffer,\
GENERIC_READ ,\
0,\
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
NULL

Quand l'utilisateur choisit un fichier dans la DialogBox qui sert à ouvrir des fichiers, nous appelons CreateFile pour l'ouvrir. Notez que nous spécifions GENERIC_READ pour ouvrir ce fichier avec comme propriété 'Read-Only'. 'dwShareMode' est mis à zéro parce que nous ne voulons pas que d'autres process n'aient accès au fichier pendant notre opération.

invoke CreateFileMapping,hFileRead,NULL,PAGE_READONLY,0,0,NULL

Alors nous appelons CreateFileMapping pour Mapper en mémoire le fichier(réel) ouvert. La syntaxe de CreateFileMapping est la suivante :

CreateFileMapping proto hFile:DWORD,\
lpFileMappingAttributes:DWORD,\
flProtect:DWORD,\
dwMaximumSizeHigh:DWORD,\
dwMaximumSizeLow:DWORD,\
lpName:DWORD

Sachez déjà, que CreateFileMapping n'est pas obligé de Mapper la totalité de la mémoire du fichier. Vous pouvez utiliser cette fonction pour Mapper seulement une partie de la mémoire du fichier réel. Vous spécifiez la taille de la mémoire du fichier à Mapper dans les paramètres dwMaximumSizeHigh et dwMaximumSizeLow. Si vous indiquez une taille plus grande que le fichier réel, le fichier réel sera agrandit à la nouvelle taille. Si vous voulez que la taille de la mémoire du 'File Mapping' soit la même que celle du fichier réel, mettez ces deux derniers paramètres à zéro.
Vous pouvez employer un NUL dans le paramètre lpFileMappingAttributes pour que ce soit Windows qui crée le 'Fichier Mappé' avec des attributs de sécurité par défaut.
FlProtect définit la protection désirée pour le 'Fichier Mappé'. Dans notre exemple, nous employons PAGE_READONLY pour permettre uniquement l'opération de lecture sur le 'Fichier Mappé'. Notez que cet attribut ne doit pas contredire l'attribut employé dans CreateFile sinon CreateFileMapping échouera.
LpName pointe sur le nom de le 'Fichier Mappé'. Si vous voulez partager ce fichier avec d'autre process, vous devez lui donner un nom. Mais dans notre exemple, notre process est le seul qui emploie ce fichier donc nous ignorons ce paramètre.

mov eax,OFFSET buffer
movzx edx,ofn.nFileOffset
add eax,edx
invoke SetWindowText,hWnd,eax

Si CreateFileMapping s'effectue avec succès, le nom du fichier ouvert prend la place du titre de la fenêtre. Le nom du fichier ainsi que son chemin complète sont stocké dans le buffer, nous voulons afficher uniquement le nom de fichier dans le titre donc nous devons ajouter la valeur du membre nFileOffset de la structure OPENFILENAME à l'adresse de ce buffer. (vous avez compris pourquoi ! ! !)

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_ENABLED

Comme précaution, nous ne voulons pas que l'utilisateur puisse ouvrir plusieurs fichiers les uns après les autres, donc nous devons griser le sous-menu 'Open' et dégriser le sous-menu 'Save'. EnableMenuItem est justement employé pour changer les attributs du menu..
Après ceci, nous attendons que l'utilisateur clique sur le sous-menu File/Save pour fermer notre programme. Au moment où l'utilisateur ferme le programme, nous devons en fait fermer le 'Fichier Mappé' ainsi que le fichier réel. Voici comment faire :

.ELSEIF uMsg==WM_DESTROY
.if hMapFile!=0
call CloseMapFile
.endif
invoke PostQuitMessage,NULL

Dans le susdit petit bout de code, quand la procédure de fenêtre reçoit le message WM_DESTROY, il vérifie d'abord la valeur d'hMapFile pour voir si c'est un zéro ou non. Si ce n'est pas le zéro, il appelle la fonction CloseMapFile qui contient le code suivant :

CloseMapFile PROC
invoke CloseHandle,hMapFile
mov hMapFile,0
invoke CloseHandle,hFileRead
ret
CloseMapFile endp

CloseMapFile ferme le 'Fichier Mappé' et le fichier réel, pour empêcher qu'il n'y ait une fuite de ressources lorsqu'on sort de notre programme Win32.
Si l'utilisateur veut sauvegarder les données dans un autre fichier, le programme l'affiche dans une DialogBox spéciale 'Sauvegarde'. Après qu'il ait tapé le nom du nouveau fichier, le fichier est créé par la fonction CreateFile.

invoke MapViewOfFile,hMapFile,FILE_MAP_READ,0,0,0
mov pMemory,eax

Immédiatement après que le fichier ait été créé, nous appelons MapViewOfFile pour Mapper la partie désirée du 'Fichier Mappé' en mémoire. Cette fonction a la syntaxe suivante :

MapViewOfFile proto hFileMappingObject:DWORD,\
dwDesiredAccess:DWORD,\
dwFileOffsetHigh:DWORD,\
dwFileOffsetLow:DWORD,\
dwNumberOfBytesToMap:DWORD

dwDesiredAccess indique quelle opération nous souhaitons faire sur le fichier. Dans notre exemple, nous voulons seulement lire les données, donc nous employons FILE_MAP_READ.
dwFileOffsetHigh et dwFileOffsetLow indiquent l'offset de départ où commence le fichier que vous voulez Mapper en mémoire. Ici, nous voulons lire dans la totalité du fichier donc nous commençons le Mapping à l'offset 0.
dwNumberOfBytesToMap indique le nombre d'octets à Mapper en mémoire. Si vous voulez Mapper le fichier en entier (Spécifié par CreateFileMapping), donnez la valeur 0 à MapViewOfFile.
Après l'appel de MapViewOfFile, la partie désirée est chargée en mémoire. On vous renverra le pointer du bloc de mémoire qui contient les données du fichier.

invoke GetFileSize,hFileRead,NULL

Regarde de quelle taille est le fichier. La taille du fichier est renvoyée dans eax. Si le fichier est plus grand que 4 GB, le mot de poids fort de la taille du fichier est stocké dans FileSizeHighWord. Puisque nous ne nous attendons pas à manipuler un si grand fichier, nous pouvons l'ignorer.

invoke WriteFile,hFileWrite,pMemory,eax,ADDR SizeWritten,NULL

Écrit des données dans le fichier Mappé en Mémoire.

invoke UnmapViewOfFile,pMemory

Quand tout est près avec le fichier d'entrée, on l''Unmappe' de la mémoire.

call CloseMapFile
invoke CloseHandle,hFileWrite

et on referme tous les fichiers.

invoke SetWindowText,hWnd,ADDR AppName

Restore le titre original de la fenêtre principale.

invoke EnableMenuItem,hMenu,IDM_OPEN,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_SAVE,MF_GRAYED

Réactive le sous-menu 'Open' et désactive le sous-menu 'Save' en le grisant.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 14: Process

Tutorial 14: Process

Nous allons voir ce qu'est un process et comment faire pour le créer et le terminer.

Downloadez l'exemple ici. Attention en fait cet exemple est dans un *.ZIP et il ne marchera pas si vous le laissez dedans, donc dézippez-le.

Préliminaire:

Qu'est ce qu'un 'process' ? Je cite cette définition de la référence Win32 API:
"Un 'process' est une application (un programme) qui sert à exécuter un autre programme. C'est un espace d'adresses virtuel privé, de code, de données et d'autres ressources du système d'exploitation, comme les fichiers, ou la synchronisation d'objets qui sont visibles pour le process.."
Comme vous pouvez le voir dans la définition ci-dessus, un process "possède" plusieurs objets : l'espace d'adresse, le(s) module(s) (les autres programmes ou dll) à exécuter et tout ce que ces programmes créent ou ouvrent. Au minimum, un process doit consister en l'exécution d'un seul module, avec un espace d'adresse privé et un lien. Chaque process doit avoir au moins un lien. Qu'est ce qu'un lien ? Un lien est en réalité une suite d'instructions d'exécution en attente. Lorsque Windows crée un premier process, il crée seulement un lien de process. D'habitude, ce lien fait en sorte d'exécuter la première instruction dans le module (dans le programme ciblé). Si le process à besoins de plusieurs autres liens par la suite, il peut les créer.
Quand Windows reçoit une commande pour créer un process, il alloue un espace d'adresses mémoire privé et ensuite il Mappe (recopie) le fichier exécutable dans cet espace. Après cela il crée le premier lien du process.
Sous Win32, vous pouvez aussi créer des process de vos propres programmes en appelant la fonction CreateProcess. CreateProcess a la syntaxe suivante :

CreateProcess proto lpApplicationName:DWORD,\
lpCommandLine:DWORD,\
lpProcessAttributes:DWORD,\
lpThreadAttributes:DWORD,\
bInheritHandles:DWORD,\
dwCreationFlags:DWORD,\
lpEnvironment:DWORD,\
lpCurrentDirectory:DWORD,\
lpStartupInfo:DWORD,\
lpProcessInformation:DWORD

Ne soyez pas effrayez par le nombre de paramètres. Nous pouvons ignorer la plupart d'entre eux.

LpApplicationName est le nom du fichier exécutable avec ou sans nom de chemin que vous voulez exécuter. Si ce paramètre est nul, vous devez mettre le nom du fichier exécutable dans le paramètre lpCommandLine.
LpCommandLine sont les arguments de la ligne de commande du programme que vous voulez exécuter. Notez que si lpApplicationName est NULL, ce paramètre doit en plus contenir le nom du fichier exécutable. Comme cela : "notepad.exe readme.txt". (LpCommandLine c'est aussi l'argument qu'on écrit dans W32Dasm quand on fait 'Debug --> Load Process [argument] --> Load' , mais c'est très rare qu'on écrive quelque chose en tant qu'argument!)
LpProcessAttributes et lpthreadAttributes Spécifient les attributs de sécurité pour le process et le lien primaire. S'ils sont NULL, les attributs de sécurité par défaut sont employés.
BInheritHandles est un flag qui indique si vous voulez qu'un autre process hérite du contrôle de toutes les manipulations ouvertes par votre process.
DwCreationFlags représente Plusieurs flags qui déterminent le comportement du process que vous voulez créé, comme : voulez-vous qu'un process soit créés, mais suspendu immédiatement pour que vous puissiez l'examiner ou le modifier avant qu'il ne reparte ? Vous pouvez aussi spécifier la classe de priorité du lien(s) dans un nouveau process. Cette classe de priorité est employée pour déterminer la priorité de planification des liens du process. Normalement nous employons le flag NORMAL_PRIORITY_CLASS.
LpEnvironment est le pointer du bloc d'environnement qui contient plusieurs instructions pour un nouveau process. Si ce paramètre est NULL, le nouveau process hérite de l'environnement du bloc process précédent.
LpCurrentDirectory est le pointer qui est sur la chaîne de caractères lequel indique l'actuel 'Lecteur ' (Drive) ainsi que le répertoire (Directory) du Child Process. Il est NULL si vous voulez que le 'Child Process' succède au 'Parent Process'.
LpStartupInfo pointe sur une structure STARTUPINFO laquelle dit comment doit apparaître la fenêtre principale du nouveau process. La structure STARTUPINFO contient beaucoup de membres servants à définir l'affichage de la fenêtre principale du 'Child Process'. Si vous ne voulez rien de spécial, vous pouvez remplir la structure STARTUPINFO avec les valeurs du 'Parent Process' en appelant la fonction GetStartupInfo.
LpProcessInformation pointe sur la structure PROCESS_INFORMATION laquelle reçoit l'information d'identification du nouveau process. La structure de PROCESS_INFORMATION est la suivante :

PROCESS_INFORMATION STRUCT
hProcess HANDLE ? ; Handle du 'Child Process'
hThread HANDLE ? ; Handle du premier lien du 'Child Process'
dwProcessId DWORD ? ; ID du 'Child Process'
dwThreadId DWORD ? ; ID du premier lien du 'Child Process'
PROCESS_INFORMATION ENDS
L'handle du process et l'ID du process sont deux choses différentes. L'ID est un identificateur unique du process dans le système. L'handle est une valeur renvoyée par Windows pour qu'on puisse l'utiliser avec d'autres process rapportés de fonctions API. L'handle du process ne peut pas être employé pour identifier un process puisque il n'est pas unique.

Après l'appel de CreateProcess, un nouveau process est créé et on retourne (au prog principal) immédiatement après cet appel de CreateProcess. Vous pouvez vérifier si le nouveau process est toujours actif en appelant la fonction GetExitCodeProcess qui a la syntaxe suivante :

GetExitCodeProcess proto hProcess:DWORD, lpExitCode:DWORD

Si cet appel est couronné de succès, lpExitCode contient le statut de terminaison du process en question. Si la valeur dans lpExitCode est égale à STILL_ACTIVE, alors ce process est toujours en cours.

Vous pouvez forcer le process à se terminer en appelant la fonction TerminateProcess. Voici sa syntaxe :

TerminateProcess proto hProcess:DWORD, uExitCode:DWORD

Vous pouvez spécifier le code de sortie désiré pour votre process, n'importe quelle valeur que vous souhaitez. TerminateProcess n'est pas une façon très propre de terminaison d'un process puisque chaque dll rattachée à ce process ne sera pas informée que le process a été fermé.

Exemple:

L'exemple suivant crée un nouveau process lorsque l'utilisateur choisit le sous-menu (le menu item) "Create Process". Il essayera d'exécuter "msgbox.exe". Si l'utilisateur souhaite terminer le nouveau process, il peut choisir le menu item "Terminate Process". Le programme vérifiera d'abord si le nouveau process est déjà fermé, si ce n'est pas le cas, le programme appellera la fonction TerminateProcess pour détruire le nouveau process.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_PROCESS equ 1
IDM_TERMINATE equ 2
IDM_EXIT equ 3

.data
ClassName db "Win32ASMProcessClass",0
AppName db "Win32 ASM Process Example",0
MenuName db "FirstMenu",0
processInfo PROCESS_INFORMATION <>
programname db "msgbox.exe",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hMenu HANDLE ?
ExitCode DWORD ? ; Contient le statut 'exitcode du process' servant à l'appel de GetExitCodeProcess.

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL startInfo:STARTUPINFO
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread
.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp
end start

Analyse:

Ce programme crée la fenêtre principale et renvoie l'handle de son menu pour une future utilisation. Il attend alors que l'utilisateur choisisse une commande du menu. Lorsque l'utilisateur choisit le sous-menu "Process" du menu principal (de la barre de menu), nous déployons le menu grâce au message WM_INITMENUPOPUP pour modifier les sous-menus à l'intérieur du 'menu popup' (menu principal) avant qu'il ne soit affichés.

.ELSEIF uMsg==WM_INITMENUPOPUP
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if eax==TRUE
.if ExitCode==STILL_ACTIVE
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_ENABLED
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif
.else
invoke EnableMenuItem,hMenu,IDM_CREATE_PROCESS,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_TERMINATE,MF_GRAYED
.endif

Pourquoi utilisons-nous ce message dans notre process? Parce que nous souhaitons préparer les sous-menus dans le menu popup avant que l'utilisateur ne puisse les voir. Dans notre exemple, lorsque le process n'est pas encore lancé, nous activons le sous-menu "Start Process" et désactivons "Terminate Process" (il est grisé). On fait le contraire quand le nouveau process est déjà actif.
Nous vérifions d'abord si le nouveau process dirige (a encore la main sur MsgBox.exe) toujours en appelant la fonction GetExitCodeProcess avec l'handle du process qui a été renvoyé par la fonction CreateProcess. Si les retours de GetExitCodeProcess sont FALSE(FAUX), alors ça signifie que notre process n'est pas encore commencé, donc nous grisons (désactivons) le sous-menu "Terminate Process". Si les retours de GetExitCodeProcess sont TRUE(VRAIS), nous savons que notre nouveau process a été lancé, mais nous devons vérifier plus loin s'il est toujours en court. Donc nous comparons la valeur dans ExitCode à la valeur STILL_ACTIVE. S'ils sont égaux,c'est que notre process en encore en court : nous devons alors désactiver le sous-menu "Start Process" puisque nous ne souhaitons pas lancer plusieurs process en même temps.

.if ax==IDM_CREATE_PROCESS
.if processInfo.hProcess!=0
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0
.endif
invoke GetStartupInfo,ADDR startInfo
invoke CreateProcess,ADDR programname,NULL,NULL,NULL,FALSE,\
NORMAL_PRIORITY_CLASS,\
NULL,NULL,ADDR startInfo,ADDR processInfo
invoke CloseHandle,processInfo.hThread

Quand l'utilisateur choisit le menu item "Start Process", nous vérifions d'abord si le membre hProcess de la structure PROCESS_INFORMATION est déjà fermé. Si c'est la première fois, la valeur d'hProcess sera toujours zéro puisque nous définissons la structure de PROCESS_INFORMATION dans la section '.data'. Si la valeur du membre hProcess n'est pas 0, ça signifie que le Child Process est sorti mais nous n'avons pas fermé son handle process encore. Donc c'est maintenant qu'il le faire.
Nous appelons la fonction GetStartupInfo pour récupérer la valeur startupinfo pour pouvoir ensuite la passer à la fonction CreateProcess. Après ça nous appelons la fonction CreateProcess pour commencer un nouveau process. Notez que je n'ai pas vérifié la valeur de retour de CreateProcess parce que ça aurait encore compliqué cet exemple. En réalité, vous devriez vérifier cette valeur de retour (de CreateProcess). Immédiatement après CreateProcess, nous fermons l'handle du premier lien, lequel est renvoyé par la structure processInfo. La fermeture du handle ne veut pas dire que est le lien terminé, mais seulement que nous ne voulons pas employer cet handle pour nous référer au lien de notre programme. Si nous ne le fermons pas, il provoquera une fuite des ressources.

.elseif ax==IDM_TERMINATE
invoke GetExitCodeProcess,processInfo.hProcess,ADDR ExitCode
.if ExitCode==STILL_ACTIVE
invoke TerminateProcess,processInfo.hProcess,0
.endif
invoke CloseHandle,processInfo.hProcess
mov processInfo.hProcess,0

Quand l'utilisateur choisit le sous-menu (le menu item) "Terminate Process", nous vérifions si le nouveau process est toujours actif en appelant la fonction GetExitCodeProcess. Si il est toujours actif, nous appelons la fonction TerminateProcess pour détruire notre process. Aussi nous fermons l'handle du Child Process puisque désormais nous n'en avons plus besoin.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 15: Multithreading Programming

Tutorial 15: Programmation Multi-liens

Dans ce Tutorial, nous allons voir comment créer un programme multi-liens. On va étudier aussi les méthodes de communication entre les liens.

Downloadez l'exemple ici.

Théorie:

Dans le Tutorial précédent, vous avez appris ce qu'était le lien minimum pour un Process: le lien primaire. Un lien est une chaîne d'exécution. Mais vous pouvez aussi créer des liens supplémentaires dans votre programme. Vous pouvez vous représenter le multi-liens comme une multigestion d'un programme. En terme d'exécution, un lien est une fonction qui tourne en parallèle avec le programme principal. Vous pouvez ouvrir puis commander plusieurs fois la même fonction ou bien vous pouvez diriger plusieurs fonctions simultanément selon vos exigences. Le multi-liens est spécifique à Win32, ça n'existe pas sous Win16.
Par exemple, le multi-liens c'est quand un Process ouvre plusieurs fois une même MessageBox (en changent son contenu si on le souhaite) ou bien quand une MessageBox est appelée par plusieurs process différents.
Les liens sont dirigés par un même process donc ils peuvent avoir accès à n'importe quelles ressources de ce process, telles que les variables globales, les Handles etc. Cependant, chaque lien a sa propre délimitation dans le programme, donc les variables locales de chaque liens sont privées vis à vis des autres liens. Chaque lien possède aussi son jeu de registres privé, donc quand Windows se sert d'autres liens, le lien peut "se rappeler" son statut précédant et peut "reprendre" son ancienne tâche quand il regagne son contrôle. Tout ça est traité intérieurement par Windows.
Voilà ce que permet le multi-liens. Dans les deux programmes 'Thread.exe' et 'Thread1.exe' on leur fait exécuter un calcul très lourd qui les occupe. Pour 'Thread.exe' qui ne possède qu'un lien simple ne peut plus rien faire d'autre que son calcul quand il est commencé, il ne répond plus aux actions de l'utilisateur comme déplacer sa fenêtre pendant le calcul.
'Thread1.exe' qui lui possède un système multi-liens, se sert de lien de travail pour faire le calcul mais laisse un lien principale pour répondre aux sollicitations de l'utilisateur. Comme ça, il répond encore, même pendant une lourde tache.
Nous pouvons diviser les liens en deux catégories :
  1. Les liens d'interface pour l'utilisateur : Ce type de liens crée sa propre fenêtre donc il reçoit des messages spécifiques aux fenêtres. Il peut répondre à l'utilisateur via sa propre fenêtre, de là son nom. Ce type de liens est soumis à la règle(autorité) Mutex de Win16 qui permet un seul lien d'interface d'utilisateur, en utilisation 16 bits avec le noyau gdi. alors qu'un lien d'interface d'utilisateur exécute le code d'utilisateur 16 bits avec le noyau gdi, les autres liens UI ne peuvent pas utiliser le service d'utilisateur 16 bits avec noyau gdi. Notez que ce Mutex Win16 est spécifique à Windows 95. Windows NT ne possède pas de Mutex Win16, c'est pourquoi les liens d'interface d'utilisateur de Windows NT s'effectuent plus doucement que sous Windows 95.
  2. Les liens de travail : Ce type de liens ne créent pas de fenêtre donc ils ne peuvent pas recevoir de message spécifiques aux fenêtres. Ils existent principalement pour faire le travail assigné en arrière-plan, de là leur nom.
Je conseille la stratégie suivante pour utiliser la pleine capacité des multi-liens sous Win32 : Laissez le lien primaire faire le truc d'interface d'utilisateur et les autres liens faire le travail complexe en arrière-plan. De cette façon, le lien primaire servira de 'Gouverneur', et les autres liens seront un peu comme le 'personnel du Gouverneur'. Le Gouverneur délègue ses responsabilités à son personnel tandis qu'il maintient le contact avec le public(l'utilisateur). Le personnel du Gouverneur exécute avec obéissance le travail et l'annonce de ses résultats. Si le Gouverneur (le lien primaire) devait exécuter chaque tâche lui-même, il ne serait pas capable de donner beaucoup d'attention au public ou à la presse. Tout ça s'apparente à une fenêtre qui a pour action de donner un long travail pour occupée son lien primaire : il ne répond pas à l'utilisateur avant que le travail ne soit achevé. Un tel programme peut en profiter pour créer un lien additionel qui sera responsable du long travail, permettant ainsi au lien primaire de répondre aux commandes de l'utilisateur.
Nous pouvons créer un lien en appelant la fonction CreateThread, laquelle a la syntaxe suivante :

CreateThread proto lpThreadAttributes:DWORD,\
dwStackSize:DWORD,\
lpStartAddress:DWORD,\
lpParameter:DWORD,\
dwCreationFlags:DWORD,\
lpThreadId:DWORD

Le fonctionnement de CreateThread ressemble beaucoup à celui de CreateProcess.
lpThreadAttributes Vous pouvez le mettre à 0 (NULL) si vous souhaitez que le lien soit le descripteur de sécurité par défaut.
dwStackSize Ça spécifie la taille à réserver sur la pile pour le lien. Si vous voulez que le lien ait la même taille de pile que le lien primaire, employez le 0 (NULL) pour ce paramètre.
lpStartAddress Adresse de la fonction qui fait le 'lien'. C'est la fonction qui exécutera le travail (qui fera le lien). Cette fonction DOIT recevoir un et seulement un paramètre 32 bits et renvoyer une valeur sur 32 bits.
lpParameter C'est le paramètre que vous devez passer à la fonction 'lien'.
dwCreationFlags 0 signifie que le lien prend le commandement immédiatement après qu'il est été créé. L'opposé c'est le flag 'CREATE_SUSPENDED'.
lpThreadId La fonction 'CreateThread' donnera l'ID du nouveau lien créé à cette adresse.

Si l'appel de CreateThread s'est déroulé avec succès, il rend l'handle du lien nouvellement créé. Sinon, il renvoie le NULL.
Le lien est en fonction aussitôt après que l'appel de CreateThread soit un succès, à moins que vous ne mettiez le flag 'CREATE_SUSPENDED' dans dwCreationFlags. Dans ce cas, le lien est suspendu jusqu'à ce que la fonction ResumeThread soit appelée.
Après les retours de la 'fonction de lien' (avec l'instruction ret), Windows appelle la fonction ExitThread tout seul. Mais vous pouvez appeler la fonction ExitThread dans votre 'fonction de lien' par vous-même, mais là je développerai pas !
Vous pouvez retrouver le code de sortie d'un lien en appelant la fonction GetExitCodeThread.
Si vous voulez terminer un lien, vous pouvez appeler la fonction TerminateThread. Mais vous devez employer cette fonction seulement dans des circonstances extrêmes puisque cette fonction termine le lien immédiatement sans lui donner aucune chance de se fermer proprement, sans laissez derrière lui des valeurs qui lui sont spécifiques dans certaines variables.

Maintenant on va vous montrer les méthodes de communications entre les liens.
Il y en a trois:

  • L'utilisation des variables globales
  • Les messages Windows
  • Les événements
Les liens partagent les ressources du process en incluant des variables globales donc les liens peuvent utiliser ces varibles globales pour communiquer les uns avec les autres. Cependant cette méthode doit être utilisée avec grand soin. La synchronisation des liens doit être prise en compte. Par exemple, si deux liens emploient la même structure de 10 membres, ce qui arrive quand Windows prend soudainement le contrôle d'un lien lorsqu'il est en pleine remise à jour de sa structure, l'autre lien sera alors abandonné avec des données incomplètes ou sans significations pour sa structure! Ne faites pas d'erreur, les programmes multi-liens sont bien plus durs à mettre au point et à maintenir. Cette sorte de bogue(défaut) semble bien arriver au hasard, ce qui fait que c'est très difficile à traquer.
Vous pouvez aussi employer des messages de fenêtre pour communiquer entre les liens. Si les liens sont tous des interfaces d'utilisateur, il n'y a aucun problème : cette méthode peut être employée en tant que communication bilatérale. Tout ce que vous devez faire c'est définir un ou plus messages de fenêtres sur mesure lesquels seront spécifiques aux liens. Vous définissez un message sur mesure en employant le message WM_USER comme valeur de base. Voilà, vous pouvez le définir comme ceci :

WM_MYCUSTOMMSG equ WM_USER+100h

Windows n'emploiera jamais aucune valeur WM_USER vers ses propres messages donc vous pouvez employer la valeur WM_USER pour vos propres messages faits sur mesure.
Si un des lien est un lien d'interface utilisateur (le lien primaire, le Gouverneur) et un autre est un des 'personnel du gouverneur', vous ne pouvez pas employer cette méthode en tant que communication bilatérale puisque un lien 'personnel' ne possède pas sa propre fenêtre. Donc il n'a y pas de file d'attente de message. Vous pouvez employer le schéma suivant :

User interface Thread ------> global variable(s)----> Worker thread
Worker Thread ------> custom window message(s) ----> User interface Thread

Soit en Français:

Lien d'interface d'utilisateur (primaire)------> variable(s) globale(s)----> lien de travail (personnel)
lien de travail (personnel)------>'message(s) de fenêtre' conçu sur mesure----> lien d'interface d'utilisateur

En fait, nous employons cette méthode dans notre exemple.
La dernière sorte de communication c'est l'événement objet. Vous pouvez vous représenter l'événement' comme une sorte de flag. Si l'événement est dans l'état "non signalé", le lien est inerte ou en sommeil, dans cet état, le lien ne reçoit pas d'ordre du CPU. Quand l'événement est dans l'état "signalé", Windows "réveille" le lien et il commence à exécuter la tâche assignée.

Exemple:

Vous devez Downloader le fichier *.Zip en exemple et lancer 'thread1.exe'. Cliquez sur le sous-menu (le menu item) "Savage Calculation". Ça ordonnera au programme d'exécuter l'instruction "add eax, eax" 600,000,000 fois. Remarquez que pendant ce temps-là, vous ne pouvez rien faire dans la fenêtre principale : vous ne pouvez pas la déplacer, vous ne pouvez pas activer son menu, etc. Quand le calcul est achevé, une boîte de message apparaît. Après ça, la fenêtre accepte de nouveau vos commandes normalement.
Pour éviter à l'utilisateur ce type d'inconvénient, nous pouvons déplacer la routine "de calcul" dans un lien de travail (personnel du gouverneur) séparé et laisser le lien primaire (le lien gouverneur) continuer avec sa tâche d'interface utilisateur. Vous pouvez voir que bien que la fenêtre principale réponde plus lentement que d'habitude, elle répond quand même toujours.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_CREATE_THREAD equ 1
IDM_EXIT equ 2
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMThreadClass",0
AppName db "Win32 ASM MultiThreading Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
ThreadID DWORD ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
0,\
ADDR ThreadID
invoke CloseHandle,eax
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

end start

Analyse:

Le programme principal se présente à l'utilisateur en tant qu'une fenêtre normale avec un menu. Si l'utilisateur choisit le menu item "Create Thread", le programme crée un lien comme indiqué ci-dessous :

.if ax==IDM_CREATE_THREAD
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

La susdite fonction crée un lien qui contrôlera une procédure nommée ThreadProc en parallèle avec le lien primaire. Après que cet appel soit couronné de succès, CreateThread revoie ses valeurs de retours immédiatement et ThreadProc se lance. Puisque nous n'employons pas d'handle de lien, nous devons le fermer sinon il y aura des fuites de mémoire. Notez que la fermeture de l'handle du lien ne termine pas ce lien. Son seul effet est que désormais nous ne pourrons plus employer l'handle du lien.

ThreadProc PROC USES ecx Param:DWORD
mov ecx,600000000
Loop1:
add eax,eax
dec ecx
jz Get_out
jmp Loop1
Get_out:
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
ret
ThreadProc ENDP

Comme vous pouvez le voir, ThreadProc exécute un calcul sauvage (un calcul immense qui occupe le système) qui prend un longue période de temps avant de se terminer, et quand il est finit il envoie enfin le message WM_FINISH vers la fenêtre principale. WM_FINISH est notre message (celui qu'on a créé sur mesure). Il est ainsi défini :

    WM_FINISH equ WM_USER+100h
Vous n'êtres pas obligez d'ajouter WM_USER et 100h mais c'est plus sûr quand même de faire ça.
Le message WM_FINISH n'a de signification uniquement que dans notre programme. Quand la fenêtre principale reçoit le message WM_FINISH, ça fait apparaître une boîte de message disant que le calcul est achevé.
Vous pouvez créer plusieurs liens en même temps en cliquant sur "Create Thread" plusieurs fois.
Dans cet exemple, la communication est à sens unique, c'est pour ça que c'est le lien principal uniquement, qui peut informer la fenêtre principale. Si vous voulez que le lien principal envoie des commandes au lien de travail, voilà comment faire :
  • Ajoutez un sous-menu disant quelque chose dans le genre " Kill Thread" (détruire le lien)
  • Employez une variable globale en tant que simple commande flag. TRUE = Stop le lien, FALSE = continue le lien
  • Modifiez ThreadProc pour vérifier la valeur de la commande flag dans la boucle.
Quand l'utilisateur choisit le sous-menu "Kill Thread", le programme principal mettra la valeur TRUE dans la commande flag. Quand ThreadProc voit que la valeur de la commande flag est TRUE(VRAI), on sort de la boucle et on rompt ainsi les liens.
[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 16: Event Object

Tutorial 16: Event Object

Nous allons apprendre ce qu'est un event object et comment l'employer dans un programme multi-liens.

Downloadez l'exemple ici.

Théorie:

Dans le Tutorial précédent, on a vu comment les liens communiquent avec un message de fenêtre sur mesure (custum). J'ai omis deux autres méthodes : les variables globales et les 'events objets'. On va utiliser les deux dans ce Tutorial.
Un 'event objet', c'est comme un commutateur : il a seulement deux états possibles: on/off. Quand un 'event objet' est allumé, il est dans l'état "signalé" (c'est à dire qu'on l'utilise). Quand il est éteint, il est dans l'état "nonsignalé"(on ne l'utilise pas, on n'y fait référence nulle part). On créer un 'event objet' et on écrit un petit bout de code au niveau des liens appropriés pour observer l'état de l' 'event objet'. Si l' 'event objet' est dans l'état nonsignalé, les liens qui se servent des 'event objet' sont inactifs. C'est bien pratique car lorsque les liens sont en état d'attente, ils consomment peu de temps CPU.
Vous créez un 'event object' en appelant la fonction CreateEvent qui a la syntaxe suivante :

CreateEvent proto lpEventAttributes:DWORD,\
bManualReset:DWORD,\
bInitialState:DWORD,\
lpName:DWORD

lpEventAttribute--> Si vous donnez la valeur NULL, l' 'event object' est créé d'une façon par défaut.
bManualReset--> Si vous souhaitez que Windows désactive automatiquement l'event object (off) après l'appel WaitForSingleObject, vous devez taper 'FALSE' pour ce paramètre. Sinon vous devez le désactiver manuellement avec l'appel ResetEvent.
bInitialState--> Au moment où il est créé, si vous souhaitez que l'event object soit dans l'état signalé, mettez TRUE pour ce paramètre, sinon l' event object sera créé avec l'état nonsignalé.
lpName --> C'est le pointer qui repère une chaîne de caractère (ASCIIZ=du texte), laquelle est le nom de l'event object. Ce nom sera employé quand vous voudrez appeler OpenEvent.(pointe sur le nom de l'Event Object)

Si l'appel s'est déroulé avec succès, il renvoie l'handle de l' event object qui vient d'être créé, autrement il renvoie le NULL.
Vous pouvez modifier l'état de l'event object avec deux des appels API : SetEvent et ResetEvent. La fonction SetEvent met l'event object à l'état signalé. ResetEvent fait le contraire.
Après que l'event object soit créé, il vous faut insérez l'appel 'WaitForSingleObject' dans le lien qui dépend de l'état de l'event object. WaitForSingleObject a la syntaxe suivante :

WaitForSingleObject proto hObject:DWORD, dwTimeout:DWORD

hObject --> L'handle d'un des l'objets de synchronisation. L' event object est un type d'objet de synchronisation.
dwTimeout --> C'est à vous de fixer (en millisecondes) le temps maximal que cette fonction peut mettre pour que l'objet passe à l'état signalé. Si le temps indiqué est passé et que l'event object est toujours à l'état nonsignalé, alors WaitForSingleObject rend la main à l'interlocuteur. Si vous voulez attendre indéfiniment, vous devez mettre la valeur 'INFINITE' pour ce paramètre.(c'est aussi -1 en décimal ou FFFFFFFF en hexadécimal).

Exemple:

L'exemple ci-dessous montre une fenêtre attendant que l'utilisateur choisisse une commande du menu. Si l'utilisateur choisit ''run thread'' (lien en route), le lien commence le calcul sauvage (un calcul immense qui occupe le système). Quand il est fini, une boîte de message apparaît, informant l'utilisateur que le travail est terminé. Mais pendant que le lien était en action, l'utilisateur peut toujours cliquer sur "stop thread" pour arrêter ce lien.

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.const
IDM_START_THREAD equ 1
IDM_STOP_THREAD equ 2
IDM_EXIT equ 3
WM_FINISH equ WM_USER+100h

.data
ClassName db "Win32ASMEventClass",0
AppName db "Win32 ASM Event Example",0
MenuName db "FirstMenu",0
SuccessString db "The calculation is completed!",0
StopString db "The thread is stopped",0
EventStop BOOL FALSE

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwnd HANDLE ?
hMenu HANDLE ?
ThreadID DWORD ?
ExitCode DWORD ?
hEventStart HANDLE ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke GetCommandLine
mov CommandLine,eax
invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_WINDOW+1
mov wc.lpszMenuName,OFFSET MenuName
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,\
ADDR AppName,\
WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
CW_USEDEFAULT,300,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
invoke GetMenu,hwnd
mov hMenu,eax
.WHILE TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.ENDW
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax
.ELSEIF uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.ELSEIF uMsg==WM_COMMAND
mov eax,wParam
.if lParam==0
.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_GRAYED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_ENABLED
.elseif ax==IDM_STOP_THREAD
mov EventStop,TRUE
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
.else
invoke DestroyWindow,hWnd
.endif
.endif
.ELSEIF uMsg==WM_FINISH
invoke MessageBox,NULL,ADDR SuccessString,ADDR AppName,MB_OK
.ELSE
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.ENDIF
xor eax,eax
ret
WndProc endp

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000
.WHILE ecx!=0
.if EventStop!=TRUE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif
.ENDW
invoke PostMessage,hwnd,WM_FINISH,NULL,NULL
invoke EnableMenuItem,hMenu,IDM_START_THREAD,MF_ENABLED
invoke EnableMenuItem,hMenu,IDM_STOP_THREAD,MF_GRAYED
jmp ThreadProc
ret
ThreadProc ENDP
end start

Analyse:

Dans cet exemple, je vous montre une autre technique de lien.

.IF uMsg==WM_CREATE
invoke CreateEvent,NULL,FALSE,FALSE,NULL
mov hEventStart,eax
mov eax,OFFSET ThreadProc
invoke CreateThread,NULL,NULL,eax,\
NULL,0,\
ADDR ThreadID
invoke CloseHandle,eax

Vous pouvez voir que je crée l'event object ainsi que le lien, pendant le traitement du message WM_CREATE. Je crée l'event object à l'état nonsignalé et avec remis automatique. Après que l'event object soit créé, je crée le lien. Cependant le lien ne se met pas immédiatement au travail parce qu'il attend que l'event object soit mis à l'état 'signalé'. Ci-dessous voici comment le mettre à signalé:

ThreadProc PROC USES ecx Param:DWORD
invoke WaitForSingleObject,hEventStart,INFINITE
mov ecx,600000000

La première ligne de la procédure de lien est l'appel WaitForSingleObject. Ici on attend indéfiniment jusqu'à ce que l'event object passe à l'état signalé avant qu'il ne retourne. Ça signifie que même après que le lien ait été créé, nous le mettons dans un état d'inactivité.
Quand l'utilisateur choisit la commande du menu "run thread", nous mettons l'event object dans l'état signalé, comme indiqué ci-dessous :

.if ax==IDM_START_THREAD
invoke SetEvent,hEventStart

L'appel SetEvent met l'event object à l'état signalé, ce qui fait qu'à son tour l'appel WaitForSingleObject nous dirige vers la procédure du lien et le lien commence à travailler. Quand l'utilisateur choisit la commande "stop thread", nous mettons la valeur de la variable globale "EventStop" à TRUE.

.if EventStop==FALSE
add eax,eax
dec ecx
.else
invoke MessageBox,hwnd,ADDR StopString,ADDR AppName,MB_OK
mov EventStop,FALSE
jmp ThreadProc
.endif

Ceci stop le lien et saute de nouveau à l'appel WaitForSingleObject. Remarquez que nous n'avons pas besoin de remettre manuellement l'event object dans l'état nonsignalé puisque nous avons mis le paramètre bManualReset à FALSE (dans l'appel de CreateEvent).

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32asm tutorial 17: Dynamic Link Libraries

Tutorial 17: Les DLL
(Dynamic Link Libraries)
Bibliothèques de Liaison, Dynamiques

Dans ce Tutorial, nous allons voir les DLLs, ce qu'elles sont et comment les créer.
Vous pouvez télécharger l'exemple ici.

Théorie:

Si vous programmez depuis un petit moment déjà, vous constaterez que les programmes que vous avez l'habitude d'écrire ont quelques routines de code en communs. C'est vraiment une perte de temps de les récrire à chaque fois que vous commencez à développer de nouveaux programmes. Revenons en arrière, aux vieux jours du DOS, les programmeurs ont faits en sorte de stocker les routines les plus généralement employées dans une ou plusieurs bibliothèques. Quand ils veulent utiliser les fonctions, ils ont juste à lier la bibliothèque avec le fichier d'objet, et le linker extrait les fonctions de la bibliothèque et les insère dans le fichier final exécutable. Ce processus est appelé la jonction statique. Les bibliothèques 'Runtime du C' sont de bons exemples. L'inconvénient de cette méthode c'est que vous avez des fonctions identiques dans chaque programme qui les appelle. Votre espace disque est gaspillé stockant plusieurs copies identiques des fonctions. Mais pour les programmes sous DOS, cette méthode est tout à fait valable puisqu'il n'y a généralement qu'un seul programme en action dans la mémoire. Ainsi il n'y a aucune perte précieuse de mémoire.
Sous Windows, la situation devient beaucoup plus critique parce que vous pouvez avoir plusieurs programmes tournant simultanément. La mémoire sera rapidement saturée si votre programme est vraiment grand. Windows a une solution pour ce type de problème : les bibliothèques de liaison dynamiques. Une bibliothèque de liaison dynamique est une sorte de regroupement commun de plusieurs fonctions. Windows ne chargera pas en mémoire plusieurs copies d'une dll, même dans le cas où il y a beaucoup de programmes tournant en même temps. Il fera seulement une copie de la DLL que le programme emploie dans la mémoire. Et je dois clarifier un peu ce point. En réalité, tous les process qui emploient la même dll, auront leurs propres copies de cette dll. C'est comme si il y avait plusieurs copies de cette dll en mémoire. Mais en réalité, Windows rend ça magique avec la pagination et tous les morceaux de process ayant le même code dll. Ainsi en mémoire physique, il y a uniquement une copie du code de la dll. Cependant, chaque process aura sa propre section de données unique de la DLL.
Le programme se lie avec une DLL pendant l'exécution, à la différence de la vieille bibliothèque statique. C'est pourquoi on appelle la bibliothèque de liaison dynamique. Vous pouvez aussi vous défaire d'une DLL pendant l'exécution quand vous n'en avez plus besoin. Si ce programme est le seul à employer la DLL, il la virera immédiatement de la mémoire. Mais si la DLL est toujours employée par d'autres programmes, la DLL reste en mémoire tant que le dernier programme qui l'emploie s'en défasse.
Cependant, le linker a un travail plus difficile quand il exécute l'adresse fixups pour le fichier final exécutable. Puisqu'on ne peut pas "extraire" les fonctions et les insérer complètement dans le fichier final exécutable, d'une façon ou d'une autre on doit stocker assez d'informations sur la DLL et ses fonctions dans le fichier executable final pour être capable de charger correctement la DLL pendant l'exécution.
C'est là ! que la bibliothèque d'importation entre en jeu. Une bibliothèque d'importation contient des informations à propos de la DLL qu'il représente. Le linker peut extraire des bibliothèques d'importation les renseignements dont il a besoin. Quand le loader de Windows charge le programme en mémoire, il voit que le programme se lie avec une DLL donc il essai de trouver cette DLL et la réécrit alors dans l'espace d'adresse du process(du programme), puis exécute les adresses hors du programme pour les fonctions Call qui appellent la DLL.
Vous pouvez vouloir charger la DLL vous-même sans compter sur le loader (chargeur) de Windows. Cette méthode a son pour et son contre :
  • S'il n'y a pas besoin d'une bibliothèque d'importation alors vous pouvez charger et employer n'importe qu'elle DLL même si elle n'est accompagnée d'aucune bibliothèque d'importation. Cependant, vous avez quand même besoin de savoir certaines choses sur les fonctions qu'elle vous propose, des choses comme combien de paramètres elles utilisent…
  • Si vous laissez le loader charger la DLL pour votre programme, et que le loader ne trouve pas cette DLL il annoncera "Le fichier .DLL exigé, xxxxx.DLL est manquant" et paf! Votre programme n'a aucune chance d'être lancé même si cette DLL n'est pas essentiel pour son fonctionnement minimal. Si vous chargez la DLL vous-même, et que la DLL ne peut pas être trouvé et que ce n'est pas essentiel pour l'opération, votre programme le fait remarquer à l'utilisateur mais peut continuer.
  • Vous pouvez utiliser et appeler d'autres fonctions *non décrites dans ces Tuts* qui ne sont pas incluses dans les bibliothèques d'importation. À condition que vous connaissiez assez de renseignements sur ces fonctions.
  • Si vous utilisez LoadLibrary, vous devez appeler GetProcAddress pour chaque fonction que vous souhaitez appeler. GetProcAddress retrouve l'adresse de l'entrypoint d'une fonction à l'intérieur d'une DLL particulière. (l'addresse où commence une fonction particulière). Donc il se peut que votre code soit un peu plus grand et plus lent, mais ça change pas grand chose.
En comparant les avantages / inconvénients d'un appel à LoadLibrary, nous entrons maintenant dans les détails pour créer une DLL.
Le code suivant est le squelette de la DLL. (DLL skeleton.)

;--------------------------------------------------------------------------------------
; DLLSkeleton.asm
;--------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

.data
.code
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp
;---------------------------------------------------------------------------------------------------
; C'est une fonction factice
; Ça ne fait rien. Je l'ai mise ici pour montrer où vous pouvez insérer des fonctions à l'intérieur d'une DLL.
;----------------------------------------------------------------------------------------------------
TestFunction proc
ret
TestFunction endp

End DllEntry

;-------------------------------------------------------------------------------------
; DLLSkeleton.def
;-------------------------------------------------------------------------------------
LIBRARY DLLSkeleton
EXPORTS TestFunction

Ce tout petit programme ci-dessus est le squelette de la DLL. Chaque DLL doit avoir une fonction d'entrypoint. Windows appellera la fonction d'entrypoint chaque fois que :

  • La DLL est loadée en premier
  • La DLL est unloadée (libérée)
  • Un lien est créé dans ce même process
  • Un lien est détruit dans ce même process
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,TRUE
ret
DllEntry Endp

Vous pouvez nommer la fonction d'entrypoint comme vous le souhaitez tant que vous avez une FIN correspondante à 'END ' . Cette fonction prend trois paramètres, dont seulement les deux premier sont importants.
hInstDLL est l'handle du module(de la fonction) de la DLL. Ce n'est pas le même que l'instance handle du process. Vous devez garder cette valeur si vous avez besoin de l'employer plus tard. Vous ne pourrez pas l'obtenir de nouveau aussi facilement. Exemple: Si vous connaissez la DLL Kernel32.DLL vous savez bien qu'elle possède plusieurs fonctions ou modules à l'intérieurs d'elle, dont GetCommandeLineA et ExitProcess.
reason peut être une des quatre valeurs :

  • DLL_PROCESS_ATTACH La DLL reçoit cette valeur quand elle est injectée la toute première fois dans l'espace d'adresses du process. Vous pouvez utiliser cette occasion pour faire l'initialisation.
  • DLL_PROCESS_DETACH La DLL reçoit cette valeur quand elle est unloadée (supprimée) de l'espace d'adresses du process. Vous pouvez employer cette occasion pour faire un peu de nettoyage comme désinffecter la mémoire etc…
  • DLL_THREAD_ATTACH La DLL reçoit cette valeur quand le process crée un nouveau lien.
  • DLL_THREAD_DETACH La DLL reçoit cette valeur quand un lien dans le process est détruit.
Renvoyez TRUE dans eax si vous souhaitez que la DLL continue son exécution. Si vous renvoyez FALSE, la DLL ne sera pas chargée. Par exemple, si votre code d'initialisation doit réserver de la mémoire et qu'il ne peut pas faire ça avec succès, la fonction d'entrypoint doit renvoyer FALSE pour indiquer que la DLL ne peut pas être lancée.
Vous pouvez placer vos fonctions dans la DLL après la fonction Entrypoint ou même avant çà. Mais si vous voulez qu'elles soient CALLables par d'autres programmes, vous devez mettre leurs noms dans la liste d'exportation du fichier de définition de module (.def).
Une DLL a besoin d'un fichier de définition de module dans son process liée à son développement. Nous y jetons un coup d'œil maintenant.

LIBRARY DLLSkeleton
EXPORTS TestFunction

Normalement vous devez avoir la première ligne. La déclaration LIBRARY définit le nom du module interne (la fonction souhaitée) de la DLL. Vous devez correspondre avec ce module grâce au nom du fichier de la DLL.
La déclaration EXPORTS dit au linker quelles fonctions sont en train de tourner dans la DLL, c'est-à-dire quelles fonctions sont déjà utilisées par d'autres programmes. Dans l'exemple, nous voulons que d'autres modules soient capables d'appeler TestFunction, donc nous mettons son nom dans la déclaration EXPORTS.
Un autre changement c'est le commutateur du linker. Vous devez mettre le commutateut /DLL et /DEF:<votre nom de fichier def> dans votre linker de commutation comme cela :

link /DLL /SUBSYSTEM:WINDOWS /DEF:DLLSkeleton.def /LIBPATH:c:\masm32\lib DLLSkeleton.obj

Les commutateurs d'assembleur sont les mêmes, à savoir /c /coff /Cp. Ainsi après que vous ayez lié le fichier objet, vous obtiendrez *.DLL et *.lib. Le *.lib est la bibliothèque d'importation que vous pouvez employer pour vous lier avec d'autres programmes qui emploient les fonctions de votre DLL.
Ensuite je vous montrerai comment employer LoadLibrary pour charger une DLL.

;---------------------------------------------------------------------------------------------
; UseDLL.asm
;----------------------------------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib

.data
LibName db "DLLSkeleton.dll",0
FunctionName db "TestHello",0
DllNotFound db "Cannot load library",0
AppName db "Load Library",0
FunctionNotFound db "TestHello function not found",0

.data?
hLib dd ? ; l' handle de la librairie (l' handle de la DLL)
TestHelloAddr dd ? ; l'adresse de la function TestHello

.code
start:
invoke LoadLibrary,addr LibName
;---------------------------------------------------------------------------------------------------------
; Appelle LoadLibrary avec le nom de la DLL désirée. Si l'appel est couronné de succès
; Il renverra l'handle de la bibliothèque (DLL). Sinon, il renverra le NULL.
; Vous pouvez passer l'handle de la bibliothèque à GetProcAddress ou n'importe quelle fonction qui a besoin
; de l'handle d'une DLL comme paramètre.
;------------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr DllNotFound,addr AppName,MB_OK
.else
mov hLib,eax
invoke GetProcAddress,hLib,addr FunctionName
;-------------------------------------------------------------------------------------------------------------
; Quand vous obtenez l'handle de la bibliothèque, vous le passez à GetProcAddress avec l'adresse
; du nom de la fonction de la DLL que vous souhaitez appeler. Ceci renvoie l'adresse
; de la fonction si tout s'est passé avec succès. Autrement, il renvoie le NULL.
; Les adresses des fonctions ne changent pas à moins que vous ne déchargiez puis rechargiez la bibliothèque.
; Donc vous pouvez les placer dans des variables globales pour de futures utilisations.
;-------------------------------------------------------------------------------------------------------------
.if eax==NULL
invoke MessageBox,NULL,addr FunctionNotFound,addr AppName,MB_OK
.else
mov TestHelloAddr,eax
call [TestHelloAddr]
;-------------------------------------------------------------------------------------------------------------
; Ensuite, vous pouvez appeler la fonction avec un appel simple avec la variable contenant
; l'adresse de la fonction comme opérande.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke FreeLibrary,hLib
;-------------------------------------------------------------------------------------------------------------
; Quand vous n'avez plus besoin de la bibliothèque, vous la déchargez avec FreeLibrary.
;-------------------------------------------------------------------------------------------------------------
.endif
invoke ExitProcess,NULL
end start

Donc vous pouvez voir que l'utilisation de LoadLibrary est un peu plus compliquée mais elle est aussi plus flexible.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion Win32asm Tutorial 18: Common Controls

Tutorial 18: Les Controls Communs
(Common Controls)

Nous allons voir ce que sont les Controls les plus utilisées et voir comment on les emploie. Ce Tutorial sera seulement une rapide introduction à ceux-ci.
Downloadez l'exemple ici.

Théorie:

Windows 95 a été doté de plusieurs nouvelles interfaces utilisateur que n'avait pas Windows 3.1x. Ceci enrichi encore le GUI (NdT : Graphic User Interface). Plusieurs d'entre eux sont largement employés avant même que Windows 95 ne s'affiche à l'écran, comme la barre de statut, des barres d'outils etc. Sous Windows 3.x, les programmeurs devaient les coder eux-mêmes. Maintenant Microsoft les a inclus dans Windows 9x et NT. Nous allons les voir ici.
Voici ces nouveaux Controls :
  • Toolbar -----> (barre d'outils)
  • Tooltip -----> (Bulle d'information: voir tuorial 27)
  • Status bar -----> (barre d'état)
  • Property sheet -----> (feuille de propriété)
  • Property page -----> (page de propriété)
  • Tree view -----> (dossiers organisés sous la forme d'un arbre. Voir tutorial 19)
  • List view -----> (dossiers organisés sous la forme d'une simple liste. Voir tutorial 31)
  • Animation -----> (animation, films AVI)
  • Drag list -----> ()
  • Header -----> (en-tete)
  • Hot-key -----> (fontion touches racourcis)
  • Image list -----> (liste d'images)
  • Progress bar -----> (barre de progression de chargement)
  • Right edit -----> ()
  • Tab -----> (la touche Tab)
  • Trackbar -----> ()
  • Up-down -----> (les flèches haut/bas pour le défilement de d'une page)
Voici une adresse ou vous trouverez de petits programmes d'exemples de chacun de ces Common Control. Ils ont étés codés en C++ et non pas en MASM mais le principe est là. Vous comprendrez ce que chacun d'eux représente.
http://www.microsoft.com/msj/defaulttop.asp?page=/msj/0798/controlspytop.htm
(Merci à Sharpsighted pour l'info. sharpsighted@caramail.com)

Puisque il y en a beaucoup, le fait de les charger en mémoire et les déclarer serait une perte de ressource. Tous, à l'exception du contrôle d'édition, sont stockés dans comctl32.dll. Et n'importe quel programme peut les charger quand il a besoin d'employer un de ces Control. Le contrôle d'édition (riche ou évolué)réside dans son propre dll, richedXX.dll, parce qu'il est très compliqué et plus volumineux que ses frères.
Vous pouvez charger comctl32.dll grâce à l'inclusion d'un Call InitCommonControls dans votre programme. InitCommonControls est une fonction de comctl32.dll, donc en vous y référant n'importe où dans votre code, fera que le Loader PE chargera comctl32.dll quand votre programme est en court. Vous ne devez pas l'exécuter, incluez la simplement quelque part dans votre code. Cette fonction ne fait RIEN! Sa seule et unique instruction est "ret". Son seul but c'est d'inclure la référence de comctl32.dll dans la section d'importation pour que le Loader PE le charge à chaque fois que le programme est chargé. Le vrai boulot se fait au niveau de l'entrypoint de la fonction DLL qui enregistre chacune des classes des différents Common Controls (common control classes) quand la DLL est chargée. Les 'Commons Controls' sont créés sur la base de ces classes, tout comme d'autre 'child window controls' tels que la zone d'édition, la liste etc.
Pour le contrôle d'édition évolué (riche) c'est une autre histoire. Si vous voulez l'employer, vous devez appeler LoadLibrary pour le charger et appeler FreeLibrary pour le décharger.
Maintenant nous allons voir comment les créer. Vous pouvez employer un éditeur de ressources pour les incorporer dans des boîtes de dialogue ou bien vous pouvez les créer vous-même. Presque tous les common controls sont créés en appelant CreateWindowEx ou CreateWindow, en leur passant le nom de la classe du contrôle. Quelques common controls ont des fonctions de création spéciales, cependant, ce ne sont que des broutilles autour de CreateWindowEx pour rendre la création de ces 'controls' plus simple. Ces fonctions spécifiques de création sont inscrites ci-dessous :
  • CreateToolbarEx
  • CreateStatusWindow
  • CreatePropertySheetPage
  • PropertySheet
  • ImageList_Create
Pour pouvoir créer ces 'common controls', vous devez connaître leurs noms de classe. Les voici inscrits ci-dessous :
Class Name | Common Control
ToolbarWindow32 | Toolbar
tooltips_class32 | Tooltip
msctls_statusbar32 | Status bar
SysTreeView32 | Tree view
SysListView32 | List view
SysAnimate32 | Animation
SysHeader32 | Header
msctls_hotkey32 | Hot-key
msctls_progress32 | Progress bar
RICHEDIT | Rich edit
msctls_updown32 | Up-down
SysTabControl32 | Tab
Les 'Property sheets', les 'property pages' et l 'image list control' (soit les feuilles de propriété, les pages de propriété, et le contrôle de liste d'image) ont leurs propres fonctions spécifiques de création. La 'Drag list control' gonfle encore la liste donc elle n'a pas sa propre classe. Ces susdits noms de classe sont vérifiés en regardant le scénario de ressource, produit par l'éditeur de ressource Visual C++. Ils diffèrent des noms de classe inscrits par win32 de la référence Borland api et de ceux de la Programmation Windows95 de Charles Petzold. La liste est précisément celle-ci.
Ces common controls peuvent utiliser les styles généraux de fenêtres tel que WS_CHILD etc. Ils ont aussi leurs propres styles spécifiques comme TVS_XXXXX pour la 'tree view control' (contrôle montrant les fichiers sous forme d'arbre ), LVS_xxxx pour la 'list view control' (contrôle montrant les fichiers sous forme d'une liste ), etc. La référence d' api Win32 sera votre meilleur ami à cet égard.
Maintenant que nous savons comment créer des common controls, nous pouvons nous intéresser à la méthode de communication entre ces common controls et leurs parents. À la différence des 'child window controls', les 'common controls' ne communiquent pas avec leur parent grâce à des messages WM_COMMAND. Au lieu de ça, ils envoient des messages WM_NOTIFY à la fenêtre parente lorsque certains événements bien précis se produisent. Le parent peut contrôler les enfants en leur envoyant des messages. Il y a aussi beaucoup de nouveaux messages pour ces nouveaux Controls. Vous devez consulter votre référence api win32 pour plus de détail.
Dans l'exemple suivant, on va examiner les deux Controls suivants, la barre de progression et la barre de statut. ('progress bar' et 'status bar controls')

échantillon de code:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDC_PROGRESS equ 1 ; Les n° d'Identités des 'controls' (par exemple n°ID de la barre de progression)
IDC_STATUS equ 2
IDC_TIMER equ 3

.data
ClassName db "CommonControlWinClass",0
AppName db "Common Control Demo",0
ProgressClass db "msctls_progress32",0 ; La 'class name' de la barre de progression
Message db "Finished!",0
TimerID dd 0

.data?
hInstance HINSTANCE ?
hwndProgress dd ?
hwndStatus dd ?
CurrentStep dd ?
.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
WS_CHILD+WS_VISIBLE,100,\
200,300,20,hWnd,IDC_PROGRESS,\
hInstance,NULL
mov hwndProgress,eax
mov eax,1000 ; Le paramètre lParam du message PBM_SETRANGE contient la gamme, (en général de 0 à 100)
mov CurrentStep,eax
shl eax,16 ; La borne haute de la gamme (ici 100) est dans le mot de poids fort
invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
mov hwndStatus,eax
invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; crée un minuteur (un timer)
mov TimerID,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.if TimerID!=0
invoke KillTimer,hWnd,TimerID
.endif
.elseif uMsg==WM_TIMER ; Quand un 'timer event' survient
invoke SendMessage,hwndProgress,PBM_STEPIT,0,0 ; Remplissage (pas à pas) de la barre de progression.
sub CurrentStep,10
.if CurrentStep==0
invoke KillTimer,hWnd,TimerID
mov TimerID,0
invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analyse:

    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax
    invoke InitCommonControls
J'ai délibérément mis InitCommonControls après ExitProcess pour vous faire voir que InitCommonControls est juste ici pour la mettre en référence comctl32.dll, dans la section d'importation. Comme vous pouvez le voir, les 'common controls' fonctionnent parfaitement même lorsque InitCommonControls n'est pas exécuter.
    .if uMsg==WM_CREATE
    invoke CreateWindowEx,NULL,ADDR ProgressClass,NULL,\
    WS_CHILD+WS_VISIBLE,100,\
    200,300,20,hWnd,IDC_PROGRESS,\
    hInstance,NULL
    mov hwndProgress,eax
Voici l'endroit où nous créons le common control. Notez que cet appel de CreateWindowEx contient la variable 'hWnd' en tant que 'handle de la fenêtre parente'. On spécifie aussi un 'Control ID' pour l'identification de ce contrôle. Cependant, puisque nous avons l' handle du 'control', cet ID n'est pas employé. Tout 'child window control' doit avoir le style WS_CHILD.
    mov eax,1000
    mov CurrentStep,eax
    shl eax,16
    invoke SendMessage,hwndProgress,PBM_SETRANGE,0,eax
    invoke SendMessage,hwndProgress,PBM_SETSTEP,10,0
Après que la barre de progression est été créée, nous pouvons définir sa gamme. La gamme par défaut va de 0 à 100. Si vous n'en êtes pas satisfaits, vous pouvez spécifier votre propre gamme avec le message PBM_SETRANGE. Le message 'lParam' contient la gamme, la borne supérieure de la gamme (ici 100d ou 64h) est placée dans le mot de poids fort alors que le minimum (0) est dans le mot de poids faible. Ce qui fait que IParam vaut 00640000h. Que fait Shl eax,16 ? Shl décale 16 bit de eax vers la gauche. Ici on est en octet donc 8 bits, ce qui fait qu'on déplace 16/8=2 octets de eax vers la gauche, ce qui fait que eax prend la valeur 00000064h. On vient de récupérer la borne haute de IParam dans eax.
Vous pouvez spécifier le 'nombre de pas' dont cette barre sera remplie, en employant PBM_SETSTEP. Dans notre exemple, on le met à 10 ce qui signifie que lorsque un message PBM_STEPIT est envoyé à la barre de progression, l'indicateur de progression montera de 10 par 10. Mais vous pouvez aussi définir le niveau de l'indicateur en envoyant des messages PBM_SETPOS. Ce message vous donne un contrôle plus précis sur la barre de progression.
    invoke CreateStatusWindow,WS_CHILD+WS_VISIBLE,NULL,hWnd,IDC_STATUS
    mov hwndStatus,eax
    invoke SetTimer,hWnd,IDC_TIMER,100,NULL ; Crée un timer
    mov TimerID,eax
Ensuite, nous créons une barre de statut en appelant CreateStatusWindow. C'est un appel facile à comprendre donc je ne ferai pas de remarques. Après que la fenêtre de statut ait été créée, nous fabriquons un minuteur. Dans cet exemple, nous mettrons à jour la barre de progrès dans un intervalle régulier de 100ms, donc nous devons créer un contrôle de minuteur (un control timer). Ci-dessous voici le 'prototype de fonction' de SetTimer.
    SetTimer PROTO hWnd:DWORD, TimerID:DWORD, TimeInterval:DWORD, lpTimerProc:DWORD
hWnd : handle de la fenêtre Parente
TimerID : Le n° d'Identité du minuteur (du Timer), différent de zéro. Vous pouvez mettre le n° que vous voulez pour l'identifier. Mais si vous avez plusieurs Timers , chacun doit avoir son propre n° d'ID
TimerInterval : L'intervalle de temps du minuteur, en millisecondes, doit être terminé avant que le minuteur appelle la procédure du minuteur ou envoie un message WM_TIMER
lpTimerProc : L'adresse de la fonction minuteur (la fonction de temps) qui sera appelée lorsque l'intervalle de temps est expiré. Si ce paramètre est NULL, le minuteur enverra le message WM_TIMER à la fenêtre parente, au lieu de cette adresse.

Si cet appel est couronné de succès, il renverra le TimerID. S'il a échoué, il retourne la valeur 0. C'est pourquoi la valeur du TimerID doit absolument être différente de zéro.

    .elseif uMsg==WM_TIMER
    invoke SendMessage,hwndProgress,PBM_STEPIT,0,0
    sub CurrentStep,10
    .if CurrentStep==0
    invoke KillTimer,hWnd,TimerID
    mov TimerID,0
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,addr Message
    invoke MessageBox,hWnd,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
    invoke SendMessage,hwndStatus,SB_SETTEXT,0,0
    invoke SendMessage,hwndProgress,PBM_SETPOS,0,0
    .endif
Quand l'intervalle de temps indiqué expire, le minuteur (le timer) envoie le message WM_TIMER. Vous pourrez placer votre propre code ici pour qu'il soit exécuté. Dans cet exemple, nous mettons à jour la barre de progression et vérifions ensuite si la limite maximale a été atteinte (dépassée). Si c'est le cas, nous fermons le Timer et mettons ensuite le texte dans la fenêtre de statut avec le message SB_SETTEXT. Une boîte de message est affichée, et quand l'utilisateur clique sur OK, nous purifions le texte de la barre de statut ainsi que la barre de progression.
[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 19: Tree View Control

Tutorial 19: Contrôle 'des dossiers organisés sous la forme d'un arbre'

(Tree View Control)

Dans ce Tutorial, nous allons voir ce qu'est un 'tree view control' (C'est exactement de cette façon là que l'Explorateur Windows nous affiche sous forme d'un arbre tous les dossiers de notre disque dur). De plus, nous apprendrons aussi à construire le mécanisme du 'drag and drop' (glisser-déposer) et comment employer une 'image list' (liste d'image).
Downloadez l'exemple ici.

Théorie:

Un 'tree view control' est une sorte de fenêtre spéciale qui affiche des objets en ordre hiérarchique. Le meilleur exemple c'est la fenêtre de gauche de l'Explorateur de Windows. Vous pouvez employer ce contrôle pour montrer les raccordements entre les différents objets (ici des dossiers).
Vous pouvez créer un 'tree view control' en appelant CreateWindowEx, en lui passant la donnée "SysTreeView32" comme nom de classe ou bien vous pouvez l'incorporer dans une boîte de dialogue. N'oubliez pas de placer un Call InitCommonControls dans votre code.
Il y a plusieurs styles différents de 'tree view control'. Ci-dessous voici les trois plus employés.
    TVS_HASBUTTONS sert à mettre les petits boutons carrés 'plus (+) et moins (-)' à côté des l'Items parentaux (à côté des dossiers par exemple). L'utilisateur clique sur un de ces bouton associés à un dossier parent pour qu'il se déploie et affiche ses sous dossiers ou se recroqueville sur lui-même. Pour pouvoir inclure des boutons au niveau de la racine des Items de l'arbre, on doit également spécifié TVS_LINESATROOT.
    TVS_HASLINES emploie des lignes pour montrer la hiérarchie entre les différents Items.
    TVS_LINESATROOT emploie des lignes pour lier des Items à la racine du 'tree-view control'. Cette valeur est ignorée si TVS_HASLINES n'est pas également spécifié.
Le 'tree view control', comme d'autre common controls, communique avec la fenêtre parente via des messages. La fenêtre parente peut lui envoyer divers messages et le 'tree view control' peut envoyer des messages "d'avis" à sa fenêtre parente. À cet égard, le 'tree view control' n'est pas différent des fenêtres habituelles.
Quand quelque chose d'intéressant lui arrive, il envoie un message WM_NOTIFY à la fenêtre parente en l'accompagnant de l'information.
    WM_NOTIFY
    WParam est le n°d'IDentité du Contrôle (Control ID), on ne garantit pas que cette valeur soit unique donc nous ne
    l'emploierons pas. Au lieu de ça, nous employons hwndFrom ou le membre IDFrom de la structure
    NMHDR pointé par lParam

    LParam est le pointer qui repère la structure NMHDR. Certaines commandes peuvent passer un pointer de plus
    grande taille. Mais il doit avoir une structure NMHDR dans son premier membre.
    C'est-à-dire qu'au moins pour lParam, vous devez être sûrs qu'il indique une structure NMHDR.
Maintenant examinons la structure NMHDR.
    NMHDR struct DWORD
    hwndFrom DWORD ?
    idFrom DWORD ?
    code DWORD ?
    NMHDR ends
HwndFrom est l'handle du contrôle qui envoie ce message WM_NOTIFY.
IdFrom est le contrôle ID du contrôle qui envoie ce message WM_NOTIFY.
Code est le message réel que le contrôle veut envoyer à la fenêtre parente.
Les réponses du 'Tree View' sont celles avec TVN_ au début du nom. Les messages du Tree View sont ceux avec TVM _, tels que TVM_CREATEDRAGIMAGE. Le 'tree view control' envoie TVN_XXXX dans le membre de code du NMHDR. La fenêtre parente peut envoyer TVM_XXXX pour le contrôler.

Ajout d'Item au tree view control

Après que vous l'ayez créé, vous pouvez même rajouter des Item (des articles) au tree view control. Vous pouvez faire ça en employant le message TVM_INSERTITEM.
    TVM_INSERTITEM
    wParam = 0;
    lParam = est le pointeur qui (pointe) sur TV_INSERTSTRUCT;
A ce niveau, on doit connaître quelques petits trucs à propos des Items (articles) du tree view control.
Un Item peut être lui-même parent, enfant (un child) , ou les deux en même temps. Un Item parent est un Item qui a d'autres sous-Items associés qui dépendent de lui. En même temps, cet Item parent peut aussi être l'enfant d'un autre Item. Un Item sans un parent est appelé un Item racine (par exemple : dans l'Explorateur de Windows, l'item 'Windows' est un item parent alors que l'Item 'System' est un de ses items enfant). Il peut y avoir plusieurs d'Items racine dans un 'tree view control'. Maintenant examinons la structure TV_INSERTSTRUCT.
    TV_INSERTSTRUCT STRUCT DWORD
    hParent DWORD ?
    hInsertAfter DWORD ?
    ITEMTYPE <>
    TV_INSERTSTRUCT ENDS
hParent est l'handle de l'Item parent. Si ce membre est la valeur TVI_ROOT ou NULL, l'Item est inséré à la racine du 'tree view control'.
hInsertAfter est l'handle d'un prochain Item, qu'il soit un 'Child Item' ou bien un nouvel 'Item Parent':
  • TVI_FIRST = > Insère un Item au début de la liste.
  • TVI_LAST = > Insère un Item en fin de liste.
  • TVI_SORT = > Insère un Item dans la liste dans l'ordre alphabétique.
    ITEMTYPE UNION
    itemex TVITEMEX <>
    item TVITEM <>
    ITEMTYPE ENDS
Nous emploierons uniquement TV_ITEM ici.
    TV_ITEM STRUCT DWORD
    imask DWORD ?
    hItem DWORD ?
    state DWORD ?
    stateMask DWORD ?
    pszText DWORD ?
    cchTextMax DWORD ?
    iImage DWORD ?
    iSelectedImage DWORD ?
    cChildren DWORD ?
    lParam DWORD ?
    TV_ITEM ENDS
Cette structure est employée pour envoyer et recevoir des renseignements (des messages) d'un Item du Tree View. Par exemple, avec TVM_INSERTITEM, il est employé pour définir l'attribut de l'Item qui doit être inséré dans le tree view control. Avec TVM_GETITEM, on envoie l'information pour choisir le type de Tree View Item.
imask est employé pour spécifier quel(s) membre(s) de la structure TV_ITEM est (sont) valide(s). Par exemple, si la valeur dans Imask est TVIF_TEXT, ça signifie qu'uniquement le membre pszText est valide. Vous pouvez combiner plusieurs flags ensembles.
hItem est l'handle de l'Item du Tree View. Chaque item a son propre handle. Lorsque vous souhaitez manipuler ou utiliser un item, vous devez le choisir grâce à son handle.
pszText est un pointeur qui (pointe) sur une chaîne de caractères terminée par un caractère nul. Cette chaîne de caractères est le label de l'Item du Tree View (Le texte associé à cet Item).
cchTextMax est uniquement employé lorsque vous voulez retrouver le label de l'Item du Tree View. Puisque vous pointer sur le buffer de pszText (on pointe sur la zone mémoire où est écrit le texte), Windows doit connaître la taille de ce buffer. Ce membre (cchTextMax) sert donc à donner la taille du buffer.
iImage et iSelectedImage font référence à l'index d'une 'image list' laquelle contient des images qui doivent être affichées lorsque l'Item n'est pas choisi (iImage) et quand il est choisi (iSelectedImage). Si vous vous souvenez de la fenêtre de gauche de l'Explorateur Windows, les images des dossiers sont contrôlées par ces deux membres.
Afin d'insérer un Item dans un tree view control, vous devez au moins mettre des valeurs dans hParent, hInsertAfter et vous devez remplir imask ainsi que le membre pszText.

Addition d'images à un tree view control :

Si vous souhaitez placer une image à gauche du label (du texte) d'un 'Tree View Item', vous devez créer une 'image list' et l'associer au 'tree view control'. Vous pouvez créer une 'image list' en appelant ImageList_Create.
    ImageList_Create PROTO cx:DWORD, cy:DWORD, flags:DWORD, \
    cInitial:DWORD, cGrow:DWORD
Cette fonction ,si elle se déroule avec succès, renvoie l'handle d'une 'image list' vide.
cx représente la largeur (en pixels) de chaque image dans cet 'image list'.
cy est la hauteur de chaque image (en pixels) de cet 'image list'. Toutes les images de l' 'image list' doivent être égales en taille. Si vous définissez un grand bitmap (image), Windows emploiera cx et cy pour la *découper* en plusieurs morceaux. Donc vous devez préparer votre propre image comme une bande d'images avec des dimensions identiques.
flags détermine le type d'images contenues dans cet 'image list', si elles sont colorées ou monochromes et leur profondeur de colorations (16millions de couleurs ou moins). Consultez votre référence win32 api pour de plus amples détails.
cInitial est le nombre d'images que cet 'image list' contiendra au commencement. Windows emploiera ces renseignements pour réserver de la mémoire pour ces images.
cGrow est le nombre d'images dont l'image list peut être agrandit lorsque le système a besoin de retaillée la liste de départ pour faire place pour de nouvelles images. Ce paramètre représente le nombre de nouvelles images que l'image list agrandit peut contenir.
L' image list n'est pas une fenêtre! C'est seulement une sorte d'entrepos d'images pour que d'autres fenêtres puisse s'en servir.
Après que l'image list soit créé, vous pouvez y ajouter des images en appelant ImageList_Add
    ImageList_Add PROTO himl:DWORD, hbmImage:DWORD, hbmMask:DWORD
Cette fonction renvoie la valeur -1 si elle échoue.
himl est l'handle de l'image list dans laquelle vous souhaitez ajouter d'autres images. Cette la valeur obtenue après le succès d'un appel à ImageList_Create
hbmImage est l'handle de l'image qui doit être rajoutée à l' 'image list'. D'habitude, on stocke les images en tant que ressources et on les charge grâce à l'appel LoadBitmap. Remarquez que vous n'avez pas à spécifier le nombre de morceaux d'images contenues dans cette image (entière) parce que cette information est représentée par les paramètres cx et cy, lesquels sont déduits lors de leur passage dans l'appel ImageList_Create.
hbmMask est l'handle du bitmap (l'image en entier) qui contient le masque. Si aucun masque n'est employé avec l'image list, ce paramètre est ignoré.
Normalement, nous ajoutons seulement deux images à une image list pour les utiliser avec le tree view control : celles-ci sont employées soit quand l'Item du Tree View n'est pas sélectionné soit l'autre quand l'Item est sélectionné.
Quand l' image list est prête, vous l'associez avec le tree view control en envoyant TVM_SETIMAGELIST au tree view control.
    TVM_SETIMAGELIST
    WParam = type d'image list à afficher. Il y a deux choix :
    • TVSIL_NORMAL est le Jeu normal d' image list, qui contient les images choisies et non choisies pour l'Item du Tree View.
    • TVSIL_STATE est le Jeu 'd'état' d' image list, qui contient les images d'Item de Tree View qui sont dans un état défini par l'utilisateur.
    lParam = handle de l' image list

Recherche des renseignements a propos des items d'un tree view.

Vous pouvez retrouver l'information d'un Item du Tree View en envoyant le message TVM_GETITEM.
    TVM_GETITEM
    wParam = 0
    lParam = pointer sur la structure TV_ITEM pour qu'on puisse la remplir avec l'information.
Avant que vous n'envoyiez ce message, vous devez remplir imask avec le flag qui dit quel(s) sorte(s) de TV_ITEM est ce que vous souhaitez que Windows prenne. Et plus important, vous devez remplir hItem avec l'handle de l'Item dont vous voulez avoir l'information. Et ça pose un problème : Comment peut-on connaître l'handle de l'Item dont on souhaite justement retrouver des renseignements? Doit-on stocker tous les handles du Tree View?
La réponse est très simple : faut pas faire ça. On peut utiliser le message TVM_GETNEXTITEM du tree view control pour retrouver l'handle de l'Item dont on a spécifié l'attribut(s). Par exemple, on peut vérifier l'handle du premier Child Item, de l'Item racine, ou de l'Item choisi, et cetera.
    TVM_GETNEXTITEM
    wParam = flag
    lParam = handle d'un Item du Tree View (seulement nécessaire pour certaines valeurs de flag)
La valeur dans wParam est très importante donc je met tous les flags ci-dessous :
    • TVGN_CARET Retrouve l'Item actuellement choisi.
    • TVGN_CHILD Retrouve le premier Child Item appartenant à l'Item indiqué par le paramètre hitem.
    • TVGN_DROPHILITE Retrouve l'Item qui est la cible d'une opération de glisser-déplacer. (Drad and Drop)
    • TVGN_FIRSTVISIBLE Retrouve le premier Item visible.
    • TVGN_NEXT Retrouve le Child Item suivant (de mêmes parents).
    • TVGN_NEXTVISIBLE Retrouve l'Item visible qui suit l'Item indiqué. L'Item indiqué doit être visible. Utilisez le message TVM_GETITEMRECT pour déterminer si un Item est visible.
    • TVGN_PARENT Retrouve le parent de l'Item indiqué.
    • TVGN_PREVIOUS Retrouve le Child Item précédent (de mêmes parents).
    • TVGN_PREVIOUSVISIBLE Retrouve le premier Item visible qui précède l'Item indiqué. L'Item indiqué doit être visible. Employez le message TVM_GETITEMRECT pour déterminer si un Item est visible.
    • TVGN_ROOT Retrouve le topmost (ex: C:) ou le tout premier Item du tree-view control.
Vous pouvez voir que, vous pouvez retrouver l'handle de n'importe quel Item du Tree View en vous intéressant à ces messages. SendMessage renvoie l'handle de l'Item du Tree View si la tache est couronnée de succès. Vous pouvez alors remplir le membre hItem (appartenant à TV_ITEM) avec la valeur de l'handle renvoyé pour ensuite pouvoir l'utiliser avec le message TVM_GETITEM.

Opération Drag and Drop (glisser-déposer) dans un tree view control

Cette partie est la raison pour laquelle j'ai décidé d'écrire ce Tutorial. Quand j'ai essayé de suivre l'exemple de la référence win32 api (le win32.hlp d'InPrise), j'ai vraiment été frustré parce qu'en fait il manque l'information essentielle. D'essais en essais et d'erreurs en erreurs, j'ai fini par comprendre comment utiliser le système de 'Drag and Drap' d'un tree view control et je ne veux pas que d'autres personnes aient à refaire le même chemin de croix que moi.
Ci-dessous voici les étapes pour accomplir une opération Drag&Drop dans un tree view control.
  1. Quand l'utilisateur essaye de déplacer un Item, le tree view control envoie le message TVN_BEGINDRAG à la fenêtre parente. Vous pouvez profiter de cette occasion pour créer une image de traînée qui est l'image qui sera employée pour représenter l'Item quand on le déplace. Vous pouvez envoyer TVM_CREATEDRAGIMAGE au tree view control pour lui dire de créer une 'image de traînée' par défaut de l'image qui est actuellement employée par l'Item que l'on déplace. Le tree view control créera une image list avec uniquement l 'image de traînée' et vous renverra l'handle de cette image list.
  2. Après que l'image de traînée aie été créée, vous définissez le point chaud de l'image de traînée en appelant ImageList_BeginDrag.
      3. ImageList_BeginDrag PROTO himlTrack:DWORD, \
      iTrack:DWORD , \
      dxHotspot:DWORD, \
      dyHotspot:DWORD
      himlTrack est l'handle de l'image list qui contient l'image de traînée.
      iTrack est l'index qui indique l'image de traînée dans l'image list.
      dxHotspot indique la distance relative du point chaud (de l'image de traînée)dans le plan horizontal. Puisque cette image sera employée à la place du curseur de la souris, nous avons donc besoin de définir où se situe le point chaud dans l'image.
      dyHotspot indique la distance relative du point chaud dans le plan vertical.
      Normalement, iTrack sera mis à 0 si vous dites au tree view control de créer une image de traînée à votre place. Et dxHotspot et dyHotspot peuvent être égales à 0 si vous voulez placer le point chaud au niveau du coin supérieur gauche de l'image de traînée.
  3. Quand l'image de traînée est prête à être affichée, nous appelons ImageList_DragEnter pour la faire apparaître dans la fenêtre.
      4. ImageList_DragEnter PROTO hwndLock:DWORD, x:DWORD, y:DWORD
      hwndLock est l'handle de la fenêtre qui affiche l'image de traînée. Cette image n'aura pas la possibilité de se déplacer à l'extérieur de cette fenêtre.
      x et y sont les coordonnées de l'endroit où l'image de traînée doit être affichée au commencement. Remarquez que ces valeurs on pour référence (0;0) le coin supérieur gauche de la fenêtre, et non pas son secteur client.
  4. Maintenant que l'image de traînée est affichée dans la fenêtre, vous devrez maintenir l'opération (DRAG) de traînée dans le tree view control. Cependant, il y a un petit problème ici. Nous devons contrôler le chemin de la traînée avec WM_MOUSEMOVE et l'emplacement où on dépose (DROP) avec le message WM_LBUTTONUP. Cependant, si l'image de traînée appartient à une autre 'Child Windows' (donc pas à notre fenêtre principale [ou parente]), la fenêtre parente ne recevra jamais le message de la souris. La solution est de capturer l'entrée souris avec SetCapture. En employant l'appel, les messages souris seront dirigés vers la fenêtre indiquée indépendamment de l'endroit où se trouve le curseur de la souris (et ici le curseur de la souris vient d'être momentanément remplacé par l'image de traînée).
  5. A l'intérieur de WM_MOUSEMOVE, vous mettrez à jour le chemin de la traînée avec l'appel de ImageList_DragMove. Cette fonction déplace l'image que l'on traîne pendant une opération de glisser-déplacer. En outre, si c'est votre souhait, vous pouvez contrôler l'Item de l'image de traînée en envoyant TVM_HITTEST pour vérifier si l'image de traînée passe au dessus d'un autre Item. Si c'est le cas, vous pouvez envoyer le message TVM_SELECTITEM avec le flag rendu par TVGN_DROPHILITE. Remarquez qu'avant l'envoi du message TVM_SELECTITEM, vous devez d'abord cacher l'image de traînée sinon votre image laissera de vilaines traces en se déplaçant. Vous pouvez cacher l'image de traînée en appelant ImageList_DragShowNolock et, après que l'opération soit finie, appelez ImageList_DragShowNolock de nouveau pour réafficher l'image de traînée.
  6. Quand l'utilisateur relâche le bouton gauche de la souris, vous devez faire plusieurs choses. Si vous perdez le contôle de votre Item, vous deviez le réactiver en envoyant le message TVM_SELECTITEM avec le flag rendu par TVGN_DROPHILITE (pour retrouver l'handle de l'item qui vient de subir le Déplacer-Poser (Drag and Drop)), mais cette fois-ci, lParam DOIT ÊTRE égal à zéro. Si vous n'avez plus le contrôle de votre Item, vous obtiendrez un étrange effet : quand vous choisissez un autre Item, celui-ci sera contenu dans un rectangle mais le contrôle sera toujours sur le dernier Item. Ensuite, vous devez appeler ImageList_DragLeave suivi d' ImageList_EndDrag. Vous devez relâcher la souris en appelant ReleaseCapture. Si vous avez créé une image list, vous devez la détruire en appelant ImageList_Destroy. Après ça, lorsque l'opération de drag and drop est terminée, vous pouvez continuer le déroulement normal de votre programme.

Code sample:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
.const
IDB_TREE equ 4006 ; c'est l'ID de la ressource bitmap (le n° d'Identité de l'image entière)
.data
ClassName db "TreeViewWinClass",0
AppName db "Tree View Demo",0
TreeViewClass db "SysTreeView32",0
Parent db "Parent Item",0
Child1 db "child1",0
Child2 db "child2",0
DragMode dd FALSE ; un flag pour déterminer si nous sommes en mode Drag (traînée)

.data?
hInstance HINSTANCE ?
hwndTreeViewdd ? ; handle du tree view control
hParent dd ? ; handle de l 'Item racine' du Tree View.
hImageList dd ? ; handle de l 'image list' employée dans le 'tree view control'.
hDragImageList dd ? ; handle de l 'image list' où on a l'habitude de stocker l'image de traînée.

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax
invoke InitCommonControls

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,200,400,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL tvinsert:TV_INSERTSTRUCT
LOCAL hBitmap:DWORD
LOCAL tvhit:TV_HITTESTINFO
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
0,200,400,hWnd,NULL,\
hInstance,NULL ; Crée le 'tree view control'
mov hwndTreeView,eax
invoke ImageList_Create,16,16,ILC_COLOR16,2,10 ; Crée l 'image list' associée.
mov hImageList,eax
invoke LoadBitmap,hInstance,IDB_TREE ; Charge le bitmap ressource (charge l'image).
mov hBitmap,eax
invoke ImageList_Add,hImageList,hBitmap,NULL ; Ajoute le bitmap dans l 'image list'.
invoke DeleteObject,hBitmap ; Supprime ce bitmap pour toujours.
invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
mov tvinsert.hParent,NULL
mov tvinsert.hInsertAfter,TVI_ROOT
mov tvinsert.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
mov tvinsert.item.pszText,offset Parent
mov tvinsert.item.iImage,0
mov tvinsert.item.iSelectedImage,1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov hParent,eax
mov tvinsert.hParent,eax
mov tvinsert.hInsertAfter,TVI_LAST
mov tvinsert.item.pszText,offset Child1
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
mov tvinsert.item.pszText,offset Child2
invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
.elseif uMsg==WM_MOUSEMOVE
.if DragMode==TRUE
mov eax,lParam
and eax,0ffffh
mov ecx,lParam
shr ecx,16
mov tvhit.pt.x,eax
mov tvhit.pt.y,ecx
invoke ImageList_DragMove,eax,ecx
invoke ImageList_DragShowNolock,FALSE
invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
.if eax!=NULL
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
.endif
invoke ImageList_DragShowNolock,TRUE
.endif
.elseif uMsg==WM_LBUTTONUP
.if DragMode==TRUE
invoke ImageList_DragLeave,hwndTreeView
invoke ImageList_EndDrag
invoke ImageList_Destroy,hDragImageList
invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
invoke ReleaseCapture
mov DragMode,FALSE
.endif
.elseif uMsg==WM_NOTIFY
mov edi,lParam
assume edi:ptr NM_TREEVIEW
.if [edi].hdr.code==TVN_BEGINDRAG
invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
mov hDragImageList,eax
invoke ImageList_BeginDrag,hDragImageList,0,0,0
invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
invoke SetCapture,hWnd
mov DragMode,TRUE
.endif
assume edi:nothing
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analyse:

Avec WM_CREATE, vous créez tree view control.
    invoke CreateWindowEx,NULL,ADDR TreeViewClass,NULL,\
    WS_CHILD+WS_VISIBLE+TVS_HASLINES+TVS_HASBUTTONS+TVS_LINESATROOT,0,\
    0,200,400,hWnd,NULL,\
    hInstance,NULL
Remarquez les styles. TVS_xxxx représentent des styles spéciaux de Tree View.
    invoke ImageList_Create,16,16,ILC_COLOR16,2,10
    mov hImageList,eax
    invoke LoadBitmap,hInstance,IDB_TREE
    mov hBitmap,eax
    invoke ImageList_Add,hImageList,hBitmap,NULL
    invoke DeleteObject,hBitmap
    invoke SendMessage,hwndTreeView,TVM_SETIMAGELIST,0,hImageList
Ensuite, vous créez une 'image list' vide à son ouverture, laquelle acceptera des images de 16x16 pixels et en couleur 16 bits. Elle contiendra 2 images, mais on peut l'agrandir à 10 images si le besoin s'en fait ressentir. Nous chargeons alors le bitmap et l'ajoutons à l' image list' juste créé. Après ça, nous supprimons l'handle du bitmap puisque il ne sera plus employé désormais. Quand l 'image list' est définitivement finie, nous l'associons au tree view control en envoyant le message TVM_SETIMAGELIST au tree view control.
    mov tvinsert.hParent,NULL
    mov tvinsert.hInsertAfter,TVI_ROOT
    mov tvinsert.u.item.imask,TVIF_TEXT+TVIF_IMAGE+TVIF_SELECTEDIMAGE
    mov tvinsert.u.item.pszText,offset Parent
    mov tvinsert.u.item.iImage,0
    mov tvinsert.u.item.iSelectedImage,1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
Nous insérons des Items dans le tree view control, en commençant à partir de l'Item racine. Puisque il s'agit de l'Item racine, le membre hParent est NULL et hInsertAfter est TVI_ROOT. Le membre imask indique que les membres pszText, iImage et iSelectedImage de la structure TV_ITEM sont valables. Nous remplissons ces trois membres des valeurs appropriée. pszText contient le label de l'Item racine (son texte), iImage est l'index de l'image dans l 'image list' qui sera montré à gauche de l'Item non choisi et iSelectedImage est l'index de l'image dans l 'image list' qui sera montré quand l'Item est choisi. Quand tous les membres appropriés sont remplis, nous envoyons le message TVM_INSERTITEM au tree view control pour y ajouter l'Item racine.
    mov hParent,eax
    mov tvinsert.hParent,eax
    mov tvinsert.hInsertAfter,TVI_LAST
    mov tvinsert.u.item.pszText,offset Child1
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
    mov tvinsert.u.item.pszText,offset Child2
    invoke SendMessage,hwndTreeView,TVM_INSERTITEM,0,addr tvinsert
Après que l'Item racine ait été rajouté, nous pouvons y attacher les Childs Items. Le membre hParent est maintenant rempli de l'handle de l'Item parent. Et nous emploierons des images identiques dans l 'image list' donc nous ne touchons plus aux membres iImage et iSelectedImage.
    .elseif uMsg==WM_NOTIFY
    mov edi,lParam
    assume edi:ptr NM_TREEVIEW
    .if [edi].hdr.code==TVN_BEGINDRAG
    invoke SendMessage,hwndTreeView,TVM_CREATEDRAGIMAGE,0,[edi].itemNew.hItem
    mov hDragImageList,eax
    invoke ImageList_BeginDrag,hDragImageList,0,0,0
    invoke ImageList_DragEnter,hwndTreeView,[edi].ptDrag.x,[edi].ptDrag.y
    invoke SetCapture,hWnd
    mov DragMode,TRUE
    .endif
    assume edi:nothing
Maintenant quand l'utilisateur essaye de déplacer un Item, le tree view control envoie le message WM_NOTIFY avec TVN_BEGINDRAG en tant que code. lParam est le pointer qui est sur la structure NM_TREEVIEW laquelle contient plusieurs informations dont nous avons besoin, ainsi nous mettons sa valeur dans edi et employons edi comme pointer pour la structure NM_TREEVIEW. 'assume edi:ptr NM_TREEVIEW' est une façon de dire à MASM de traiter edi en tant que pointer sur la structure NM_TREEVIEW. Nous créons alors une image de traînée en envoyant TVM_CREATEDRAGIMAGE au tree view control. Il renvoie l'handle de l 'image list' nouvellement créé avec une image de traînée à l'intérieur. Nous appelons ImageList_BeginDrag pour définir le point chaud de l'image de traînée. Alors nous passons à l'opération de traînée en appelant ImageList_DragEnter. Cette fonction affiche l'image de traînée à l'emplacement indiqué dans la fenêtre indiquée. Nous employons la structure ptDrag qui est un membre de la structure NM_TREEVIEW en tant que point de départ où l'image de traînée doit être placée au commencement. Après ça, nous capturons l'entrée souris et nous (activons) le flag pour indiquer que nous passons désormais en mode Drag (traînée).
    .elseif uMsg==WM_MOUSEMOVE
    .if DragMode==TRUE
    mov eax,lParam
    and eax,0ffffh
    mov ecx,lParam
    shr ecx,16
    mov tvhit.pt.x,eax
    mov tvhit.pt.y,ecx
    invoke ImageList_DragMove,eax,ecx
    invoke ImageList_DragShowNolock,FALSE
    invoke SendMessage,hwndTreeView,TVM_HITTEST,NULL,addr tvhit
    .if eax!=NULL
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,eax
    .endif
    invoke ImageList_DragShowNolock,TRUE
    .endif
Maintenant nous nous concentrons sur WM_MOUSEMOVE. Quand l'utilisateur traîne l'image, notre fenêtre parente reçoit des messages WM_MOUSEMOVE. En réponse à ces messages, nous mettons à jour la position de l' image de traînée avec ImageList_DragMove. Après ça, nous vérifions si l'image de traînée passe au-dessus d'un Item. Nous faisons ça en envoyant le message TVM_HITTEST au tree view control. ISi l'image de traînée passe par dessus d'un autre Item, nous reprenons le contrôle de notre Item en envoyant le message TVM_SELECTITEM avec le flag TVGN_DROPHILITE (pour dire que c'est l'Item Drag and Drop qui nous intéresse et pas l'autre) au tree view control. Pendant cette opération, nous cachons l'image de traînée pour qu'elle ne laisse pas de taches dégueulasses sur le tree view control.
    .elseif uMsg==WM_LBUTTONUP
    .if DragMode==TRUE
    invoke ImageList_DragLeave,hwndTreeView
    invoke ImageList_EndDrag
    invoke ImageList_Destroy,hDragImageList
    invoke SendMessage,hwndTreeView,TVM_GETNEXTITEM,TVGN_DROPHILITE,0
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_CARET,eax
    invoke SendMessage,hwndTreeView,TVM_SELECTITEM,TVGN_DROPHILITE,0
    invoke ReleaseCapture
    mov DragMode,FALSE
    .endif
Quand l'utilisateur relâche le bouton gauche de la souris, l'opération de traînée se termine. Nous sortons du mode traînée en appelant ImageList_DragLeave, suivi par ImageList_EndDrag et ImageList_Destroy.
[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's Win32asm Tutorial 20: Window Subclassing

Tutorial 20: Les Sousclassifications de Fenêtre
(Window Subclassing)

Dans ce Tutorial, nous allons voir les éléments de sousclassifications d'une fenêtre, ce que c'est et comment les employer à votre avantage.
Downloadez l'exemple ici.

Théorie:

Si ça fait un petit moment que vous programmez sous Windows, vous avez dû rencontrer certains cas où une fenêtre possède pratiquement les attributs (les caractéristiques) dont vous auriez besoin pour votre programme, mais pas tout à fait quand même. Vous avez sûrement déjà dû rencontrer cette situation où il vous faut un contrôle d'édition un peu spécial qui puisse filtrer certains types de textes indésirables ! La meilleur chose à faire, c'est de coder votre propre fenêtre. Mais c'est le travail vraiment difficile et très long. Voilà donc à notre secours les 'Sousclassifications' de fenêtre.
Grâce aux sousclassifications de fenêtre on peut "reprendre" (corriger améliorer) une fenêtre sousclassée. Vous y aurez un contrôle absolu. On va prendre un exemple pour que ce soit plus clair. Supposons que vous avez besoin d'une boîte de texte qui n'accepte que des nombres hexadécimaux. Si vous utilisez un simple contrôle d'édition, rien n'interdit à l'utilisateur de taper autre chose que des nombres hexadécimaux. Si l'utilisateur tape "zb+q *" dans la boîte de texte, vous ne pouvez rien en faire sauf rejeter la totalité de cette chaîne de caractères. C'est pas très professionnel. En réalité, vous avez besoin de pouvoir examiner chaque caractère tapé par l'utilisateur au moment même où il le tape dans la boîte de saisie.
Nous allons examiner comment faire çà. Quand l'utilisateur tape quelque chose dans une boîte de texte, Windows envoie le message WM_CHAR à la procédure de fenêtre qui s'occupe de la gestion du contrôle d'édition. Cette procédure de fenêtre réside à l'intérieur de Windows lui-même donc nous ne pouvons rien modifier. Cependant nous pouvons faire suivre le flux de message de notre propre procédure de fenêtre pour que notre procédure de fenêtre devienne la première (avant que Windows s'en occupe) à traiter n'importe quel message envoyé au contrôle d'édition. Si on veut faire réagir notre procédure de fenêtre à un éventuel message (si on veut détecter des caractères non désirés), on doit le faire ainsi. Mais si elle ne veut pas prendre en compte le message, on peut le passer à la procédure de fenêtre originale. De cette manière, notre procédure de fenêtre s'insère entre Windows et le contrôle d'édition. Voyez plutôt le flux ci-dessous :

    Avant la Sousclassification :

    Windows ==> contrôle d'édition de la procédure de fenêtre.

    Après mise en place d'un système de Sousclassification :

    Windows ==> notre procédure de fenêtre -----> contrôle d'édition de la procédure de fenêtre.
Maintenant concentrons notre attention sur le fait de savoir comment sous-classer une fenêtre. Remarquez que la sousclassification n'est pas uniquement limitée aux simples 'controls' (commandes), on peut aussi l'employée avec n'importe quelle fenêtre.
Réfléchissons ! Comment Windows sait à quel endroit est-ce que le contrôle d'édition (de la procédure de fenêtre) réside. Vous avez devinez ? ...... regardez le membre lpfnWndProc de la structure WNDCLASSEX. Si nous pouvons remplacer ce membre par l'adresse de notre propre procédure de fenêtre, alors Windows enverra des messages à notre proc de fenêtre au lieu de l'autre.
On peut faire ça en appelant SetWindowLong.
    SetWindowLong PROTO hWnd:DWORD, nIndex:DWORD, dwNewLong:DWORD
hWnd = handle de la fenêtre où l'on doit changer la valeur à l'intérieur de la structure WNDCLASSEX.
nIndex = valeur à changer.
    GWL_EXSTYLE Défini un nouveau 'style étendu' de fenêtre.
    GWL_STYLE Défini un nouveau style de fenêtre.
    GWL_WNDPROC défini une nouvelle adresse pour la procédure de fenêtre.
    GWL_HINSTANCE Défini une nouvelle application d 'instance handle'.
    GWL_ID Défini un nouvel (ID) identificateur de la fenêtre.
    GWL_USERDATA impose de travailler avec des valeurs de 32 bits pour cette fenêtre. Chaque fenêtre a une valeur correspondante à 32 bits destinée à être utilisée par l'application qui a créé cette fenêtre.
dwNewLong = C'est la valeur de substitution.
Donc notre travail est facile : Nous codons (écrivons) une proc de fenêtre qui manipulera les messages du contrôle d'édition et appellera ensuite SetWindowLong avec flag renvoyé par GWL_WNDPROC, en faisant passer l'adresse de notre proc de fenêtre comme troisième paramètre. Si la fonction réussit, la valeur de retour est la valeur vue précédemment (l'entier 32 bits), dans notre cas, c'est l'adresse de la procédure de fenêtre originale. Nous avons besoin de stocker cette valeur pour pouvoir ensuite l'utiliser dans notre propre procédure de fenêtre.
Rappelez-vous qu'il y a certains messages (ici des chiffres ou des lettres non-hexadécimales) que nous ne souhaitons pas récupérer, donc nous les passerons à la procédure de fenêtre originale. Nous pouvons faire ça en appelant la fonction CallWindowProc.
    CallWindowProc PROTO lpPrevWndFunc:DWORD, \
    hWnd:DWORD,\
    Msg:DWORD,\
    wParam:DWORD,\
    lParam:DWORD
LpPrevWndFunc = adresse de la 'procédure de fenêtre' originale.
Ci-dessus, ces quatre paramètres sont ceux passés à notre procédure de fenêtre. On les passe uniquement grâce à CallWindowProc.

Echantillon de Code :

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SubclassWinClass",0
AppName db "Subclassing Demo",0
EditClass db "EDIT",0
Message db "You pressed Enter in the text box!",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?
OldWndProc dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
.if uMsg==WM_CREATE
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR EditClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
20,300,25,hWnd,NULL,\
hInstance,NULL
mov hwndEdit,eax
invoke SetFocus,eax
;-----------------------------------------
; On le sous-classe !
;-----------------------------------------
invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
mov OldWndProc,eax
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

Analyse:

    invoke SetWindowLong,hwndEdit,GWL_WNDPROC,addr EditWndProc
    mov OldWndProc,eax
Après que le contrôle d'édition soit créé, nous le sous-classons en appelant SetWindowLong, en remplaçant l'adresse de la procédure de fenêtre originale par notre propre procédure de fenêtre. Remarquez que nous stockons l'adresse de la procédure de fenêtre originale pour pouvoir l'utiliser plus tard avec CallWindowProc. Notez qu' EditWndProc est une procédure de fenêtre ordinaire.
    .if uMsg==WM_CHAR
    mov eax,wParam
    .if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
    .if al>="a" && al<="f"
    sub al,20h
    .endif
    invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
    ret
    .endif
Grâce à EditWndProc, nous filtrons les messages WM_CHAR. Si le caractère est compris entre 0-9 ou A-F, nous l'acceptons en le laissant arriver jusqu'à la procédure de fenêtre originale. Si c'est une lettre comprise entre A-F, nous la convertissons en lettre entre a-f en ajoutant 20h à la valeur hexadécimale qui représente cette lettre. Notez bien que, si le caractère n'est pas celui nous attendons, nous le rejetons. Nous ne le passons pas à la procédure de fenêtre originale. Donc lorsque l'utilisateur tape quelque chose d'autre que 0-9 ou a-f, le caractère n'apparaît pas dans le contrôle d'édition.
    .elseif uMsg==WM_KEYDOWN
    mov eax,wParam
    .if al==VK_RETURN
    invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
    invoke SetFocus,hEdit
    .else
    invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
    ret
    .end
Je vous démontre en plus (dans le programme) la puissance de la méthode de sous-classification avec la détection de la touche Entre. EditWndProc vérifie si la valeur du message WM_KEYDOWN est VK_RETURN (la Touche Enter). Si c'est le cas, une boîte de message affiche s'ouvre pour afficher : "You pressed the Enter key in the text box!" (Vous avez appuyé sur la touche Enter dans la boîte de texte!). Si ce n'est pas la touche Enter, on passe le message à la procédure de fenêtre originale.
Vous pouvez employer la méthode de sous-classification de fenêtre pour prendre le contrôle d'autres fenêtres. C'est une des corde que vous devez avoir à votre arc.
[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 21: Pipe

Tutorial 21: Le Pipe
Pipe = Cable de communication entre deux programmes

(en plus : couleur du fond d'un contrôle d'édition)

Dans ce Tutorial, nous allons nous intéresser au 'Pipe', ce que ça représente et à quoi nous pouvons l'employer. Pour rendre tout ça plus intéressant, j'ajoute en plus à cette technique le fait de savoir comment changer le fond et la couleur du texte d'un contrôle d'édition.
Downloadez l'exemple ici.
En réalité cet exemple fait appel au programme ml.exe qui se trouve dans C:\Masm32\Bin\ donc vous devez d'abord le dézipper dans ce dossier pour qu'il puisse fonctionner.

Théorie:

Le pipe est un conduit ou bien une sorte de cable de communication muni de deux bouts. Vous pouvez l'employer pour échanger des données entre deux process différents (entre deux programmes différents), ou dans un même process. C'est comme un walkie-talkie. Vous donnez une information à l'autre partie et elle peut l'employer pour communiquer avec vous.
Il y a deux types de pipes : les pipes anonymes et les pipes nommées. Un pipe anonyme est, eh bien, anonyme : c'est-à-dire que vous pouvez l'utiliser sans connaître son nom. Pour un pipe nommée c'est le contraire : vous devez connaître son nom avant de pouvoir l'utiliser.
Vous pouvez aussi ranger les pipes par catégories selon leur propriété : certains fonctionnent en sens unique, les autres communique bilatéralement. Dans un pipe à sens unique, les données circulent uniquement dans une seule direction : d'un bout à l'autre. Tandis que pour un pipe bilatérale, les données peuvent être échangées entre les deux bouts.
Un pipe anonyme est toujours à sens unique tandis qu'un pipe nommée peut être à sens unique ou bilatérale. Un pipe nommée est d'habitude employé dans un environnement réseau dans lequel un serveur peut se connecter à plusieurs clients.
Dans ce Tutorial, nous examinerons le pipe anonyme en détail. Le but principal d'un pipe anonyme c'est d'être utilisé comme un cable de communcation entre un parent et des process enfants (entre un programme principal et ses sous-programmes) ou entre plusieurs process enfants.
Un pipe anonyme est réellement utile lorsque vous traitez avec une application console. Une application console est une sorte de programme win32 qui utilise une console en tant qu'Entrée/Sortie. Une console est un outils de DOS. Cependant, une application de console est un programme entièrement 32 bits. Elle peut employer n'importe quelle fonction du GUI (NdT : Graphic User Interface, le GUI c'est l'ensemble des objets tels qu'une boîte de saisie de texte, les boutons…), de la même façon que d'autres programmes.
Une application console a trois handles qu'elle peut utiliser pour ses Entrées/Sorties. Ont les appelle 'handles standards'. Il y en a trois : 'Entrée Standard'(ou Standard Input), 'Sortie Standard'(ou Standard Output) et 'Erreur Standard'(ou Standard Error). L'handle de l'Entrée Standard est employé pour lire/récupérer les informations de la console, alors que l'handle de la Sortie Standard est utilisé pour renvoyer les données vers la sortie/ou l 'imprimante. L'handle de l'Erreur Standard est employé pour donner un compte rendu sur les conditions d'erreurs lorsque sa sortie n'est pas accessible.
Une application console peut retrouver ces trois 'handles standards' en appelant la fonction GetStdHandle, en spécifiant bien quel handle est ce qu'on souhaite récupérer. Une application GUI n'a pas de console. Si vous appelez GetStdHandle, il renverra une erreur. Si vous voulez vraiment utiliser une console, vous pouvez appeler AllocConsole pour allouer une nouvelle console. Cependant, n'oubliez pas d'appeler FreeConsole quand vous mettez en place une console.
Le pipe anonyme est fréquemment employée pour faire suivre les entrées/sorties d'une console d'une application enfant. Le process parent peut être une console ou une application du GUI, mais l'enfant doit être un appendice de console. Pour que ça marche. Comme vous le savez, une application console utilise des handles standards pour ses Entrées/Sorties. Si nous voulons faire suivre les Entrées/Sorties d'une application console, nous pouvons remplacer l'handle par un des handles du pipe. Une application console ne se rendra pas compte qu'on emploie l'handle d'un pipe. Elle l'utilisera de la même manière qu'un handle normal. C'est une sorte de polymorphisme, en le jargon OOP. Cette approche est puissante puisque nous n'avons pas besoin de modifier le process enfant où que ce soit.
Une autre chose que vous devez savoir à propos des applications console c'est à quel moment on récupère ces 'handles standards'. Lorsqu'une application console est créée, le parent process a deux choix : il peut créer une nouvelle console pour l'enfant ou il peut laisser l'enfant hériter de sa propre console (le parent process et le child prosses auront des consoles de mêmes caractéristiques). Avec la deuxième approche, le parent process doit être une application console ou bien si c'est une application du GUI, elle doit d'abord appeler AllocConsole pour allouer une console.
On va commencer le boulot. Pour créer un pipe anonyme vous avez besoin d'appeler CreatePipe. CreatePipe a le prototype suivant :
CreatePipe proto pReadHandle:DWORD, \
pWriteHandle:DWORD,\
pPipeAttributes:DWORD,\
nBufferSize:DWORD
  • pReadHandle est un pointer qui est sur une variable de type dword qui recevra l'handle d'écriture du pipe.
  • pWriteHandle est un pointer qui est sur une variable de type dword laquelle recevra l 'handle d'écriture' du pipe.
  • pPipeAttributes pointe sur la structure SECURITY_ATTRIBUTES laquelle détermine si les handles de lecture et d'écriture renvoyés ont bien étés générés par un process enfant.
  • nBufferSize est la taille supposée du buffer que le pipe réservera pour l'utilisation. C'est seulement une taille supposée. Vous pouvez employer NULL pour dire à la fonction d'employer la taille par défaut.
Si l'appel est couronné de succès, la valeur en retour est différent de zéro. Si elle a échouée, la valeur de retour est nulle.
Après que l'appel est été couronné de succès, vous obtiendrez deux handles, un pour le bout 'lecture' du pipe, et autre pour le bout 'écriture'. Maintenant on va voir les étapes nécessaires à la réexpédition des Sorties Standards d'un programme console enfant, vers votre propre process. Notez que ma méthode diffère de celle de la référence win32 api de Borland. La méthode dans la référence win32 api impose que le parent process soit une application console et par conséquent le prosses enfant peut hériter de ses 'handles standards'. Mais la plupart du temps, nous aurons besoin de faire suivre la Sortie d'une application console vers une application du GUI (par exemple vers une boîte de saisie).
  1. Créez une pipe anonyme avec CreatePipe. N'oubliez pas de mettre le membre bInheritable à TRUE dans la structure SECURITY_ATTRIBUTES puisque les handles ne proviennent pas d'un parent.
  2. Maintenant nous devons préparer les paramètres que nous passerons à CreateProcess puisque nous l'emploierons pour charger l'application console enfant. Une structure importante est la structure de STARTUPINFO. Elle détermine l'apparence de la fenêtre principale du process enfant quand elle apparaît en premier lieu. Cette structure est essentielle pour nous. Vous pouvez cacher la fenêtre principale et passer l'handle du pipe au process console enfant comme ça. Ci-dessous voici les membres que vous devez remplir :
    • cb : est la taille de structure de STARTUPINFO
    • dwFlags : les flags binaires (donc 0 ou 1) qui déterminent si les membres de la structure sont valables, ils déterminent aussi l'état apparent/caché de la fenêtre principale. Pour notre but, on doit utiliser une combinaison des deux flags : STARTF_USESHOWWINDOW et STARTF_USESTDHANDLES
    • hStdOutput et hStdError : sont les handles qui seront utilisés en tant 'handle standard de sortie' et 'handle standard d'erreur' pour votre process enfant. Pour arriver à notre but, nous passons l'handle d'écriture du pipe en tant que sortie/erreur standard du process enfant. De cette façon, lorsque les Sorties enfants sont en actions (Sorties/Erreurs standard), on passe en réalité les renseignements par le pipe vers le parent process.
    • wShowWindow a un contrôle sur l'état affiché/caché de la fenêtre principale. Dans notre cas, nous ne souhaitons pas que la console de notre fenêtre enfant soit montrée donc nous mettons SW_HIDE dans ce membre.
  3. On appelle CreateProcess pour charger l'application enfant. Après que CreateProcess soit couronné de succès, l'enfant est toujours inactivé. Il est chargé en mémoire mais il n'est pas immédiatement en action.
  4. On ferme l'handle d'écriture du pipe. C'est nécessaire. Parce que le parent process n'a pas besoin de se servir de l'handle d'écriture du pipe. En plus, le pipe ne fonctionnera pas si il y a plus d'une sortie d'écriture, nous DEVONS le fermer avant la lecture des données du pipe. Cependant, ne fermez pas l'handle d'écriture avant l'appel CreateProcess, sinon le fonctionnement de votre pipe sera transgressé. Vous devez seulement le fermer après le retour de CreateProcess mais surtout pas avant que vous n'ayez lu les données de lecture du pipe.
  5. Maintenant vous pouvez lire les données du bout de lecture du pipe avec ReadFile. Grâce à ReadFile, vous influencez le process enfant pendant son déroulement même. Ça débute l'exécution et lorsqu'on ''écrit'' quelque chose à l'handle standard de sortie (qui est en réalité l'handle d'écriture du pipe), les données sont envoyées par le pipe vers le bout d'écriture. Vous pouvez vous représenter ReadFile un peu comme un aspirateur de données du bout d'écriture du pipe. Vous devez appeler ReadFile à plusieurs reprises avant qu'il ne retourne la valeur 0, ce qui signifie qu'il n'y a plus de données à lire. Vous pouvez faire ce qu'il vous plais des données lues dans le pipe. Dans notre exemple, je les ai mises dans un contrôle d'édition. (cas classique)
  6. On ferme l'handle de lecture du pipe.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101 ; l' ID du menu principal
IDM_ASSEMBLE equ 40001

.data
ClassName db "PipeWinClass",0
AppName db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError db "Error during pipe creation",0
CreateProcessError db "Error during process creation",0
CommandLine db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,IDR_MAINMENU
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL rect:RECT
LOCAL hRead:DWORD
LOCAL hWrite:DWORD
LOCAL startupinfo:STARTUPINFO
LOCAL pinfo:PROCESS_INFORMATION
LOCAL buffer[1024]:byte
LOCAL bytesRead:DWORD
LOCAL hdc:DWORD
LOCAL sat:SECURITY_ATTRIBUTES
.if uMsg==WM_CREATE
invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL
mov hwndEdit,eax
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetBkColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
.elseif uMsg==WM_SIZE
mov edx,lParam
mov ecx,edx
shr ecx,16
and edx,0ffffh
invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
.elseif uMsg==WM_COMMAND
.if lParam==0
mov eax,wParam
.if ax==IDM_ASSEMBLE
mov sat.niLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE
invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
.if eax==NULL
invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
.else
mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE
invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
.if eax==NULL
invoke MessageBox,hWnd,addr CreateProcessError,addr AppName,MB_ICONERROR+MB_OK
.else
invoke CloseHandle,hWrite
.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw
.endif
invoke CloseHandle,hRead
.endif
.endif
.endif
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
.endif
xor eax,eax
ret
WndProc endp
end start

Analyse:

Notre exemple appellera le petit programme ml.exe pour assembler un fichier nommé test.asm, puis fera suivre la sortie (les données en sortie) de ml.exe vers le contrôle d'édition (du secteur client de notre programme).
Quand le programme est chargé, on enregistre la 'window class' et on crée comme d'habitude la fenêtre principale. La première chose qu'on fait pendant la création de la fenêtre principale c'est de créer un contrôle d'édition qui sera employé pour afficher les données de sortie de ml.exe (pour afficher par exemple: ml.exe vient d'assembler Test.asm avec succès…..).
Maintenant nous voici dans la partie intéressante (mais c'est le deuxième point de ce tutorial. Ça n'a aucun rapport avec ce qu'on a vu avant), nous allons changer la couleur du texte et la couleur de fond du contrôle d'édition. Quand un contrôle d'édition repeindre son secteur client, il envoie le message WM_CTLCOLOREDIT à sa fenêtre parente..
wParam contient l'handle du style d'écriture que le contrôle d'édition emploiera pour écrire son propre secteur de client. Nous pouvons profiter de cette occasion pour modifier les caractéristiques de l'HDC.
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wParam,Yellow
invoke SetTextColor,wParam,Black
invoke GetStockObject,BLACK_BRUSH
ret
SetTextColor change la couleur du texte en jaune. SetTextColor change la couleur du fond en noir. Et finalement, nous obtenons l'handle du pinceau noir que nous renvoyons à Windows. Avec le message WM_CTLCOLOREDIT, on renvoie l'handle du pinceau que Windows emploiera pour repeindre le fond du contrôle d'édition. Dans notre exemple, je souhaite que le fond soit noir, donc je renvoie à Windows l'handle du pinceau noir.
Maintenant quand l'utilisateur choisit le menuitem (le sous-menu) Assemble, il crée un pipe anonyme.

.if ax==IDM_ASSEMBLE
mov sat.niLength,sizeof SECURITY_ATTRIBUTES
mov sat.lpSecurityDescriptor,NULL
mov sat.bInheritHandle,TRUE

Avant l'appel CreatePipe, nous devons d'abord remplir la structure SECURITY_ATTRIBUTES. Notez que nous pouvons employer NULL dans le membre lpSecurityDescriptor si nous ne nous soucions pas de la sécurité. Et le membre bInheritHandle doit être TRUE pour que les handles du pipe soient les mêmes pour le process parent et le process enfant.

invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

Après ça, nous appelons CreatePipe qui, si il est couronné de succès, remplira les variables hRead et hWrite respectivement avec les handles de lecture et d'écriture des bouts du pipe.

mov startupinfo.cb,sizeof STARTUPINFO
invoke GetStartupInfo,addr startupinfo
mov eax, hWrite
mov startupinfo.hStdOutput,eax
mov startupinfo.hStdError,eax
mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
mov startupinfo.wShowWindow,SW_HIDE

Ensuite nous devons remplir la structure STARTUPINFO. Nous appelons GetStartupInfo pour remplir la structure STARTUPINFO avec les valeurs de défaut du process parent. Vous DEVEZ remplir la structure STARTUPINFO avec cet appel si vous avez l'intention que votre code puisse travailler à la fois sous win9x et NT. Après les valeurs retournées par l'appel GetStartupInfo, vous pouvez modifier les membres qui sont importants. Nous copions l'handle d'écriture du bout du pipe dans hStdOutput et hStdError puisque nous voulons que le process enfant l'utilise au lieu des handles standards de Sortie/Erreur par défaut. Nous souhaitons aussi cacher la fenêtre de console du process enfant, donc nous mettons la valeur SW_HIDE dans le membre wShowWidow. Et finalement, nous devons indiquer que les membres hStdOutput, hStdError et wShowWindow sont valables (ils sont utilisables car ils sont porteur d'une information) et doivent être utilisés en plaçant les flags STARTF_USESHOWWINDOW et STARTF_USESTDHANDLES dans le membre dwFlags.

invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo

Nous créons maintenant le process enfant grâce à l'appel CreateProcess. Remarquez que le paramètre bInheritHandles doit être mis à TRUE pour que l'handle du pipe soit le même que son parent, et ainsi fonctionne normalement.

invoke CloseHandle,hWrite

Après que nous ayons créé le process enfant avec succès, nous devons fermer le bout d'écriture du pipe. Rappelez-vous que nous avons passé l'handle d'écriture au process enfant via la structure STARTUPINFO. Si nous ne fermons pas cet handle nous même, il y aura deux bouts d'écriture. Et alors le pipe ne fonctionnera pas. Nous devons fermer l'handle d'écriture : Après CreateProcess, mais aussi, Avant que nous ne lisions les données appartenant au bout de lecture du pipe.

.while TRUE
invoke RtlZeroMemory,addr buffer,1024
invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
.if eax==NULL
.break
.endif
invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
.endw

Maintenant nous sommes prêts à lire les données de la Sortie Standard du process enfant. Nous tournerons dans une boucle infinie jusqu'à ce qu'il n'y ait plus de données de lecture. Nous appelons RtlZeroMemory pour qu'il remplisse le buffer avec des zéros lors de l'appel ReadFile, en passant l'handle de lecture du pipe au lieu de l'handle d'un fichier (pour lire dans le Buffer et non dans un fichier). Notez bien que nous ne pouvons lire qu'un maximum de 1023 octets puisque nous avons besoin des données qu'en tant que chaîne de caractères ASCIIZ, lesquels nous pouvons maintenant passer au contrôle d'édition.
Lorsque ReadFile nous retourne les données du buffer, nous les envoyons dans le contrôle d'édition. Cependant, il y a un léger problème ici. Si nous employons SetWindowText pour mettre les données dans le contrôle d'édition, nos nouvelles données recouvriront et effaceront des données déjà existantes! Nous préférons plutôt que nos données s'ajoutent à la fin des données existantes.
Pour réussir à faire ça, nous devons d'abord déplacer notre curseur à la fin du texte dans le contrôle d'édition en envoyant le message EM_SETSEL avec wParam =-1. Ensuite, nous ajoutons les données à partir de ce point grâce au message EM_REPLACESEL.

invoke CloseHandle,hRead

Dès que ReadFile renvoie le NULL, nous sortons de la boucle de lecture et refermons justement l'handle de lecture.

Iczelion's Win32 Assembly Homepage

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 22: SuperClassing

Tutorial 22: Superclassification
(Superclassing)

Dans ce Tutorial, nous allons voir, ce qu'est la superclassification et à quoi ça sert. Vous allez aussi apprendre à munir vos 'Controls' de la fonction de la touche TAB. (vous savez ! en appuyant sur TAB on met en sélection les boutons ou le contrôle d'édition… les uns après les autres)
Downloadez l'exemple ici

Théorie:

Dans votre carrière de programmeur, vous avez sûrement déjà rencontré une situation où vous avez besoin de plusieurs 'Controls' avec un comportement *peu* différent. Par exemple, vous pouvez avoir besoin de 10 'contrôles d'édition' qui acceptent seulement des nombres. Il y a plusieurs façons de réaliser ceci :
  • Créez votre propre 'window class' et instance.
  • Créez ce 'contrôle d'édition' et ensuite sous-classez chacun d'entre eux (les prochains).
  • Superclassez le contrôle d'édition
La première méthode est trop ennuyeuse. On doit réaliser chaque fonctionnalité du contrôle d'édition par nous-même. Il vaut mieux laisser tomber ça. La deuxième méthode est meilleure que la première, mais c'est encore un travail de longue haleine. C'est bon si vous sous-classez uniquement quelques 'controls' mais ce sera un véritable cauchemar si vous sous-classer plus d'une douzaine de 'Controls'. La superclassification est la technique que vous devez employer pour cette occasion.
La sousclassification est la méthode qu'on doit utiliser pour *prendre le contrôle* d'une classe de fenêtre particulière. Par *la prise de contrôle*, je veux dire que vous pouvez modifier les propriétés de la (Window Class) classe de fenêtre pour la modeler à votre convenance, ainsi on crée une série de 'Controls'.
Les étapes de la superclassification sont décrites ci-dessous :
  • call GetClassInfoEx sert à obtenir l'information sur la 'Window Class' vous voulez pour la superclasse. GetClassInfoEx exige que le pointer soit sur la structure WNDCLASSEX laquelle sera remplie de l'information si l'appel est renvoyé avec succès.
  • Modifiez les membres que vous voulez dans la structure WNDCLASSEX. Cependant, il y a deux membres que vous DEVEZ changer :
    • hInstance Vous devez mettre l'instance handle de votre programme dans ce membre.
    • lpszClassName Vous devez mettre ici le pointer qui est sur le nouveau 'class name'.

      • Vous n'avez pas besoin de modifier le membre lpfnWndProc, mais la plupart du temps, vous avez quand même besoin de le faire. Surtout, n'oubliez pas de sauver le membre lpfnWndProc d'origine si vous souhaitez l'appeler avec CallWindowProc.
  • Enregistrez votre structure WNDCLASSEX modifiée. Vous aurez une nouvelle (Window Class) classe de fenêtre qui aura plusieurs des caractéristiques de la vieille classe de fenêtre.
  • Créez des fenêtres avec la nouvelle 'class'.
La superclassification, c'est mieux que la sousclassification si vous souhaitez créer pas mal de 'Controls' ayant les mêmes caractéristiques.

Exemple:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WM_SUPERCLASS equ WM_USER+5
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
EditWndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SuperclassWinClass",0
AppName db "Superclassing Demo",0
EditClass db "EDIT",0
OurClass db "SUPEREDITCLASS",0
Message db "You pressed the Enter key in the text box!",0

.data?
hInstance dd ?
hwndEdit dd 6 dup(?)
OldWndProc dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND

mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE+WS_EX_CONTROLPARENT,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,220,NULL,NULL,\
hInst,NULL
mov hwnd,eax

.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc uses ebx edi hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL wc:WNDCLASSEX
.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc
push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass
invoke RegisterClassEx, addr wc
xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit
.elseif uMsg==WM_DESTROY
invoke PostQuitMessage,NULL
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

EditWndProc PROC hEdit:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
.if uMsg==WM_CHAR
mov eax,wParam
.if (al>="0" && al<="9") || (al>="A" && al<="F") || (al>="a" && al<="f") || al==VK_BACK
.if al>="a" && al<="f"
sub al,20h
.endif
invoke CallWindowProc,OldWndProc,hEdit,uMsg,eax,lParam
ret
.endif
.elseif uMsg==WM_KEYDOWN
mov eax,wParam
.if al==VK_RETURN
invoke MessageBox,hEdit,addr Message,addr AppName,MB_OK+MB_ICONINFORMATION
invoke SetFocus,hEdit
.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
.else
invoke CallWindowProc,OldWndProc,hEdit,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
EditWndProc endp
end start

Analyse:

Le programme créera une simple fenêtre avec 6 contrôles d'édition "retouchés" dans son secteur client. Les contrôles d'édition accepteront uniquement des chiffres hexdécimaux. En réalité, j'ai modifié l'exemple de 'Sousclassification' pour le transformer en superclassification. Le début du programme reste le même mais la partie intéressante c'est quand on crée la fenêtre principale :

.if uMsg==WM_CREATE
mov wc.cbSize,sizeof WNDCLASSEX
invoke GetClassInfoEx,NULL,addr EditClass,addr wc

Nous devons d'abord remplir la structure de WNDCLASSEX avec les données de class que nous souhaitons imposer comme superclasse, dans ce cas, c'est (l'Edit Class) la classe D'ÉDITION. Rappelez-vous aussi que vous devez mettre le membre cbSize (de la structure WNDCLASSEX) avant que vous n'appeliez GetClassInfoEx, sinon la structure WNDCLASSEX ne sera pas correctement remplie. Après les retours de GetClassInfoEx, wc est rempli de toute l'information que nous avons besoin pour créer une nouvelle 'Window Class'.

push wc.lpfnWndProc
pop OldWndProc
mov wc.lpfnWndProc, OFFSET EditWndProc
push hInstance
pop wc.hInstance
mov wc.lpszClassName,OFFSET OurClass

Maintenant nous devons modifier certains membres de wc. Le premier c'est le pointer de la procédure de fenêtre. Puisque nous avons besoin d'enchaîner notre propre procédure de fenêtre avec l'originale, nous devons la sauvegarder dans une variable, c'est pourquoi on l'appelle avec CallWindowProc. Cette technique est identique à la sousclassification sauf que vous modifiez directement la structure WNDCLASSEX sans avoir besoin d'appeler SetWindowLong. Les deux membres suivants doivent aussi être modifiés, sinon vous serez dans l'incapacité d'enregistrer votre nouvelle 'Window Classe',idem pour ce qui concerne hInstance et lpsClassName. Vous devez remplacer l'hInstance original par l'hInstance de votre propre programme. Et vous devez choisir un nouveau nom pour la nouvelle 'class'.

invoke RegisterClassEx, addr wc

Quand tout est enfin prêt, on registre la nouvelle classe. on obtient une nouvelle class avec quand même certaines caractéristiques de la vieille class.

xor ebx,ebx
mov edi,20
.while ebx<6
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR OurClass,NULL,\
WS_CHILD+WS_VISIBLE+WS_BORDER,20,\
edi,300,25,hWnd,ebx,\
hInstance,NULL
mov dword ptr [hwndEdit+4*ebx],eax
add edi,25
inc ebx
.endw
invoke SetFocus,hwndEdit

Maintenant que nous avons enregistré la class, nous pouvons y créer des fenêtres de base. Dans le susdit petit bout de code, j'emploie ebx en tant que compteur de nombre de fenêtres créées. edi est employé comme coordonnée 'Y' du coin gauche supérieur de la fenêtre. Quand une fenêtre est créée, son handle est stocké dans un tableau (informations de type dword). Quand toutes les fenêtres sont créées, le centre d'entrée est sur la première fenêtre. (le centre d'entrée = ex : un bouton est encadré de pointillé où un contrôle d'édition est activée [curseur présent], alors c'est que le centre d'entrée est sur lui.)
À ce niveau, on a 6 contrôles d'édition qui acceptent uniquement des chiffres hexadécimaux. La procédure de fenêtre de substitution manipule les filtres (donc pas de caractère autres que 0-9 ou a-f). En réalité, c'est la même chose que pour la procédure de fenêtre de l'exemple de sousclassification. Donc, comme vous pouvez le voir, vous n'avez pas besoin de refaire le travail de sousclassification pour chacun des contrôles d'édition.

Pour rendre cet exemple encore plus intéressant, je rajoute un petit bout de code qui manipule le contrôle de navigation grâce à la touche TAB. Normalement, si vous mettez des 'Controls' dans une boîte de dialogue, le manager de boîte de dialogue gère les touches de navigation pour vous. De cette manière vous pouvez grâce à la touche TAB passer au contrôle suivant ou bien avec Shift-Tab revenir au contrôle précédent. Hélas, cette particularité n'est pas disponible si vous placez vos 'Controls' dans une simple fenêtre (au lieu d'une Dialog Box). Vous devez les sous-classer, comme ça vous pourrez gérer les touches Tab et Shift-Tab par vous-même. Dans notre exemple, nous n'avons pas besoin de sous-classer les 'Controls' l'un après l'autre vu qu'on les a déjà superclassés, donc nous pouvons leur fournir "un manager de navigation de contrôle" commun.

.elseif al==VK_TAB
invoke GetKeyState,VK_SHIFT
test eax,80000000
.if ZERO?
invoke GetWindow,hEdit,GW_HWNDNEXT
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDFIRST
.endif
.else
invoke GetWindow,hEdit,GW_HWNDPREV
.if eax==NULL
invoke GetWindow,hEdit,GW_HWNDLAST
.endif
.endif
invoke SetFocus,eax
xor eax,eax
ret

Le susdit petit bout de code est la procédure EditWndClass. Il vérifie si l'utilisateur appuie sur la touche TAB, si c'est le cas, on appelle GetKeyState pour vérifier si la TOUCHE DES MAJUSCULES (SHIFT) est aussi pressée. GetKeyState renvoie une valeur dans eax qui détermine si la touche indiquée est pressée ou non. Si la touche est appuyée, le Bit de poids fort de eax est rempli. Si 'SHIFT' n'est pas pressé, on laisse ce Bit à 0. Donc on compare la valeur de retour avec 80000000. Si le Bit de poids fort est à 8, ça signifie que l'utilisateur a appuyé sur shift+tab, que nous devons gérer séparément.
Un Dword , donc 32 bit donc (0000 0000 0000 0000 0000 0000 0000 0000)h on met sa partie haute à 1 (1111 0000 0000 0000 0000 0000 0000 0000)h =(8 0 0 0 0 0 0 0)d, car Masm manipule des décimaux par défaut.
Si l'utilisateur appuie seulement sur la touche TAB, nous appelons GetWindow pour retrouver l'handle du contrôle suivant. Nous utilisons le flag GW_HWNDNEXT pour dire à GetWindow de récupérer l'handle de la fenêtre qui suit la ligne du hEdit courant. Si cette fonction renvoie un NULL, on ne le considère pas comme un handle, ainsi le courant hEdit est le dernier contrôle. Nous "revenons" donc au premier contrôle en appelant GetWindow avec son flag GW_HWNDFIRST. C'est la même chose pour la configuration SHIFT-TAB que pour la touche TAB, sauf qu'on inverse tout.

[Iczelion's Win32 Assembly Homepage]
Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 23: Tray Icon

Tutorial 23: Icône de Barre de Taches
(Tray Icon)

Dans ce Tutorial, nous allons voir comment mettre des icônes dans le 'System Tray'(système de barre de tâches) et comment créer/employer un menu popup.
Downloadez l'exemple ici. Vous obtiendrez une fenêtre, qui si vous la réduisez au maximum placera son icône dans la barre de tâches au lieu de la laisser à côté du menu 'Démarrer'.

Théorie:

Le 'System Tray' est la région rectangulaire dans la barre de tâche où plusieurs icônes sont rangées. Normalement, vous y verrez au moins l'icône de l'horloge digitale. Vous pouvez aussi mettre des icônes sur le 'System Tray'. Ci-dessous voici les étapes que vous devez exécuter pour mettre une icône sur le 'System Tray' :
  1. Remplissez la structure NOTIFYICONDATA avec les membres suivants:
    • cbSize est la taille de cette structure.
    • hwnd est l'handle de la fenêtre qui recevra l'avis (l'information) quand un événement souris arrive au 'Tray Icon'. (le Tray Icon c'est donc l'Icône de notre programme lorsquelle est réduite dans la barre de tâches)
    • uID est une constante qui est employée comme identificateur de cette icône. C'est à vous de choisir cette valeur. Au cas où vous avez plus d'une 'Tray Icon', vous serez ainsi capables de vérifier sur quelle 'Tray Icon' la souris agit.
    • uFlags indique que les membres de cette structure sont valables.
      • NIF_ICON le membre 'hIcon' est valide.
      • NIF_MESSAGE le membre uCallbackMessage est valide.
      • NIF_TIP le membre szTip est valide.
    • uCallbackMessage est notre message créé sur mesures (custum message) que Windows enverra à la fenêtre spécifiée grâce au membre hwnd, lorsqu'on clique sur notre 'tray icon' avec la souris. Il faudra créez ce message par nous-même.
    • hIcon est l'handle de l'icône que l'on souhaite mettre dans le 'system tray'.
    • szTip est un tableau de 64 octets qui contient la chaîne de caractères qui sera employée en tant que texte 'tooltip' quand la souris plane au dessus du 'tray icon'. (un texte tooltip, vous savez! c'est une sorte de bulle qui sert à informer sur la nature d'un objet).
  2. Call Shell_NotifyIcon laquelle est définie dans shell32.inc. Cette fonction a le prototype suivant :

    3. Shell_NotifyIcon PROTO dwMessage:DWORD ,pnid:DWORD

    dwMessage est le type de message à envoyer au Shell.
    NIM_ADD ajoute une icône au secteur de statut.
    NIM_DELETE supprime une icône du secteur de statut.
    NIM_MODIFY modifie une icône dans le secteur de statut.
    pnid est le pointer qui est sur la structure NOTIFYICONDATA. Elle doit être remplie des valeurs appropriées.
    Si vous voulez ajouter une icône dans le 'System Tray', vous devez utiliser le message NIM_ADD, si vous voulez enlever l'icône, il faut employer NIM_DELETE.

C'est tous pour le moment. Mais la plupart du temps, on ne se contente pas seulement de mettre une icône à cet endroit. On a besoin d'être capables de répondre aux événements (au clique) de la souris sur un 'tray icon'. On peut faire ça en traitant le message que nous avons spécifié dans le membre uCallbackMessage de la structure NOTIFYICONDATA. Ce message prend les valeurs suivantes dans wParam et lParam (un petit remerciement spécial à s__d pour ses renseignements) :
  • wParam contient l'ID de l'icône. C'est la même valeur que celle que vous mettez dans le membre uID de structure NOTIFYICONDATA.
  • lParam est le mot de poids fable qui contient le message de la souris. Par exemple, si l'utilisateur effectue un clique de droit sur l'icône, lParam contiendra WM_RBUTTONDOWN.
Cependant, beaucoup de 'Tray Icons' déploient un menu popup quand l'utilisateur clique dessus. Nous pouvons exécuter cette particularité en créant un menu popup et en appelant ensuite TrackPopupMenu pour l'afficher. Les étapes sont décrits ci-dessous :
  1. Créez un menu popup en appelant CreatePopupMenu. Cette fonction crée un menu vide. Elle renvoie l'handle du menu dans eax, en cas de succès.
  2. Ajoutez-y des Items de menu grâce à AppendMenu, InsertMenu or InsertMenuItem.
  3. Lorsque vous souhaitez montrer le menu popup à l'endroit où est placée le curseur de la souris, appelez GetCursorPos pour obtenir la coordonnée du curseur sur l'écran puis appeler ensuite TrackPopupMenu pour afficher le menu. Quand l'utilisateur choisit un Item (un article) du menu popup (c'est aussi un sous menu si vous préférez), Windows envoie le message WM_COMMAND à votre procédure de fenêtre exactement comme pour une sélection d'un menu normal.
Note : Prenez garde de deux choses un peu ennuyeuses lorsque vous utilisez un menu popup avec un 'Tray Icon' :
  1. Si vous cliquez n'importe où à l'extérieur du menu popup lorsqu'il est affiché, celui-ci ne disparaîtra pas immédiatement comme il devrait le faire. Ça se passe comme ça parce que la fenêtre qui recevra les avis du menu popup DOIT ÊTRE la fenêtre de premier plan. Le simple appel SetForegroundWindow corrigera ce défaut.
  2. Après l'appel SetForegroundWindow, vous constaterez que la toute première fois où le menu popup est affiché, ça fonctionne bien mais les fois suivantes, notre menu popup s'ouvrira puis se fermera immédiatement. Ce comportement n'est "pas intentionnel", pour citer MSDN. Le commutateur de tâche du programme, lequel contrôle le 'tray icon', est nécessaire. Vous pouvez forcer ce commutateur de tâche en postant n'importe quel message à la fenêtre du programme. Utilisez juste le simple message 'PostMessage', et non pas 'SendMessage'!

Exemple:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\shell32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\shell32.lib

WM_SHELLNOTIFY equ WM_USER+5
IDI_TRAY equ 0
IDM_RESTORE equ 1000
IDM_EXIT equ 1010
WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "TrayIconWinClass",0
AppName db "TrayIcon Demo",0
RestoreString db "&Restore",0
ExitString db "E&xit Program",0

.data?
hInstance dd ?
note NOTIFYICONDATA <>
hPopupMenu dd ?

.code
start:
invoke GetModuleHandle, NULL
mov hInstance,eax
invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
LOCAL wc:WNDCLASSEX
LOCAL msg:MSG
LOCAL hwnd:HWND
mov wc.cbSize,SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW or CS_DBLCLKS
mov wc.lpfnWndProc, OFFSET WndProc
mov wc.cbClsExtra,NULL
mov wc.cbWndExtra,NULL
push hInst
pop wc.hInstance
mov wc.hbrBackground,COLOR_APPWORKSPACE
mov wc.lpszMenuName,NULL
mov wc.lpszClassName,OFFSET ClassName
invoke LoadIcon,NULL,IDI_APPLICATION
mov wc.hIcon,eax
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
WS_OVERLAPPED+WS_CAPTION+WS_SYSMENU+WS_MINIMIZEBOX+WS_MAXIMIZEBOX+WS_VISIBLE,CW_USEDEFAULT,\
CW_USEDEFAULT,350,200,NULL,NULL,\
hInst,NULL
mov hwnd,eax
.while TRUE
invoke GetMessage, ADDR msg,NULL,0,0
.BREAK .IF (!eax)
invoke TranslateMessage, ADDR msg
invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
LOCAL pt:POINT
.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString
.elseif uMsg==WM_DESTROY
invoke DestroyMenu,hPopupMenu
invoke PostQuitMessage,NULL
.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif
.elseif uMsg==WM_COMMAND
.if lParam==0
invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif
.endif
.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
WndProc endp

end start

Analyse:

Notre programme affichera une simple fenêtre. Lorsque vous appuyez sur le bouton de réduction (au minimum), il se cachera et mettra une icône dans le 'system tray' tout en bas à droite à côté de votre horloge. Quand vous double cliquez sur l'icône, le programme se rétablira et enlèvera l'icône du 'system tray'. Quand vous simple-cliquez dessus, un menu popup est affiché. Grâce à ce menu, vous pourrez soit le rétablir (réagrandir) soit en sortir.

.if uMsg==WM_CREATE
invoke CreatePopupMenu
mov hPopupMenu,eax
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_RESTORE,addr RestoreString
invoke AppendMenu,hPopupMenu,MF_STRING,IDM_EXIT,addr ExitString

Quand la fenêtre principale est créée, on crée un menu popup et on y ajouter deux Items (deux sous-menu). La fonction 'AppendMenu' a la syntaxe suivante :

AppendMenu PROTO hMenu:DWORD, uFlags:DWORD, uIDNewItem:DWORD, lpNewItem:DWORD
  • hMenu est l'handle du menu auquel vous voulez ajouter l'Item.
  • uFlags indique à Windows que l'Item du menu qui doit y être ajouté est un bitmap ou une chaîne de caractères ou bien encore un Item possédant d'autres sous-Items, activé (enable), grisé (gray) ou désactivé (disable) etc. Vous pouvez obtenir la liste complète avec la référence api win32. Dans notre exemple, nous employons MF_STRING ce qui signifie que l'Item du menu est une simple chaîne de caractères.
  • uIDNewItem est l'ID de l'Item du menu. C'est une valeur définie par l'utilisateur, elle est employée pour représenter l'Item du menu.
  • lpNewItem est le contenu de l'Item, d'après ce que vous avez spécifié dans le membre uFlags. Puisque nous mettons MF_STRING dans le membre uFlags, lpNewItem doit contenir le pointer de la chaîne de caractères qui doit être affichée dans le menu popup.
Après que le menu popup est été créé, la fenêtre principale attend patiemment que l'utilisateur appuie sur le bouton de réduction (minimum).
Quand une fenêtre est réduite au minimum, elle reçoit le message WM_SIZE avec la valeur SIZE_MINIMIZED dans wParam.

.elseif uMsg==WM_SIZE
.if wParam==SIZE_MINIMIZED
mov note.cbSize,sizeof NOTIFYICONDATA
push hWnd
pop note.hwnd
mov note.uID,IDI_TRAY
mov note.uFlags,NIF_ICON+NIF_MESSAGE+NIF_TIP
mov note.uCallbackMessage,WM_SHELLNOTIFY
invoke LoadIcon,NULL,IDI_WINLOGO
mov note.hIcon,eax
invoke lstrcpy,addr note.szTip,addr AppName
invoke ShowWindow,hWnd,SW_HIDE
invoke Shell_NotifyIcon,NIM_ADD,addr note
.endif

Nous profitons de cette occasion pour remplir la structure NOTIFYICONDATA. 'IDI_tray' est juste une constante définie au début du code source. Vous pouvez lui donner la valeur que vous souhaitez. Ce n'est pas important puisque on a seulement un seul 'Tray Icon'. Mais si on en avait plusieurs dans notre 'System Tray', on aurait besoin d'IDs uniques pour chaque 'tray icon'. On définit tous les flags suivants dans le membre uFlags parce que nous spécifions une icône (NIF_ICON), nous spécifions un (message sur mesure NIF_MESSAGE et nous spécifions le texte tooltip (NIF_TIP) (Le texte qui est pésenté dans une bulle). WM_SHELLNOTIFY est seulement un message sur mesure (custum) défini comme WM_USER+5. La valeur réelle n'est pas importante tant qu'elle reste unique. Ici, j'emploie l'icône winlogo comme 'tray icon' mais vous pouvez employer n'importe quelle icône dans votre programme. Chargez-le juste en ressource avec LoadIcon et mettez l'handle renvoyé dans le membre hIcon. Finalement, nous mettons (dans szTip) le texte que nous voulons que la bulle affiche lorsque la souris passe au dessus de notre icône.
Nous cachons la fenêtre principale pour donner l'illusion de sa disparition en la "réduisant au minimum en une icône du 'System Tray'".
Ensuite nous appelons Shell_NotifyIcon avec le message NIM_ADD pour ajouter une icône au system tray.

Maintenant notre fenêtre principale est cachée et l'icône est réduite dans le 'System Tray'. Si vous passez la souris au-dessus, vous verrez un tooltip (une bulle) qui montre le texte que nous avons mis dans le membre szTip. Ensuite, si vous double cliquez sur l'icône, la fenêtre principale réapparaîtra et le 'tray icon' disparaîtra.

.elseif uMsg==WM_SHELLNOTIFY
.if wParam==IDI_TRAY
.if lParam==WM_RBUTTONDOWN
invoke GetCursorPos,addr pt
invoke SetForegroundWindow,hWnd
invoke TrackPopupMenu,hPopupMenu,TPM_RIGHTALIGN,pt.x,pt.y,NULL,hWnd,NULL
invoke PostMessage,hWnd,WM_NULL,0,0
.elseif lParam==WM_LBUTTONDBLCLK
invoke SendMessage,hWnd,WM_COMMAND,IDM_RESTORE,0
.endif
.endif

Quand un événement souris arrive au tray icon (un clique de souris sur notre icône), notre fenêtre reçoit le message WM_SHELLNOTIFY qui est le message fait sur mesure que nous avons déclaré dans le membre uCallbackMessage. Rappelons-nous qu'à la réception de ce message, wParam contient l'ID du 'tray icon' et lParam contient le message actuel de la souris. Dans le code ci-dessus, nous vérifions d'abord si ce message vient du 'tray icon' auquel nous nous intéressons. Si c'est le cas, on vérifie le message actuel de la souris. Puisque nous sommes uniquement intéressés au 'clique droit de la souris' et au "double clique gauche", on s'occupe seulement des messages WM_RBUTTONDOWN et WM_LBUTTONDBLCLK.
Si le message de la souris est WM_RBUTTONDOWN, nous appelons GetCursorPos pour obtenir les coordonnées actuelles du curseur de la souris sur l'écran. Après avoir reçu les données renvoyées par cette fonction, la structure POINT est remplie des coordonnées du curseur de la souris. Par les coordonnées, je veux dire les coordonnées entières sans qu'il n'y ait aucune frontière de fenêtre (on parle vraiment de l'écran dans sa totalité). Par exemple, si la résolution de votre écran est 640*480, le coin 'inférieur droit' de l'écran est x = 639 et y = 479. Si vous voulez convertir les coordonnées de l'écran en coordonnées de fenêtre, utilisez la fonction ScreenToClient.
Cependant, pour notre but, nous voulons afficher le menu popup à la position actuelle du curseur de la souris avec l'appel TrackPopupMenu et cette fonction exige des coordonnées d'écran, nous pouvons donc directement utiliser les coordonnées remplies par GetCursorPos.
TrackPopupMenu a la syntaxe suivante :

    TrackPopupMenu PROTO hMenu:DWORD, uFlags:DWORD, x:DWORD, y:DWORD, nReserved:DWORD, hWnd:DWORD, prcRect:DWORD
  • hMenu est l'handle du menu popup être affiché.
  • uFlags indique les options de la fonction. Comme l'endroit où placer le menu par rapport aux coordonnées spécifiées plus tard ou bien quel bouton de la souris sera employé pour montrer le menu. Dans notre exemple, nous employons TPM_RIGHTALIGN pour placer le menu popup à gauche des coordonnées.
  • x et y indiquent l'emplacement du menu (en coordonnées d'écran).
  • nReserved doit être NULL
  • hWnd est l'handle de la fenêtre qui recevra les messages du menu.
  • prcRect est un rectangle sur l'écran dans lequel il est possible de cliquer sans écarter le menu. Normalement nous mettons un NULL ici, comme ça lorsque l'utilisateur clique où que ce soit à l'extérieur du menu popup, le menu est écarté (déplacé).
Quand l'utilisateur double clique sur le 'tray icon', on envoie un message WM_COMMAND à notre propre fenêtre en spécifiant IDM_RESTORE pour imiter une sélection faite par l'utilisateur pour 'restorer'(réagrandir) la fenêtre principale et enlever l'icône (de notre programme) du 'System Tray'. Pour être capable de recevoir le message de double-clic, la fenêtre principale doit avoir le style CS_DBLCLKS.

invoke Shell_NotifyIcon,NIM_DELETE,addr note
mov eax,wParam
.if ax==IDM_RESTORE
invoke ShowWindow,hWnd,SW_RESTORE
.else
invoke DestroyWindow,hWnd
.endif

Quand l'utilisateur sélectionne l'Item de 'restoration' du menu, nous enlevons le 'tray icon' en appelant à nouveau Shell_NotifyIcon, cette fois nous spécifions NIM_DELETE comme message. Ensuite, nous rétablissons la fenêtre principale à son état d'origine. Si l'utilisateur choisit l'Item 'de Sortie' du menu, nous enlevons aussi l'icône du 'System tray' et détruisons la fenêtre principale en appelant DestroyWindow.

[Iczelion's Win32 Assembly Homepage]
Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 24: Windows Hooks

Tutorial 24: Windows Hooks
(Hook=Crochet)

Nous allons voir ce que sont les Windows Hooks dans ce Tutorial. Les Windows Hooks sont très puissants. Grâce à eux, vous pouvez fouiner à l'intérieur d'autres process (programmes) et quelques fois changer leurs comportements.
Downloadez l'exemple ici.

Théorie:

Les Windows Hooks peuvent être considérés comme une des particularités les plus puissantes de Windows. Avec elles, vous pourrez prendre au piège les événements qui se produisent dans votre propre process ou bien dans d'autres process. (exemple vous verrez apparaître dans votre fenêtre le n° qui sert d'handle à un bouton ou à une boîte se saisie…ça sert à ça un Hook). En "accrochant" (en Hookant), vous informez Windows sur les caractéristiques de la fonction Hookée. Notre fonction de filtre est aussi appelée la procédure d'hook, laquelle sera appelée à chaque fois qu'un événement intéressant se produit. Il y en a deux types en fait : les Hooks locaux et éloignés.
  • Les Hooks locaux prennent au piège les événements qui sont en train de se passer dans votre propre process.
  • Les Hooks éloignés prennent au piège les événements qui sont en train de se passer dans d'autres process. Il y a deux types d'Hooks éloignés.
    • thread-specific (ou lien-spécifique) prend au piège les événements qui se passent dans un lien spécifique dans d'autres process. Bref, il vous sert à observer des événements d'un lien spécifique à l'intérieur d'un process spécifique.
    • system-wide (système-large) prend au piège tous les événements destinés à tous les liens dans tous les process dans le système.
Quand vous mettez en place des Hooks, rappelez-vous qu'ils ont une influence sur les performances du système. Les 'System-Wide Hooks' sont tristement célèbres. Puisque TOUS les événements de liens seront mis en déroute par votre fonction de filtre (votre procédure d'Hooks), votre système peut être relativement ralentir. Ainsi si vous employez un 'System-Wide Hook', vous devez l'utiliser judicieusement et non pas l'accrocher aussitôt même si vous n'en avez pas encore besoin. Donc, vous avez de grandes chances de faire planter les autres process puisque vous pouvez toucher au fonctionnement d'autres process et si il y a quelque chose d'incorrecte dans votre fonction de filtre, il se peut que les autres programmes restent en attente. Souvenez-vous en : 'le pouvoir vient de paire avec la responsabilité'.
Vous devez vraiment comprendre le fonctionnement d'un hook avant que vous ne puissiez l'employer efficacement. Quand vous créez un hook, Windows crée une structure de données dans la mémoire, contenant l'information sur le hook et l'ajoute à une liste liée de Hooks existant. Le nouvel hook est ajouté devant les vieux Hooks. Lorsqu'un événement arrive, si vous faites appel à un hook local, la fonction de filtre de votre programme sera appelée, donc c'est plus direct. Mais si c'est un hook éloigné, le système doit injecter le code pour la procédure d'hook dans l'espace d'adresse d'un autre process. Et le système peut faire ça uniquement si la fonction se trouve dans une DLL. Ainsi, si vous voulez employer un hook éloigné, votre procédure d'hook doit résider dans une DLL. Il y a deux exceptions à cette règle: l'écriture d'un journal et la lecture d'un journal d'Hooks. Les procédures d'hook pour ces deux Hooks doivent résider dans le lien qui met en place les Hooks. La raison pour laquelle ça doit être ainsi c'est que : les deux Hooks fonctionnent avec des événements de type interruptions d'entrée matériel de bas niveau. Les événements d'entrée doivent être (recorded/playbacked) enregistrés/lus dans l'ordre où ils sont apparus. Si le code de ces deux Hooks sont dans une DLL, les événements d'entrée peuvent se disperser parmi plusieurs liens et il est impossible de connaître leur ordre les uns par rapport aux autres. Donc la solution c'est que : la procédure d'hook de ce deux Hooks doit être dans un lien simple seulement c'est-à-dire le lien qui installe les Hooks.
Il y a 14 types d'Hooks :
  • WH_CALLWNDPROC est appelé quand SendMessage est appelé
  • WH_CALLWNDPROCRET est appelé lors du retours de SendMessage.
  • WH_GETMESSAGE est appelé quand GetMessage ou PeekMessage sont appelés
  • WH_KEYBOARD est appelé quand GetMessage ou PeekMessage récupèrent le message WM_KEYUP ou WM_KEYDOWN dans la file d'attente (des messages).
  • WH_MOUSE est appelé quand GetMessage ou PeekMessage récupère un message provenant de la souris dans la file d'attente des messages.
  • WH_HARDWARE est appelé quand GetMessage ou PeekMessage récupère certains messages en provenance de votre matériel, mais qui n'ont pas de rapport avec le clavier ou la souris.
  • WH_MSGFILTER est appelé pour une DialogBox, quand son menu ou un scrollbar est sur le point de traiter un message. Cet hook est local. Il a été spécifiquement conçu pour des objets qui ont leurs propres boucles de message internes.
  • WH_SYSMSGFILTER c'est la même chose que pour WH_MSGFILTER mais pour un système-large (system-wide).
  • WH_JOURNALRECORD est appelé quand Windows récupère le message dans la file d'attente d'une entrée matériel.
  • WH_JOURNALPLAYBACK est appelé quand un événement est requit dans la file d'attente d'entrée matériel du système.
  • WH_SHELL est appelé quand quelque chose d'intéressant provenant du Sell arrive, comme quand la barre de tâche a besoin de rafraîchir (redessiner) ses boutons.
  • WH_CBT est spécialement employé pour la formation assistée par ordinateur. (CBT)
  • WH_FOREGROUNDIDLE est employé intérieurement par Windows. C'est vraiment très peu utilisé pour applications générales.
  • WH_DEBUG est employé pour 'debugger' la procédure se faisant Hooker.
Maintenant que nous avons pris connaissance de la théorie, nous pouvons nous concentrer sur le fait de savoir comment installer/désinstaller des Hooks.
Pour installer un hook, on appelle la fonction SetWindowsHookEx qui a la syntaxe suivante :
SetWindowsHookEx proto HookType:DWORD, pHookProc:DWORD, hInstance:DWORD, ThreadID:DWORD
  • HookType est une des valeurs inscrites ci-dessus, par exemple,WH_MOUSE, WH_KEYBOARD
  • pHookProc est l'adresse de la procédure d'hook qui sera appelée pour traiter les messages du hook spécifié. Si c'est un hook éloigné, il doit résider dans une DLL. Si ce n'est pas le cas, il doit appartenir à votre prog.
  • hInstance est l'instance handle de la DLL dans laquelle la procédure d'hook réside. Si le hook est local, cette valeur doit être NULL.
  • ThreadID est l'ID du lien sur lequel vous aimeriez installer votre hook pour l'espionner. C'est ce paramètre qui détermine si le hook est local ou éloigné. Si ce paramètre est NULL, Windows interprétera votre hook comme un hook système-large éloigné qui s'attaque à tous les liens, dans le système. Si vous spécifiez l'ID d'un lien de votre propre process (programme), cet hook est local. Si vous spécifiez l'ID d'un autre process, votre hook est un lien-spécifique éloigné. Il y a deux exceptions à cette règle :WH_JOURNALRECORD et WH_JOURNALPLAYBACK sont toujours des 'system-wide hooks' locaux (système-large), lesquels n'ont pas besoin d'être dans une DLL. Et WH_SYSMSGFILTER est toujours un 'system-wide hook' éloigné. En réalité il est identique au hook WH_MSGFILTER avec ThreadID=0 (avec pour ID du lien une valeur NULL).
si l'appel est couronné de succès, il renvoie l'handle du hook dans eax. Si c'est pas le cas, un NULL est renvoyé dans eax. Vous devez sauvegarder l'handle du hook pour pouvoir le 'déhooker' plus tard.
Vous pouvez désinstaller votre hook en appelant UnhookWindowsHookEx lequel accepte qu'un seul paramètre, l'handle du hook que vous voulez désinstaller. Si l'appel réussit, il renvoie une valeur non-nulle dans eax. Sinon, il renvoie le NULL (forcément).
Maintenant que vous savez comment installer/désinstaller des Hooks, nous pouvons examiner la procédure d'hook en elle-même.
La procédure d'hook sera appelée à chaque fois qu'un événement étant associé au type de hook que vous a installé, arrive. Par exemple, si vous installez le hook WH_MOUSE quand un événement de souris arrive, votre procédure d'hook sera appelée. Indépendamment du type de hook que vous avez installé, la procédure d'hook a toujours le prototype suivant :
    HookProc proto nCode:DWORD, wParam:DWORD, lParam:DWORD
    • nCode indique le code du hook.
    • wParam et lParam contiennent une information complémentaire à propos de l'événement.
HookProc est en réalité le détenteur du nom de la fonction. Vous pouvez le nommer comme ça vous chante tant qu'il a le prototype vu ci-dessus. L'interprétation de nCode, wParam et lParam dépend du type de hook que vous avez installé. Donc de la valeur de retour de la procédure d'hook. Par exemple :
WH_CALLWNDPROC
  • nCode peut seulement être HC_ACTION ce qui signifie qu'il y a un message envoyé à une fenêtre.
  • wParam contient le message qui vient d'être envoyé, si ce n'est pas le cas il prendra la valeur zéro.
  • lParam pointe sur la structure CWPSTRUCT.
  • La valeur de retour: si on ne l'emploie pas , on gardera un retour NULL.
WH_MOUSE
  • nCode peut être soit HC_ACTION soit HC_NOREMOVE.
  • wParam contient le message de la souris.
  • lParam pointe sur la structure MOUSEHOOKSTRUCT.
  • La valeur de retour: est '0' si le message doit être traité. Et '1' si le message doit être abandonné.
Vous devez consulter votre référence win32 api pour de plus amples détails à propos de la signification des paramètres et de la valeur de retour des hooks que vous souhaitez installer.
Maintenant il reste un petit truc à comprendre à propos de la procédure d'hook. Rappelez-vous que votre Hook est mis à la suite et en tête de la liste des hooks installés les plus récemment (dans une liste liée à votre prog). Quand un événement arrive, Windows appellera uniquement le premier hook de la liste. C'est le devoir de votre procédure d'hook d'appeler le hook suivant dans la liste. Vous pouvez choisir de ne pas appeler le hook suivant mais vous, mieux que quiconque, savez ce que vous faites. La plupart du temps, c'est une bonne chose d'appeler la procédure suivante, comme ça les autres Hooks sont prêts. Vous pouvez appeler le hook suivant en appelant CallNextHookEx lequel possède le prototype suivant :
CallNextHookEx proto hHook:DWORD, nCode:DWORD, wParam:DWORD, lParam:DWORD
  • hHook est l'handle de votre propre hook. La fonction emploie cet handle pour fouiller la liste et chercher la procédure d'hook qu'elle doit appeler ensuite.
  • nCode, wParam et lParam sont seulement trois valeurs que vous avez la possibilité de passer à d'autres fonctions. On les reçoit après l'appel: CallNextHookEx.
Un point important sur les Hooks éloignés : la procédure d'hook doit résider dans une DLL laquelle sera 'mappée' (configurée) dans d'autres process. (Voir le tutorial 13). Lorsque Windows 'mappe' la DLL dans d'autres process, il ne 'mappera' pas la section des données à l'intérieur de ces autres process. Bref, tous les process (les autres programmes) partagent une unique copie du code mais ils auront leur propre copie privée de la section des données de la DLL! Ça peut être une surprise de taille pour les imprudents. Vous pouvez penser que lorsque vous stockez une valeur dans une variable appartenant à la section des données de la DLL, que cette valeur sera partagée entres chacun des process qui font appel à cette DLL., mais ce n'est pas tout à fait vrai. Dans une situation normale, ça se passe bien comme ça puisque on a l'illusion que chaque process a sa propre copie de la DLL. Mais ce n'est pas le cas quand hook de Windows est concerné. Nous souhaitons que la DLL soit identique pour tous les process, incluant les données. La solution : on doit marquer la section des données en tant que section partagée. On peut faire ça en spécifiant l'attribut de section dans le commutateur de linker. Pour MASM, vous avez besoin d'employer ce commutateur :
/SECTION:<section name>, S
Le nom de la section des données initialisées est .data et les données non initialisées sont .bss. Par exemple si vous voulez assembler une DLL qui contient une procédure d'hook et que vous voulez que la section des données non initialisée soit partagée entre plusieurs process, vous devez employer la ligne suivante :
link /section:.bss,S /DLL /SUBSYSTEM:WINDOWS ..........
L'attribut 'S' marque la section en tant que section partagée.

Exemple:

En fait il faut deux modules : Il y a le programme principal qui affichera les éléments graphiques du GUI (comme d'habitude, ex : les boutons la zone de saisie…) et l'autre partie c'est la DLL qui installera/désinstallera le hook.

;--------------------------------------------- Voici le code source du programme principal --------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include mousehook.inc
includelib mousehook.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

wsprintfA proto C :DWORD,:DWORD,:VARARG
wsprintf TEXTEQU <wsprintfA>

.const
IDD_MAINDLG equ 101
IDC_CLASSNAME equ 1000
IDC_HANDLE equ 1001
IDC_WNDPROC equ 1002
IDC_HOOK equ 1004
IDC_EXIT equ 1005
WM_MOUSEHOOK equ WM_USER+6

DlgFunc PROTO :DWORD,:DWORD,:DWORD,:DWORD

.data
HookFlag dd FALSE
HookText db "&Hook",0
UnhookText db "&Unhook",0
template db "%lx",0

.data?
hInstance dd ?
hHook dd ?
.code
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,hInstance,IDD_MAINDLG,NULL,addr DlgFunc,NULL
invoke ExitProcess,NULL

DlgFunc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
LOCAL hLib:DWORD
LOCAL buffer[128]:byte
LOCAL buffer1[128]:byte
LOCAL rect:RECT
.if uMsg==WM_CLOSE
.if HookFlag==TRUE
invoke UninstallHook
.endif
invoke EndDialog,hDlg,NULL
.elseif uMsg==WM_INITDIALOG
invoke GetWindowRect,hDlg,addr rect
invoke SetWindowPos, hDlg, HWND_TOPMOST, rect.left, rect.top, rect.right, rect.bottom, SWP_SHOWWINDOW
.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif
.elseif uMsg==WM_COMMAND
.if lParam!=0
mov eax,wParam
mov edx,eax
shr edx,16
.if dx==BN_CLICKED
.if ax==IDC_EXIT
invoke SendMessage,hDlg,WM_CLOSE,0,0
.else
.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif
.else
invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL
.endif
.endif
.endif
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
DlgFunc endp

end start

;--------------------------------------------------- Et voici enfin le code source de la DLL ------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.const
WM_MOUSEHOOK equ WM_USER+6

.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD
.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif
mov eax,TRUE
ret
DllEntry Endp

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

UninstallHook proc
invoke UnhookWindowsHookEx,hHook
ret
UninstallHook endp

End DllEntry

;--------------------------------C'est le makefile de la DLL (le moyen de l'assembler)------------------------------

NAME=mousehook
$(NAME).dll: $(NAME).obj
Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib $(NAME).obj
$(NAME).obj: $(NAME).asm
ml /c /coff /Cp $(NAME).asm

Analyse:

Cet exemple ouvrira une boîte de dialogue avec trois commandes d'éditions dans lesquels seront affichés : le nom de classe (Class Name), l'handle de la fenêtre (ou de l'objet) et l'adresse de la procédure de fenêtre (Window Proc). Ces 3 paramètres n'appartiendront pas forcément à votre propre programme, mais plus généralement à n'importe quel objet de n'importe quelle fenêtre sur laquelle passe votre curseur de souris. Il y a deux boutons, 'hook' et 'Exit'. Quand vous appuyez sur le bouton 'hook', le programme Hooke l'entrée souris, et le texte du bouton se modifie en 'Unhook'. Quand vous déplacez le curseur de la souris vers une autre fenêtre ou sur un objet quelconque, les renseignements de cette fenêtre seront affichés dans votre fenêtre principale (votre DialogBox). Lorsque vous appuyez sur le bouton 'Unhook', le programme arrête de hooker les objets qui se trouvent sous la souris.
Ce programme utilise une boîte de dialogue en tant que fenêtre principale. On définit un message construit sur mesure (Custum) WM_MOUSEHOOK, lequel sera employé entre le programme principal et sa DLL, laquelle contient la fonction d'hook. Lorsque le programme principal reçoit ce message, wParam contient l'handle de la fenêtre (ou de l'objet) sur lequel votre curseur de souris est placé. Bien sûr, c'est un choix arbitraire. J'ai décidé d'envoyer la valeur de l'handle dans wParam pour plus de simplicité. Mais vous pouvez très bien choisir votre propre méthode de communication entre le programme principal et la DLL (contenant le hook).

.if HookFlag==FALSE
invoke InstallHook,hDlg
.if eax!=NULL
mov HookFlag,TRUE
invoke SetDlgItemText,hDlg,IDC_HOOK,addr UnhookText
.endif

Le programme maintient un 'Flag' nommé HookFlag, pour rendre compte de l'état du hook. S'il est 'FALSE'(faux) alors c'est que le hook n'est pas activé. Au contraire s'il est TRUE(vrai) c'est qu'il est actif.
Quand l'utilisateur appuie sur le bouton 'hook', le programme vérifie s'il est déjà installé (actif). Si ce n'est pas le cas, on appelle la fonction InstallHook du hook de la DLL pour l'installer. Notez que nous passons l'handle de notre dialogBox principale en tant que paramètre de la fonction pour que la DLL (contenant la fonction d'hook) puisse envoyer les différents messages WM_MOUSEHOOK vers la bonne fenêtre, c'est-à-dire la nôtre.
A partir du moment où le programme est chargé, la DLL est chargée elle aussi. En réalité, les DLLs sont immédiatement chargées après que le programme ait été mis en mémoire. L'entrypoint de la DLL est appelé avant même que la première instruction du programme principal ne soit exécuté. (EntryPoint=première ligne de code lue dans un programme=début) Ainsi quand le programme principal se met en route, la(les) DLL(s) est/sont déjà initialisée(s). Nous écrivons le code suivant pour l'entrypoint de la DLL :

.if reason==DLL_PROCESS_ATTACH
push hInst
pop hInstance
.endif

Ce code sauvegarde juste l'instance handle de la DLL dans une variable globale nommée hInstance pour pouvoir l'utiliser dans la fonction InstallHook. Puisque l'entrypoint de la DLL est appelé avant les autres fonctions dans la DLL, l'hInstance est toujours valable. On définit hInstance dans la section .data pour que cette valeur soit prête "pour le process". la DLL est 'mappée'(configurée ou temporairement recopiée en mémoire) dans le process, avant même que le curseur de souris ne plane au-dessus des fenêtres ou objets qu'elle doit hooker. Imaginez-vous que par hasard il y ait déjà une DLL qui occupe l'adresse de chargement destinée au DLL (contenant notre hook), alors dans ce cas la DLL serait reconfigurée à une autre adresse. La valeur de 'hInstance' serait remise à jour en prenant en compte la nouvelle adresse de chargement. Lorsque l'utilisateur presse le bouton 'Unhook' puis par la suite le bouton 'Hook', SetWindowsHookEx sera de nouveau appelé. Cependant, cette fois, il utilisera la nouvelle adresse de chargement en tant qu' instance handle mais qui malheureusement sera faux puisque dans notre programme d'exemple, l'adresse de chargement de la DLL n'aura pas été changée. Le Hook sera local, c'est pourquoi vous pourrez uniquement hooker les événements de la souris qui arriveront dans votre propre fenêtre. À peine désirable.

InstallHook proc hwnd:DWORD
push hwnd
pop hWnd
invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
mov hHook,eax
ret
InstallHook endp

La fonction InstallHook est très simple en elle-même. Elle sauvegarde l'handle de la fenêtre dans une variable globale nommée hWnd pour l'utiliser plus tard. On appelle alors SetWindowsHookEx pour installer (activer) le hook sous la souris. La valeur de retour de SetWindowsHookEx est stockée dans une variable globale nommée hHook pour l'utiliser avec UnhookWindowsHookEx au moment où on souhaite désactiver ce hook.
Après qu'on ait appelé SetWindowsHookEx, le hook sous la souris est fonctionnel. Chaque fois qu'un événement souris arrive dans le système, MouseProc (votre procédure d'hook) est appelé.

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD
invoke CallNextHookEx,hHook,nCode,wParam,lParam
mov edx,lParam
assume edx:PTR MOUSEHOOKSTRUCT
invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
assume edx:nothing
xor eax,eax
ret
MouseProc endp

La première chose à faire, c'est d'appeler CallNextHookEx pour donner à d'autres Hooks la chance de traiter de nouveaux événements souris. Après ça, on appelle la fonction WindowFromPoint pour retrouver l'handle de la fenêtre (ou de l'objet) auxquelles sont indiquées les coordonnée (écran) de la souris. Remarquez que nous employons POINT dans la structure MOUSEHOOKSTRUCT représenté par lParam en tant que coordonnées actuelles de la souris. Après ça nous envoyons l'handle de la fenêtre (ou de l'objet) au programme principal via PostMessage avec le message WM_MOUSEHOOK. Une chose à se rappeler, c'est que : vous ne devez pas employer SendMessage à l'intérieur de la procédure d'hook, ceci peut provoquer une impasse pour les messages. Je recommande PostMessage. La structure MOUSEHOOKSTRUCT est définie ainsi:

MOUSEHOOKSTRUCT STRUCT DWORD
pt POINT <>
hwnd DWORD ?
wHitTestCode DWORD ?
dwExtraInfo DWORD ?
MOUSEHOOKSTRUCT ENDS

  • 'pt' représente les coordonnées (coordonnées écran) actuelles du curseur de la souris.
  • hwnd est l'handle de la fenêtre qui recevra le message souris. En général, c'est la fenêtre qui se trouve sous le curseur de la souris, mais pas toujours. Si une fenêtre appelle SetCapture, l'entrée souris sera redirigée spécialement vers cette fenêtre au lieu de celle sur laquelle passe notre souris. À cause de ça, je n'utilise pas le membre hwnd de cette structure, mais par contre j'appelle WindowFromPoint à la place.
  • wHitTestCode représente une valeur de coup d'essai. La valeur de coup d'essai donne plus d'information sur la position actuelle du curseur de la souris. Il indique dans quelle partie de la fenêtre est ce que le curseur souris se trouve. Pour la liste complète, vérifiez votre référence api win32 avec le message WM_NCHITTEST.
  • dwExtraInfo contient l'information supplémentaire associée au message. Normalement cette valeur est créée à partir de l'appel mouse_event et retrouvée en appelant GetMessageExtraInfo.
Quand la fenêtre principale reçoit le message WM_MOUSEHOOK, on emploie l'handle de la fenêtre placé dans wParam pour retrouver l'information de la fenêtre.

.elseif uMsg==WM_MOUSEHOOK
invoke GetDlgItemText,hDlg,IDC_HANDLE,addr buffer1,128
invoke wsprintf,addr buffer,addr template,wParam
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_HANDLE,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer1,128
invoke GetClassName,wParam,addr buffer,128
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,addr buffer
.endif
invoke GetDlgItemText,hDlg,IDC_WNDPROC,addr buffer1,128
invoke GetClassLong,wParam,GCL_WNDPROC
invoke wsprintf,addr buffer,addr template,eax
invoke lstrcmpi,addr buffer,addr buffer1
.if eax!=0
invoke SetDlgItemText,hDlg,IDC_WNDPROC,addr buffer
.endif

Pour éviter des scintillements, nous vérifions le texte qui est déjà inscrit dans les commandes d'édition avec le texte que nous souhaitons placer à leur place. s'ils sont identiques, on saute par dessus l'instruction de réécriture.
Nous retrouvons le nom de classe (la Class Name) en appelant GetClassName, et l'adresse de la procédure de fenêtre (La Window Proc) en appelant GetClassLong avec GCL_WNDPROC puis les mettons ensuite sous forme de chaînes de caractères pour les placer dans les contrôles d'édition appropriés.

invoke UninstallHook
invoke SetDlgItemText,hDlg,IDC_HOOK,addr HookText
mov HookFlag,FALSE
invoke SetDlgItemText,hDlg,IDC_CLASSNAME,NULL
invoke SetDlgItemText,hDlg,IDC_HANDLE,NULL
invoke SetDlgItemText,hDlg,IDC_WNDPROC,NULL

Quand l'utilisateur appuie sur le bouton 'Unhook', le programme appelle la fonction UninstallHook qui se trouve dans la DLL (avec la fonction hook). UninstallHook appelle uniquement UnhookWindowsHookEx. Après ça, le texte du bouton se retransforme en "Hook", le flaf HookFlag est remis à 'False' et on bloque le contenu des contrôles d'édition.
Notez comment est décrit le linker dans le makefile.

Link /SECTION:.bss,S /DLL /DEF:$(NAME).def /SUBSYSTEM:WINDOWS

La section .bss est définie comme une section partagée pour faire en sorte que plusieurs process (programmes) puissent utiliser les données non initialisée de cette DLL en même temps. Sans ce commutateur, votre DLL ne fonctionnerait pas correctement.

[Iczelion's Win32 Assembly Homepage]

Traduit pas Morgatte


Iczelion's Win32asm Tutorial 25: Simple Bitmap

Tutorial 25: Un Simple Bitmap

Dans ce tutorial, nous allons voir comment utiliser des bitmaps dans notre programme. Pour être exact, nous allons apprendre à afficher un bitmap dans le secteur client de notre fenêtre.
Downloadez l'exemple.

Théorie

Les simples sont de simples images stockées dans l'ordinateur. Il y a beaucoup de formats d'images utilisés pour les ordinateurs, mais à l'origine Windows ne traitait qu'uniquement les fichiers Graphiques (.bmp). Les Bitmaps dont nous allons parler dans ce tutorial sont des bitmap Windows *.bmp. La plus simple façon d'utiliser un bitmap est de l'employer en tant que ressource. Il y a deux façons pour faire ça. Vous pouvez inclure le bitmap dans un fichier de définition de ressource (.rc) comme suit :
#define IDB_MYBITMAP 100
IDB_MYBITMAP BITMAP "c:\project\example.bmp"
Cette méthode emploie une constante pour représenter le bitmap. La première ligne crée juste une constante nommé IDB_MYBITMAP qui pour valeur 100. Nous utiliserons ce label (ce nom) pour nous référer à ce bitmap dans le programme. La ligne suivante déclare un bitmap ressource. On indique au compilateur de ressource à quel endroit retrouver le fichier bmp réel.
L'autre méthode utilise un nom pour représenter le bitmap comme suit :
MyBitMap BITMAP "c:\project\example.bmp"
Cette méthode exige que vous vous référiez au bitmap dans votre programme grâce au nom "MyBitMap" au lieu d'une valeur.
L'unes ou l'autres de ces méthodes restent excellentes tant que vous sachiez bien quelle méthode vous employez.
Maintenant que nous avons placé le bitmap dans le fichier ressource, nous pouvons continuer les étapes de l'explication pour afficher ce bitmap dans le secteur client de notre fenêtre.
  1. Appelez LoadBitmap pour obtenir l'handle du bitmap. LoadBitmap possède le prototype de fonction suivant :
      LoadBitmap proto hInstance:HINSTANCE, lpBitmapName:LPSTR

    Cette fonction renvoie l'handle du bitmap. hInstance est l'instance handle de notre programme. LpBitmapName est un pointer qui est sur la châine de caractères qui définit le nom du bitmap (dans le cas où vous employez la deuxième méthode pour se référer au bitmap). Si vous employez une constante pour vous référer au bitmap (comme IDB_MYBITMAP), vous pouvez mettre sa valeur ici. (Dans l'exemple au-dessus, ce serait 100). Voici un court exemple:

      First Method:

      .386
      .model flat, stdcall
      ................
      .const
      IDB_MYBITMAP equ 100
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,IDB_MYBITMAP
      ...........

      Second Method:

      .386
      .model flat, stdcall
      ................
      .data
      BitmapName db "MyBitMap",0
      ...............
      .data?
      hInstance dd ?
      ..............
      .code
      .............
      invoke GetModuleHandle,NULL
      mov hInstance,eax
      ............
      invoke LoadBitmap,hInstance,addr BitmapName
      ...........

    Pour info: un 'contexte de dispositif' c'est en quelque sorte la zone blanche principale de votre fenêtre sur laquelle on affichera le dessin bitmap. Elle est réelle quand on parle d'une page physiquement afficher sur notre écran. Mais on peut aussi se le réprésenter comme une page fictive n'exsitant que sous forme de données en mémoire.

  1. Récupérez l'handle du contexte de dispositif (DC=device context ). Vous pouvez obtenir cet handle en appelant BeginPaint après le retour du message WM_PAINT ou en appelant GetDC à n'importe quel moment.
  2. Créez un contexte de dispositif en mémoire qui a le même attribut que le contexte de dispositif que nous venons juste d'obtenir. L'idée ici est de créer une sorte de feuille de dessin "cachée" sur laquelle nous pouvons dessiner le bitmap. Quand nous avons finis cette opération, il nous suffit de copier le contenu de la feuille de dessin (cachée) sur le contexte de dispositif réel, grâce à un appel de fonction. C'est un exemple technique de double-affichage, utilisé pour une rapide exposition d'images à l'écran. Vous pouvez créer cette feuille de dessin "cachée" en appelant CreateCompatibleDC.
      CreateCompatibleDC proto hdc:HDC

    Si cette fonction réussit, elle renvoie l'handle du contexte de dispositif de mémoire dans eax. hdc est l'handle du contexte de dispositif pour lequel vous souhaitez que la mémoire DC soit compatible avec lui.

  1. Maintenant que vous avez obtenu une feuille de dessin cachée, vous pouvez dessiner dessus en choisissant le bitmap. On fait ça en appelant SelectObject avec l'handle de la mémoire DC en tant que premier paramètre et l'handle de bitmap comme deuxième paramètre. SelectObject est définit ainsi :
      SelectObject proto hdc:HDC, hGdiObject:DWORD
  1. Le bitmap est dessiné sur le contexte de dispositif en mémoire maintenant. Tout ce que nous avons besoin de faire ici, c'est de le recopier sur le dispositif d'affichage réel, à savoir (qui est) le vrai contexte de dispositif. Il y a plusieurs fonctions qui peuvent exécuter cette opération comme BitBlt et StretchBlt. BitBlt recopie seulement le contenu d'un DC dans un autre donc c'est rapide, tandis que StretchBlt peut décompresser ou compresser le bitmap pour le redimensionner à la taille du secteur d'affichage de votre fenêtre. Ici, nous emploierons BitBlt pour simplifier. BitBlt a la définition suivante :
      BitBlt proto hdcDest:DWORD, nxDest:DWORD, nyDest:DWORD, nWidth:DWORD, nHeight:DWORD, hdcSrc:DWORD, nxSrc:DWORD, nySrc:DWORD, dwROP:DWORD
    hdcDest est l'handle du contexte de dispositif réel qui sert de destination pour l'opération de transfert du bitmap.
    nxDest, nyDest sont les coordonnées du coin supérieur gauche du secteur d'affichage.
    nWidth, nHeight sont la largeur et la hauteur du secteur d'affichage.
    hdcSrc est l'handle du contexte de dispositif qui sert de source dans l'opération de transfert du bitmap.
    nxSrc, nySrc sont les coordonnées du coin supérieur gauche du rectangle source.
    dwROP est le code de l'opération qui permet d'appliquer au mieux les données de couleur du bitmap avec les données de couleur mis à disposition par le secteur d'affichage, pour réaliser le résultat final. La plupart du temps, on souhaite seulement recopier les données de couleur existantes dans le nouveau.
  1. Quand vous avez affiché le bitmap, vous pouvez le supprimer avec l'appel d'API DeleteObject.
Voilà! Pour récapituler, vous avez besoin de placer le bitmap dans un script de ressource (voir plus bas, après la source du programme). Chargez-le alors en ressource avec LoadBitmap. Vous obtiendrez l'handle du bitmap. Ensuite vous obtenez l'handle du contexte de dispositif du secteur sur lequel vous souhaitez redessiner le bitmap. Alors vous créez un contexte de dispositif en mémoire qui est compatible avec le contexte de dispositif que vous venez juste d'obtenir. Choisissez le bitmap dans la mémoire DC et recopiez alors le contenu de la mémoire DC vers votre DC réel.

Example Code:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
IDB_MAIN   equ 1

.data
ClassName db "SimpleWin32ASMBitmapClass",0
AppName  db "Win32ASM Simple Bitmap Example",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hBitmap dd ?

.code
start:
 invoke GetModuleHandle, NULL
 mov    hInstance,eax
 invoke GetCommandLine
 mov    CommandLine,eax
 invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
 invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
 LOCAL wc:WNDCLASSEX
 LOCAL msg:MSG
 LOCAL hwnd:HWND
 mov   wc.cbSize,SIZEOF WNDCLASSEX
 mov   wc.style, CS_HREDRAW or CS_VREDRAW
 mov   wc.lpfnWndProc, OFFSET WndProc
 mov   wc.cbClsExtra,NULL
 mov   wc.cbWndExtra,NULL
 push  hInstance
 pop   wc.hInstance
 mov   wc.hbrBackground,COLOR_WINDOW+1
 mov   wc.lpszMenuName,NULL
 mov   wc.lpszClassName,OFFSET ClassName
 invoke LoadIcon,NULL,IDI_APPLICATION
 mov   wc.hIcon,eax
 mov   wc.hIconSm,eax
 invoke LoadCursor,NULL,IDC_ARROW
 mov   wc.hCursor,eax
 invoke RegisterClassEx, addr wc
 INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
 mov   hwnd,eax
 invoke ShowWindow, hwnd,SW_SHOWNORMAL
 invoke UpdateWindow, hwnd
 .while TRUE
  invoke GetMessage, ADDR msg,NULL,0,0
  .break .if (!eax)
  invoke TranslateMessage, ADDR msg
  invoke DispatchMessage, ADDR msg
 .endw
 mov     eax,msg.wParam
 ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   LOCAL ps:PAINTSTRUCT
   LOCAL hdc:HDC
   LOCAL hMemDC:HDC
   LOCAL rect:RECT
   .if uMsg==WM_CREATE
      invoke LoadBitmap,hInstance,IDB_MAIN
      mov hBitmap,eax
   .elseif uMsg==WM_PAINT
      invoke BeginPaint,hWnd,addr ps
      mov    hdc,eax
      invoke CreateCompatibleDC,hdc
      mov    hMemDC,eax
      invoke SelectObject,hMemDC,hBitmap
      invoke GetClientRect,hWnd,addr rect
      invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
      invoke DeleteDC,hMemDC
      invoke EndPaint,hWnd,addr ps
 .elseif uMsg==WM_DESTROY
  invoke DeleteObject,hBitmap
  invoke PostQuitMessage,NULL
 .ELSE
  invoke DefWindowProc,hWnd,uMsg,wParam,lParam
  ret
 .ENDIF
 xor eax,eax
 ret
WndProc endp
end start

;---------------------------------------------------------------------
;                            le script resource
;---------------------------------------------------------------------
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"

Analyse:

Il n'y a pas grand chose à analyser dans ce tutorial ;)
#define IDB_MAIN 1
IDB_MAIN BITMAP "tweety78.bmp"
Ça définit la constante nommée IDB_MAIN, on lui associe la valeur 1. Et on emploie ensuite cette constante en tant qu'identificateur du bitmap en ressource. Le fichier bitmap qui est inclus dans la ressource est "tweety78.bmp" lequel réside dans le même dossier que le script de ressource.

   .if uMsg==WM_CREATE
      invoke LoadBitmap,hInstance,IDB_MAIN
      mov hBitmap,eax

En réponse à WM_CREATE, nous appelons LoadBitmap pour charger le bitmap en ressource, en passant l'identificateur du bitmap en ressource comme deuxième paramètre de l'API. Nous obtenons l'handle du bitmap au moment du retour de la fonction.
Maintenant que le bitmap est chargé, nous pouvons le dessiner dans le secteur client de notre fenêtre principale.

   .elseif uMsg==WM_PAINT
      invoke BeginPaint,hWnd,addr ps
      mov    hdc,eax
      invoke CreateCompatibleDC,hdc
      mov    hMemDC,eax
      invoke SelectObject,hMemDC,hBitmap
      invoke GetClientRect,hWnd,addr rect
      invoke BitBlt,hdc,0,0,rect.right,rect.bottom,hMemDC,0,0,SRCCOPY
      invoke DeleteDC,hMemDC
      invoke EndPaint,hWnd,addr ps

Nous décidons de repeindre le bitmap après la réponse du message WM_PAINT. Nous appelons d'abord BeginPaint pour obtenir l'handle du contexte de dispositif. Ensuite, nous créons une mémoire compatible DC avec CreateCompatibleDC. Puis on sélectionne le bitmap à l'intérieur de la mémoire DC grâce à SelectObject. Déterminez la dimension de votre secteur client avec GetClientRect. Maintenant nous pouvons faire apparaître le bitmap dans le secteur client en appelant BitBlt, lequel recopie le bitmap de la mémoire DC vers le DC réel. Lorsque la repeinte est effectue, nous n'avons plus besoin de la mémoire DC donc nous le supprimons grâce à DeleteDC. On fini la session de peinture avec EndPaint.

 .elseif uMsg==WM_DESTROY
  invoke DeleteObject,hBitmap
  invoke PostQuitMessage,NULL
Dès lors que nous n'avons plus besoin du bitmap, nous le supprimons avec DeleteObject.
[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 26: Splash Screen

Tutorial 26: Splash Screen

Maintenant que nous savons comment utiliser un bitmap, nous pouvons progresser en s'en servant d'une manière plus créatrice. Le 'splash screen'. Downloadez l'exemple. Mais en fait cet exemple est zippé et puisqu'il fait appelle à une DLL, il ne fonctionnera correctement qu'en l'ayant dézippé d'abord!

Théorie

Un splash screen est une fenêtre qui n'a aucune barre de titre, aucun système de menu, aucune frontière et qui affiche un bitmap un court instant avant de le faire redisparaître automatiquement. On s'en sert d'habitude au démarrage d'un programme, pour montrer un logo ou pour détourner l'attention de l'utilisateur pendant que le programme rame à cause de longues initialisations. Nous allons donc créer un splash screen dans ce tutorial.
La première étape c'est d'inclure le bitmap dans le fichier de ressource. Cependant, si vous réfléchissez, c'est une perte de mémoire précieuse de charger un bitmap qui ne sera employé qu'une seule fois. Une solution bien meilleure, c'est de créer une sorte de *DLL resource* qui contient le bitmap et qui aura pour unique but d'afficher notre Splash screen. De cette façon, vous pouvez charger la DLL lorsque vous souhaitez afficher le Splash screen puis le décharger quand il n'est plus nécessaire. Donc on aura deux modules : le programme principal et la DLL Splash. Nous plaçons le bitmap dans la DLL ressource.
Le schéma général est comme suit :
  1. Mettez le bitmap dans la DLL comme un bitmap mis en ressource.
  2. Le programme principal appelle LoadLibrary pour charger la DLL en mémoire.
  3. On appelle l'entrypoint de la DLL. On créera un minuteur (un Timer) et nous définiront la durée d'affichage du Splash screen. Ensuite nous enregistrerons et créerons une fenêtre sans titre ni bords puis nous afficherons le bitmap dans son secteur client.
  4. Quand la durée indiquée s'est écoulée, le Splash screen est enlevé de l'écran et le contrôle est rendu au programme principal.
  5. Le programme principal appelle FreeLibrary pour enlever (décharger) la DLL de la mémoire et continue ensuite avec n'importe quelle autre tâche que nous supposons avoir mis à sa suite.
Nous allons examiner les mécanismes en détail.

Chargement/Déchargement de la DLL (Load/Unload)

Vous pouvez charger dynamiquement une DLL grâce à la fonction LoadLibrary qui a la syntaxe suivante :
LoadLibrary proto lpDLLName:DWORD
Elle ne prend seulement qu'un paramètre : l'adresse du nom de la DLL que vous voulez charger en mémoire. Si l'appel est couronné de succès, il renvoie l'handle de la DLL, sinon elle renvoie un NULL.
Pour décharger une DLL, appelez FreeLibrary :
FreeLibrary proto hLib:DWORD
Il ne prend aussi qu'un seul paramètre : l'handle de la DLL que vous souhaitez décharger. Normalement, vous avez obtenu cet handle grâce à LoadLibrary.

Comment employer un 'TIMER' (minuteur) ?

D'abord, vous devez créer un Timer avec SetTimer :
SetTimer proto hWnd:DWORD, TimerID:DWORD, uElapse:DWORD, lpTimerFunc:DWORD

hWnd est le handle de la fenêtre qui recevra le message d'avis du Timer. Ce paramètre peut être NULL pour spécifier qu'il n'y a aucune fenêtre étant associée au Timer.
TimerID est une valeur définie par l'utilisateur, laquelle est employée en tant qu'ID du Timer.
uElapse est la valeur du temps d'attente en millisecondes.
lpTimerFunc est l'adresse de la fonction qui traitera les messages d'avis du Timer. Si vous passez un NULL, les messages du Timer seront envoyés à la fenêtre indiquée par le paramètre hWnd.

SetTimer renvoie l'ID du Timer en cas de succès. Autrement il renvoie un NULL. Donc il est préférable de ne pas mettre l'ID du Timer à 0.

Vous pouvez créer un Timer de deux façons :
  • Si vous avez une fenêtre et que vous souhaitez que les messages d'avis du Timer aillent vers cette fenêtre, alors vous devez passer Les quatre paramètres ci-dessus à SetTimer (le lpTimerFunc doit rester NULL).
  • Si vous n'avez pas encore de fenêtre ou bien que vous ne voulez pas traiter les messages du Timer dans la procédure de fenêtre, vous devez passer un NULL à la fonction à la place de l'handle de la fenêtre. Vous devez aussi définir l'adresse de la fonction du Timer qui traitera les messages du Timer.
Nous emploierons la première approche dans cet exemple.
Quand la période de temps mort s'est écoulée, le message WM_TIMER est envoyé à la fenêtre qui est associée au Timer. Par exemple, si vous spécifiez un uElapse de 1000, votre fenêtre recevra WM_TIMER chaque seconde.
Lorsque vous n'avez plus besoin du Timer, on le détruit avec KillTimer :
KillTimer proto hWnd:DWORD, TimerID:DWORD

Exemple:

;-----------------------------------------------------------------------
;                         Voici le programme principal:
;-----------------------------------------------------------------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

.data
ClassName db "SplashDemoWinClass",0
AppName  db "Splash Screen Example",0
Libname db "splash.dll",0

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
.code
start:
 invoke LoadLibrary,addr Libname
 .if eax!=NULL
    invoke FreeLibrary,eax
 .endif
 invoke GetModuleHandle, NULL
 mov    hInstance,eax
 invoke GetCommandLine
 mov    CommandLine,eax
 invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
 invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
 LOCAL wc:WNDCLASSEX
 LOCAL msg:MSG
 LOCAL hwnd:HWND
 mov   wc.cbSize,SIZEOF WNDCLASSEX
 mov   wc.style, CS_HREDRAW or CS_VREDRAW
 mov   wc.lpfnWndProc, OFFSET WndProc
 mov   wc.cbClsExtra,NULL
 mov   wc.cbWndExtra,NULL
 push  hInstance
 pop   wc.hInstance
 mov   wc.hbrBackground,COLOR_WINDOW+1
 mov   wc.lpszMenuName,NULL
 mov   wc.lpszClassName,OFFSET ClassName
 invoke LoadIcon,NULL,IDI_APPLICATION
 mov   wc.hIcon,eax
 mov   wc.hIconSm,eax
 invoke LoadCursor,NULL,IDC_ARROW
 mov   wc.hCursor,eax
 invoke RegisterClassEx, addr wc
 INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
           WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
           CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
           hInst,NULL
 mov   hwnd,eax
 invoke ShowWindow, hwnd,SW_SHOWNORMAL
 invoke UpdateWindow, hwnd
 .while TRUE
  invoke GetMessage, ADDR msg,NULL,0,0
  .break .if (!eax)
  invoke TranslateMessage, ADDR msg
  invoke DispatchMessage, ADDR msg
 .endw
 mov     eax,msg.wParam
 ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
 .IF uMsg==WM_DESTROY
  invoke PostQuitMessage,NULL
 .ELSE
  invoke DefWindowProc,hWnd,uMsg,wParam,lParam
  ret
 .ENDIF
 xor eax,eax
 ret
WndProc endp
end start

;--------------------------------------------------------------------
;                         Et voici la DLL contenant le Bitmap:
;--------------------------------------------------------------------
.386
.model flat, stdcall
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib
.data
BitmapName db "MySplashBMP",0
ClassName db "SplashWndClass",0
hBitMap dd 0
TimerID dd 0

.data
hInstance dd ?

.code

DllEntry proc hInst:DWORD, reason:DWORD, reserved1:DWORD
   .if reason==DLL_PROCESS_ATTACH  ; Quand la DLL est chargée.
      push hInst
      pop hInstance
      call ShowBitMap
   .endif
   mov eax,TRUE
   ret
DllEntry Endp
ShowBitMap proc
        LOCAL wc:WNDCLASSEX
        LOCAL msg:MSG
        LOCAL hwnd:HWND
        mov   wc.cbSize,SIZEOF WNDCLASSEX
        mov   wc.style, CS_HREDRAW or CS_VREDRAW
        mov   wc.lpfnWndProc, OFFSET WndProc
        mov   wc.cbClsExtra,NULL
        mov   wc.cbWndExtra,NULL
        push  hInstance
        pop   wc.hInstance
        mov   wc.hbrBackground,COLOR_WINDOW+1
        mov   wc.lpszMenuName,NULL
        mov   wc.lpszClassName,OFFSET ClassName
        invoke LoadIcon,NULL,IDI_APPLICATION
        mov   wc.hIcon,eax
        mov   wc.hIconSm,0
        invoke LoadCursor,NULL,IDC_ARROW
        mov   wc.hCursor,eax
        invoke RegisterClassEx, addr wc
        INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
           WS_POPUP,CW_USEDEFAULT,\
           CW_USEDEFAULT,250,250,NULL,NULL,\
           hInstance,NULL
        mov   hwnd,eax
        INVOKE ShowWindow, hwnd,SW_SHOWNORMAL
        .WHILE TRUE
                INVOKE GetMessage, ADDR msg,NULL,0,0
                .BREAK .IF (!eax)
                INVOKE TranslateMessage, ADDR msg
                INVOKE DispatchMessage, ADDR msg
        .ENDW
        mov     eax,msg.wParam
        ret
ShowBitMap endp
WndProc proc hWnd:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
        LOCAL ps:PAINTSTRUCT
        LOCAL hdc:HDC
        LOCAL hMemoryDC:HDC
        LOCAL hOldBmp:DWORD
        LOCAL bitmap:BITMAP
        LOCAL DlgHeight:DWORD
        LOCAL DlgWidth:DWORD
        LOCAL DlgRect:RECT
        LOCAL DesktopRect:RECT

        .if uMsg==WM_DESTROY
                .if hBitMap!=0
                        invoke DeleteObject,hBitMap
                .endif
                invoke PostQuitMessage,NULL
        .elseif uMsg==WM_CREATE
                invoke GetWindowRect,hWnd,addr DlgRect
                invoke GetDesktopWindow
                mov ecx,eax
                invoke GetWindowRect,ecx,addr DesktopRect
                push  0
                mov  eax,DlgRect.bottom
                sub  eax,DlgRect.top
                mov  DlgHeight,eax
                push eax
                mov  eax,DlgRect.right
                sub  eax,DlgRect.left
                mov  DlgWidth,eax
                push eax
                mov  eax,DesktopRect.bottom
                sub  eax,DlgHeight
                shr  eax,1
                push eax
                mov  eax,DesktopRect.right
                sub  eax,DlgWidth
                shr  eax,1
                push eax
                push hWnd
                call MoveWindow
                invoke LoadBitmap,hInstance,addr BitmapName
                mov hBitMap,eax
                invoke SetTimer,hWnd,1,2000,NULL
                mov TimerID,eax
        .elseif uMsg==WM_TIMER
                invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
                invoke KillTimer,hWnd,TimerID
        .elseif uMsg==WM_PAINT
                invoke BeginPaint,hWnd,addr ps
                mov hdc,eax
                invoke CreateCompatibleDC,hdc
                mov hMemoryDC,eax
                invoke SelectObject,eax,hBitMap
                mov hOldBmp,eax
                invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
                invoke StretchBlt,hdc,0,0,250,250,\
                       hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
                invoke SelectObject,hMemoryDC,hOldBmp
                invoke DeleteDC,hMemoryDC
                invoke EndPaint,hWnd,addr ps
        .elseif uMsg==WM_LBUTTONDOWN
                invoke DestroyWindow,hWnd
        .else
                invoke DefWindowProc,hWnd,uMsg,wParam,lParam
                ret
        .endif
        xor eax,eax
        ret
WndProc endp

End DllEntry

Analyse:

Dans un premier temps, nous allons nous pencher sur le code du programme principal.
 invoke LoadLibrary,addr Libname
 .if eax!=NULL
    invoke FreeLibrary,eax
 .endif
Nous appelons LoadLibrary pour charger la DLL nommée "splash.DLL". Et après ça, déchargez-la de la mémoire avec FreeLibrary. LoadLibrary ne retournera rien avant que la DLL n'en ait finie avec son initialisation..
C'est tout pour le programme principal. La partie intéressante c'est la DLL.

   .if reason==DLL_PROCESS_ATTACH  ; Quand la DLL est chargée.
      push hInst
      pop hInstance
      call ShowBitMap

Lorsque la DLL est chargée, Windows appelle son entrypoint avec le flag DLL_PROCESS_ATTACH. Nous en profitons pour afficher le Splash screen. En premier lieu, nous stockons l'instance handle de la DLL pour pouvoir l'utiliser plus tard. On appelle donc une fonction nommée ShowBitMap pour faire ce travail. ShowBitMap enregistre (une Window Class) une classe de fenêtre, et on crée la fenêtre puis on entre dans la boucle de message comme d'habitude. La partie intéressante est dans l'appel de CreateWindowEx :

        INVOKE CreateWindowEx,NULL,ADDR ClassName,NULL,\
           WS_POPUP,CW_USEDEFAULT,\
           CW_USEDEFAULT,250,250,NULL,NULL,\
           hInstance,NULL

Notez que le style de fenêtre est seulement WS_POPUP ce qui fera qu'on obtiendra une fenêtre sans bords et sans titre. Nous limitons aussi la largeur et la hauteur de la fenêtre à 250x250 pixels.
Maintenant que la fenêtre est créée, avec le message WM_CREATE nous plaçons la fenêtre au centre de l'écran grâce au code suivant.

                invoke GetWindowRect,hWnd,addr DlgRect
                invoke GetDesktopWindow
                mov ecx,eax
                invoke GetWindowRect,ecx,addr DesktopRect
                push  0
                mov  eax,DlgRect.bottom
                sub  eax,DlgRect.top
                mov  DlgHeight,eax
                push eax
                mov  eax,DlgRect.right
                sub  eax,DlgRect.left
                mov  DlgWidth,eax
                push eax
                mov  eax,DesktopRect.bottom
                sub  eax,DlgHeight
                shr  eax,1
                push eax
                mov  eax,DesktopRect.right
                sub  eax,DlgWidth
                shr  eax,1
                push eax
                push hWnd
                call MoveWindow

Ceci retrouve les dimensions de notre bureau (qui vont servir de référence) puis celles de la fenêtre en calculant les coordonnées appropriées du coin supérieur gauche de la fenêtre pour pouvoir la placer au centre.

                invoke LoadBitmap,hInstance,addr BitmapName
                mov hBitMap,eax
                invoke SetTimer,hWnd,1,2000,NULL
                mov TimerID,eax

Ensuite on charge le 'bitmap ressource' avec LoadBitmap et on crée un minuteur (un Timer) avec l'ID du Timer ayant la valeur de '1', et un intervalle de temps de 2 secondes. Le Timer enverra des messages WM_TIMER à la fenêtre toutes les 2 secondes.

        .elseif uMsg==WM_PAINT
                invoke BeginPaint,hWnd,addr ps
                mov hdc,eax
                invoke CreateCompatibleDC,hdc
                mov hMemoryDC,eax
                invoke SelectObject,eax,hBitMap
                mov hOldBmp,eax
                invoke GetObject,hBitMap,sizeof BITMAP,addr bitmap
                invoke StretchBlt,hdc,0,0,250,250,\
                       hMemoryDC,0,0,bitmap.bmWidth,bitmap.bmHeight,SRCCOPY
                invoke SelectObject,hMemoryDC,hOldBmp
                invoke DeleteDC,hMemoryDC
                invoke EndPaint,hWnd,addr ps

Quand la fenêtre reçoit le message WM_PAINT, elle crée une mémoire DC, choisit le bitmap dans la mémoire DC, obtient la taille du bitmap avec GetObject et met ensuite le bitmap sur la fenêtre en appelant StretchBlt, lequel se comporte comme BitBlt (il recopie le bitmap sur notre fenêtre) mais il est en plus capable de compresser ou de décompresser le bitmap à la dimension désirée. Dans notre cas, nous voulons que le bitmap s'accorde aux dimensions de la fenêtre, c'est pourquoi nous utilisons StretchBlt au lieu de BitBlt. Nous supprimons la mémoire DC après ça.

        .elseif uMsg==WM_LBUTTONDOWN
                invoke DestroyWindow,hWnd

Ce serait frustrant pour l'utilisateur qu'il soit obligé d'attendre que le Splash screen disparaisse. On aimerait que l'utilisateur ait le choix. Quand il clique sur Splash screen, il faudrait qu'il disparaisse. C'est pourquoi nous avons besoin de traiter le message WM_LBUTTONDOWN dans la DLL. En recevant ce message, la fenêtre est détruite par l'appel de DestroyWindow.

        .elseif uMsg==WM_TIMER
                invoke SendMessage,hWnd,WM_LBUTTONDOWN,NULL,NULL
                invoke KillTimer,hWnd,TimerID

Si l'utilisateur attend, le Splash screen disparaîtra lorsque le temps indiqué se sera écoulé (dans notre exemple, au bout de 2 secondes). On fait ça en traitant le message WM_TIMER. Après avoir reçu ce message, nous fermons la fenêtre en lui envoyant le message WM_LBUTTONDOWN. Ça permet d'éviter la duplication du code. Puisque nous n'avons plus besoin d'utiliser le Timer, nous le détruisons avec KillTimer.
Après que la fenêtre soit fermée, la DLL renverra le contrôle au programme principal.

[Iczelion's Win32 Assembly HomePage]

Traduit par Morgatte


Iczelion's Win32asm Tutorial 27: Tooltip Control

Tutorial 27: Les Bulles d'Information
(Tooltip Control)

Nous allons voir ce qu'est un tooltip et comment le créer puis l'employer. Downloadez l'exemple.

Théorie:

Un tooltip est une petite fenêtre rectangulaire qui apparaît lorsque le pointeur de la souris plane au dessus d'une zone spécifique. En se moment même vous êtes en train de lire ce tutorial, et bien regardez tout en bas à droite, il y a votre horloge. Déplacez votre souris dessus et ne la touchez plus ne cliquez sur rien… voilà ! un ToolTip vient d'apparaître ! c'est ça un ToolTip ! Chaque fenêtre tooltip contient le texte que le programmeur souhaite montrer. À cet égard, un serveur tooltip à le même rôle qu'une fenêtre de statut sauf que ce premier disparaît lorsque l'utilisateur clique ou déplace le pointeur de la souris en dehors du secteur désigné. Vous êtes probablement familiers avec les tooltips qui sont associés aux boutons de la barre d'outils. Ces "tooltips" sont utilement fourni par le contrôle de barre d'outils. Si vous souhaitez avoir des tooltips pour d'autres fenêtres ou commandes, vous avez besoin de créer votre propre contrôle tooltip.
Maintenant que vous savez ce qu'est un tooltip, on va continuer pour voir comment nous pouvons le créer puis l'employer. Les étapes sont déclinées ci-dessous :
  1. Créez un tooltip control avec CreateWindowEx.
  2. Définissez une région, qui si le pointeur de la souris passe dessus, déclenche l'affichage d'un tooltip.
  3. Soumettez cette région au tooltip control.
  4. Retransmettez les messages souris de la région soumise vers le tooltip control (cette étape peut arriver plus tôt, ça dépend de la méthode employée pour retransmettre les messages).
Nous allons ensuite examiner chaque étape en détail.

Création d'un Tooltip:

Un 'tooltip control' est un 'common control' (voir tutorial 18). En tant que tel, vous avez besoin d'appeler InitCommonControls à un moment dans votre code source pour que MASM lie implicitement votre programme avec comctl32.dll. Vous créez un tooltip control avec CreateWindowEx. Le scénario typique est le suivant :
.data
TooltipClassName db "Tooltips_class32",0
.code
.....
invoke InitCommonControls
invoke CreateWindowEx, NULL, addr TooltipClassName, NULL, TIS_ALWAYSTIP, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL
Remarquez le 'window style': TIS_ALWAYSTIP. Ce style indique que l'on affichera le tooltip lorsque le pointeur de la souris est placée au-dessus du secteur désigné, indépendamment du statut de la fenêtre qui contient ce secteur. Mis seul, si vous employez ce Flag, quand le pointeur de la souris plane sur le secteur, vous faites référence au tooltip control, et la bulle tooltip apparaîtra même si la fenêtre (ou l'objet) sous le pointeur de la souris est inactive.
Vous n'avez pas besoin d'inclure les styles WS_POPUP ni WS_EX_TOOLWINDOW dans CreateWindowEx parce que la procédure de fenêtre du tooltip control les ajoute automatiquement. Vous n'avez pas besoin non plus d'indiquer les coordonnées, la hauteur ni la largeur de la fenêtre tooltip : le tooltip control les ajustera automatiquement en fonction de la longueur de votre texte tooltip qui devra être affichéé, donc nous mettons CW_USEDEFAULT dans les quatre paramètres. Les paramètres restants ne sont négligeables.

Propriétés et définition du Tool (de l'outil).

Le tooltip control est créé mais on ne le montre pas immédiatement. Nous souhaitons que la fenêtre tooltip s'affiche seulement quand le pointeur de la souris plane sur un lieu bien défini. Il est maintenant temps de définir ce secteur. Nous appelons un tel secteur "tool". Chaque tool est un secteur rectangulaire affiché au-dessus du secteur client d'une fenêtre que le tooltip control contrôlera avec le pointeur de la souris. Si la souris passe au dessus d'un tool, la fenêtre tooltip apparaîtra. Ce secteur rectangulaire peut recouvrir le secteur client entier ou bien seulement une partie. Donc nous pouvons divisé les tools dans deux catégories : un qui est fonctionne comme une fenêtre et l'autre qui est fonctionne comme un petit secteur rectangulaire dans le secteur client d'une fenêtre. Tous les deux ont leurs propres utilisations. Un Tool qui recouvre le secteur client d'une fenêtre en entier est le plus fréquemment utilisé, avec en prime d'autres commandes tels que des boutons, des commandes d'édition etc…. Vous n'avez pas besoin de définir les coordonnées ni les dimensions de ce genre de tool : il sera forcément de la taille du secteur client de la fenêtre. Pour un Tool qui fonctionne comme un petit secteur rectangulaire sur le secteur client, on l'utilise seulement lorsque on veut diviser le secteur client d'une fenêtre en plusieurs régions sans employer de fenêtres d'enfant (Child Windows). Avec ce type de tool, vous avez besoin de définir les coordonnées du coin supérieur gauche ainsi que sa largeur et sa hauteur.
Vous définissez un tool grâce à la structure TOOLINFO laquelle a le prototype de fonction suivant :
TOOLINFO STRUCT
  cbSize             DWORD      ?
  uFlags             DWORD      ?
  hWnd               DWORD      ?
  uId                DWORD      ?
  rect               RECT      <>
  hInst              DWORD      ?
  lpszText           DWORD      ?
  lParam             LPARAM     ?
TOOLINFO ENDS
Noms | Explications
cbSize | est la taille de la structure TOOLINFO. Vous DEVEZ remplir ce membre. Windows vous renverra un flag pour dire qu'il y a une erreur si ce champ n'est pas rempli correctement et vous obtiendrez des résultats étranges, imprévisibles.
uFlags est un flag (de la taille d'un bit) qui défini les caractéristiques du tool. Cette valeur peut être une combinaison de plusieurs flags que voici :
  • TTF_IDISHWND ou "ID est hWnd". Si vous mettez ce flag, ça signifie que vous souhaitez utiliser un tool qui recouvre le secteur client tout entier d'une fenêtre (le premier type de tool vu plus haut). Si vous employez ce flag, vous devez remplir le membre uID de cette structure avec l' handle de la fenêtre que vous souhaitez employer. Si vous ne mettez pas ce Flag, c'est que vous préférez utiliser le deuxième type de tool, celui qui fonctionne comme un petit secteur rectangulaire sur votre fenêtre client. Dans ce cas, vous avez besoin de remplir le membre rect avec les dimensions du rectangle.
  • TTF_CENTERTIP Normalement la fenêtre tooltip apparaîtra à droite et au-dessous du pointeur de la souris. Si vous mettez ce Flag, la fenêtre tooltip apparaîtra toujours directement au-dessous du tool et sera placé indépendamment de la position de la souris.
  • TTF_RTLREADING Vous pouvez laisser tomber ce Flag si votre programme n'est pas conçu spécialement pour des systèmes arabes ou hébreux. Ce Flag affiche le texte tooltip en laissant la possibilité du sens de lecture. Ça ne fonctionnera pas avec d'autres systèmes (comme le notre, système Cyrillique).
  • TTF_SUBCLASS Si vous utilisez ce Flag, ça signifie que vous indiquez au tooltip control de sous-classer la fenêtre sur laquelle le tool est branché, ainsi le tooltip control pourra intercepter les messages souris qui sont envoyés à cette fenêtre. Ce Flag est très pratique. Si vous ne l'employez pas, vous devez faire plus de boulot pour relayer les messages souris vers le tooltip control.
hWnd est l'handle de la fenêtre qui contient le tool. Si vous mettez le flag TTF_IDISHWND, ce champ sera ignoré puisque Windows utilisera la valeur du membre uId en tant qu' handle de la fenêtre. Vous avez besoin de remplir ce champ si :
  • Vous n'utilisez pas le Flag TTF_IDISHWND (Autrement dit, vous utilisez un petit tool rectangulaire)
  • Vous mettez la valeur LPSTR_TEXTCALLBACK dans le membre lpszText. Cette valeur informe le tooltip control que, lorsqu'on a besoin d'afficher la fenêtre tooltip, on doit solliciter cette fenêtre pour que le texte soit affiché. C'est une sorte de mise à jour dynamique du texte tooltip en temps réel. Si vous souhaitez changer dynamiquement votre texte tooltip, vous devez mettre la valeur LPSTR_TEXTCALLBACK dans le membre lpszText. Le tooltip control enverra le message d'avis (l'information) TTN_NEEDTEXT dans la fenêtre identifiée par l'handle hWnd.
uId La valeur a mettre dans ce champ peut avoir deux significations. ça dépend si le membre uFlags (vu plus haut) contient le Flag TTF_IDISHWND.
  • C'est une application définissant l'ID du tool si le flagTTF_IDISHWND n'a pas été indiqué. Puisque dans ce cas vous employez un tool qui recouvre uniquement une petite partie du secteur client, c'est logique que vous puissiez avoir beaucoup de ces mêmes outils semblables sur votre secteur client (sans qu'ils se chevauchent). Le tooltip control a besoin de pouvoir les différencier. Dans ce cas, l'handle de la fenêtre qui est dans le membre hWnd n'est pas assez sélectif puisque tous ces outils sont sur cette même fenêtre. Les 'IDs' sont donc nécessaires. L'IDs représenter n'importe quelles valeurs tant qu'elle restent uniques entre elles.
  • C'est l'handle de la fenêtre dont on utilise la totalité de son secteur client pour afficher le tool si le Flag TTF_IDISHWND a été spécifié. Vous pouvez vous demander pourquoi ce champ est utilisé pour stocker l'handle de la fenêtre au lieu de hWnd. La réponse c'est que le membre hWnd peut déjà être rempli si la valeur LPSTR_TEXTCALLBACK a déjà été spécifiée dans le membre lpszText , en plus la fenêtre qui est responsable de l'approvisionnement du texte tooltip ainsi que la fenêtre qui contient le tool ne peuvent pas être les mêmes (Vous pouvez construire votre programme pour qu'une simple fenêtre puisse jouer ces deux rôles mais c'est trop restrictif. Dans ce cas, Microsoft vous donne plus de liberté. Acclamations.)
rect | La structure RECT définie les dimensions du tool. Cette structure définit un rectangle positionné par rapport au coin supérieur gauche du secteur client de la fenêtre (qui peut aussi être un simple objet) choisi par le membre hWnd. Bref, vous devez remplir cette structure si vous voulez avoir un tool qui ne recouvre qu'une partie du secteur client. Le tooltip control ignorera ce champ si vous avez déjà mis le Flag TTF_IDISHWND (Car dans ce cas, c'est que souhaitiez utiliser un tool qui recouvre la totalité de secteur client. Enfin Normalement ! ! ! )
hInst | est l'handle de l'instance (du groupe de données) qui contient la chaîne de caractères en ressource lequel sera employé en tant que 'tooltip texte' uniquement si la valeur dans le membre lpszText pointe sur cette chaîne de caractères. Ça peut paraître confus. Lisez d'abord l'explication sur le membre lpszText et vous comprendrez à quoi sert réellement ce champ. Le tooltip control ignora ce champ si le champ lpszText ne contient pas l'identificateur de la chaîne de caractères en ressource.
lpszText Ce champ peut avoir plusieurs valeurs :
  • Si vous mettez la valeur LPSTR_TEXTCALLBACK dans ce champ, le tooltip control enverra le message d'avis TTN_NEEDTEXT à la fenêtre identifiée par son handle dans le champs hWnd pour que la chaîne de caractères soit affichée dans la fenêtre tooltip. C'est la méthode la plus dynamique de mise à jour du texte tooltip : vous pouvez changer ce tooltip texte à chaque fois que la fenêtre tooltip est affichée.
  • Si vous mettez l'identificateur de la chaîne de caractères en ressource dans ce champ, alors lorsque le tooltip control en aura besoin pour montrer le tooltip texte dans la fenêtre tooltip, il recherchera cette chaîne dans le tableau des chaînes de caractères de grâce au membre hInst. Le tooltip control identifie l'identificateur de la chaîne de caractères (en ressource) en vérifiant le mot de poids fort de ce champ (de ce paramètre). Puisque c'est une valeur sur 16 bits, le mot de poids fort dans ce paramètre sera toujours initialisé à zéro. C'est une méthode utile si vous destinez adapter votre programme à d'autres langues. Puisque la chaîne de caractères est définie dans un scénario de ressource, vous n'avez pas besoin de modifier le code source de votre programme lui-même. Il vous suffit seulement de modifier la table des chaînes de caractères et ainsi les tooltip textes changeront sans qu'il y ait le risque que des bogues viennent foutre le bordel dans votre programme.
  • Si la valeur dans ce paramètre n'est pas LPSTR_TEXTCALLBACK et que le mot de poids fort n'est pas à zéro, le tooltip control interprète cette valeur comme l'indicateur (le pointeur) qui est sur la chaîne de caractères qui servira de tooltip texte. Cette méthode est la plus facile à utiliser, mais elle reste moindre flexible.

Pour récapituler, vous avez besoin de remplir la structure TOOLINFO avant de la soumettre au tooltip control. Cette structure définit les caractéristiques du tool que vous désirez obtenir.

Prendre en compte l'existence du 'tool' avec le 'tooltip control'

Après que vous ayez rempli la structure TOOLINFO, vous devez la soumettre au tooltip control. Le tooltip control peut gérer en même temps plusieurs Tools donc il est en général inutile de créer plusieurs tooltip control pour une même fenêtre. Pour associer un tool à un tooltip control, on envoie le message TTM_ADDTOOL au tooltip control. wParam n'est pas utilisé, mais lParam doit absolument contenir l'adresse de la structure TOOLINFO que vous souhaitez enregistrer (lui associer).
.data?
ti TOOLINFO <>
.......
.code
.......
<fill the TOOLINFO structure>
.......
invoke SendMessage, hwndTooltip, TTM_ADDTOOL, NULL, addr ti
SendMessage: Ce message renverra la valeur TRUE si le tool est enregistré avec succès au près du tooltip control, FALSE dans le cas contraire.
Vous pouvez 'désenregistrer' un tool en envoyant le message TTM_DELTOOL au tooltip control.

Transmission des Messages Souris jusqu'au Tooltip Control

Quand on a achevé l'étape ci-dessus, le tooltip control sait quels secteurs (zones) est-ce qu'il doit contrôler quand la souris passe dessus et aussi quel texte il doit afficher dans la fenêtre tooltip. La seule chose qui manque à cette action c'est l' *élément qui la déclenche*. Pensez-y : la zone indiqué par le tool est sur le secteur client de l'autre fenêtre. Comment un tooltip control peut-il intercepter les messages souris destinés à cette fenêtre ? On a besoin de chronométrer les temps de passage du pointeur de la souris au dessus d'un tool pour que si ce temps écoulé est suffisant, pour que le tooltip control affiche la fenêtre du tooltip. Il y a deux méthodes pour faire ça, la première exige la coopération de la fenêtre qui contient le tool alors que l'autre s'effectue sans coopération de la part de cette fenêtre.
  • La fenêtre qui contient le tool doit transmettre les messages souris jusqu'au tooltip control en les lui envoyant avec le message TTM_RELAYEVENT. La partie lParam de ce message doit contenir l'adresse de la structure MSG laquelle signifie que le message doit être transmis au tooltip control. Le tooltip control traite seulement les messages souris suivants :
    • WM_LBUTTONDOWN
    • WM_MOUSEMOVE
    • WM_LBUTTONUP
    • WM_RBUTTONDOWN
    • WM_MBUTTONDOWN
    • WM_RBUTTONUP
    • WM_MBUTTONUP
    Tous les autres messages seront ignorés. Ainsi dans la 'procédure de fenêtre' de la fenêtre qui contient le tool, il doit y avoir un commutateur un peu dans le genre :

    WndProc proc hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .......
    if uMsg==WM_CREATE
    .............
    elseif uMsg==WM_LBUTTONDOWN || uMsg==WM_MOUSEMOVE || uMsg==WM_LBUTTONUP || uMsg==WM_RBUTTONDOWN || uMsg==WM_MBUTTONDOWN || uMsg==WM_RBUTTONUP || uMsg==WM_MBUTTONUP
    invoke SendMessage, hwndTooltip, TTM_RELAYEVENT, NULL, addr msg
    ..........

  • Vous pouvez mettre le Flag TTF_SUBCLASS dans le membre uFlags de la structure TOOLINFO. Ce Flag dit au tooltip control de sous-classer la fenêtre qui contient le tool ainsi on peut intercepter les messages souris sans aucune coopération de la fenêtre. Cette méthode est plus facile à utiliser puisque elle n'exige pas de code supplémentaire grâce au Flag TTF_SUBCLASS et en plus le tooltip control s'occupe de toutes les interceptions de message par lui-même.
Voilà. À ce niveau, votre tooltip control est entièrement fonctionnel. Mais il y a plusieurs messages utiles, liés au tooltip, que vous devez connaître.
  • TTM_ACTIVATE. Sert à Activer/Désactiver le fonctionnement dynamyge du tooltip control , ce message est pour vous. Si la valeur de wParam est TRUE,alors le tooltip control est activé. Si la valeur de wParam est FALSE, le tooltip control est mise hors de service. On active notre tooltip control avant même qu'il n'ait été créé ainsi vous n'avez pas besoin d'envoyer aucun message pour l'activer.
  • TTM_GETTOOLINFO et TTM_SETTOOLINFO. l'utilisation ces messages sert à obtenir/changer des valeurs dans la structure TOOLINFO après qu'il ait été soumis au tooltip control. Vous avez besoin d'indiquer le tool que vous avez besoin de changer avec les valeurs correctes de uID et hWnd. Si vous souhaitez seulement changer le membre rect, utilisez le message TTM_NEWTOOLRECT. Si vous voulez changer le 'tooltip texte', utilisez TTM_UPDATETIPTEXT.
  • TTM_SETDELAYTIME. Grâce à ce message, vous pouvez définir le temps d'attente avant que le tooltip control n'affiche le tooltip texte.

Exemple:

L'exemple suivant repésente une simple boîte de dialogue munie de deux boutons. Son secteur client est divisé en 4 secteurs : un secteur supérieur gauche, supérieur droit, inférieur droit et inférieur gauche. Chaque secteur sera définit en tant que tool avec son propre tooltip texte. Les deux boutons auront eux aussi leurs propres tooltip textes.
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
EnumChild proto :DWORD,:DWORD
SetDlgToolArea proto :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.const
IDD_MAINDIALOG equ 101
.data
ToolTipsClassName db "Tooltips_class32",0
MainDialogText1 db "This is the upper left area of the dialog",0
MainDialogText2 db "This is the upper right area of the dialog",0
MainDialogText3 db "This is the lower left area of the dialog",0
MainDialogText4 db "This is the lower right area of the dialog",0
.data?
hwndTool dd ?
hInstance dd ?
.code
start:
    invoke GetModuleHandle,NULL
    mov hInstance,eax
    invoke DialogBoxParam,hInstance,IDD_MAINDIALOG,NULL,addr DlgProc,NULL
    invoke ExitProcess,eax

DlgProc proc hDlg:DWORD,uMsg:DWORD,wParam:DWORD,lParam:DWORD
    LOCAL ti:TOOLINFO
    LOCAL id:DWORD
    LOCAL rect:RECT
    .if uMsg==WM_INITDIALOG
        invoke InitCommonControls
        invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
            TTS_ALWAYSTIP,CW_USEDEFAULT,\
            CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
            hInstance,NULL
        mov hwndTool,eax
        mov id,0
        mov ti.cbSize,sizeof TOOLINFO
        mov ti.uFlags,TTF_SUBCLASS
        push hDlg
        pop ti.hWnd
        invoke GetWindowRect,hDlg,addr rect
        invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect
        inc id
        invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText2,id,addr rect
        inc id
        invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText3,id,addr rect
        inc id
        invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText4,id,addr rect
        invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
    .elseif uMsg==WM_CLOSE
        invoke EndDialog,hDlg,NULL
    .else
        mov eax,FALSE
        ret
    .endif
    mov eax,TRUE
    ret
DlgProc endp

EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
    LOCAL buffer[256]:BYTE
    mov edi,lParam
    assume edi:ptr TOOLINFO
    push hwndChild
    pop [edi].uId
    or [edi].uFlags,TTF_IDISHWND
    invoke GetWindowText,hwndChild,addr buffer,255
    lea eax,buffer
    mov [edi].lpszText,eax
    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
    assume edi:nothing
    ret
EnumChild endp

SetDlgToolArea proc uses edi esi hDlg:DWORD,lpti:DWORD,lpText:DWORD,id:DWORD,lprect:DWORD
    mov edi,lpti
    mov esi,lprect
    assume esi:ptr RECT
    assume edi:ptr TOOLINFO
    .if id==0
        mov [edi].rect.left,0
        mov [edi].rect.top,0
        mov eax,[esi].right
        sub eax,[esi].left
        shr eax,1
        mov [edi].rect.right,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        shr eax,1
        mov [edi].rect.bottom,eax
    .elseif id==1
        mov eax,[esi].right
        sub eax,[esi].left
        shr eax,1
        inc eax
        mov [edi].rect.left,eax
        mov [edi].rect.top,0
        mov eax,[esi].right
        sub eax,[esi].left
        mov [edi].rect.right,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        mov [edi].rect.bottom,eax
    .elseif id==2
        mov [edi].rect.left,0
        mov eax,[esi].bottom
        sub eax,[esi].top
        shr eax,1
        inc eax
        mov [edi].rect.top,eax
        mov eax,[esi].right
        sub eax,[esi].left
        shr eax,1
        mov [edi].rect.right,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        mov [edi].rect.bottom,eax
    .else
        mov eax,[esi].right
        sub eax,[esi].left
        shr eax,1
        inc eax
        mov [edi].rect.left,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        shr eax,1
        inc eax
        mov [edi].rect.top,eax
        mov eax,[esi].right
        sub eax,[esi].left
        mov [edi].rect.right,eax
        mov eax,[esi].bottom
        sub eax,[esi].top
        mov [edi].rect.bottom,eax
    .endif
    push lpText
    pop [edi].lpszText
    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti
    assume edi:nothing
    assume esi:nothing
    ret
SetDlgToolArea endp
end start

Analyse:

Après que la fenêtre de dialogue principale ait été créée, nous créons le tooltip control à son tour avec CreateWindowEx.
invoke InitCommonControls
invoke CreateWindowEx,NULL,ADDR ToolTipsClassName,NULL,\
       TTS_ALWAYSTIP,CW_USEDEFAULT,\
       CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
       hInstance,NULL
mov hwndTool,eax
Après ça, on définit quatre Tools pour chaque coin de la boîte de dialogue.

    mov id,0        ; Employé en tant qu' ID du tool.
    mov ti.cbSize,sizeof TOOLINFO
    mov ti.uFlags,TTF_SUBCLASS    ; Impose au tooltip control de sous-classer la fenêtre de dialogue.
    push hDlg
    pop ti.hWnd    ; handle de la fenêtre qui contient le tool.
    invoke GetWindowRect,hDlg,addr rect    ; Obtient les dimensions du secteur client.
    invoke SetDlgToolArea,hDlg,addr ti,addr MainDialogText1,id,addr rect

Nous initialisons les membres de la structure TOOLINFO. Remarquez que nous voulons diviser le secteur client en 4 Tools donc nous avons besoin de connaitre les dimensions du secteur client. C'est pourquoi nous appelons GetWindowRect. Nous ne souhaitons pas transmettre les messages souris au tooltip control nous-mêmes donc nous mettons le Flag TIF_SUBCLASS.
SetDlgToolArea est une fonction qui construit le rectangle de limitation de chaque tool et enregistre ce tool vis à vis du tooltip control. Je n'entrerai pas dans les détails complexes du calcul, il suffirai pour dire qu'elle divise le secteur client en 4 secteurs de mêmes tailles. Donc on envoie le message TTM_ADDTOOL au tooltip control, en passant l'adresse de la structure TOOLINFO dans le paramètre lParam.

    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,lpti

Après que ces 4 tools soient enregistrés, nous pouvons nous intéresser aux boutons de la boîte de dialogue. Nous pouvons mamipuler chaque bouton par son ID mais ce n'est pas pratique. A la place, nous emploierons l'appel API EnumChildWindows pour dénombrer tous les 'Controls' sur la boîte de dialogue et nous les enregistrons ensuite au près de notre tooltip control. EnumChildWindows a la syntaxe suivante :

EnumChildWindows proto hWnd:DWORD, lpEnumFunc:DWORD, lParam:DWORD
hWnd est l'handle de la fenêtre parente. LpEnumFunc est l'adresse de la fonction EnumChildProc laquelle sera appelée pour chaque contrôle ayant été dénombré. lParam est la valeur que l'on passera à la fonction EnumChildProc. La fonction EnumChildProc a la définition suivante :
EnumChildProc proto hwndChild:DWORD, lParam:DWORD
hwndChild est l'handle d'un contrôle ayant été dénombré par EnumChildWindows. lParam est la même valeur lParam que celle que nous avons passé à EnumChildWindows.
Dans notre exemple, nous appelons EnumChildWindows comme ça :
invoke EnumChildWindows,hDlg,addr EnumChild,addr ti
Nous passons l'adresse de la structure TOOLINFO dans le paramètre lParam parce que nous enregistrerons chaque Child Control de tooltip control dans la fonction EnumChild. Si nous n'utilisons pas cette méthode, nous devons déclarer ti en tant que variable globale, ce qui peut faire apparaître des bogues(défauts).
Lorsqu'on appelle EnumChildWindows, Windows dénombre les Childs Controls de notre boîte de dialogue et appelle la fonction EnumChild à chaque fois qu'il trouve un nouveau control. Donc si notre boîte de dialogue possède deux Childs Controls (ici 2 boutons), EnumChild sera appelée deux fois.
La fonction EnumChild remplit les membres appropriés de la structure TOOLINFO et enregistre ensuite le tool au près du tooltip control.
EnumChild proc uses edi hwndChild:DWORD,lParam:DWORD
    LOCAL buffer[256]:BYTE
    mov edi,lParam
    assume edi:ptr TOOLINFO
    push hwndChild
    pop [edi].uId    ; Nous utilisons la totalité du secteur client du 'Control' en tant que 'tool'.
    or [edi].uFlags,TTF_IDISHWND
    invoke GetWindowText,hwndChild,addr buffer,255
    lea eax,buffer    ; Ceci utilise le texte de la fenêtre comme tooltip texte.
    mov [edi].lpszText,eax
    invoke SendMessage,hwndTool,TTM_ADDTOOL,NULL,edi
    assume edi:nothing
    ret
EnumChild endp
Notez que dans ce cas, nous employons un type de tool différent: celui-ci recouvre le secteur client de la fenêtre en entier. Nous avons donc besoin de remplir l'élément uID avec l'handle de la fenêtre qui contient ce tool. C'est pourquoi nous devons mettre le Flag TTF_IDISHWND dans le membre uFlags.
[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 28: Win32 Debug API Part 1

Tutorial 28: Win32 Debug API Partie 1/3

Dans ce tutorial, vous allons voir ce que Win32 offre comme mises au point primaires pour les développeurs. Lorsque vous en aurez terminés avec ce tutorial, vous saurez comment debugger un programme.
Downloadez l'exemple.

Théorie:

Win32 possède plusieurs APIs qui permettent aux programmeurs d'utiliser certaines des fonctions d'un débugger. On les appelle, les 'Win32 Debug APIs' ou 'primitives'. Avec elles, vous pouvez :

  • Charger un programme ou s'attacher à un programme en cours d'exécution pour le debugger.
  • Obtenir une information de bas niveau dans le programme que vous êtes en train de debugger, comme l'ID du process (du programme), l'adresse de son entrypoint, 'son image base' etc... (Par exemple quand vous cracker un logiciel avec W32Dasm, tous les programmes que vous désassemblez commencent à l'adresse 00400000. Ça c'est l'Image Base).
  • Prendre conscience des liens de parentés pour les phases de debugging qui ne vont pas l'un sans l'autre tels que pour un programme multi-liens 'Process/Liens' 'Start/Exit', les DLLs sont Chargés/déchargés en mémoire etc.
  • Modifier le process/lien qu'on est en train de debugger.

Bref, vous pouvez fabriquer un programme qui sert à debugger, grâce à ces APIs là (les Win32 Debug APIs). Puisque ce sujet est vaste, je le divise en plusieurs parties : ce tutorial étant la première partie. Dans celle-ci, j'expliquerai les concepts de base ainsi que la structure générale de l'utilisation des 'Win32 Debug APIs'.
Les étapes pour l'utilisation des 'Win32 Debug APIs' sont :

  1. Créez un process (dans W32Dasm c'est: Load Process) (Dans SoftIce c'est son: Loader) ou attachez votre programme à un process en cours de fonctionnement (Dans W32Dasm c'est: Attach to an Active Process) (Dans SoftIce c'est: CTRL+d). C'est la première étape de l'utilisation des 'Win32 debug APIs'. Puisque votre programme agira comme un DEBUGGER, vous aurez besoin d'un programme sur lequel faire vos essais. Un programme étant en train de se faire débugger (la cible) est appelé un debuggee (non je ne me suis pas trompé). Vous pouvez lancer une 'action debuggee' (action qui fait en sorte qu'on debug la cible, le debuggee) de deux façons :
    • Vous pouvez créer un 'Debuggee Process' par vous-même avec CreateProcess. Pour créer un process pour debugger, vous devez mettre le Flag DEBUG_PROCESS. Ce Flag précise à Windows que nous souhaitons debugger le process (de la même façon que Load Process pour W32Dasm). Windows enverra des notifications sur les événements importants servant à debugger (on va les appeler les 'debugging événements') qui arrivent dans votre programme debuggee. Votre debuggee sera immédiatement suspendu jusqu'à ce que votre programme ne soit prêt. Si le debuggee crée aussi des Child Process, Windows lui aussi enverra les 'debugging événements' dans tous les Child Process de votre programme. C'est un comportement qui d'habitude est indésirable. C'est pourquoi on peut le mettre hors service en spécifiant le Flag DEBUG_ONLY_THIS_PROCESS dans la combinaison de Flags DEBUG_PROCESS.
    • Vous pouvez 'attacher ' votre debugger à un programme cible qui est déjà en train de tourner, grâce à DebugActiveProcess.
  2. Attendez les 'debugging événements'. Après que votre programme n'ait acquis sa fonction de debugger, le lien primaire du debuggee est suspendu et continuera de l'être jusqu'à ce que vous appeliez WaitForDebugEvent dans votre programme. Cette fonction marche comme d'autres WaitForXXX fonctionnent, c'est-à-dire. Elle bloque le lien appelant tant que l'événement "attendu" ne se produise. Dans ce cas, elle attend que des 'debugging événements' soient envoyés par Windows. On va voir leur définition :

    WaitForDebugEvent Proto lpDebugEvent:DWORD, dwMilliseconds:DWORD

    lpDebugEvent est l'adresse de la structure DEBUG_EVENT, laquelle sera remplie de l'information d'un 'debugging événement' quand un événement particulier est en train de se passer dans le debuggee.

    dwMilliseconds est le temps en millisecondes que cette fonction attendra qu'un 'debugging événement' ne se produise. Si ce temps est écoulé et qu'aucun événement (servant à debugger) n'est arrivé, WaitForDebugEvent revient au Call. D'autre part, si vous mettez la constante INFINITE dans cet argument, la fonction ne retournera pas avant qu'un 'debugging événement' ne soit produit.

    Maintenant on va examiner la structure DEBUG_EVENT plus en détail.

    DEBUG_EVENT STRUCT
    dwDebugEventCode dd ?
    dwProcessId dd ?
    dwThreadId dd ?
    u DEBUGSTRUCT <>
    DEBUG_EVENT ENDS

    dwDebugEventCode contient la valeur (l'information) qui indique quel type de 'debugging événement' s'est produit dans la cible. Bref, il peut y avoir plusieurs types d'événements, votre programme a besoin de vérifier la valeur de cet élément pour qu'il sache de quel genre il est, et lui répondre convenablement. Les valeurs possibles sont les suivantes:

    3. Valeurs | Significations
CREATE_PROCESS_DEBUG_EVENT | Un process est créé. Cet événement sera envoyé lorsque le process du debuggee vient juste d'être créé (et n'est pas encore en fonction) ou lorsque votre programme s'attache juste à un process (un programme) en cours avec DebugActiveProcess. C'est le tout premier événement que votre programme recevra.
EXIT_PROCESS_DEBUG_EVENT | Un process vient de se terminer.
CREATE_THEAD_DEBUG_EVENT | Un nouveau lien est créé dans le process du debuggee ou quand votre programme s'attache d'abord à un process en cours. Notez que vous ne recevrez pas cet avis lorsque le lien primaire du debuggee est déjà créé.
EXIT_THREAD_DEBUG_EVENT | Un lien dans le debuggee vient de se terminer. Votre programme ne recevra pas cet événement pour le lien primaire. Bref, vous pouvez penser que le lien primaire du debuggee est un équivalent du process du debuggee lui-même. Ainsi, quand votre programme voit CREATE_PROCESS_DEBUG_EVENT, ça revient à CREATE_THREAD_DEBUG_EVENT pour le lien primaire.
LOAD_DLL_DEBUG_EVENT | Le debuggee charge une DLL. Vous recevrez cet événement quand le PE Loader résout les premiers liens de la DLL (vous appelez CreateProcess pour charger le debuggee), et au moment où le debuggee appelle LoadLibrary.
UNLOAD_DLL_DEBUG_EVENT | Une DLL est déchargé du process du debuggee.
EXCEPTION_DEBUG_EVENT | Une exception vient d'arriver dans le process du debuggee. Important: Cet événement arrivera uniquement avant que le debuggee ne commence à exécuter sa première instruction. L'exception est en réalité une pause debugging (int 3h) = (c'est un Break Point). Quand vous voulez reprendre le cours du debuggee, appelez ContinueDebugEvent avec le flag DBG_CONTINUE. N'utilisez pas le flag DBG_EXCEPTION_NOT_HANDLED sinon le debuggee refusera de repartir sous NT (sur Win98, ça marche très bien).
OUTPUT_DEBUG_STRING_EVENT | Cet événement est produit quand le debuggee appelle la fonction DebugOutputString pour envoyer une chaîne de caractères en tant que message à votre programme.
RIP_EVENT | Une erreur de debugging s'est produite.

    dwProcessId et dwThreadId sont les IDs du process et du lien, du programme dans lequel un 'debugging événement' s'est produit. Vous pouvez employer ces valeurs en tant qu'identificateurs du process et du lien auquel vous vous intéressez. Rappelez-vous que si vous employez CreateProcess pour charger le debuggee, vous obtenez aussi les IDs du process et du lien du debuggee dans la structure PROCESS_INFO. Vous pouvez utiliser ces valeurs pour différencier les événements qui se produisent dans le debuggee lui-même de ceux qui se produisent dans ses Child Process (au cas où vous n'auriez pas spécifié le Flag DEBUG_ONLY_THIS_PROCESS).

    u est une union qui contient plus d'informations à propos du 'debugging événement'. ça peut être une des structures suivantes selon la valeur de dwDebugEventCode vu plus haut.

    Valeur dans dwDebugEventCode | Interprétation de 'u'
CREATE_PROCESS_DEBUG_EVENT | La structure CREATE_PROCESS_DEBUG_INFO nommée CreateProcessInfo
EXIT_PROCESS_DEBUG_EVENT | La structure EXIT_PROCESS_DEBUG_INFO nommée ExitProcess
CREATE_THREAD_DEBUG_EVENT | La structure CREATE_THREAD_DEBUG_INFO nommée CreateThread
EXIT_THREAD_DEBUG_EVENT | La structure EXIT_THREAD_DEBUG_EVENT nommée ExitThread
LOAD_DLL_DEBUG_EVENT | La structure LOAD_DLL_DEBUG_INFO nommée LoadDll
UNLOAD_DLL_DEBUG_EVENT | La structure UNLOAD_DLL_DEBUG_INFO nommée UnloadDll
EXCEPTION_DEBUG_EVENT | La structure EXCEPTION_DEBUG_INFO nommée Exception
OUTPUT_DEBUG_STRING_EVENT | La structure OUTPUT_DEBUG_STRING_INFO nommée DebugString
RIP_EVENT | La structure RIP_INFO nommée RipInfo

    Je n'entrerai pas dans les détails de toutes ces structures dans ce tutorial, on va seulement s'attarder un peu sur la structure CREATE_PROCESS_DEBUG_INFO.
    Pour s'assurer que notre programme appelle WaitForDebugEvent et qu'il continue son déroulement ensuite, la première chose à faire est de retrouver quel 'debugging événement' s'est produit dans la cible'. Cette événement a été placé dans le membre dwDebugEventCode donc il suffit d'examiner la valeur dans dwDebugEventCode pour voir quel type de 'debugging événement' s'est produit dans le process du debuggee. Par exemple, si la valeur dans dwDebugEventCode est CREATE_PROCESS_DEBUG_EVENT, on peut interpréter le membre dans u comme étant CreateProcessInfo et y accéder grâce à u.CreateProcessInfo .

  3. Faites ce que vous voulez des réponses d'un 'debugging événement'. Lorsqu'on a un retour de WaitForDebugEvent, ça signifie qu'un 'debugging événement' vient juste de se dérouler dans le process du debuggee ou bien c'est que le temps d'attente s'est écoulé sans que rien ne se soit passé. Votre programme a besoin d'examiner la valeur dans dwDebugEventCode pour réagir convenablement à cet événement. À cet égard, ça se passe comme pour le traitement des 'Window Messages' (ex: on vient de détecter le déplacement de la souris, frappe clavier...) : vous choisissez d'en traiter certains mais on en ignore d'autres.
  4. Laissez le debuggee continuer son déroulement. Quand un 'debugging événement' se produit, Windows suspend le debuggee. Mais dès que vous avez terminé de traiter cet événement, vous avez besoin de le relancer (le debuggee) de nouveau, là où il s'était arrêté. On fait ça en appelant la fonction ContinueDebugEvent.

    ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD

    Cette fonction reprend le lien qui a été précédemment suspendu à cause du fait qu'un 'debugging événement' soit arrivé.
    dwProcessId et dwThreadId sont les 'IDs du process et du lien', du programme cible qui va enfin être relancé. On obtient d'habitude ces deux valeurs des membres dwProcessId et dwThreadId de la structure DEBUG_EVENT.
    dwContinueStatus indique comment continuer le lien qui a annoncé le 'debugging événement'. Il y a deux valeurs possibles: DBG_CONTINUE et DBG_EXCEPTION_NOT_HANDLED. En ce qui concerne tous les autres 'debugging événements', ces deux valeurs font la même chose : reprendre le lien. L'exception est EXCEPTION_debug_EVENT. Si le lien annonce un 'événement exception' de debugging, ça signifie qu'une exception s'est produite dans un lien du debuggee. Si vous mettez DBG_CONTINUE , Le lien ignorera son propre traitement d'exception et continuera son exécution. Dans ce scénario, votre programme doit examiner et résoudre l'exception lui-même avant de pouvoir reprendre son lien avec DBG_CONTINUE sinon l'exception arrivera de nouveau à maintes reprises.... Si vous mettez DBG_EXCEPTION_NOT_HANDLED, Votre programme indique à Windows qu'il n'a pas récupéré l'handle de l'exception : Windows devra utiliser le manipulateur d'exception par défaut du debuggee, pour s'occuper de cette exception.
    Pour conclure, si le 'debugging événement' se réfère à une exception dans le process du debuggee, vous devez appeler ContinueDebugEvent avec le flag DBG_CONTINUE si votre programme a déjà éradiqué la cause de l'exception. Autrement, votre programme doit appeler ContinueDebugEvent avec le flag DBG_EXCEPTION_NOT_HANDLED. Sauf dans un cas, pour lequel vous devez toujours utiliser le flag DBG_CONTINUE: le premier EXCEPTION_debug_EVENT a la valeur EXCEPTION_BREAKPOINT dans le membre ExceptionCode. Quand le debuggee va exécuter sa toute première instruction, votre programme recevra un événement d'exception de debugging. C'est en réalité une pause debugging (int 3h) (un Break Point). Si vous répondez en appelant ContinueDebugEvent avec le flag DBG_EXCEPTION_NOT_HANDLED, Windows NT refusera de redémarrer le debuggee (parce que personne ne se soucie de ça). Vous devez toujours employer le flag DBG_CONTINUE dans ce cas, pour dire à Windows que vous voulez que le lien reprenne.

  5. On continue ce cycle indéfiniment tant que le debuggee ne tombe pas sur son propre Exit Process . Votre programme (le debugger) doit présenter une boucle infinie un peu comme une boucle de message. La boucle ressemble à ça :

    .while TRUE
    invoke WaitForDebugEvent, addr DebugEvent, INFINITE
    .break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
    ; S'occupe des événements de debugging.
    invoke ContinueDebugEvent, DebugEvent.dwProcessId, DebugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
    .endw

    Une fois le programme (à debugger) pris au piège : vous commencez à le debugger, vous ne pouvez plus vous en détacher (en sortir) tant qu'il ne s'est pas terminé (tant qu'on arrive pas à un des ses Exits).

On va récapituler les étapes à nouveau :

  1. On Créer un process ou on attache notre debugger à un process (un programme) en cours d'exécution..
  2. On attend que des 'debugging événements' ne se produisent.
  3. On fait ce qu'on veut (des réponses) des 'debugging événements'.
  4. Laissez le debuggee reprendre son exécution..
  5. On continue ce cycle (les points 2,3,4 vus précédemment) dans une boucle infinie tant qu'on ne tombe pas sur l'ExitProcess du debuggee.

Exemple:

Cet exemple debugge un programme win32 et affiche les informations importantes telles que son handle, son ID, son Image Base, etc...

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib
.data
AppName db "Win32 Debug Example no.1",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
             db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0
NewThread db "A new thread is created",0
EndThread db "A thread is destroyed",0
ProcessInfo db "File Handle: %lx ",0dh,0Ah
            db "Process Handle: %lx",0Dh,0Ah
            db "Thread Handle: %lx",0Dh,0Ah
            db "Image Base: %lx",0Dh,0Ah
            db "Start Address: %lx",0
.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
.code
start:
mov ofn.lStructSize,sizeof ofn
mov ofn.lpstrFilter, offset FilterString
mov ofn.lpstrFile, offset buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi
.while TRUE
   invoke WaitForDebugEvent, addr DBEvent, INFINITE
   .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
       .break
   .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
       invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
       invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
   .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
       .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
         .continue
       .endif
   .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif
   invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
.endif
invoke ExitProcess, 0
end start

Analyse:

Le programme remplit la structure OPENFILENAME et appelle ensuite GetOpenFileName pour laisser l'utilisateur choisir un programme à debugger.

invoke GetStartupInfo,addr startinfo
invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, NULL, NULL, addr startinfo, addr pi

Quand l'utilisateur en a choisi un, on appelle CreateProcess pour charger ce programme. on appelle GetStartupInfo pour remplir la structure STARTUPINFO avec ses valeurs par défaut. Remarquez que nous employons debug_PROCESS combiné avec le flag debug_ONLY_THIS_PROCESS pour ne debugger que ce programme, et ne pas toucher à ses Child Process.

.while TRUE
   invoke WaitForDebugEvent, addr DBEvent, INFINITE

Quand le debuggee est chargé, notre debugger entre dans sa boucle de debugging infinie, en appelant WaitForDebugEvent. WaitForDebugEvent ne retournera pas avant qu'un 'debugging événement' ne se soit produit dans le debuggee parce que nous avons mis INFINITE en tant que deuxième paramètre. Quand un 'debugging événement' se produit, WaitForDebugEvent retourne et 'DBEvent' (DB-Event) est rempli de l'information représentant le 'debugging événement'. (Par exemple: CREATE_PROCESS_DEBUG_EVENT, ou EXIT_THREAD_DEBUG_EVENT...)

   .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       invoke MessageBox, 0, addr ExitProc, addr AppName, MB_OK+MB_ICONINFORMATION
       .break

Nous vérifions d'abord la valeur dans dwDebugEventCode. Si c'est EXIT_PROCESS_DEBUG_EVENT, nous affichons une MessageBox disant "The debuggee exits" (le debugee s'est arrêté car il est tombé sur sa fonction de terminaison ExitProcess) et ensuite on sort de la boucle de debugging (du debugger).

   .elseif DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
       invoke wsprintf, addr buffer, addr ProcessInfo, DBEvent.u.CreateProcessInfo.hFile, DBEvent.u.CreateProcessInfo.hProcess, DBEvent.u.CreateProcessInfo.hThread, DBEvent.u.CreateProcessInfo.lpBaseOfImage, DBEvent.u.CreateProcessInfo.lpStartAddress
       invoke MessageBox,0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION

Si la valeur dans dwDebugEventCode est CREATE_PROCESS_DEBUG_EVENT, alors nous affichons plusieurs informations intéressantes à propos du debuggee dans une MessageBox. Nous obtenons ces informations de u.CreateProcessInfo. CreateProcessInfo est une structure de type CREATE_PROCESS_DEBUG_INFO. Vous pouvez obtenir plus de renseignements sur cette structure en regardant votre référence Win32 API.

   .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
       .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
         .continue
       .endif

Si la valeur dans dwDebugEventCode est EXCEPTION_DEBUG_EVENT, nous devons vérifier plus loin si le type d'exception est exact. C'est une longue ligne de références emboîtées mais vous pouvez obtenir le type d'exception grâce au membre ExceptionCode. Si la valeur dans ExceptionCode est EXCEPTION_BREAKPOINT et que c'est la première fois (ou bien que nous sommes certains que le debuggee ne s'est jamais servit de l'interruption 3 du Dos (int 03h)), alors nous pouvons dire sans risque que cette exception s'est produite lorsque le debuggee a voulut exécuter sa toute première instruction. Quand on en a fini avec le traitement de cette exception, nous devons appeler ContinueDebugEvent avec le Flag DBG_CONTINUE pour permettre au debuggee de reprendre là où il s'était arrêté. Après ça, nous retournons pour attendre le prochain 'debugging événement'.

   .elseif DBEvent.dwDebugEventCode==CREATE_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr NewThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .elseif DBEvent.dwDebugEventCode==EXIT_THREAD_DEBUG_EVENT
       invoke MessageBox,0, addr EndThread, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif

Si la valeur dans dwDebugEventCode est CREATE_THREAD_DEBUG_EVENT ou bien EXIT_THREAD_DEBUG_EVENT, on affiche une MessageBox disant que le lien est créé ou bien détruit.

invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw

Excepté pour le cas EXCEPTION_debug_EVENT, vu au-dessus, nous appelons ContinueDebugEvent avec le flag DBG_EXCEPTION_NOT_HANDLED pour reprendre le cours du debuggee.

invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread

Quand le debuggee se referme, nous sommes déjà en dehors de la boucle de debugging (de notre debugger)et devons fermer à la fois le process du debuggee et les handles de ses liens. La simple fermeture de ses handles ne signifie pas que nous détruisons le rapport process/lien. Ça signifie seulement que nous ne souhaitons plus désormais employer ces handles pour faire la relation entre process et lien.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 29: Win32 Debug API part 2

Tutorial 29: Win32 Debug API Partie 2/3

Nous continuons sur le sujet , soit sur les : 'win32 debug API'. Dans ce tutorial, nous allons voir comment à l'aide de notre programme debugger nous pouvons modifier le process du debuggee.
Downloadez l'exemple.

Théorie:

Dans le tutorial précédent, nous avons vu comment charger le debuggee et gérer les 'debugging événements' qui se produisent dans son process. Pour être efficace, notre programme doit être capable de modifier le process de debuggee. Il existe plusieurs APIs pour faire ça.

  • ReadProcessMemory Cette fonction vous permet de lire la mémoire dans le process désigné. Son prototype de fonction est le suivant :

    ReadProcessMemory proto hProcess:DWORD, lpBaseAddress:DWORD, lpBuffer:DWORD, nSize:DWORD, lpNumberOfBytesRead:DWORD

    hProcess est l'handle du process dans lequel vous souhaitez effectuer votre lecture.
    lpBaseAddress est l'adresse dans le process cible où vous souhaitez commencer votre lecture. Par exemple, si vous voulez lire 4 octets du debuggee, lequel commence à l'offset 401000, la valeur dans ce paramètre doit être 401000.
    lpBuffer est l'adresse du buffer qui recevra les octets qu'on va lire dans le process.
    nSize est le nombre d'octets que vous souhaitez lire. (Dans l'exemple ci-dessus, c'est donc 4)
    lpNumberOfBytesRead est l'adresse de la variable (de taille dword) qui reçoit le numbre d'octets qu'on aura réellement lus. Si vous vous en foutez, vous pouvez employer un NULL.

  • WriteProcessMemory est un homologue de ReadProcessMemory. Ça vous permet d'écrire dans la mémoire du process cible. Ses paramètres sont exactement les mêmes que ceux de ReadProcessMemory.

    Les deux fonctions API suivantes ont besoin d'une petite explication quant à leur utilisation. Dans un OS multi-taches tel que Windows, on peut avoir plusieurs programmes tournant en même temps. Windows donne un temps d'activité restreint à chacun d'eux. Quand ce temps d'activité expire, Windows gèle le programme qui était en train de tourner et passe au programme suivant qui prend maintenant la priorité la plus haute. juste avant de commuter sur ce nouveau programme, Windows a sauvegardé les valeurs du premier dans les Registres de lien (du premier) pour qu'au moment où il est temps de reprendre le lien, Windows puisse rétablir l'ancien *environnement* de ce lien. Les valeurs des Registres sauvegardés, représentent un ensemble qu'on appelle un contexte.
    Revenons à notre sujet. Lorsqu'un 'debugging événement' se produit, Windows suspend le debuggee. Le contexte du debuggee est sauvegardé. Puisque le debuggee est suspendu, on est sûrs que les valeurs dans le contexte resteront inchangées. Nous pouvons obtenir les valeurs de ce contexte (donc les valeurs des registres) grâce à la fonction GetThreadContext et nous pouvons les changer avec SetThreadContext.
    Ces deux APIs sont extrêmement puissantes. Avec elles, on a à notre portée la puissance du VxD pour contrôler le debuggee cible : on peut changer les valeurs des registres sauvegardés, et juste avant que le debuggee ne reprenne son exécution, on récupère les anciennes valeurs du contexte de nouveau dans les Registres. Quelque soit les changements qu'on a fait subir au contexte avant de revenir au 'relancement' du debuggee, notre debugger perçoit et contrôle tout. Pensez-y : on peut même changer la valeur du Registre EIP et ainsi détourner le flux exécution n'importe où, où on le souhaite! C'est à dire, l'EIP représente à chaque instant la ligne du programme qui est en train d'être lue par l'ordinateur. De fil en aiguille on avance dans la lecture du programme ligne par ligne. Maintenant, si on est capable de changer la valeur du registre EIP, on peut sauté vraiment où on veut. Alors que normalement c'est l'ordinateur et personne d'autre qui contrôle l'EIP. C'est quelque chose qu'on serait bien incapable de faire en temps normal.

    GetThreadContext proto hThread:DWORD, lpContext:DWORD

    hThread est l'handle du lien que vous voulez obtenir dans le context
    lpContext est l'adresse de la structure CONTEXT qui sera remplie, lorsque cette fonction retourne avec succès.

    SetThreadContext a exactement les mêmes paramètres. On va voir à quoi ressemble une structure de CONTEXT :

  • CONTEXT STRUCT
  • ContextFlags dd ?

    ;----------------------------------------------------------------------------------------------------------
    ; Cette section est renvoyée si ContextFlags contient la valeur CONTEXT_DEBUG_REGISTERS
  • ;-----------------------------------------------------------------------------------------------------------
    iDr0 dd ?
    iDr1 dd ?
    iDr2 dd ?
    iDr3 dd ?
    iDr6 dd ?
    iDr7 dd ?

  • ;----------------------------------------------------------------------------------------------------------
    ; Cette section est renvoyée si ContextFlags contient la valeur CONTEXT_FLOATING_POINT
  • ;-----------------------------------------------------------------------------------------------------------
  • FloatSave FLOATING_SAVE_AREA <>

  • ;----------------------------------------------------------------------------------------------------------
    ; Cette section est renvoyée si ContextFlags contient la valeur CONTEXT_SEGMENTS
  • ;-----------------------------------------------------------------------------------------------------------
  • regGs dd ?
    regFs dd ?
    regEs dd ?
    regDs dd ?

  • ;----------------------------------------------------------------------------------------------------------
    ; Cette section est renvoyée si ContextFlags contient la valeur CONTEXT_INTEGER
  • ;-----------------------------------------------------------------------------------------------------------
  • regEdi dd ?
    regEsi dd ?
    regEbx dd ?
    regEdx dd ?
    regEcx dd ?
    regEax dd ?

  • ;----------------------------------------------------------------------------------------------------------
    ; Cette section est renvoyée si ContextFlags contient la valeur CONTEXT_CONTROL
  • ;-----------------------------------------------------------------------------------------------------------
  • regEbp dd ?
    regEip dd ?
    regCs dd ?
    regFlag dd ?
    regEsp dd ?
    regSs dd ?

  • ;----------------------------------------------------------------------------------------------------------
    ; Cette section est renvoyée si ContextFlags contient la valeur CONTEXT_EXTENDED_REGISTERS
  • ;-----------------------------------------------------------------------------------------------------------
  • ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) CONTEXT ENDS

    Comme vous pouvez le voir, les membres de ces structures sont des imitations des Registres réels du processeur. Avant que vous ne puissiez employer cet artifice, vous devez indiquer quels groupes de Registres vous souhaitez lire/écrire dans le membre ContextFlags. Par exemple, si vous voulez lire/écrire tous les Registres, vous devez mettre le membre CONTEXT_FULL dans la structure ContextFlags. Si vous voulez seulement lire/écrire regEbp, regEip, regCs, regFlag, regEsp ou regSs, vous devez mettre le membre CONTEXT_CONTROL dans ContextFlags.

    Encore une chose que vous devez vous rappeler, en employant la structure CONTEXT: elle doit absolument fonctionner avec des modèles dword sinon vous obtiendriez des résultats étranges sous NT. Vous devez mettre "align dword" juste au-dessus de la ligne qui la déclare, de cette façon :

    align dword
    MyContext CONTEXT <>

Exemple:

Ce premier exemple sert à montrer comment on utilise un Debugger de type ActiveProcess. D'abord, on doit lancer une cible qui s'appelle win.exe, ce programme est une boucle infinie qui tourne en rond sans pouvoir en sortir et on ne peut donc pas arriver à son autre fonction (qui est juste après) qui affiche une fenêtre à l'écran. A partir de là, on lance notre exemple (notre debugger appelé debug2.exe), il s'attachera à win.exe et modifiera son code tel que win.exe puisse sortir de sa boucle infinie et ainsi il pourra afficher sa fenêtre.

Si ça ressemble pas à un Memory-Patch çà... :)

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.2",0
ClassName db "SimpleWinClass",0
SearchFail db "Cannot find the target process",0
TargetPatched db "Target patched!",0
buffer dw 9090h

.data?
DBEvent DEBUG_EVENT <>
ProcessId dd ?
ThreadId dd ?
align dword
context CONTEXT <>

.code
start:
invoke FindWindow, addr ClassName, NULL
.if eax!=NULL
    invoke GetWindowThreadProcessId, eax, addr ProcessId
    mov ThreadId, eax
    invoke DebugActiveProcess, ProcessId
    .while TRUE
       invoke WaitForDebugEvent, addr DBEvent, INFINITE
       .break .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
       .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
          mov context.ContextFlags, CONTEXT_CONTROL
          invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
          invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL
          invoke MessageBox, 0, addr TargetPatched, addr AppName, MB_OK+MB_ICONINFORMATION
       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
          .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
             invoke ContinueDebugEvent, DBEvent.dwProcessId,DBEvent.dwThreadId, DBG_CONTINUE
             .continue
          .endif
       .endif
       invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
   .endw
.else
    invoke MessageBox, 0, addr SearchFail, addr AppName,MB_OK+MB_ICONERROR
.endif
invoke ExitProcess, 0
end start

;--------------------------------------------------------------------
; Le code source de win.asm est en réalité une simple fenêtre comme
; celle de l'exemple du tutorial 2 avec une boucle infinie insérée
; juste avant qu'on ne puisse arriver jusqu'à elle (la fenêtre).
;----------------------------------------------------------------------

......
mov wc.hIconSm,eax
invoke LoadCursor,NULL,IDC_ARROW
mov wc.hCursor,eax
invoke RegisterClassEx, addr wc
INVOKE CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL
mov hwnd,eax
jmp $ ;<---- Voici notre boucle infinie. Elle boucle sur elle-même.
invoke ShowWindow, hwnd,SW_SHOWNORMAL
invoke UpdateWindow, hwnd
.while TRUE
   invoke GetMessage, ADDR msg,NULL,0,0
   .break .if (!eax)
   invoke TranslateMessage, ADDR msg
   invoke DispatchMessage, ADDR msg
.endw
mov eax,msg.wParam
ret
WinMain endp

Analyse:

invoke FindWindow, addr ClassName, NULL

Notre programme a besoin de s'attacher au debuggee avec DebugActiveProcess lequel a besoin de l'ID du process du debuggee. Nous pouvons obtenir l'ID de son process en appelant GetWindowThreadProcessId qui a à son tour à besoin de l'handle de la fenêtre en tant que paramètre. Donc nous avons d'abord besoin d'obtenir cet handle.
Avec FindWindow, nous pouvons spécifier le nom de la Window Class (classe de fenêtre) dont nous avons besoin. Il renvoie l'handle de la fenêtre créée par cette Window Class. S'il renvoie un NULL, aucune fenêtre de cette classe n'est présente.

.if eax!=NULL
    invoke GetWindowThreadProcessId, eax, addr ProcessId
    mov ThreadId, eax
    invoke DebugActiveProcess, ProcessId

Après avoir obtenu l'ID du process, nous pouvons appeler DebugActiveProcess. Comme ça, nous passons à la boucle traitant les 'debugging événements'.

       .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
          mov context.ContextFlags, CONTEXT_CONTROL
          invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context

Quand on arrive à la partie du programme qui contient la ligne CREATE_PROCESS_debug_INFO, ça signifie que le debuggee est suspendu, disponible pour que nous puissions faire l'acte de chirurgie en son process. Dans cet exemple, nous remplaçons l'instruction de la boucle infinie dans le debuggee (qui est un Jmp codé en langage machine par 0EBh 0FEh) par deux NOP (90h 90h).
D'abord, nous avons besoin d'obtenir l'adresse où se situe cette instruction (ce Jmp). Puisque le debuggee est déjà dans la boucle au moment où notre programme est attaché à lui, alors EIP indique forcément l'adresse de cette instruction. Tout ce que nous avons besoin de faire, c'est de récupérer la valeur de EIP. On utilise GetThreadContext pour faire ça. Nous plaçons le membre ContextFlags dans le CONTEXT_CONTROL pour indiquer à GetThreadContext que nous souhaitons qu'il remplisse les Registres de "control" avec les membres de la structure CONTEXT. Ainsi par exemple EIP sera récupéré dans notre Registre de Control appelé regEip.

invoke WriteProcessMemory, DBEvent.u.CreateProcessInfo.hProcess, context.regEip ,addr buffer, 2, NULL

Maintenant que nous avons la valeur de l'EIP, nous pouvons appeler WriteProcessMemory pour récrire par dessus l'instruction "jmp $" avec 2 NOP, ainsi on aura aidé efficacement le debuggee à sortir de sa boucle infinie. Après ça, nous affichons un message destiné à l'utilisateur et appelons ensuite ContinueDebugEvent pour que le debuggee redémarre. Maintenant que l'instruction "jmp $" est remplacée par deux instructions NOP, le debuggee est capable de continuer jusqu'à atteindre l'affichage de sa fenêtre et du message. La preuve, c'est que nous verrons apparaître cette fenêtre à l'écran.

L'autre exemple (Debug3.exe) utilise une approche un peu différente pour casser la boucle infinie.

.......
.......
.if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
   mov context.ContextFlags, CONTEXT_CONTROL
   invoke GetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
   add context.regEip,2
   invoke SetThreadContext,DBEvent.u.CreateProcessInfo.hThread, addr context
   invoke MessageBox, 0, addr LoopSkipped, addr AppName, MB_OK+MB_ICONINFORMATION
.......
.......

Comme pour l'exemple précédent on appelle toujours GetThreadContext pour obtenir la valeur actuelle de l'EIP, mais au lieu de l'action : 'effacer la boucle du "jmp$" avec deux instructions NOP (pour briser la boucle), ici on incrémente la valeur de regEip par 2 (regEip-->EIP+2) grâce à l'instruction "skip over". Le résultat, c'est que lorsque le debuggee reprend le contrôle, il reprend son exécution à l'instruction suivante, juste après le "jmp$". Donc on ne détruit pas la boucle mais on passe par dessus. Vous allez me dire pourquoi +2 ! le saut 'jmp' est codé en langage machine sur deux octets (EB et FE ou 0EBh 0FEh) donc quand l'EIP est sur le 'jmp' on est sur EB si on ajoute 2 à l'EIP, on passe sur EB puis sur FE et donc on arrive à ce qui suit le jmp donc à l'instuction suivante. On vient de sauter par dessus !

Maintenant vous comprenez la puissance de 'Get & SetThreadContext'. Vous pouvez aussi modifier d'autres Registres et leurs valeurs seront ensuite rétablies après le passage du debuggee. Vous pouvez même insérer l'instruction 'int 3h' laquelle sert à placer des Break Points dans le programme qu'on est en train de debugger.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's Win32 Assembly Tutorial 30: Win32 Debug API part 3

Tutorial 30: Win32 Debug API partie 3/3

Dans ce tutorial, nous allons continuer l'exploration des 'win32 debug API'. Et spécialement, nous allons voir comment tracer le debuggee.
Downloadez l'exemple.

Théorie:

Si vous avez déjà utilisé un débugger auparavant, vous êtes familiers avec le fait de tracer un autre programme grâce à lui. Quand vous "tracez" un programme, les Break Points sur n'importe quelle instruction, vous donne la chance d'examiner les valeurs des Registres et des mémoires. Le fait de tracer un programme, on appelle ça officiellement 'Single-stepping'.
Plus concrètement, tracer un programme c'est exécuter une ligne d'instruction du programme cible puis voir ce que ça produit,( ou bien regarder les valeurs des Registres ou des mémoires qui viennent de changer) puis passer à la ligne suivante et encore voir ce qu'elle fait et ainsi de suite pour comprendre comment fonctionne le programme. Seul un debugger est capable de faire ça.
Le traçage pas à pas (ou 'Single-stepping') a la particularité d'être générer pas le CPU lui-même. (Le CPU, 'Central Processing Unit' ou 'Unité Centrale de Traitement'). Le 8ème bit du flag 'REGISTER' est appelé le Trap Flag. Si ce flag est mis à 1, le CPU fonctionne en mode pas à pas ou traçage, c'est pareil, enfin bon en 'Single-stepping'. Le CPU produira un 'debugging événement' de type 'exception' (voir tut28) pour le stopper après chaque instruction. Après que cette 'exception' se soit produite, le 'Trap Flag' est automatiquement purifié.
Nous pouvons aussi tracer le debuggee, en employant les win32 debug API. Les étapes sont les suivantes :

  1. Appelez la fonction GetThreadContext, en spécifiant le Flag CONTEXT_CONTROL dans le membre ContextFlags, pour obtenir la valeur du Flag REGISTER.
  2. Mettez le Trap Bit dans le membre regFlag de la structure CONTEXT.
  3. Appelez SetThreadContext
  4. Comme d'habitude, attendez qu'un 'debugging événement' se produise. Le debuggee s'exécutera en mode single-step. Après qu'il exécute chaque instruction, nous obtiendrons l'événement suivant : EXCEPTION_DEBUG_EVENT, et avec la valeur EXCEPTION_SINGLE_step dans u.Exception.pExceptionRecord.ExceptionCode nous pourrons tracer pas à pas.
  5. Si vous avez besoin de tracer (donc d'arrêter) l'instruction suivante, vous avez besoin de remettre le trap bit de nouveau.

Exemple:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\user32.lib

.data
AppName db "Win32 Debug Example no.4",0
ofn OPENFILENAME <>
FilterString db "Executable Files",0,"*.exe",0
             db "All Files",0,"*.*",0,0
ExitProc db "The debuggee exits",0Dh,0Ah
         db "Total Instructions executed : %lu",0
TotalInstruction dd 0

.data?
buffer db 512 dup(?)
startinfo STARTUPINFO <>
pi PROCESS_INFORMATION <>
DBEvent DEBUG_EVENT <>
context CONTEXT <>

.code
start:
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER \
               or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
    invoke GetStartupInfo,addr startinfo
    invoke CreateProcess, addr buffer, NULL, NULL, NULL, FALSE, \
                  DEBUG_PROCESS+ DEBUG_ONLY_THIS_PROCESS, \
                  NULL, NULL, addr startinfo, addr pi
    .while TRUE
       invoke WaitForDebugEvent, addr DBEvent, INFINITE
       .if DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
          invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction
          invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION
          .break
       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
          .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
             mov context.ContextFlags, CONTEXT_CONTROL
             invoke GetThreadContext, pi.hThread, addr context
             or context.regFlag,100h
             invoke SetThreadContext,pi.hThread, addr context
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
             .continue
          .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
             inc TotalInstruction
             invoke GetThreadContext,pi.hThread,addr context
             or context.regFlag,100h
             invoke SetThreadContext,pi.hThread, addr context
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
             .continue
          .endif
       .endif
       invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, \
                                  DBG_EXCEPTION_NOT_HANDLED
    .endw
.endif
invoke CloseHandle,pi.hProcess
invoke CloseHandle,pi.hThread
invoke ExitProcess, 0
end start

Analyse:

Ce programme ouvre une boîte de dialogue openfile. Quand l'utilisateur choisit un fichier exécutable, il exécute le programme cible en mode single-step (tracage pas à pas), et il compte le nombre d'instructions exécutées jusqu'à la sortie de ce programme cible.

       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
          .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT

Nous en profitons pour faire fonctionner le programme cible en mode single-step. Souvenez-vous que Windows envoie un EXCEPTION_BREAKPOINT juste avant qu'il n'exécute la première instruction du prog cible.

             mov context.ContextFlags, CONTEXT_CONTROL
             invoke GetThreadContext, pi.hThread, addr context

On appelle GetThreadContext pour remplir la structure CONTEXT avec les valeurs actuelles des Registres du debuggee. Plus spécifiquement, nous avons besoin de la valeur actuelle du flag REGISTER.

             or context.regFlag,100h

Nous mettons le trap bit (le 8ème bit) à 1 dans l'image du flag REGISTER.

             invoke SetThreadContext,pi.hThread, addr context
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE
             .continue

Ainsi nous appelons la fonction SetThreadContext pour écrire par dessus les valueurs de la structure CONTEXT avec les nouvelles on appelle la fonction ContinueDebugEvent munie en plus du flag DBG_CONTINUE pour relancer le debuggee.

          .elseif DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
             inc TotalInstruction

Quand une instruction est exécutée dans le debuggee, nous recevons le fameux 'debugging événement' (de type 'Exeption') EXCEPTION_DEBUG_EVENT. Nous devons examiner la valeur de u.Exception.pExceptionRecord.ExceptionCode. Si sa valeur est EXCEPTION_SINGLE_step, alors ce 'debugging événement' s'est produit à cause du mode Pas à Pas (single-step). Dans ce cas, nous pouvons incrémenter la variable TotalInstruction car nous savons qu'une seule et unique instruction vient d'être exécutée dans le programme cible.

             invoke GetThreadContext,pi.hThread,addr context or context.regFlag,100h
             invoke SetThreadContext,pi.hThread, addr context
             invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE
             .continue

Après que ce 'debugging exception' se soit produit on enlève (on purifie) le 'trap flag'. On doit remettre le trap flag de nouveau si on souhaite continuer en mode Pas à Pas (single-step).
Avertissement : n'employez pas l'exemple dans ce tutorial avec un grand programme : le traçage est LENT. Il vous faudrait attendre pendant dix minutes avant que vous ne puissiez fermer le debuggee.

[Iczelion's Win32 Assembly Homepage]

Taduit par Morgatte


Iczelion's Win32 Assembly Tutorial 31: ListView Control

Tutorial 31: Fichiers organisés sous la forme d'une liste
(Listview Control)

Dans ce tutorial, nous allons voir comment créer et employer un listview control.

Downloadez l'exemple.

Théorie:

Un listview control est aussi un 'common controls' (tutorial 18) tout comme le treeview (tutorial 19), richedit etc. En fait vous l'utilisez couramment sans même connaître son nom. Par exemple, la fenêtre de droite de l'Explorateur Windows est un listview control. Un listview control sert à afficher sous forme d'une liste un groupe de fichiers. En fait, c'est un peu comme une liste, mais avec certaines capacités en plus.
Vous pouvez créer un listview control de deux façons. La première méthode est aussi la plus simple : créez-le avec un éditeur de ressource. Simplement, n'oubliez pas ensuite d'appeler InitCommonControls dans votre code asm source. L'autre méthode c'est d'appeler CreateWindowEx dans votre code source. Vous devez spécifier le nom de la 'Window Class' (classe de fenêtre) correcte de ce control, c'est-à-dire SysListView32. La 'Window Class' "WC_LISTVIEW" n'est pas correcte.
Il y a quatre types de vues des données, dans un listview control : une vue sous forme d'icônes, de petites icônes, sous forme de texte inscrit et des vues sous forme de détails. Vous pouvez voir les exemples de ces vues en choisissant Affichage->Grandes icônes (la vue d'icônes), Affichage->Petites icônes (la vue en petites icônes), Affichage-> Liste (la vue de la liste) et Affichage-> Détails (la vue sous forme de détails). Ces vues sont juste une méthode de représentation des données : Elles affectent seulement la présentation des données. Par exemple, vous pouvez avoir beaucoup de données dans un listview control, mais si vous préférez, vous pouvez voir certaines d'entre elles. La vue détail est la plus informative alors que les vues restantes donnent moins de renseignements. Vous pouvez spécifier la vue que vous voulez quand vous créez un listview control. Vous pourrez même changer de vue plus tard en appelant SetWindowLong, en spécifiant flag GWL_STYLE.

Maintenant que nous savons comment créer un listview control, nous continuerons pour voir comment on peut l'employer. Je me concentrerai de la vue sous forme de détails qui fait appelle à pas mal des particularités du listview control. Les étapes dans l'utilisation d'un listview control sont les suivantes :

  1. Créez le listview control avec CreateWindowEx, et indiquez SysListView32 comme class name. Vous pouvez spécifier la vue initiale à ce niveau là.
  2. (Si elles existes) Créez et initialisez des listes d'images (Tutorial 19) pour les associer aux noms des fichiers du listview.
  3. Insérez des colonnes dans listview control. Cette étapes est nécessaire si votre listview control emploie la vue 'détails'. Explication : revenez sous l'Explorateur Windows et mettez vous en vue 'détails'. Vous voyez bien qu'il y a plusieurs colonnes, une pour le nom des fichiers, une autre pour leur date de création, une pour leur emplacement sur votre Ddur.
  4. Insérez des items et des sous-items dans votre listview control. ex: Nom, Taille ,Type ,Modifier...

Colonnes

Dans la vue 'détails', il y a une ou plusieurs colonnes. Ces données qui sont mises dans différentes colonnes (en vue détails), représentes en fait un tableau de données : les données sont arrangées dans des rangées et des colonnes. Vous devez avoir au moins une colonne dans votre listview control (uniquement vrai pour la vue 'détails'). Pour les autres vues, vous n'avez pas besoin d'insérer de colonne parce qu'il nous faut une et seulement une seule colonne pour ces vues là.
Vous pouvez insérer une colonne en envoyant LVM_INSERTCOLUMN au listview control.

LVM_INSERTCOLUMN
wParam = iCol (nombre de colonnes)
lParam = pointe sur la structure LV_COLUMN

iCol est le nombre de colonnes, en partant de 0.
LV_COLUMN contient l'information sur la colonne qui doit être insérée. Voici sa définition :

LV_COLUMN STRUCT
imask dd ?
fmt dd ?
lx dd ?
pszText dd ?
cchTextMax dd ?
iSubItem dd ?
iImage dd ?
iOrder dd ?
LV_COLUMN ENDS

Nom | Significations
imask | Est un groupe de flags indiquant que les membres dans cette structure sont valides pour cet appel. En général, ce paramètre est semblable au membre imask de la structure LV_COLUMN vue plus haut. Référez vous à votre 'référence win32 api' pour plus de détail à propos des différents flags disponibles.
iItem | Est l'index de l'item auquel cette structure se réfère. L'index de base est zéro. Vous pouvez vous représenter ce paramètre comme le numéro de la "rangée" d'un tableau.
iSubItem | Est l'index du sous-item associé à l'item indiqué par iItem ci-dessus. Vous pouvez vous représenter ce paramètre comme étant le numéro de la "colonne" d'un tableau. Par exemple, si vous souhaitez insérer un item dans un tout nouveau 'listview control' juste créé, la valeur à mettre dans iItem serait 0 (parce que cet item est le premier) et la valeur dans iSubItem serait aussi 0 (car nous voulons insérer cet item dans la première colonne). Si vous souhaitez associer un sous-item à cet item, alors le iItem représenterait l'index de l'item avec lequel vous voulez l'associer (dans cet exemple, c'est 0). Le iSubItem ne pourrait être que 1 ou plus que 1, suivant la colonne où vous voulez insérer ce sous-item. Par exemple, si votre listview control a 4 colonnes, la première colonne contiendra tous les Items. Les 3 colonnes restantes sont utilisées par les sous-items. Si vous voulez insérer un sous-item dans la 4ème colonne, vous devez mettre valeur 3 dans iSubItem.
state | Ce membre contient les flags qui reflètent l'état de l'item. L'état de l'item peut changer à cause des actions de l'utilisateur, ou bien il peut être modifié par notre programme lui-même. The state includes whether the item has the focus/is hilited/is selected for cut operation/is selected. In addition to the state flags, It can also contains one-based index into the overlay image/state image for use by the item.
stateMask | Puisque le membre 'state' ci-dessus peut contenir les flags qui définissent l'état de nos items, nous avons besoin de dire à Windows quelle valeur nous souhaitons mettre (ou remplacer si on a déjà définit son état). La valeur de ce paramètre sert à ça.
pszText | Est l'adresse de la chaîne de caractère qui sera utilisée en tant que titre pour votre item dans le cas où vous souhaitez mettre ou insérer un item. Si nous nous servons de cette structure pour récupérer la propriété d'un item donné, ce membre doit contenir l'adresse du buffer qui sera rempli avec le titre de cet item.
cchTextMax | Ce paramètre est uniquement utilisé lorsque vous employez cette structure pour recevoir des renseignements à propos de l'item. Dans ce cas, ce paramètre contient la taille en octets du buffer indiqué dans pszText, vu ci-dessus.
iImage | Est l'index d'une icône contenue dans l'imagelist. Cet index indique quelle icône doit être employé et associée avec votre item.
lParam | C'est une valeur définie par l'utilisateur, laquelle sera réutilisée lorsque vous trierez les Items de votre listview control. Bref, lorsque vous direz au listview control de trier vos Items, celui-ci comparera les Items deux par deux. Il vous renverra les valeurs des lParam des deux Items, et vous pourrez ainsi décider lequel des deux doit être inscrit en premier. Si vous êtes encore un peu dans le coaltar avec ça, ne vous inquiéter pas. Vous en apprendrez plus tard, bien plus du triage.

Ainsi après que le listview control est été créé, vous devez y insérer une ou plusieurs colonnes. Les colonnes ne sont pas nécessaires si vous ne projetez pas de faire fonctionner le listview control en vue 'détails'. Pour insérer une colonne, vous avez besoin de créer la structure LV_COLUMN, la remplir de l'information nécessaire, et indiquer le nombre de colonnes puis envoyer ensuite cette structure au listview control avec LVM_INSERTCOLUMN message.

   LOCAL lvc:LV_COLUMN
   mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
   mov lvc.pszText,offset Heading1
   mov lvc.lx,150
   invoke SendMessage,hList, LVM_INSERTCOLUMN,0,addr lvc

Ce petit bout de code vous montre comment faire. Il détermine le texte d'en-tête de la colonne ainsi que sa largeur à envoyer au listview control, grâce au message LVM_INSERTCOLUMN. C'est aussi simple que ça.

Items et sous-items

Les items sont les principales éléments dans un listview control. Dans les vues autres, que la vue 'détails', vous ne verrez que les items. Les sous-items sont les détails des items. Un unique item peut avoir un ou plus sous-items associés. Par exemple, si l'item est le nom d'un fichier, alors vous pouvez regarder (en tant que sous-items) ses attributs de fichier comme, sa taille, ou sa date de création. Dans la vue 'détails', la colonne extrême gauche contient les items et les colonnes restantes contiennent les sous-items. Vous pouvez vous représenter les items et leurs sous-items un peu comme une base de données. L'item est la clef principale du tableau alors que les sous-items sont ses éléments de description dans ce même tableau.
Au minimum, il y aura quelques Items dans votre listview control : les sous-items ne sont pas nécessaires. Cependant, si vous voulez présenter plus d'informations à l'utilisateur à propos de ces Items, vous devrez leurs associer leurs sous-items respectifs. De cette façon, l'utilisateur pourra voir les détails dans la vue 'détails'.
On insère un item dans le listview control en envoyant le message LVM_INSERTITEM. Vous avez aussi besoin de lui passer l'adresse de la structure LV_ITEM dans lParam. LV_ITEM a la définition suivante :

LV_ITEM STRUCT
  imask dd ?
  iItem dd ?
  iSubItem dd ?
  state dd ?
  stateMask dd ?
  pszText dd ?
  cchTextMax dd ?
  iImage dd ?
  lParam dd ?
  iIndent dd ?
LV_ITEM ENDS

Nom | Significations
imask | Est un groupe de flags indiquant que les membres dans cette structure sont valides pour cet appel. En général, ce paramètre est semblable au membre imask de la structure LV_COLUMN vue plus haut. Référez vous à votre 'référence win32 api' pour plus de détail à propos des différents flags disponibles.
iItem | Est l'index de l'item auquel cette structure se réfère. L'index de base est zéro. Vous pouvez vous représenter ce paramètre comme le numéro de la "rangée" d'un tableau.
iSubItem | Est l'index du sous-item associé à l'item indiqué par iItem ci-dessus. Vous pouvez vous représenter ce paramètre comme étant le numéro de la "colonne" d'un tableau. Par exemple, si vous souhaitez insérer un item dans un tout nouveau 'listview control' juste créé, la valeur à mettre dans iItem serait 0 (parce que cet item est le premier) et la valeur dans iSubItem serait aussi 0 (car nous voulons insérer cet item dans la première colonne). Si vous souhaitez associer un sous-item à cet item, alors le iItem représenterait l'index de l'item avec lequel vous voulez l'associer (dans cet exemple, c'est 0). Le iSubItem ne pourrait être que 1 ou plus que 1, suivant la colonne où vous voulez insérer ce sous-item. Par exemple, si votre listview control a 4 colonnes, la première colonne contiendra tous les Items. Les 3 colonnes restantes sont utilisées par les sous-items. Si vous voulez insérer un sous-item dans la 4ème colonne, vous devez mettre valeur 3 dans iSubItem.
state | Ce membre contient les flags qui reflètent l'état de l'item. L'état de l'item peut changer à cause des actions de l'utilisateur, ou bien il peut être modifié par notre programme lui-même. The state includes whether the item has the focus/is hilited/is selected for cut operation/is selected. In addition to the state flags, It can also contains one-based index into the overlay image/state image for use by the item.
stateMask | Puisque le membre 'state' ci-dessus peut contenir les flags qui définissent l'état de nos items, nous avons besoin de dire à Windows quelle valeur nous souhaitons mettre (ou remplacer si on a déjà définit son état). La valeur de ce paramètre sert à ça.
pszText | Est l'adresse de la chaîne de caractère qui sera utilisée en tant que titre pour votre item dans le cas où vous souhaitez mettre ou insérer un item. Si nous nous servons de cette structure pour récupérer la propriété d'un item donné, ce membre doit contenir l'adresse du buffer qui sera rempli avec le titre de cet item.
cchTextMax | Ce paramètre est uniquement utilisé lorsque vous employez cette structure pour recevoir des renseignements à propos de l'item. Dans ce cas, ce paramètre contient la taille en octets du buffer indiqué dans pszText, vu ci-dessus.
iImage | Est l'index d'une icône contenue dans l'imagelist. Cet index indique quelle icône doit être employé et associée avec votre item.
lParam | C'est une valeur définie par l'utilisateur, laquelle sera réutilisée lorsque vous trierez les Items de votre listview control. Bref, lorsque vous direz au listview control de trier vos Items, celui-ci comparera les Items deux par deux. Il vous renverra les valeurs des lParam des deux Items, et vous pourrez ainsi décider lequel des deux doit être inscrit en premier. Si vous êtes encore un peu dans le coaltar avec ça, ne vous inquiéter pas. Vous en apprendrez plus tard, bien plus du triage.

On va récapituler les étapes clef pour l'ajout d'un item ou sous-item dans le listview control.

  1. Créez une variable de type la structure LV_item
  2. Remplissez-la des informations nécessaires
  3. Envoyez le message LVM_INSERTITEM au listview control si vous voulez insérer un item. Ou si vous voulez *insert* un sous-item, envoyez LVM_SETITEM a la place. Ce sera plutôt le bordel si vous ne comprenez pas le rapport entre un item et ses sous-items. Les Sous-items représentent un peu les propriétés de l'item . Ainsi vous pouvez insérer des items, mais pas des sous-items et vous ne pouvez pas avoir un sous-item sans avoir un item qui lui soit associé. C'est pourquoi pour ajouter un sous-item vous avez besoin d'envoyer le message LVM_SETITEM au lieu de LVM_INSERTITEM.

Messages d'ordres et messages de réponses dans le ListView

Maintenant que vous savez comment créer et remplir un ListView control, l'étape suivante c'est de communiquer avec lui. Le ListView control communique avec la fenêtre parente grâce à des messages et des avis. La fenêtre parente peut commander le ListView control en lui envoyant des messages. En retour, le ListView control fait le contraire, il informe sa fenêtre parente des événements importants ou intéressants grâce à WM_NOTIFY, comme les autres 'common controls'.

Classer les items/sous-items

Vous pouvez spécifier le type de triage par défaut dans le listview control en indiquant les types de tris LVS_SORTASCENDING ou bien LVS_SORTDESCENDING dans CreateWindowEx. Ces deux types classent les Items d'après leurs noms uniquement. Si vous voulez les classer selon un autre critère, vous devez mettre le message LVM_SORTITEMS à l'intention du listview control.

LVM_SORTITEMS
wParam = lParamSort
lParam = pCompareFunction

lParamSort est une valeur définie par l'utilisateur laquelle on passera à la fonction de comparaison. Vous pouvez employer cette valeur de n'importe quelle façon, c'est à vous de voir.
pCompareFunction est l'adresse de la fonction(type de tri) que l'utilisateur à défini pour son listview control. Cette fonction a le prototype suivant :

CompareFunc proto lParam1:DWORD, lParam2:DWORD, lParamSort:DWORD

lParam1 et lParam2 sont les valeurs dans le membre de lParam de LV_item Lequel vous avez défini lorsque vous avez inséré vos Items dans le listview control.
lParamSort est la valeur de wParam que vous avez envoyée avec LVM_SORTITEMS

Quand le listview control reçoit le message LVM_SORTITEMS, il fait appel à la fonction de tri (ou de comparaison) indiquée par lParam, ainsi il nous retourne le résultat de la comparaison entre deux items. Bref, la fonction de comparaison servira à savoir lequel de deux items lui ayant été envoyé sera placé en premier par rapport à l'autre. La règle est simple : si la fonction renvoie une valeur négative, premier item (représenté par lParam1) doit être placé après l'autre. Si la fonction renvoie une valeur positive, cet item doit être placé en premier. Si les deux items sont égaux, un NULL sera renvoyé.

Ce qui fait que cette méthode fonctionne, c'est la valeur de lParam qui se trouve dans la structure LV_item. Si vous avez besoin de trier des Items (comme lorsque l'utilisateur clique sur l'en-tête d'une colonne, essayez ça marche !), vous avez besoin d'imaginer une méthode de tri qui se servira des valeurs dans le membre lParam (IParam1 et IParam2). Dans cet exemple, j'ai mis l'index de l'item dans ce paramètre donc je peux obtenir d'autres informations à propos de ces items en envoyant le message LVM_GETITEM. Remarquez que lorsque les Items sont réarrangés, leurs index changent aussi . De cette façon, quand le tri est fini dans mon exemple, j'ai encore besoin de remettre à jour les valeurs dans lParam pour retrouver les nouveaux index. Si vous voulez trier les Items lorsque l'utilisateur clique sur l'en-tête d'une colonne, vous avez besoin de traiter le message d'avis LVN_COLUMNCLICK dans votre procédure de fenêtre. On passe LVN_COLUMNCLICK à votre proc de fenêtre grâce au message WM_NOTIFY.

Exemple:

Cet exemple crée un 'listview control' et le remplit avec les noms et les tailles des fichiers du dossier actuel où vous êtes. La vue par défaut sera la vue 'détails'. Dans la vue 'détails', vous pouvez cliquer sur les en-têtes de colonne et les Items seront classés par ordre croissant/décroissant (soit par nom, soit par taille). Vous pouvez choisir la vue que vous souhaitez grâce au menu. Quand vous double cliquez sur un item, une boîte de message affichera le nom de l'item que vous venez de sélectionner.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

IDM_MAINMENU equ 10000
IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

RGB macro red,green,blue
  xor eax,eax
  mov ah,blue
  shl eax,8
  mov ah,green
  mov al,red
endm

.data
ClassName db "ListViewWinClass",0
AppName db "Testing a ListView Control",0
ListViewClassName db "SysListView32",0
Heading1 db "Filename",0
Heading2 db "Size",0
FileNamePattern db "*.*",0
FileNameSortOrder dd 0
SizeSortOrder dd 0
template db "%lu",0

.data?
hInstance HINSTANCE ?
hList dd ?
hMenu dd ?

.code
start:
  invoke GetModuleHandle, NULL
  mov hInstance,eax
  invoke WinMain, hInstance,NULL, NULL, SW_SHOWDEFAULT
  invoke ExitProcess,eax
  invoke InitCommonControls
WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
  LOCAL wc:WNDCLASSEX
  LOCAL msg:MSG
  LOCAL hwnd:HWND

  mov wc.cbSize,SIZEOF WNDCLASSEX
  mov wc.style, NULL
  mov wc.lpfnWndProc, OFFSET WndProc
  mov wc.cbClsExtra,NULL
  mov wc.cbWndExtra,NULL
  push hInstance
  pop wc.hInstance
  mov wc.hbrBackground,COLOR_WINDOW+1
  mov wc.lpszMenuName,IDM_MAINMENU
  mov wc.lpszClassName,OFFSET ClassName
  invoke LoadIcon,NULL,IDI_APPLICATION
  mov wc.hIcon,eax
  mov wc.hIconSm,eax
  invoke LoadCursor,NULL,IDC_ARROW
  mov wc.hCursor,eax
  invoke RegisterClassEx, addr wc
  invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName, WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInst,NULL
  mov hwnd,eax
  invoke ShowWindow, hwnd,SW_SHOWNORMAL
  invoke UpdateWindow, hwnd
  .while TRUE
    invoke GetMessage, ADDR msg,NULL,0,0
    .break .if (!eax)
      invoke TranslateMessage, ADDR msg
      invoke DispatchMessage, ADDR msg
  .endw
  mov eax,msg.wParam
  ret
WinMain endp

InsertColumn proc
  LOCAL lvc:LV_COLUMN

  mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
  mov lvc.pszText,offset Heading1
  mov lvc.lx,150
  invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc
  or lvc.imask,LVCF_FMT
  mov lvc.fmt,LVCFMT_RIGHT
  mov lvc.pszText,offset Heading2
  mov lvc.lx,100
  invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc
  ret
InsertColumn endp

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
  LOCAL lvi:LV_ITEM
  LOCAL buffer[20]:BYTE
  mov edi,lpFind
  assume edi:ptr WIN32_FIND_DATA
  mov lvi.imask,LVIF_TEXT+LVIF_PARAM
  push row
  pop lvi.iItem
  mov lvi.iSubItem,0
  lea eax,[edi].cFileName
  mov lvi.pszText,eax
  push row
  pop lvi.lParam
  invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi
  mov lvi.imask,LVIF_TEXT
  inc lvi.iSubItem
  invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
  lea eax,buffer
  mov lvi.pszText,eax
  invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
  assume edi:nothing
  ret
ShowFileInfo endp

FillFileInfo proc uses edi
  LOCAL finddata:WIN32_FIND_DATA
  LOCAL FHandle:DWORD

  invoke FindFirstFile,addr FileNamePattern,addr finddata
  .if eax!=INVALID_HANDLE_VALUE
    mov FHandle,eax
    xor edi,edi
    .while eax!=0
      test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
      .if ZERO?
         invoke ShowFileInfo,edi, addr finddata
         inc edi
      .endif
      invoke FindNextFile,FHandle,addr finddata
    .endw
    invoke FindClose,FHandle
  .endif
  ret
FillFileInfo endp

String2Dword proc uses ecx edi edx esi String:DWORD
  LOCAL Result:DWORD

  mov Result,0
  mov edi,String
  invoke lstrlen,String
  .while eax!=0
    xor edx,edx
    mov dl,byte ptr [edi]
    sub dl,"0"
    mov esi,eax
    dec esi
    push eax
    mov eax,edx
    push ebx
    mov ebx,10
    .while esi > 0
      mul ebx
      dec esi
    .endw
    pop ebx
    add Result,eax
    pop eax
    inc edi
    dec eax
  .endw
  mov eax,Result
  ret
String2Dword endp

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
  LOCAL buffer[256]:BYTE
  LOCAL buffer1[256]:BYTE
  LOCAL lvi:LV_ITEM

  mov lvi.imask,LVIF_TEXT
  lea eax,buffer
  mov lvi.pszText,eax
  mov lvi.cchTextMax,256
  .if SortType==1
    mov lvi.iSubItem,1
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke String2Dword,addr buffer
    mov edi,eax
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke String2Dword,addr buffer
    sub edi,eax
    mov eax,edi
  .elseif SortType==2
    mov lvi.iSubItem,1
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke String2Dword,addr buffer
    mov edi,eax
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke String2Dword,addr buffer
    sub eax,edi
  .elseif SortType==3
    mov lvi.iSubItem,0
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke lstrcpy,addr buffer1,addr buffer
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke lstrcmpi,addr buffer1,addr buffer
  .else
    mov lvi.iSubItem,0
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke lstrcpy,addr buffer1,addr buffer
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke lstrcmpi,addr buffer,addr buffer1
  .endif
  ret
CompareFunc endp

UpdatelParam proc uses edi
   LOCAL lvi:LV_ITEM

   invoke SendMessage,hList, LVM_GETITEMCOUNT,0,0
   mov edi,eax
   mov lvi.imask,LVIF_PARAM
   mov lvi.iSubItem,0
   mov lvi.iItem,0
   .while edi>0
     push lvi.iItem
     pop lvi.lParam
     invoke SendMessage,hList, LVM_SETITEM,0,addr lvi
     inc lvi.iItem
     dec edi
   .endw
   ret
UpdatelParam endp

ShowCurrentFocus proc
   LOCAL lvi:LV_ITEM
   LOCAL buffer[256]:BYTE

   invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED
   mov lvi.iItem,eax
   mov lvi.iSubItem,0
   mov lvi.imask,LVIF_TEXT
   lea eax,buffer
   mov lvi.pszText,eax
   mov lvi.cchTextMax,256
   invoke SendMessage,hList,LVM_GETITEM,0,addr lvi
   invoke MessageBox,0, addr buffer,addr AppName,MB_OK
   ret
ShowCurrentFocus endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
  .if uMsg==WM_CREATE
    invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
    mov hList, eax
    invoke InsertColumn
    invoke FillFileInfo
    RGB 255,255,255
    invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
    RGB 0,0,0
    invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
    RGB 0,0,0
    invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax
    invoke GetMenu,hWnd
    mov hMenu,eax
    invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED
  .elseif uMsg==WM_COMMAND
    .if lParam==0
      invoke GetWindowLong,hList,GWL_STYLE
      and eax,not LVS_TYPEMASK
      mov edx,wParam
      and edx,0FFFFh
      push edx
      or eax,edx
      invoke SetWindowLong,hList,GWL_STYLE,eax
      pop edx
      invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
    .endif
  .elseif uMsg==WM_NOTIFY
    push edi
    mov edi,lParam
    assume edi:ptr NMHDR
    mov eax,[edi].hwndFrom
    .if eax==hList
      .if [edi].code==LVN_COLUMNCLICK
        assume edi:ptr NM_LISTVIEW
        .if [edi].iSubItem==1
          .if SizeSortOrder==0 || SizeSortOrder==2
            invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc
            invoke UpdatelParam
            mov SizeSortOrder,1
          .else
            invoke SendMessage,hList,LVM_SORTITEMS,2,addr CompareFunc
            invoke UpdatelParam
            mov SizeSortOrder,2
          .endif
        .else
          .if FileNameSortOrder==0 || FileNameSortOrder==4
            invoke SendMessage,hList,LVM_SORTITEMS,3,addr CompareFunc
            invoke UpdatelParam
            mov FileNameSortOrder,3
          .else
            invoke SendMessage,hList,LVM_SORTITEMS,4,addr CompareFunc
            invoke UpdatelParam
            mov FileNameSortOrder,4
          .endif
        .endif
        assume edi:ptr NMHDR
      .elseif [edi].code==NM_DBLCLK
        invoke ShowCurrentFocus
      .endif
    .endif
    pop edi
  .elseif uMsg==WM_SIZE
    mov eax,lParam
    mov edx,eax
    and eax,0ffffh
    shr edx,16
    invoke MoveWindow,hList, 0, 0, eax,edx,TRUE
  .elseif uMsg==WM_DESTROY
    invoke PostQuitMessage,NULL
  .else
    invoke DefWindowProc,hWnd,uMsg,wParam,lParam
    ret
  .endif
  xor eax,eax
  ret
WndProc endp
end start

Analyse:

La première chose que le programme fait lorsque la fenêtre principale est créée, c'est de créer notre listview control.

  .if uMsg==WM_CREATE
    invoke CreateWindowEx, NULL, addr ListViewClassName, NULL, LVS_REPORT+WS_CHILD+WS_VISIBLE, 0,0,0,0,hWnd, NULL, hInstance, NULL
    mov hList, eax

On appelle CreateWindowEx, en lui passant le nom de sa window class soit : "SysListView32". La vue par défaut est la vue 'détails' définie par le style LVS_REPORT.

    invoke InsertColumn

Après que le listview control est été créé, nous y insérons des colonnes.

  LOCAL lvc:LV_COLUMN

  mov lvc.imask,LVCF_TEXT+LVCF_WIDTH
  mov lvc.pszText,offset Heading1
  mov lvc.lx,150
  invoke SendMessage,hList, LVM_INSERTCOLUMN, 0, addr lvc

Nous initialisons le label (son nom) et la largeur de la première colonne, qui sert au stockage des noms de fichiers, dans la structure LV_COLUMN. Donc nous avons besoin de mettre les flags LVCF_TEXT et LVCF_WIDTH dans imask . On rempli pszText avec l'adresse du label et lx avec la largeur de la colonne, en pixels. Quand tout est prêt, nous envoyons le message LVM_INSERTCOLUMN au listview control, en lui passant cette structure.

  or lvc.imask,LVCF_FMT
  mov lvc.fmt,LVCFMT_RIGHT

Quand on en a fini avec l'insertion de la première colonne, nous en insérons une autre servant cette fois-ci au stockage des tailles de fichiers. Puisque nous avons besoin des tailles pour les aligner à droite dans la colonne, nous avons besoin de mettre le flag LVCFMT_RIGHT dans le membre fmt. Nous devons aussi mettre le flag LVCF_FMT dans imask, en plus de LVCF_TEXT et LVCF_WIDTH.

  mov lvc.pszText,offset Heading2
  mov lvc.lx,100
  invoke SendMessage,hList, LVM_INSERTCOLUMN, 1 ,addr lvc

Le code restant est simple. On Met l'adresse du label (du titre de la colonne) dans pszText et sa largeur dans lx. On envoie alors le message LVM_INSERTCOLUMN au listview control, en indiquant le nombre de colonnes et l'adresse de la structure.

Quand les colonnes sont insérées, nous pouvons placer les Items dans notre listview control.

    invoke FillFileInfo

FillFileInfo présente le code suivant.

FillFileInfo proc uses edi
  LOCAL finddata:WIN32_FIND_DATA
  LOCAL FHandle:DWORD

  invoke FindFirstFile,addr FileNamePattern,addr finddata

Nous appelons FindFirstFile pour obtenir l'information sur le premier fichier qui correspond à nos critères de recherche. FindFirstFile possède le prototype de fonction suivant :

FindFirstFile proto pFileName:DWORD, pWin32_Find_Data:DWORD

pFileName est l'adresse du nom du fichier qu'on chercher. Cette chaîne de caractères peut contenir des caractères de substitutions. Dans notre exemple, nous employons *.*, ce qui fait qu'on mène une recherche sur tous les fichiers du dossier actuel où on se trouve.
pWin32_Find_Data est l'adresse de la structure WIN32_FIND_DATA laquelle sera remplie de l'information du fichier (si on l'a trouvé).

Cette fonction renvoie INVALID_handle_VALUE dans eax si aucun fichier correspondant à ce que l'on cherche n'a été trouvé. Sinon l'handle de la recherche est renvoyée, lequel sera utilisé dans les appels suivant FindNextFile.

  .if eax!=INVALID_HANDLE_VALUE
    mov FHandle,eax
    xor edi,edi

Si un fichier est trouvé, on stocke l'handle de la recherche dans une variable et ensuite nous mettons edi à zéro pour pouvoir l'employé en tant qu'index pour les Items (c'est le numéro de rangée).

    .while eax!=0
      test finddata.dwFileAttributes,FILE_ATTRIBUTE_DIRECTORY
      .if ZERO?

Dans cet exemple, je ne souhaite rien toucher dans les autres dossiers, donc je les filtre en vérifiant dwFileAttributes. Comme ça, si on trouve des fichiers qui ont un flag FILE_ATTRIBUTE_DIRECTORY associés, on saute par dessus grâce à l'appel FindNextFile.

          invoke ShowFileInfo,edi, addr finddata
          inc edi
      .endif
      invoke FindNextFile,FHandle,addr finddata
    .endw

Nous insérons le nom et la taille du fichier dans le listview control en appelant la fonction ShowFileInfo. Donc on augmente le numéro de la rangée actuelle (dans edi). Finalement on continue à appeler FindNextFile pour chercher le fichier suivant dans le dossier actuel jusqu'à ce que FindNextFile retourne un 0 (Ce qui signifie qu'il n'y a plus de fichiers à chercher).

    invoke FindClose,FHandle
  .endif
  ret
FillFileInfo endp

Quand tous les fichiers dans le dossier actuel ont étés passés à la loupe, on doit refermer l'handle de la recherche.

Maintenant on va regarder la fonction ShowFileInfo. Cette fonction accepte deux paramètres, l'index de l'item (son numéro de rangée) et l'adresse de la structure WIN32_FIND_DATA.

ShowFileInfo proc uses edi row:DWORD, lpFind:DWORD
  LOCAL lvi:LV_ITEM
  LOCAL buffer[20]:BYTE
  mov edi,lpFind
  assume edi:ptr WIN32_FIND_DATA

On stocke l'adresse de la structure WIN32_FIND_DATA dans edi.

  mov lvi.imask,LVIF_TEXT+LVIF_PARAM
  push row
  pop lvi.iItem
  mov lvi.iSubItem,0

Nous donne le nom de l'item et sa valeur dans lParam donc nous mettons les flags LVIF_TEXT et LVIF_PARAM dans imask. Ensuite nous plaçons l'item dans la bonne rangée (dont on avait passé son numéro à la fonction) et puisque c'est l'item principal, on doit mettre un 0 dans iSubItem (car la première colonne, c'est la colonne 0).

  lea eax,[edi].cFileName
  mov lvi.pszText,eax
  push row
  pop lvi.lParam

Ensuite on met l'adresse de son nom, (dans ce cas), le nom du fichier dans pszText de la structure WIN32_FIND_DATA. Puisque nous effectuons un tri dans le listview control, nous devons remplir lParam avec une valeur. Je souhaite mettre le numéro de rangée dans ce membre donc je peux me servir des renseignements de l'item grâce à son index pour le retrouver.

  invoke SendMessage,hList, LVM_INSERTITEM,0, addr lvi

Lorsque tous les paramètres nécessaires dans LV_item sont remplis, on envoie le message LVM_INSERTITEM au listview control pour y insérer un item.

  mov lvi.imask,LVIF_TEXT
  inc lvi.iSubItem
  invoke wsprintf,addr buffer, addr template,[edi].nFileSizeLow
  lea eax,buffer
  mov lvi.pszText,eax

On place, dans la deuxième colonne, le sous-item associé à l'item qu'on vient juste d'insérer. Un sous-item peut seulement posséder un label (un titre). Donc on met LVIF_TEXT dans imask. Ensuite dans iSubItem, on indique la colonne dans laquelle le sous-item doit se trouver. Dans notre cas, nous met iSubItem à 1 en l'incrémentant. Le label que nous utiliserons représentera 'la taille du fichier'. Cependant, nous devons d'abord le convertir en une chaîne de caractères en appelant wsprintf. C'est pourquoi nous mettons l'adresse de la chaîne de caractères dans pszText .

  invoke SendMessage,hList,LVM_SETITEM, 0,addr lvi
  assume edi:nothing
  ret
ShowFileInfo endp

Quand tous les paramètres nécessaires à LV_item sont remplis, nous envoyons le message LVM_SETITEM à notre listview control, en lui passant l'adresse de la structure LV_ITEM. Remarquez que nous utilisons LVM_SETITEM, et non pas LVM_INSERTITEM parce qu'un sous-item est considéré comme étant une propriété de l'item. Donc rappelez vous de ça, c'est important, on *pose* (set) la propriété de l'item, mais on n'insère pas un nouvel item.

Lorsque tous les Items sont insérés dans le listview control, on défini le type de texte et les couleurs de fond de notre listview control.

    RGB 255,255,255
    invoke SendMessage,hList,LVM_SETTEXTCOLOR,0,eax
    RGB 0,0,0
    invoke SendMessage,hList,LVM_SETBKCOLOR,0,eax
    RGB 0,0,0
    invoke SendMessage,hList,LVM_SETTEXTBKCOLOR,0,eax

J'emploie la macro RGB pour convertir les valeurs rouge, verte, est bleue dans eax et les employer pour définir la couleur réelle que je souhaite obtenir. On défini les couleurs de premier plan et d'arrière plan du texte avec les messages LVM_SETTEXTCOLOR et LVM_SETTEXTBKCOLOR. On défini la couleur de fond de notre listview control en lui envoyant le message LVM_SETBKCOLOR.

    invoke GetMenu,hWnd
    mov hMenu,eax
    invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, IDM_REPORT,MF_CHECKED

On laisse l'utilisateur choisit ses vues grâce au menu. Donc on doit d'abord récupérer l'handle du menu. Pour aider l'utilisateur à retrouver la vue actuelle, on incorpore un système de bouton radio dans notre menu. L'item du menu qui est associé à la vue actuelle sera ainsi précédé d'un bouton tout rond. Pour faire ça, on doit appeler CheckMenuRadioItem. Cette fonction affichera un bouton radio juste devant l'item voulu dans votre menu.

Remarquez qu'on crée un listview control d'une largeur et d'une hauteur égales à 0. Elle sera redimensionnée plus tard à chaque fois que la fenêtre parente sera elle-même redimensionnée. De cette façon, on peut être sûr que la taille de notre listview control correspondra toujours à celle de la fenêtre parente. Dans notre exemple, on souhaite que le listview control remplisse la totalité du secteur de client de la fenêtre parente.

  .elseif uMsg==WM_SIZE
    mov eax,lParam
    mov edx,eax
    and eax,0ffffh
    shr edx,16
    invoke MoveWindow,hList, 0, 0, eax,edx,TRUE

Quand la fenêtre parente reçoit le message WM_SIZE, le mot de poids faible de lParam contient la nouvelle largeur du secteur client et le mot de poids fort la nouvelle hauteur. Ensuite on appelle MoveWindow pour redimensionner le listview control pour qu'il recouvre le secteur client de la fenêtre parente, en entier.

Lorsque l'utilisateur sélectionne une des vue dans le menu. On doit faire en sorte de changer la vue du listview control en conséquence. On fait ça en mettant un nouveau style dans le listview control avec SetWindowLong.

  .elseif uMsg==WM_COMMAND
    .if lParam==0
      invoke GetWindowLong,hList,GWL_STYLE
      and eax,not LVS_TYPEMASK

La première chose à faire, c'est obtenir les styles actuels du listview control. Donc on purifie le vieux type de vue renvoyé dans les flags. LVS_TYPEMASK est une constante qui représente la valeur combinée des 4 types de vue (LVS_ICON+LVS_SMALLICON+LVS_LIST+LVS_REPORT). Ainsi quand on exécute l'opération and sur les types actuels de flags avec la valeur "not LVS_TYPEMASK", ça équivaut à nettoyer le type de vue actuel.

Dans la conception du menu, je triche un peu. J'emploie le type de vue des constantes en tant qu'IDs du menu.

IDM_ICON equ LVS_ICON
IDM_SMALLICON equ LVS_SMALLICON
IDM_LIST equ LVS_LIST
IDM_REPORT equ LVS_REPORT

Ainsi quand la fenêtre parente reçoit le message WM_COMMAND, le type de vue voulu est dans le mot de poids faible de wParam pour représenter l'ID du menu.

      mov edx,wParam
      and edx,0FFFFh

On retrouvera le type de vue souhaité dans le mot de poids faible de wParam. Tout ce que nous devons faire c'est de mettre le mot de poids fort à 0 (je parle de wParam).

      push edx
      or eax,edx

Et ajoutez le type de vue désiré aux styles déjà existants (moins le style de vue actuel) du listview control.

      invoke SetWindowLong,hList,GWL_STYLE,eax

Et mettez les nouveaux types avec SetWindowLong.

      pop edx
      invoke CheckMenuRadioItem,hMenu,IDM_ICON,IDM_LIST, edx,MF_CHECKED
   .endif

Nous avons aussi besoin de mettre le bouton radio devant l'item choisi dans notre menu. Ainsi on appelle CheckMenuRadioItem, en lui passant le type de vue actuel (en double, en tant qu'ID du menu).

Lorsque l'utilisateur clique sur l'en-tête de la colonne dans la vue 'détails', on fait en sorte de déclencher le comportement de triage des Items dans notre listview control. Nous devons répondre au message WM_NOTIFY.

  .elseif uMsg==WM_NOTIFY
    push edi
    mov edi,lParam
    assume edi:ptr NMHDR
    mov eax,[edi].hwndFrom
    .if eax==hList

Quand on reçoit le message WM_NOTIFY, lParam contient le pointer qui est sur la structure NMHDR. On peut vérifier si ce message provient de notre listview control en comparant le membre hwndFrom dans NMHDR à l'handle de notre listview control. S'ils correspondent, alors on est sûr que cet avis provient bien de notre listview control.

      .if [edi].code==LVN_COLUMNCLICK
        assume edi:ptr NM_LISTVIEW

S'il provient bien du listview control, on vérifie si le code est LVN_COLUMNCLICK. Et si c'est ça, ça signifie que l'utilisateur a cliqué sur l'en-tête de la colonne. Dans le cas où le code serait LVN_COLUMNCLICK, on est sûr que lParam contient le pointer qui est sur la structure NM_LISTVIEW laquelle est un 'superset' de la structure NMHDR. On aura alors besoin de savoir sur lequel des en-tête de colonne, l'utilisateur à cliqué. L'examen du membre iSubItem nous révèle ces renseignements. La valeur dans iSubItem peut être considérée comme le numéro de colonne, en commençant par 0 pour la première colonne.

        .if [edi].iSubItem==1
          .if SizeSortOrder==0 || SizeSortOrder==2

Dans le cas où iSubItem est égal à 1, ça signifie que l'utilisateur à cliqué sur la deuxième colonne, (la taille). On utilise des variables d'état pour récupérer le statut actuel de l'ordre de triage. 0 signifie "Pas encore trié", 1 signifie qu'"on tri l'item en le faisant monté d'une place dans la hiérarchie", 2 signifie qu'"on tri l'item en le faisant descendre dans la hiérarchie" des items. Si les Items ou sous-items de la colonne n'ont pas été trié auparavant, ou on été trié en ayant perdu des places, on impose un tri pour les remonter.

            invoke SendMessage,hList,LVM_SORTITEMS,1,addr CompareFunc

On envoie le message LVM_SORTITEMS à notre listview control, en lui passant 1 dans wParam et l'adresse de notre fonction de tri dans lParam. Remarquez que la valeur dans wParam est définie par l'utilisateur, vous pouvez l'employer de la façon que vous le souhaitez. Je l'emploie en tant que méthode de triage dans cet exemple. Bon, on va d'abord jeter un coup d'oeil à la fonction de tri.

CompareFunc proc uses edi lParam1:DWORD, lParam2:DWORD, SortType:DWORD
  LOCAL buffer[256]:BYTE
  LOCAL buffer1[256]:BYTE
  LOCAL lvi:LV_ITEM

  mov lvi.imask,LVIF_TEXT
  lea eax,buffer
  mov lvi.pszText,eax
  mov lvi.cchTextMax,256

Dans la fonction de tri, le listview control passera les lParams (provenant de LV_item) de deux Items dont il a besoin de comparer pour les triés l'un par rapport à l'autre, soit lParam1 et lParam2. Vous vous rappellerez qu'on a mis l'index des l'items dans lParam. Ainsi on peut récupérer l'information des Items en interrogeant le listview control en employant ces index. Les renseignements dont nous avons besoin sont les labels (leurs noms) des Items / sous-items étant triés. Ainsi on prépare la structure LV_ITEM dans ce but, en déclarant LVIF_TEXT dans imask et l'adresse du buffer dans pszText et la taille du buffer dans cchTextMax.

  .if SortType==1
    mov lvi.iSubItem,1
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi

Si la valeur dans SortType est 1 ou 2, on sait que la colonne (de taille) a été cliquée. 1 signifie que les Items ont été triés selon leurs tailles dans un ordre montant. 2 signifie le contraire. Ainsi, on impose iSubItem à 1 (pour spécifier la colonne de taille) et on envoie le message LVM_GETITEMTEXT à notre listview control pour obtenir le label (le nom) de la chaîne de caractères (de taille) du sous-item.

    invoke String2Dword,addr buffer
    mov edi,eax

On cache plus ou moins la taille de la chaîne de caractères dans une valeur Dword avec la fonction String2Dword que j'ai écrite. Elle renvoie cette valeur Dword dans eax. Et en plus on la stocke dans edi pour la trier par la suite.

    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke String2Dword,addr buffer
    sub edi,eax
    mov eax,edi

Faites de même avec la valeur dans lParam2. Quand nous avons les tailles des deux fichiers, nous pouvons alors les comparer.
La règle de la fonction de tri est définie ainsi :

  • Si le premier item doit être placé avant l'autre, vous devez renvoyer une valeur négative dans eax
  • Si le premier item doit être placé après le second, vous devez renvoyer une valeur positive dans eax
  • Si les deux Items sont égaux, on renvoie un zéro dans eax.

Dans ce cas, on souhaite trier les Items selon leurs critères de tailles par ordre croissant. Donc on peut utiliser la ruse d'une simple soustraction des tailles des deux items et renvoyer le résultat dans eax.

  .elseif SortType==3
    mov lvi.iSubItem,0
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam1,addr lvi
    invoke lstrcpy,addr buffer1,addr buffer
    invoke SendMessage,hList,LVM_GETITEMTEXT,lParam2,addr lvi
    invoke lstrcmpi,addr buffer1,addr buffer

Dans le cas où l'utilisateur clique sur la colonne 'de nom de fichier', nous devons forcément comparer les noms des fichiers. Il nous faut d'abord récupérer les noms de fichier et on les compare ensuite avec la fonction 'lstrcmpi'. On peut renvoyé la valeur de retour de lstrcmpi sans aucune modification puisqu'elle emploie aussi la même règle de tri. C'est à dire qu'on aura une valeur négative dans eax si la première chaîne de caractères est moindre (B<A...) que la deuxième.

Une fois que les Items ont été triés, nous avons besoin de remettre à jour les valeurs de lParam de chaques Items pour refléter les nouveaux index en appelant la fonction UpdatelParam.

            invoke UpdatelParam
            mov SizeSortOrder,1

Cette fonction énumère simplement tout les Items du listview control et met à jour les valeurs dans lParam avec les nouveaux index. On doit absolument faire ça sinon le tri suivant ne marchera pas comme attendu, parce qu'on suppose que la valeur dans lParam est l'index de l'item.

      .elseif [edi].code==NM_DBLCLK
        invoke ShowCurrentFocus
      .endif

Quand l'utilisateur double clique sur l'item, on souhaite que son nom s'affiche dans une boîte de message. On doit donc vérifier si le code dans NMHDR est NM_DBLCLK. Si c'est ça, on peut continuer pour récupérer son nom et le montrer dans la MessageBox.

ShowCurrentFocus proc
   LOCAL lvi:LV_ITEM
   LOCAL buffer[256]:BYTE

   invoke SendMessage,hList,LVM_GETNEXTITEM,-1, LVNI_FOCUSED

Comment savons-nous quel item vient d'être double cliqué ? Et bien, lorsque un item est cliqué ou double cliqué, son état est remis à "jour". Même si beaucoup d'Items sont (hilited) sélectionnés avec un encadré pointillé, uniquement un d'entre eux a le centre (est réellement sélectionné). Notre travail, c'est de retrouver l'item qui a le centre. Nous faisons ça en envoyant le message LVM_GETNEXTITEM à notre listview control, en spécifiant l'état désiré dans lParam. -1 dans wParam signifie qu'on recherche tous les Items. L'index de l'item est renvoyé dans eax.

   mov lvi.iItem,eax
   mov lvi.iSubItem,0
   mov lvi.imask,LVIF_TEXT
   lea eax,buffer
   mov lvi.pszText,eax
   mov lvi.cchTextMax,256
   invoke SendMessage,hList,LVM_GETITEM,0,addr lvi

On continue alors pour récupérer son titre (son nom) en envoyant le message LVM_GETITEM au listview control.

   invoke MessageBox,0, addr buffer,addr AppName,MB_OK

Finalement, nous affichons le nom du fichier dans une MessageBox.

Si vous souhaitez savoir comment utiliser des icônes dans votre listview control, vous pouvez lire ça dans mon tutorial sur le treeview (Tutorial 19). Les étapes sont à peu près les mêmes.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Vocabulaire

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

ASSEMBLAGE (Phase d') : Il s'agit en fait de savoir comment créer un programme à partir d'un fichier texte nommé *.asm
Voilà, vous avez un fichier nommé : ESSAI.ASM
Certains auront en plus un fichier de resource:
- il peut prendre le même nom que son fichier ESSAI.RC
- Ou bien un nom défini par défaut RSRC.RC

Ces deux fichiers (ou bien seulement ESSAI.ASM si il n'a pas de ressources) doivent êtres placés dans un dossier où se trouvent aussi les fichiers suivants:
- ML.exe
- RC.exe (+Rcdll.dll)
- LINK.exe (+Mspdb50.dll)

ensuite il suffit d'écrire sous dos:
C:\......\ml /c /coff /Cp ESSAI.ASM
C:\......\rc ESSAI.RC (ou bien : rc RSRC.RC)
C:\......\link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib ESSAI.obj ESSAI.res
(ou bien RSRC.res) Attention à ce 'c', si vous avez installés masm32 sur d: devinez ce que vous devez faire.

Pour vous aider, j'ai automatisé tout ça dans un petit programme en *.bat qui s'appelle 'Assemble_le.bat'. Il s'occupera de tout pour vous, à cette condition près: Vous installez masm32 sur c: et créer votre sous dossier spécial assemblage (peu importe son nom) sous masm32. Ce qui pour moi donne c:\masm32\_essai\ Dans ce dossier vous devez forcément mettre ml, rc, link, Cvtres et les deux dll. Placez y aussi Assemble_le.bat et votre fichier à assembler sous le nom unique de essai.asm, si vous avez un fichier de ressource en plus il doit aussi s'y trouver et s'appelé essai.rc ou rsrc.rc.

Le même processus peut être entrepris pour ASSEMBLER UNE DLL.

CHILD WINDOW :
Voir 'Control'.

Classe de fenêtre
Voir 'Window Class'

COMMAND LINE
Le paramètre lpCmdLine est un pointeur vers la ligne de commande ayant executé l'application (du style "c:\monapp.exe /v"), ce qui est utile pour récupérer des paramètres en ligne de commande. Vous obtiendrez ce pointeur en appelant la fonction GetCommandLineA.
(info pêchée dans un Tut sur Nasm d'X-or)

Control
Control n'est pas une traduction du mot français Contrôle, rien à voir. Un Control est un objet. Par exemple vous avez créé votre fenêtre principale, si vous y ajoutez une boite de saisie de texte, des boutons, une barre de scrolling verticale et horizontale (pour faire défiler un texte plus grand qu'une page), tout ça sont des 'Controls' ou bien des 'Child Window'. D'ailleurs, dans certains tuts vous trouverez: 'Child Window Controls'. Donc quand on parle d'un 'Child Window' ou une 'Fenêtre Enfant' ce n'est pas une fenêtre comme on l'entend de la même manière que notre fenêtre principale mais c'est un objet quelconque un 'Control' qui est commandé par une vraie fenêtre ou bien même un autre Control mais de plus haute autorité. Comme la 'fenêtre principale' est dite 'la fenêtre parente' de tous les sous objets tels que les boutons qui la composent... qui sont ses objets enfants ou ses 'Child Window'

handle
Sachez qu'un handle est une sorte d'index dans un tableau contenant des caractèristiques. Ici windows tient un tableau d'info sur le programme et notre numéro d'handle lui permet de savoir ou regarder dans son tableau pour avoir le numéro qu'on lui donne.
(info pêchée dans un Tut sur Nasm d'X-or)

Chaque objet possède son propre et unique Handle, la fenêtre principale ses fenêtres enfants, les boutons les boîtes d'édition...etc. Un Handle, c'est le n° qui sert à faire appel à un objet. Un programme se sert des handles pour savoir comment sont liés les objets les uns par rapport aux autres. Par exemple une fenêtre pincipale portant le Handle=0054 ouvre une MessageBox qui aura comme dernier paramètre 0054 qui représente le handle de son parent. Ainsi on sait a qui appartient cette MessageBox.
Si on a récupéré du texte dans un Buffer (un espace de mémoire pour stocker des infos) et qu'on souhaite envoyer ce texte vers un receveur, par exemple une boîte d'édition, une fenêtre fille, ou un bouton ou la fenêtre principale, Alors on doit envoyer ce texte vers le Handle du bouton pour que le texte de ce bouton soit remplacé par le nouveau. Sans Handle, ce texte n'aurait pas su à quel objet il était destiné.

hInstance
Le paramètre hInstance est un nombre identifiant le programme en cours d'execution. hInstance signifie 'handle Instance', sachez qu'un handle est une sorte d'index dans un tableau contenant des caractèristiques. Ici windows tient un tableau d'info sur le programme et notre numéro d'handle lui permet de savoir ou regarder dans son tableau pour avoir le numéro qu'on lui donne. Vous pouvez l'obtenir en appelant la fonction GetModuleHandleA qui renvoie l'Handle (nb) correspondant à la tache en cours. Notez que sous win32, Handle=hInstance.
(info pêchée dans un Tut sur Nasm d'X-or)

HUTCH : Le hutch est l'ensemble des fichiers *.inc . Tous les fichiers INCLUDE font parti du hutch.

-------------------------------------------------------
| THE HUTCH |
| |
| |--------------------------------------------| |
| | Fichier INCLUDE n°1 (extension *.inc) | |
| |--------------------------------------------| |
| |
| |--------------------------------------------| |
| | Fichier INCLUDE n°2, n°3, n°4, n°...etc | |
| |--------------------------------------------| |
| |
| |
| |--------------------------------------------| |
| | Windows.inc est aussi un fichier | |
| | INCLUDE (c'est le principal fichier du hutch) | |
| |--------------------------------------------| |
| |
-------------------------------------------------------

MOT : (Le mot de poids faible ou de poids fort)
Par exemple si LParam est une variable de 32 bit (un DWord)
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
et qu'on rempli le mot (the Word donc 16 bit) de poids fort de LParam on aura ça:
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Si au contraire on avait rempli le mot de poids faible de LParam on aurait eu ceci:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

En fait, quand on change la valeur du mot de poids fort de LParam, on ne met pas que des 1 mais une suite de 1 et de 0, mais surtout ça signifie qu'on ne touche en aucun cas à sa partie basse. Et inversement.

PROCEDURE (de fenêtre, de messages, de debugging<-- j'arrive pas à traduire ça) :
La 'procédure de fenêtre' est la procédure principale de notre programme. En fait, c'est un bout de code qui forme une boucle sur lui-même et qui s'occupe d'afficher tous les éléments à l'intérieur de notre fenêtre (il gère les boutons...)
La 'procédure de messages' c'est en fait aussi la même chose mais en plus, on fait en sorte qu'elle s'occupe de traiter des messages quand l'un d'eux lui parvient. Ex: Si on clique sur le bouton, elle le détecte et on peut utiliser cette information.
La 'procédure de debugging' c'est encore pareil, c'est une procédure de messages quelconque à ceci prêt que les seuls messages qui nous intéressent dans ce cas sont des messages provoqués par des événement qui sont spécifiques à tous ce qui touche le débogage. (Genre : Ouverture d'un nouveau lien, création d'un process, lire ou écrire dans un octet d'un autre programme...etc)

PROTOTYPE DE FONCTION : Le prototype d'une fonction n'est en aucun cas une instruction du language Assembleur. ça n'a rien à voir avec des instructions comme 'Mov', 'Push', 'Call'... c'est simplement une notice pour qu'on sache comment utiliser une fonction. Mais jamais on écrira un prototype de fonction dans le programme lui-même.
Exemple: Le 'Prototype de Fonction' de 'ExitProcess' est ExitProcess proto uExitCode:DWORD. ce qui signifie que la fonction ExitProcess s'utilise avec une seule valeur de type Dword. D'après ce qu'on vient d'appendre de son prototype de fonction juste au-dessus, vous devrez écrire dans votre programme un truc dans le genre :
invoke ExitProcess, 0 (Si vous faites appelle à un fichier INCLUDE)
Ou bien:
Push 0 (Si vous ne faites pas appelle à un fichier INCLUDE)
Call ExitProcess (ça, c'est de l'assembleur tout ce qu'il y a de plus classique)

WINDOW CLASS :
Une classe de fenetre est une structure WNDCLASSEX (EX pour extended, vous pouvez utiliser la structure WNDCLASS mais elle propose moins d'options) contenant toutes les informations concernant la fenetre : sa taille, ses attributs, la fonction chargée de gérer les messages qui lui sont envoyés, etc... Vous commencez donc par la remplir.
Ensuite, vous l'enregistrez avec la fonction RegisterClassExA (respectivement RegisterClass), vous créez la fenetre avec la fonction CreateWindowExA, vous l'affichez avec la fonction ShowWindow et vous rentrez dans la boucle des messages. Cette dernière est arrêtée par l'envoi d'un message de fermeture à l'application. Le rôle de cette boucle est de demander à windows de lui envoyer un message de la liste d'attente (par la fonction GetMessageA), de le traduire (par la fonction TranslateMessage qui par exemple traduit les scan codes du clavier en codes ASCII) et de l'envoyer à la fenetre de l'application à laquelle elle est destinée (par la fonction DispatchMessageA).
(info pêchée dans un Tut sur Nasm d'X-or)


ASSEMBLER UNE DLL

Si vous avez parcouru les Tuts d'Aczelion, vous avez dû voir dans le Tut 17 comment programmer vos propres DLL (Dynamic Link Libraries), il donne également une brève explication pour les assembler mais revenons sur ce point. Je ne vais pas m'attarder de trop, on va passer à la pratique, ça vaut bien toutes les explications.

Faites l'exemple avec moi:

Je pratique sur la DLL du Tut24 'MouseHook.asm'. D'abord il faut étudier comment elle a été construite et c'est extrêmement important. Car pour compiler une DLL il faut deux fichiers. le fichier *.asm et le fichier *.def qui en découle.

;--------------
;MouseHook.asm
;--------------
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib

.const
WM_MOUSEHOOK equ WM_USER+6
.data
hInstance dd 0

.data?
hHook dd ?
hWnd dd ?

.code
DllEntry proc hInst:HINSTANCE, reason:DWORD, reserved1:DWORD ;<-- on commence toujours pas
    push hInst				   		; déclarer le point d'entrée
    pop hInstance					  	; de la DLL.
      mov  eax,TRUE						; Mais peu importe...
      ret
DllEntry Endp

MouseProc proc nCode:DWORD,wParam:DWORD,lParam:DWORD	; Ce qui nous intéressent ce sont les
    invoke CallNextHookEx,hHook,nCode,wParam,lParam	; noms des fonctions appartenant à la
    mov edx,lParam					; DLL
    assume edx:PTR MOUSEHOOKSTRUCT
    invoke WindowFromPoint,[edx].pt.x,[edx].pt.y
    invoke PostMessage,hWnd,WM_MOUSEHOOK,eax,0
    assume edx:nothing
    xor eax,eax
    ret
MouseProc endp

InstallHook proc hwnd:DWORD
    push hwnd
    pop hWnd
    invoke SetWindowsHookEx,WH_MOUSE,addr MouseProc,hInstance,NULL
    mov hHook,eax
    ret
InstallHook endp

UninstallHook proc
    invoke UnhookWindowsHookEx,hHook
    ret
UninstallHook endp

End DllEntry

Et voilà à partir du contenu de la DLL on va pouvoir créer le fichier *.def. Puisqu'on sait que notre MouseHook.asm contient les fonctions MouseProc, InstallHook et UninstallHook, le fichier *.def sera ainsi définit:
;-------------
;MouseHook.def
;-------------
LIBRARY MouseHook  ; <-- c'est le nom de votre DLL

EXPORTS		   ; <-- et les fonctions qu'elle met à notre disposition.
  MouseProc
  InstallHook
  UninstallHook
Voilà vous écrivez ça dans MouseHook.def

Maintenant nous avons les deux fichiers en question (*.asm et *.def), nous pouvons passer à la phase d'assemblage de la DLL en elle-même.

D'abord sachez que j'ai créé un dossier spécial assemblage en tout genre dans mon dossier Masm32. Mon dossier s'appelle C:\Masm32\_essai et dedans se trouvent les petits outils indispenssables que sont :
- ML.exe
; - RC.exe (+Rcdll.dll)
; ça c'est pour passer - LINK.exe (+Mspdb50.dll)
; d'un *.asm --> *.exe - Cvtres.exe
; Normalement ils sont présent dans c:\Masm32\bin
Pour une DLL vous devez en plus rajouter:
- Lib.exe

Allons-y gaiement:
J'ouvre une fenêtre DOS et je me déplace dans ce répertoire:
cd\
cd Masm32
cd _essai
Pour vous se sera normalement cd bin

ET VOICI LA VÉRITABLE OPÉRATION:

(Les majuscules et minuscules n'ont pas d'importance sur le nom de votre DLL mais elles en ont pour le reste. Attention au paramètre /Cp par exemple.)
ml /c /coff /Cp mousehook.asm
link /DLL /NOENTRY /def:mousehook.def /subsystem:windows mousehook.obj

J'ai automatisé ce processus dans un fichier *.bat nommé Assembles_DLL.bat. Vous pourrez vous en servir en plaçant Assembles_DLL dans ..C:\Masm32\Bin ou pour moi ..C:\Masm32\_essai. Placez dans le même répertoire vos deux fichiers *.asm et *.def sous le nom de 'DLL.asm' et 'DLL.def'. Exécutez 'Assembles_DLL' et vous obtiendrez une DLL du nom de (Et non pas DLL.dll) Nom_qui_est_dans_DLLdef.dll. Il sera donc inutile de la renommer.

Par Morgatte


Tutorial Window

- Chap.1: Création d'une fenêtre -

Ce chapitre risque de vous rebuter quelque peu, mais il faut savoir qu'en fait, il est le plus difficile à assimiler. Cependant je vais vous faciliter la tâche. En effet seront présentées ici des fonctions comportant parfois une douzaine d'arguments. Je prendrai alors le parti suivant: celui de ne pas documenter les arguments qui servent rarement. Si le coeur vous en dit, vous pourrez toujours vous y intéresser à travers la documentation Microsoft.

De même, il faut savoir que le squelette présenté ici sert quasiment pour 90 % des applications. Il semble donc plutôt évident que s'en servir comme base d'un programme est la meilleure façon de commencer un projet.

- 1.1: Fonctionnement d'une application -

Avant toute chose, il faut connaître la manière dont se comporte une application Windows. Celle-ci est identifiée par une variable nommée "variable d'instance". Le type donné par Windows est HINSTANCE. Nous pourrons par exemple déclarer une variable d'instance par :

  hInstance HINSTANCE ?

Où 'hInstance' est le nom qu'on aurra choisi pour identifié cet instance plus tard, alors que 'HINSTANCE' est le type de cette variable. On peut donc choisir un autre nom que 'hInstance'. Tel que:
hInstanceProgPrincipal HINSTANCE ?

Une application est composée d'une fenêtre principale, laquelle contient des fenêtre filles, lesquelles peuvent elles-même contenir des fenêtres filles, etc ... La dépendance mère/fille se matérialise par la propagation de la prise de focus, de la destruction des fenêtres filles quand la fenêtre mère est détruite, etc ... (il serait en effet bizarre par exemple que Notepad soit quitté et que le contrôle d'édition de texte reste flottant). Chacune de ces fenêtres sont-elles aussi identifiées par une variable nommée "handle". Le type donné par Windows est HWND. Nous pourons par exemple récupérer le handle d'une fenêtre à l'aide d'une variable déclarée comme suit :

  LOCAL hwnd:HWND

Ici encore c'est bien 'hwnd' qui est la variable alors que 'HWND' est le type de variable. Ici on déclara donc la variable 'hwnd' (qu'on aurait pu appeler 'HandleFenetrePrincipale' par exemple) en tant que variable de type: handle. On aurrait donc tout aussi bien pu mettre,
LOCAL HandleFenetrePrincipale:HWND

Voilà tout pour la manière dont Windows identifie tout son beau monde. Plus tard nous apprendrons que Windows identifie le reste de son environnement à l'aide de contexte de périphérique (imprimante, clavier, affichage zone écran, etc ...).

C'est bien beau de savoir comment Windows identifie les fenêtres, mais comment permettre à l'application de dialoguer avec l'OS et les autres fenêtres ? Et bien tout simplement à l'aide d'envoi de messages. Ceux-ci sont destinés soit à Windows, soit à une autre fenêtre. Toute fenêtre a alors un comportement défini face à la réception de ceux-ci.
Par exemple, prenons une fenêtre texte d'handle hWndText affichant le résultat d'une compilation. Celui-ci devient visible par clik sur un bouton "Afficher Resultat" d'handle hWndButton. Il faut donc que lorqu'un clik se produise sur le bouton, un message soit envoyé à la fenêtre texte pour qu'elle affiche le résultat ! Ces envois se font à l'aide de la fonction SendMessage() que nous détaillerons plus loin. Pour info, nous aurons dans notre cas :

  invoke SendMessage, hWndText, WM_SETTEXT,IDM_GETTEXT,0

Reste à savoir que nombre de messages sont générés par Windows lui-même, ou situés dans le code des contrôle communs comme le simple clik d'un bouton qui envoi le message WM_CLICK.

- 1.2: Inclusions et prototypes -

Première chose à savoir, les inclusions nécessaires. Déclarez toujours 'windows.inc'. Par contre vous n'aurez besoin de déclarer 'user32.inc' uniquement si vous faites appel à une de ses fonctions API (tel que SetMessage...) c'est la même chose pour Kernel (ex: ExitProcess) et les autres...

  include \masm32\include\windows.inc

  include \masm32\include\user32.inc
  include \masm32\include\kernel32.inc
  includelib \masm32\lib\user32.lib
  includelib \masm32\lib\kernel32.lib
  ...

C'est quand même beau la vie ! Vous avez alors à disposition tout le nécessaire pour programmer. Bien qu'il soit possible d'utiliser les fonctions C-ANSI, il est préférable de s'adapter à Windows, à l'aide des fonctions propres à Windows (comme "lstrcmp()" au lieu de "strcmp()", ou encore "GlobalAlloc(GPTR, ...)" au lieu de "malloc(...)").

Ensuite le prototype principal. Pas de "int main(int argc, char **argv);", mais celui-ci :

  WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

Où HINSTANCE, LPSTR, et DWORD représentent les types de ces données.
hInst et hPrevInst seront dont deux handles d'instances
CmdLine sera donc une chaîne de caractères (du texte en fait)
et CndShow un nombre de dimension DWORD donc sur 32bit (4 octets)

Voici le détail des arguments passés par windows au lancement de l'application :

- hInst : la variable d'instance de l'application (Windows nous le donne gentillement)
- hPrevInst : c'est rigolo, çà sert à rien en fait :) (toujours 0)
- CmdLine : Une chaine représentant les arguments en ligne (utile pour récupérer le nom d'un fichier lors d'un double-clik sur celui-ci lorsque son extension est associée à notre programme).
- cmdShow : La manière dont notre application doit être affichée (minimisée, normale, agrandie ...)

Il est utile de récupérer la variable d'instance. Notre programme basé sur nos connaissances actuelles a donc cette forme :

  ...
  ...
  ...
  include \masm32\include\windows.inc

  include \masm32\include\user32.inc
  include \masm32\include\kernel32.inc
  includelib \masm32\lib\user32.lib
  includelib \masm32\lib\kernel32.lib

  WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD

  .DATA
   ...
   ...

  .DATA?
  hInstance HINSTANCE ?
  CommandLine LPSTR ?
   ...
   ...

  .CODE
   ...
   ...
   ...
  invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
   ...
   ...

- 1.3: Classes de fenêtres -

Nous allons maintenant créer notre fenêtre. Une fenêtre est un objet, et comme tout objet (pour peu que vous connaissiez la programmation objet), celui-ci est instancié à partir d'une classe. Pour ceux qui ne maitrisent pas ce genre de concept, il s'agit en fait d'un "moule" à partir duquel notre fenêtre saura prendre forme (si vous êtes rebutez, partez du principe qu'une classe est une sorte de structure dont les valeurs définnisent la "forme" des objets qui s'y réfèrent).
Pour cela nous vient en aide une classe de fenêtre. Il nous faut donc initialiser certaines de ses variables (je réserve le terme de "propriété" aux experts). Il est préférable de le faire à l'aide d'une fonction pour la lisibilité du code. Sachez, avant la présentation du programme, que le type "classe de fenêtre" sous Windows est WNDCLASS.

Voici notre fonction d'initialisation d'une classe de fenêtre :

 WNDCLASSEX STRUCT DWORD
  cbSize            DWORD      ?	   ;size = SIZEOF WNDCLASSEX
  style             DWORD      ?	   ;style = 0 ou CS_HREDRAW
  lpfnWndProc       DWORD      ?	   ;lpfnWndProc = WindowProcedure (OFFSET WndProc)
  cbClsExtra        DWORD      ?	   ;cbClsExtra = 0
  cbWndExtra        DWORD      ?	   ;cbWndExtra = 0
  hInstance         DWORD      ?	   ;hInstance = hInst
  hIcon             DWORD      ?	   ;hIcon = invoke LoadIcon....
  hCursor           DWORD      ?	   ;hCursor = invoke LoadCursor....
  hbrBackground     DWORD      ?	   ;hbrBackground = COLOR_3DFACE +  1
  lpszMenuName      DWORD      ?	   ;lpszMenuName = 0
  lpszClassName     DWORD      ?	   ;lpszClassName = MonNomDeClasse
  hIconSm           DWORD      ?	   ;hIconSm = NULL
 WNDCLASSEX ENDS

Voici le détail des champs que nous modifions :

- style : le style de notre fenêtre. Positionnez à zéro.
- lpfnWndProc : il s'agit ici de passer le nom d'une fonction très utile, celle qui va gérer la réception des évènements !!! J'en présente une de base à la fin de ce chapitre.
- cbClsExtra et cbWndExtra : servent à l'allocation d'octets supplémentaires. Mettez à zéro.
- hInstance : Fournir l'instance de l'application.
- hCursor : Quel pointeur voulez-vous ? ben celui par défaut ! Cool, la fonction LoadCursor() nour le permet !
- hbrBackground : Si on ne modifie pas ce champou qu'on met (COLOR_WINDOW + 1), le fond de notre fenêtre sera blanc. Ici, on affecte la couleur système de fond de fenêtre.(COLOR_3DFACE + 1)
- lpszMenuName : Mettez zéro. On préfèrera placer un menu d'une autre manière.
- lpszClassName : Le nom que vous donnez à la classe. Préférez utiliser une variable globale de pointeur sur char, le nom de la classe sera réutiliser.
- hIcon : Nous chargerons ici une icône dans le 2ème chapitre.

Ainsi, notre WinMain() s'étoffe :

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    {
        Structure WNDCLASS wndClass	; pour initialiser la Window Class
        hInst = hInstance  		; en retour
        invoke RegisterClassEx, addr wc ; Pour enregistrer cette classe

    }

Nous avons une instance d'application et une classe de fenêtre bien définie, ce n'est pas si mal ! C'est même dejà beaucoup !!!

- 1.4: Phase de création principale -

En effet, tout va maintenant très vite ! Il nous reste à créer notre fenêtre à l'aide de CreateWindow(), qui retourne le handle de la fenêtre créée (lequel doit être récupéré dans une variable globale). Voici comment cela se passe :

  HWND hWnd;

  WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    {
        Structure WNDCLASS wndClass	; pour initialiser la Window Class
        hInst = hInstance  		; en retour
        invoke RegisterClassEx, addr wc ; Pour enregistrer cette classe

        invoke CreateWindowEx("MonNomDeClasse",	  	 	  ; Structure
                             // le nom de la classe de fenêtre	  ; CreateWindow
                        "Programmation API Win32",
                             // le titre de la fenêtre
                        WS_OVERLAPPEDWINDOW,
                             // le style de la fenêtre
                        CW_USEDEFAULT, CW_USEDEFAULT,
                             // ses positions x et y au début
                        300, 200,
                             // la taille dx et dy de la fenêtre
                        NULL,
                             // le handle de sa fenêtre parent
                        NULL,
                             // réservé au chargement d'un menu
                        hInst,
                             // l'instance de l'appli
                        NULL
                             // mettre toujours à NULL
                       );

        invoke ShowWindow, hwnd,CmdShow		; on affichee notre fenêtre
    }

Rien de bien compliqué je pense. Sinon, regardez ce que dit la doc Microsoft sur cette fonction. Pour les styles de fenêtres, ils sont innombrables. Ils se combinent à l'aide de l'opérateur OR. En général WS_OVERLAPPEDWINDOW convient dans la plupart des cas. Sinon, voici une liste peu exhaustive (le site proposera par la suite un ensemble de références plus consistant) :

- WS_BORDER : crée une fenêtre avec bordure.
- WS_CAPTION : crée une fenêtre avec barre de titre.
- WS_OVERLAPPED : combine les 2 précédents.
- WS_SYSMENU : ajoute le menu système en haut à gauche dans la barre de titre.
- WS_THICKFRAME : Possibilité de changer la taille de la fenêtre à la souris.
- WS_MINIMIZEBOX : Bouton "réduire" en haut à droite.
- WS_MINIMIZEBOX : Bouton "agrandir" en haut à droite.
- WS_OVERLAPPEDWINDOW : Combinaison de tous les styles précédents.
- WS_MAXIMIZE : La fenêtre est créée avec la taille maximum.
- et bien d'autres encore ...

Pour le dernier paramètre, sachez qu'il sert à joindre une chaine de caractère au message WM_CREATE que recevra notre fenêtre lors de sa création. Personnellement, il faudra m'expliquer l'utilité de ce système.

- 1.5: Boucle évènementielle -

C'est bien beau, mais pour l'instant, notre programme quitte directement. En fait, il ne reçoit même pas les messages de l'OS. Pour cela, nous nous servons d'une boucle classique, que nous ne modifierons jamais :

  HWND hWnd;

  WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
    {
        Structure WNDCLASS wndClass	; pour initialiser la Window Class
        hInst = hInstance  		; en retour
        invoke RegisterClassEx, addr wc ; Pour enregistrer cette classe
            Strucure CreateWindowEx	   		;pour initialiser la fenêtre
        invoke ShowWindow, hwnd,CmdShow		; on affichee notre fenêtre

    // on reçoit les messages en boucle à l'aide de GetMessage()
    .WHILE TRUE
                invoke GetMessage, ADDR msg,NULL,0,0	; // msg reçoit les messages
                .BREAK .IF (!eax)  			; // Sort de la boucle si eax=-1
                invoke TranslateMessage, ADDR msg 	; // on traduit le message reçu
                invoke DispatchMessage, ADDR msg 	; // et on l'envoie vers notre
                               ; procédure de gestion de msgs
   .ENDW

   mov     eax,msg.wParam		  		; // Retoune le code de sortie dans eax

    }
ret							; // on termine la procédure
WinMain endp  						; WinMain
    // on renvoie désormais le code d'arrêt contenu dans l'objet msg

Je crois ne pas pouvoir être plus explicite. C'est de toute façon une méthode standard. Inutile de s'intéresser ici à la composition de l'objet MSG, ou au fonctionnement exacte de la réception des messages. Pour plus de compléments, référez-vous à la documentation. Pour l'heure, nous nous contenterons de prendre la boucle telle qu'elle.

- 1.6: Fonctions évènementielles -

Je vous ai parlé d'une fonction évènementielle dans l'initialisation de la classe de fenêtre sans vraiment la définir. Celle-ci repond aussi à un prototype, et permet de lancer différents traitements selon le type de message reçu. La voici incluse dans la version finale de notre programme :

Tout WinMain

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    .IF uMsg==WM_DESTROY                           ; si l'utilisateur ferme notre fenêtre (parce qu'on vient de recevoir un message WM_DESTROY)...alors
        invoke PostQuitMessage,NULL             ; on quitte notre application
    .ELSE
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam     ; Traitement par défaut de tous les autres messages qui ne nous intéressent pas.
        ret
    .ENDIF
    xor eax,eax
    ret
WndProc endp

  }

Le prototype d'une fonction évènementielle est toujours le même. Les arguments reçus sont :

- • hwnd : Le handle de la fenêtre concernée par le message
- • uMsg : La référence du message
- • wParam : le 1er paramètre du message
- • lParam : le second

Pour les différents messages reçus, je n'en ferai pas la liste ici. Nous aurons l'occasion de les rencontrer tout au long de ce tutorial (WM_CLOSE, WM_CREATE, WM_SIZE, WM_MOVE, etc ...). En ce qui concerne le traitement particulier réservé à WM_DESTROY, il permet de fermer l'application à l'aide de la fonction PostQuitMessage() (qui en fait envoie le message WM_QUIT à notre fenêtre (et oui, nombre de fonctions remplacent l'utilisation de SendMessage() pour faciliter la programmation)). En effet, WM_DESTROY est un message sans traitement par défaut qui est envoyé lors d'un clik sur la croix en haut à droite, ou lors du choix "quitter" dans le menu système en haut à gauche. Il nous faut donc notifier la fermeture de l'application nous même.


ASM Mode Reel

;---------------------------------------------------------------------------------------------------- Assemble_le.BAT
@echo off

if not exist ESSAI.rc goto over1
rc ESSAI.RC
Cvtres.exe /machine:ix86 essai.res
:over1

if not exist rsrc.rc goto over2
rc rsrc.rc
Cvtres.exe /machine:ix86 rsrc.res
:over2

if exist %1.obj del ESSAI.obj
if exist %1.exe del ESSAI.exe

ml /c /coff /Cp ESSAI.ASM
if errorlevel 1 goto errasm

if not exist ESSAI.res goto nores
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib ESSAI.obj ESSAI.res
if errorlevel 1 goto errlink
dir ESSAI
goto TheEnd

:nores
if not exist rsrc.res goto noresss
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib ESSAI.obj rsrc.res
if errorlevel 1 goto errlink
dir ESSAI
goto TheEnd

:noresss
Link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib ESSAI.obj
if errorlevel 1 goto errlink
dir %1
goto TheEnd

:errlink
echo _
echo Link error
goto TheEnd

:errasm
echo _
echo Assembly Error
goto TheEnd

:TheEnd
echo.
echo.
echo.
echo           ***Pour m utilisez, lisez VOCABULAIRE.HTML (voir Assemblage)***
echo.
pause

;---------------------------------------------------------------------------------------------------- Assembles_DLL.BAT
@echo off

if exist %1.obj del dll.obj
if exist %1.dll del dll.dll

ml /c /coff /Cp dll.ASM
if errorlevel 1 goto errasm

link /DLL /NOENTRY /def:dll.def /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib dll.obj
if errorlevel 1 goto errlink
dir dll

goto TheEnd

:errlink
echo _
echo Erreur de la part de Link.exe
goto TheEnd

:errasm
echo _
echo Erreur d Assemblage
goto TheEnd

:TheEnd

;---------------------------------------------------------------------------------------------------- Assembles_VxD.BAT
@echo off

if exist %1.obj del vxd.obj
if exist %1.vxd del vxd.vxd

ml -coff -c -Cx  -DMASM6 -DBLD_COFF -DIS_32 vxd.asm
if errorlevel 1 goto errasm

link -vxd -def:vxd.def vxd.obj
if errorlevel 1 goto errlink
dir dll

goto TheEnd

:errlink
echo _
echo Erreur de la part de Link.exe
goto TheEnd

:errasm
echo _
echo Erreur d Assemblage
goto TheEnd

:TheEnd

;----------------------------------------------------------------------------------------------------


Win32 Tips/Techniques for MASM

Visit Iczelion's Win32 Assembly Homepage

Updated at: 14 Sep 2000

Win32 Tips/Techniques for MASM Part #1 by `Ates (ates@anet.net.tr)

Q1. How can i hide/show taskbar ?
Q2. How can i Disable/Enable/Show/Hide Start Button ?
Q3. How can i make a real Stay On Top window ?
Q4. How can i create a hotkey ? like CTRL + ALT + A
Q5. How can i get Windows and System Directory ?
Q6. How can I open the Start-menu from my program ?
Q7. How can i close active program ?
Q8. How can i remove caption of my window ?
Q9. How can i understand if window is in taskbar/visible or not ?
Q10. How can ihide a window ?
Q11. How can i make a window Windows foreground window ?
Q12. How can i restrict CTRL+ALT+DEL,ALT+TAB+CTRL+ESC keys ?
Q13. Is Windows taskbar's auto hide feature enabled ?
Q14. How can i open my default browser or mail program ?
Q15. How to bring up the network connections dialog using Win32 API ?

- How can i hide/show taskbar ?

shell db "Shell_TrayWnd",0 ; this is taskbar's ClassName
invoke FindWindow,addr shell,NULL ; Get handle first then hide it.
.if eax != 0
invoke ShowWindow,eax,SW_HIDE ; use SW_SHOW to show it again
.endif

<TOP>

- How can i Disable/Enable/Show/Hide Start Button ?
.data?
buffer db 127 dup(?)

.data
shell db "Shell_TrayWnd",0
sbar db "BUTTON",0
child dd ?
slen dd ?

.code
invoke FindWindow,addr shell,NULL ; Get TrayBar Handle
mov tray, eax
invoke GetWindow,tray, GW_CHILD ; Get Child of Tray Bar if exists
mov child, eax
.if child != 0
invoke GetClassName,child,offset buffer, sizeof buffer ; Get the classname of Child window
.if eax > 0
invoke lstrlen, offset buffer ; get length of classname
mov slen,eax
invoke CharUpperBuff,offset buffer,slen ; convert to uppercase
invoke lstrcmp,addr buffer, addr sbar ; Compare classname with BUTTON
.if eax == 0
invoke ShowWindow,child,SW_HIDE ; Hide StartButton
; invoke ShowWindow,child,SW_SHOW ; Show StartButton
; invoke EnableWindow,child,FALSE ; Disable StartButton
; invoke EnableWindow,child,TRUE ; Enable StartButton
.endif
.endif
.endif

<TOP>

- How can i make a real Stay On Top window ?

invoke SetWindowPos,hWin, HWND_TOPMOST,NULL,NULL,NULL,NULL,SWP_NOACTIVATE or SWP_NOMOVE or SWP_NOSIZE

<TOP>

- How can i create a hotkey ? like CTRL + ALT + A

.data
hmsg db "HotKey CTRL + ALT + A Works good!",0
hcap db "Hotkey Example",0

.code
.if uMsg == WM_CREATE
invoke RegisterHotKey,hWnd,065h,MOD_CONTROL or MOD_ALT, 041h ; CTRL + ALT + A (041h is 65 - 065h is 101)

.elseif uMsg == WM_HOTKEY
invoke MessageBox,hWin,addr hmsg,addr hcap, MB_OK or MB_ICONINFORMATION

.elseif uMsg == WM_DESTROY
invoke UnregisterHotKey,hWin,065h
invoke PostQuitMessage,NULL
return 0
.endif

<TOP>

- How can i get Windows and System Directory ?

.data
buffer db 50 dup(?)
hCap db "WindowsDirectory",0

.code
invoke GetWindowsDirectory, addr buffer, sizeof buffer ; Getting windows directory into buffer
; invoke GetSystemDirectory, addr buffer, sizeof buffer ; Getting system directory into buffer
invoke MessageBox,hWnd, addr buffer, addr hCap, MB_OK or MB_ICONINFORMATION

<TOP>

- How can I open the Start-menu from my program?

invoke SendMessage,hWnd,WM_SYSCOMMAND,SC_TASKLIST,NULL

<TOP>

- How can i close active program ?

.data
fwin dd ?

.code
invoke GetForegroundWindow
mov fwin,eax
invoke SendMessage, fwin, WM_CLOSE,NULL

<TOP>

- How can i remove caption of my window ?

invoke GetWindowLong,hWnd,GWL_STYLE ; Get current window long
and eax,not WS_CAPTION ; remove WS_CAPTION
invoke SetWindowLong,hWnd,GWL_STYLE,eax ; Set it

<TOP>

- How can i understand if window is in taskbar/visible or not ?

invoke IsWindowVisible,hWin
.if eax == TRUE
; Window is visible here
.else
; Window is invisible here
.endif

<TOP>

- How can i hide a window ?

.data
mirc db "mIRC32",0
mhand dd ?

.code
invoke FindWindow,addr mirc, NULL ; we are looking for mIRC32
mov mhand,eax
.if mhand != 0 ; got handle ?
invoke ShowWindow,mhand,SW_SHOW ; Shows window
; invoke ShowWindow,mhand,SW_HIDE ; Hides window
.else
; mIRC32 is not working...
.endif

<TOP>

- How can i make a window Windows foreground window ?

invoke SetForegroundWindow, mhand

<TOP>

- How can i restrict CTRL+ALT+DEL,ALT+TAB+CTRL+ESC keys ?

invoke SystemParametersInfo,SPI_SCREENSAVERRUNNING,1,NULL,NULL
; Windows98 only 1 disables 0 enables

<TOP>

- Is Windows taskbar's auto hide feature enabled ?

.data
AppBar APPBARDATA {} ; {} means use defaults... Thanks to TTom

.code
mov AppBar.cbSize, sizeof AppBar
invoke SHAppBarMessage, ABM_GETSTATE, addr AppBar ; ShellApi command
and eax, ABS_AUTOHIDE
.if eax == TRUE
; Taskbar is hidden
.else
; Taskbar is not hidden
.endif

<TOP>

- How can i open my default browser or mail program ?

.data
lpPage db "http://win32asm.cjb.net",0
lpMail db "ates@anet.net.tr",0
lpOperation db "open",0

.code
invoke ShellExecute,hWin,addr lpOperation, addr lpPage, NULL, NULL, SW_SHOWNORMAL
invoke ShellExecute,hWin,addr lpOperation, addr lpMail, NULL, NULL, SW_SHOWNORMAL

<TOP>

- How to bring up the network connections dialog using Win32 API ?

include \MASM32\INCLUDE\mpr.inc
includelib \MASM32\LIB\mpr.lib
invoke WNetConnectionDialog,hWnd,RESOURCETYPE_DISK

<TOP>

Designed By Atilla Yurtseven


Iczelion's Guide to Winsock Programming

Winsock or windows socket takes the concept from Unix. The idea behind sockets is to use them as communication devices between different machines in the network. Each machine can create one or more sockets to connect to other machines. Sockets make uses of TCP/IP protocol as such, it operates in the higher network layers. We use sockets in any higher protocol that wants to use TCP/IP as its transport media such as HTTP,FTP and so on. The socket itself doesn't understand or care about the content that passes through it. It just does its job of sending and receiving data to from its remote counterpart.

Types of Socket
There are two types of socket: stream socket and datagram socket. A stream socket makes use of TCP so its transmission requires connection and is reliable. You use a stream socket when you want to send large files over the net or you want sequenced data packets (that is, the order of data packets is important). A datagram socket provides simple transfer of data. It's not guaranteed to be reliable (the data may not reach its destination), sequenced (the data packets may arrive in different order than intended), or unduplicated (the destination socket may receive two or more identical copies of the same data packets). HTTP and FTP and some other protocols use stream sockets while some broadcast protocols use datagram sockets.
You can think of stream socket connection as a telephone chat. You first have to make a call and if the call is successful, a connection is established. Then you can exchange information with the other end in a reliable way. If some communication problem occurs you know of it immediately and can take measures to rectify it. Datagram socket connection is like sending several mails to someone. You cannot know beforehand that they will reach the intended person. Even if he received them, there's no guarantee that the mails will reach him in the order you sent. And the mails may not reach the target at all.
The socket model sometimes uses client-server concept that is, one socket is thought of as server since it has services that it can render for other sockets, the opposite end is the client which asks the server for some services. HTTP and FTP make uses of this client-server concept.

Byte Ordering
Since we must deal with IP addresses in winsock programming, we should know of different byte ordering first. There are two types of byte ordering: big Endian and little Endian. In big Endian scheme, the leftmost byte are stored in the most significant byte. Little Endian scheme is the reverse of big Endian. For example, if the IP address is 205.34.67.24, in big Endian scheme it will be 205 34 67 24. But in little Endian, it will be 24 67 34 205.
Intel CPU uses little endian while some other CPUs such as Motorola uses big endian. The final word is: the internet uses big endian scheme so if you use a CPU with little endian byte ordering, you must convert the IP addresses before using them with the net.

Blocking and non-blocking modes
Socket functions can operate in two modes: blocking and non-blocking. Originally, in Berkeley Unix implementation, the sockets operate in blocking mode, that is, a socket function will not return until the operation is completed. We can call "blocking mode" synchronous operation and "non-blocking mode" asynchronous operation. Blocking operation may take an arbitrarily long time to complete such as waiting for the data to arrive from the remote socket. During that time, the program will seem frozen. This is usually unacceptable in Windows environment. So Windows implementation of socket API includes asynchronous (non-blocking) versions of the original blocking functions. You should use non-blocking versions whenever possible since it conforms to Windows paradigm and provides better results than the blocking ones.

Ports
When you create a socket and connect it to the remote socket, you must specify the port that the sockets will communicate with each other. Ports in this case is not hardware ports like COM1 or COM2. Ports in winsock programming are virtual ones for communication purpose only. Maybe an example will make this clear. Say, the server creates a socket and instructs it to listen for incoming connection on port 21, although there may be many network packets coming into the server, those packets that are destined for port number 21 will be routed to that socket. Several Internet protocols has their own default ports. HTTP uses port 80 (decimal) and FTP uses port 21 (decimal). These are default ports only. It means if both client and server agree to communicate via different ports, they can do so without any repercussion.

Winsock Programming Overview
Programming winsock normally includes these steps:

  1. Initialize winsock dll
  2. Create socket
  3. Specify the operation mode: blocking or non-blocking
  4. Connect the socket
  5. Perform some tasks with the socket such as sending or receiving data
  6. Close the socket
  7. Release winsock dll
We will explore these steps in some detail.
Socket Library Initialization
You must call WSAStartup to initialize the winsock dll before using any winsock function. This function has the following syntax:
    WSAStartup PROTO wVersionRequired:DWORD, LPWSADATA:DWORD
Parameters
wVersionRequried == The winsock version your application wants to use. At this time, version 1.1 and 2 are available. Version 1.1 ships with Windows 95 and version 2.x comes with Windows NT 4.0. If you want to specify version 1.1, use 101h if you want to use version 2.0, use 200h
lpWSADATA == Pointer to WSADATA structure that the winsock dll will fill in. This structure contains implementation details of the current winsock dll. Normally, you just create an uninitialized instance of WSADATA and pass its address to WSAStartup.

Return Value
WSAStartup returns NULL if the call is successful. Otherwise it returns error code. You can look up the error codes in winsock.hlp. This is the only winsock function that returns the actual error code because you cannot call WSAGetLastError if the socket dll is not initialized first.

Comment
If this function is not successful, you cannot use any winsock function. In fact, this function also serves as a negotiation routine for the best version of winsock api service for the application. After the call is successful, WSADATA structure will be filled with the capabilities of the current winsock implementation. One of the members is the highest version of winsock that the library supports. Say, if the application requests for at least winsock version 1.1 support and the current winsock library can support version 2, WSAStartup call will be successful and the winsock library returns 200h in a member of WSADATA. If the application can make use of winsock 2 support, it may call WSACleanup to close the previous initialization and call WSAStartup again, this time with 200h in its wVersionRequired parameter. If your application supports only winsock 1.1, you don't have to examine the WSADATA structure.

Code Snippet:

    .data
    wsadata WSADATA <>
    .....
    .code
    .......
    invoke WSAStartup, 101h,addr wsadata
    .if eax!=NULL
    <An error occured>
    .else
    <The initialization is successful. You may proceed with other winsock calls>
    .endif
Socket Creation
After the successful WSAStartup call, you can proceed to create the socket.
    socket PROTO af:DWORD, type:DWORD, protocol:DWORD
Parameters
af == Address format which at the current time there is only one: PF_INET
type == type of the socket you want to create, stream or datagram one. If you want a stream socket, use SOCK_STREAM else use SOCK_DGRAM
protocol == if the af param is AF_UNSPEC (unspecified), you must specify the protocol here. However, since we always use PF_INET, you can use 0.

Return Value
If the call is unsuccessful, the return value is INVALID_SOCKET and you can call WSAGetLastError to retrieve the actual error code. If the call is successful, it returns the socket descriptor which you must use in subsequent winsock calls.

Comment:
You must create at least one socket to be used as the communication device from your end. After successful call, you must save the returned socket descriptor for use with subsequent winsock function calls.

Code Snippet:

    .data?
    sock dd ?
    ....
    .code
    ....
    invoke socket,AF_INET,SOCK_STREAM,0 ; Create a stream socket for internet use
    .if eax!=INVALID_SOCKET
    mov sock,eax
    .else
    invoke WSAGetLastError
    ...
    .endif
Specifying Socket Options
After the socket is created, it defaults to blocking mode. If you want it to operate in non-blocking mode, you have to set that with setsockopt or WSAAsyncSelect. Normally, you should use WSAAsyncSelect to turn on non-blocking mode for the socket. So we'll demonstrate the use of WSAAsyncSelect here. setsockopt is not hard to use, you can figure it out yourself. setsockopt can alter many characteristics of a socket so you may want to look at it later.
    WSAAsyncSelect PROTO socket:DWORD, hwnd:DWORD, msg:DWORD, Event:DWORD
Parameters
socket == socket descriptor that is returned from socket call.
hwnd == handle to the window that will receive notification of winsock events.
msg == custom windows message that you create and want the winsock library to send to your window in case the events you're interested in occur.
Event == winsock events your application is interested in. You can specify one of the constant below or add them together if your application is interested in more than one winsock event.
    • FD_READ Want to receive notification of readiness for reading
    • FD_WRITE Want to receive notification of readiness for writing
    • FD_OOB Want to receive notification of the arrival of out-of-band data
    • FD_ACCEPT Want to receive notification of incoming connections
    • FD_CONNECT Want to receive notification of completed connection
    • FD_CLOSE Want to receive notification of socket closure
    Out-of-band data deserves some explanation here. Out-of-band data is the data that are sent without regard to the normal order of the general data. You can think of out-of-band data as an emergency notification as such it must be distinguished from the normal data. There are two ways to receive out-of-band data: in-line and separate channel. Normally, winsock library will put out-of-band data in a separate channel by default and will notify the application when out-of-band data arrive. However, some application may want to receive out-of-band data in the same channel as the normal data. In this case, that application must call setsockopt to modify SO_OOBINLINE option and call ioctlsocket to check if there are some out-of-band data in the socket.
Return Value
If the call is successful, the return value is NULL. Otherwise it returns SOCKET_ERROR and you may call WSAGetLastError to retrieve the actual error code.

Comment
This function is the instrument of non-blocking paradigm. It lets an application specify which winsock events it's interested in and when the registered winsock events occur, the winsock library will send the specified message to the window. Contrast this to the blocking scenario in which you must poll for the winsock events. This function also changes the socket to non-blocking mode. I recommend that you use WSAAsyncSelect whenever applicable since it conforms to Windows paradigm.
If you don't want the winsock library to send notifications to your window anymore, you must call WSAAsyncSelect with value 0 in lEvent parameter. However, beware that there may be winsock messages in the queue before you cancel winsock notification. You must be prepared to handle them. Normally you should extract them from the message queue by PeekMessage call with PM_REMOVE flag.
When a winsock event that the application is interested occurs, the message specified by msg parameter will be sent to the window procedure of the window specified by hwnd parameter. With the message, wParam contains the socket descriptor, high word of lParam contains error code (if any) and the low word of lParam contains the event.

Code Snippet

    .data?
    hwnd dd ? ; handle of the window to receive the winsock message.
    socket dd ? ; socket descriptor
    ....
    .const
    WM_SOCKET equ WM_USER+100 ; create a custom window message. You can use any name not
    ; limited to WM_SOCKET and you can use any number not
    ; limited to 100.
    ......
    .code
    ...........
    invoke WSAAsyncSelect, socket, hwnd,WM_SOCKET, FD_CONNECT+FD_READ+FD_CLOSE
    ; Register interest in connect, read and close events.
    .if eax==SOCKET_ERROR
    <put your error handling routine here>
    .else
    ........
    .endif

    WndProc PROC hWnd:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
    .......
    .if uMsg==WM_SOCKET ; the message we specified in WSAAsyncSelect
    mov eax,lParam
    .if ax==FD_CONNECT ; the low word of lParam contains the event code.
    shr eax,16 ; the error code (if any) is in the high word of lParam
    .if ax==NULL
    <no error occurs so proceed>
    .else
    <an error occurs. Put your error handling routine here>
    .endif
    .elseif ax==FD_READ
    shr eax,16
    .if ax==NULL
    <no error occurs so proceed>
    .else
    <an error occurs. Put your error handling routine here>
    .endif
    .elseif ax==FD_CLOSE
    shr eax,16
    .if ax==NULL
    <no error occurs so proceed>
    .else
    <an error occurs. Put your error handling routine here>
    .endif
    .endif
    .else
    ..........
    .endif
    WndProc ENDP

Connection to a Remote Socket
After the socket is created, you have two choices: wait for an incoming connection or connect to a remote socket. If you want to wait for incoming connection, you must call listen to listen for an incoming connection and call accept to establish connection with the remote socket. If you want to connect to a remote socket, you must call connect which has the following syntax:
    connect PROTO socket:DWORD, lpSockAddr_in:DWORD, namelen:DWORD
Parameters
socket == socket descriptor of the local socket. You can pass the socket descriptor returned by socket call as this parameter.
lpSockAddr_in == a pointer to a SOCKADDR_IN structure which is declared as follows:
      sockaddr_in STRUCT
      sin_family WORD ?
      sin_port WORD ?
      sin_addr DWORD ?
      sin_zero BYTE 8 dup (?)
      sockaddr_in ENDS

      You must fill in this structure and pass its address to connect.

    • sin_family is the same as af parameter in socket call. You must use AF_INET here.
    • sin_port is the port that the socket will use to communicate with the remote socket. This value depends on the higher level protocol that you want to use. If you want to connect to the remote socket for HTTP, use port 80 (decimal). However, note that the port value MUST be in network byte order that is big Endian. So you cannot use the port value per se but you must call htons to convert the value to network byte order first. This is one of the most common error the newcomers to winsock programming encounter.
    • sin_addr is the IP address of the remote host. Again, you must convert the IP address to network byte order before using it.
    • sin_zero is reserved. Don't mess with it.
namelen == the size of SOCKADDR_IN structure

Return Value
Depend on the mode of the socket.

  • non-blocking mode: it returnsSOCKET_ERROR and you must call WSAGetLastError to retrieve the error code. If the error code is WSAEWOULDBLOCK ,it means that the operation is in progress and cannot return immediately. The application will be informed of the result later by custom window message specified in WSAAsyncSelect call. In a nutshell, WSAEWOULDBLOCK is not an error.
  • blocking mode: The return value indicates the success or failure of the call.
Code Snippet
    .data
    sin SOCKADDR_IN <>
    IPAddress db "206.34.234.23",0
    Port dd 80 ; We use port 80, HTTP port for demonstration purpose

    .data?
    socket dd ?

    .code
    ........
    mov sin.sin_family, AF_INET
    invoke htons, Port ; convert port number into network byte order first
    mov sin.sin_port,ax ; note that this member is a word-size param.
    invoke inet_addr, addr IPAddress ; convert the IP address into network byte order
    mov sin_addr,eax
    invoke connect,socket,addr sin,sizeof sin
    .if eax==SOCKET_ERROR ; assuming we use non-blocking mode, connect
    ; will always return SOCKET_ERROR
    invoke WSAGetLastError ; retrieve the actual error code
    .if eax!=WSAEWOULDBLOCK ; if it's not WSAEWOULDBLOCK
    <put your error handling code here> ; it means a true error occurred
    .endif
    .endif

If you only got URL string such as "http://members.xoom.com/Iczel", you must parse the string for the host name, in this case "members.xoom.com". And pass the address of the host name as a parameter to gethostbyname function which has the following syntax:
    gethostbyname PROTO lphostname:DWORD
Parameters
lphostname == pointer to the host name string.

Return Value
If successful, it returns a pointer to hostent structure which is allocated by Windows sockets implementation. If the call fails, it returns NULL and you may call WSAGetLastError to retrieve the error code.

    hostentStru STRUCT
    h_name DWORD ?
    h_alias DWORD ?
    h_addr WORD ?
    h_len WORD ?
    h_list DWORD ?
    hostentStru ENDS
Note that in windows.inc supplied by hutch, hostent structure is named hostentStru.
    h_name Official name of the host (PC).
    h_alias A NULL-terminated array of alternate names.
    h_addr The type of address being returned; for Windows Sockets this is always PF_INET.
    h_len The length, in bytes, of each address; for PF_INET, this is always 4.
    h_list Pointer to a pointer that points to the list of ip addresses. In summary, h_list is a double pointer indirection. Addresses are returned in network byte order so you can use them immediately.
Comment
This function is used to retrieve the IP address(es) of the host specified as a string. Notice that what you want mostly is in h_list member.

Code Snippet
.data
sin SOCKADDR_IN <>
hostname db "members.xoom.com",0
Port dd 80 ; We use port 80, HTTP port for demonstration purpose

.data?
socket dd ?

.code
........
mov sin.sin_family, AF_INET
invoke htons, Port ; convert port number into network byte order first
mov sin.sin_port,ax ; note that this member is a word-size param.
invoke gethostbyname, addr hostname
mov eax,[eax+12] ; move the value of h_list member into eax
mov eax,[eax] ; copy the pointer to the actual IP address into eax
mov eax,[eax] ; copy IP address into eax
mov sin.sin_addr,eax
invoke connect,socket,addr sin,sizeof sin
.if eax==SOCKET_ERROR ; assuming we use non-blocking mode, connect
; will always return SOCKET_ERROR
invoke WSAGetLastError ; retrieve the actual error code
.if eax!=WSAEWOULDBLOCK ; if it's not WSAEWOULDBLOCK
<put your error handling code here> ; it means a true error occurred
.endif
.endif

Operations on the Socket
There are several operations you can perform on the socket such as sending or receiving data. We'll examine both of them in detail.
Sending Data to the Remote Socket
We use send to send data on a stream socket and sendto to send data on a datagram socket. I'll examine send here since many popular protocols such as HTTP and FTP use stream sockets.
    send PROTO socket:DWORD, buffer:DWORD, len:DWORD, flags:DWORD
Parameters
socket == socket descriptor
buffer == address of the data that you want to send. The data need not be NULL-terminated since its size is specified in len parameter.
len == the size of data to send
flags == flags specifying the behavior of the function. There are two flags you can use:
    MSG_DONTROUTE : specifies that the data should not be subject to routing. Notice the word "should". Some windows socket implementation may ignore this flag.
    MSG_OOB : specifies that the data is out-of-band data.

    Normally we don't use any of these two flags, flags parameter should be 0 in this case.

Return Value
If the call is unsuccessful, the value SOCKET_ERROR is returned in eax. If it's successful, the actual number of bytes sent is returned in eax. Note that this number may not be equal to len parameter.

Comment
This function sends data to the connected remote socket. It neither cares nor knows about the data it sends.

Code Snippet

    .data?
    buffer db 100 dup(?)
    ....
    .code
    .....
    invoke send, socket, buffer, 100, 0
    .if eax==SOCKET_ERROR
    <put your error handling code here>
    .else
    .....
    .endif
Reading Data from the Socket
There are two variants of the winsock call to read data from the socket: recv and recvfrom. recv is for use with a stream socket while recvfrom is for a datagram socket.
    recv PROTO socket:DWORD, buffer:DWORD, len:DWORD, flags:DWORD
Parameters
socket == socket descriptor
buffer == address of the memory block to store the incoming data.
len == the size of the memory block
flags == flags specifying the behavior of the function. There are two flags you can use:
    MSG_PEEK Peek at the incoming data. The data is copied into the buffer but is not removed from the input queue.
    MSG_OOB Process out-of-band data. This flag is usually used when FD_OOB notification is received.
Return Value
If the call is successful, it returns the number of bytes read from the socket. If it is unsuccessful, the value SOCKET_ERROR is returned. If the return value is 0, the remote socket has been closed.

Comment
This function reads the data from the socket and stores them in the specified buffer.A question arises: how do you know how many bytes are available for read from the socket? The answer is the function ioctlsocket. It has the following syntax:

    ioctlsocket PROTO socket:DWORD, cmd:DWROD, lpArgument:DWORD
Parameters
socket == socket descriptor
cmd == the command to perform on socket. There are three commands: FIONBIO, FIONREAD, and SIOCATMARK. Since we want to get the size of data available in the socket, we will focus on FIONREAD. FIONREAD command determines the amount of data which can be read from the socket. The amount of data is stored at the location pointed to by lpArgument.
lpArgument == address of an additional parameter for cmd.

Return Value
If successful, the return value in eax is NULL. Otherwise the value SOCKET_ERROR is returned and you may call WSAGetLastError to retrieve the error code.

Comment
This function is used to control the mode of a socket. With FIONBIO command, it can switch the socket into blocking or non-blocking mode. With FIONREAD command, it returns the amount of data available in the socket. With SIOCATMARK command, it checks whether or not all out-of-band data has been read.

Code Snippet

    .data?
    socket dd ?
    hMemory dd ? ; handle to memory block
    buffer dd ? ; address of the memory block
    available_data dd ? ; the amount of data available from the socket
    actual_data_read dd ? ; the actual amount of data read from the socket
    .....
    .code
    .....
    invoke ioctlsocket, socket, FIONREAD, addr available_data
    .if eax==NULL
    invoke GlobalAlloc, GHND, available_data
    mov hMemory,eax
    invoke GlobalLock, eax
    mov buffer,eax
    invoke recv, socket, buffer, available_data, 0
    mov actual_data_read, eax
    .....
    <use the data in buffer>
    ....
    invoke GlobalUnlock, buffer
    invoke GlobalFree, hMemory
    .endif
Closing the Socket
After you are finished with using the socket, you should close it by calling closesocket.
    closesocket PROTO socket:DWORD
Parameter
socket == socket descriptor

Return Value
If successful, it returns NULL. Otherwise SOCKET_ERROR is returned and you may call WSAGetLastError to retrieve the error code.

Comment
This function closes a socket. Every resource of the socket will be released.

Code Snippet

    .data?
    socket dd ?
    ....
    .code
    ....
    invoke closesocket, socket
    .if eax==SOCKET_ERROR
    <put your error handling code here>
    .else
    endif
Release Winsock Library
When the application doesn't need to use winsock library anymore, it calls WSACleanup.
    WSACleanup PROTO
Parameter
This function takes no parameter.

Return Value
If successful, it returns NULL. Otherwise it returns SOCKET_ERROR.

Comment
There must be matching number of WSAStartup and WSACleanup calls. Say, if you call WSAStartup three times, you must also call WSACleanup three times as well else the winsock library will not be able to free any unused resource. This is the last function that you call when you're through with the winsock library.

Code Snippet

    .code
    .....
    invoke WSACleanup
    .if eax==SOCKET_ERROR
    <put your error handling code here>
    .else
    .endif


How toCreate your own MASM Import Libraries

by Iczelion
This short essay is about the mechanics of creating import libraries for use with MASM. I assume you already know something about import libraries, ie. you know what an import library is and so on. I will focus on the technique used to generate your own custom MASM import libraries.

MASM Import Library Format

MASM and Visual C++ can use the same import libraries which is very handy. Microsoft import libraries use a variation of COFF file format which is different from the OMF format used by TASM. That is the reason TASM cannot use MASM's import libs and vice versa. I won't go into detail about the details of Microsoft import library format. Suffice to say that every Microsoft import lib contains information about functions in some DLLs. That information includes the function names and the overall size of parameters passed to functions. If you examine kernel32.lib with a hex editor, you will find entries with this format:
_ExitProcess@4
_CreateProcessA@40
The function names are "decorated" with a leading underscore. The number following @ is the overall size of parameters of that function, in bytes. ExitProcess takes only one dword parameter, so that number is 4.
Why includes the information about the size of parameters? That information is used by MASM to check if the correct number and size of parameters are passed to a function. You get this feature when you call that function with invoke keyword. If you just push parameters on the stack and execute the function with call, you won't get this MASM check.
It's this extra boon which makes it nearly impossible to create MASM import libs directly from DLLs because DLLs don't contain explicit information about the size of parameters passed to their own functions.

Creating MASM Import Libs from DLLs

If you are willing to push parameters on the stack and execute functions with call, you can create import lib from any DLL for use with MASM like this:
  • use dumpbin.exe which comes with Visual C++ to dump the names of the export functions in the DLL.
      Dumpbin /EXPORTS blah.dll > output.txt
  • After you got the list of function names, create a module definition file (.def) from them. For example, if the function contains only one function, GetSomeLine, type the following lines into a text file:
      LIBRARY blah
      EXPORTS
      GetSomeLine
    Save it as blah.def
  • Run lib.exe to create the import lib from the module definition file, like this:

      • lib /DEF:blah.def
That's it. You'll get blah.lib which you can use with MASM so long as you don't use invoke with the functions in the import lib.

Creating MASM Import Libs for Use with invoke

I, for one, am reluctant to use the above approach. invoke is a nice function call wrapper. It's one of the reasons I prefer MASM to TASM. But as I pointed out earlier, it's next to impossible to create a 100% working MASM import lib from any DLL. You can't use the above method to create an MASM import lib which can be used with invoke. For example, you may think that if you modify the function names in .def file to include "@xx", the import lib should be ok. Trust me. It won't work.
The easy way to create an "invokable" import lib is to use MASM itself. If you have coded a DLL, you will observe that you also get an import lib for that DLL as well. And that import lib is fully invokable! Our strategy is as follow:
  1. Obtain the function names and the overall sizes of parameters
  2. Create a DLL source code that includes all those functions with the correct number and size of arguments.
  3. Create a module definition file that describes the corresponding functions in the asm source code.
  4. Assemble the asm source code as a DLL project.
That's it. You'll obtain a fully functional MASM import lib. The above steps deserve more explanation

Obtain the function names and overall sizes of parameters

This is the most difficult part of the process. If you have only the DLL, you're in for a tiresome adventure. Below are the methods I can think of, none of them works 100%.
  • Use Interactive Disassembler (IDA) to disassemble the DLL. With this wonderful tool, you can obtain the rough size of parameters expected by the functions. The information is not perfect, however. IDA is an awesome disassembler but sometimes, only humans can decide which is which. You will have to peruse and check the whole disassembling listing.
  • Observe the value of the stack pointer before and after calls to all the functions in the DLL. The method is:
    1. Obtain the address of the functions with GetProcAddress
    2. Call each function without passing any parameter on the stack. Note the value of esp before the call.
    3. When the function returns, compare the value of esp after the call with the one before the call. The rationale is: with stdcall parameter passing convention, the function is the one who takes care of the stack balancing. The difference of the esp values is the size of parameters expected by that function.
    Alas, this method is not bullet-proof. It can fail under these circumstances:
    • If the functions in the DLL use other parameter passing convention other than stdcall or pascal.
    • If the functions fail to clean up the stack, such as when exceptions occur.
    • If the functions are designed to do something dangerous such as formatting the hard disk (God forbids!)
  • Study the existing programs that use the DLL. You can debug/disassemble those programs to see the number and size of parameters they pass to functions in the DLL. However, if there are some functions in the DLL which are never used in any program, you're left with the above two methods.

Create a DLL asm source code which contains all those functions

After you obtain the function names and their parameter sizes, the rest is easy. You just create a DLL asm skeleton and then create functions with the same names as those in the DLL in the file. For example, if the DLL has only one function, GetSomeLine, which takes 16 bytes of parameters. In the asm file, you type the following lines:
.386
.model flat,stdcall
.code
GetSomeLine proc param1:DWORD, param2:DWORD, param3:DWORD, param4:DWORD
GetSomeline endp
end
What's this? You may ask. A procedure with no instruction inside? An import lib doesn't contain any info on what the functions are supposed to do. Its sole purpose is to provide information about the function names and their parameters. So we don't need to put any instruction into the dummy procedure. We will discard the useless DLL generated by the assembling process anyway. All we want is to put the info about the function names and the size of parameters into the asm source code so that MASM can generate the working import lib. The size of each parameter is NOT essential. For your information, currently MASM always regards each parameter as a DWORD no matter which size specifier you use. For example, we can use:
.386
.model flat,stdcall
.code
GetSomeLine proc param1:BYTE, param2:BYTE, param3:BYTE, param4:BYTE
GetSomeline endp
end
And MASM will happily create _GetSomeLine@16 entry in the import lib.

Create a matching module definition file

This is a simple process. You need this file so that MASM can generate the DLL and import lib. A module defintion file template is like this:
LIBRARY <The name of the DLL>
EXPORTS
<The names of the functions>
You just fill the name of the DLL which will be used as the name of the import lib as well. And then put the list of the function names below the EXPORTS line, each name on its own line. Save the file and you got a working module defintion file.

Assemble the asm source code as a DLL project

The last step is the simplest one. Just use ml.exe and link.exe.
ml /c /coff /Cp blah.asm
link /DLL /NOENTRY /def:blah.def /subsystem:windows blah.obj
And you'll get an invokable import lib.

An Example

The dry explanation above may not be clear enough. I'm a firm believer in learning by doing. So I provide an example that demonstrates the above process. The example files are:
  • The asm source code that contains all functions in kernel32.dll (the documented ones anyway)
  • The matching module definition file
  • A batch file which you can use to build the import lib
Assembling the example will give you a kernel32.lib which you can use in place of the one provided by Microsoft.

More Tools

If you want to add/remove functions to/from a certain import lib, you can use two simpleminded tools I coded. For example, if you want to add undocumented functions to kernel32.lib, you'll find these tools useful.

Lib2Def

It extracts function names and the corresponding sizes of parameters from any MASM/VC++ import lib. Just run it and it will process ALL import libraries that are in the same folder. The output files have .icz extensions. The content looks like this:
_ExitProcess@4
_ExitThread@4
_ExitVDM@8
_ExpandEnvironmentStringsA@12
_ExpandEnvironmentStringsW@12 @12
If you want to add a function, you just insert a new line and put a leading underscore, followed by the function name and the combined size of its parameters. If the function is exported by ordinal, there will be another @xx following the name. The "xx" being the ordinal number. Note that this simple tool doesn't check for duplicate function names. And there can be duplicate names in some import libs.

MLib

This is the tool that accepts the output files of Lib2Def and creates import libs from them. It will process ALL files with .icz extension. For your information, it parses the lines in .icz files and creates .asm and .def from them. Then it calls ml.exe and link.exe to generate the import lib. The .obj, .asm, .exp and .dll are deleted afterwards so only .lib is left. If this tool fails to generate .lib file, please check if there are some duplicate function names in the .icz file: it's the most common cause of failure.

Download these two tools

[Iczelion's Win32 Assembly Homepage]


Calling Win32 API Routines from HLA

by Randy Hyde

The win32 libraries are probably second only to the HLA Standard Library it terms of frequency of use in an HLA program. Although the HLA Standard Library shields you from much of the details concerning win32 API calling convensions when writing console applications, the Standard Library does not provide a buffer layer between you and all the possible win32 calls. Furthermore, for GUI and other non-console applications, the HLA Standard Library provides very little help, you will have no choice but to interface directly with the win32 Application Programmer's Interface. This document will describe how this is done from within an HLA program.

The first, and perhaps most important point to note is that HLA uses the "Pascal" calling convention while the Windows API uses the "STDCALL" calling convention. Pascal/HLA procedures push the parameters on the stack in the order they are encountered in the actual parameter list; Pascal/HLA procedures automatically pop all parameters off the stack upon return. The "STDCALL" calling convention also pops all actual parameters off the stack upon return; however, the "STDCALL" convention pushes parameters on the stack in the reverse order the compiler encounters them in the actual parameter list. Since both calling mechanisms expect the called procedure to remove all parameters from the stack, HLA is compatible with the STDCALL convention on this point. As for the order of parameters, the win32 API expects the HLA caller to push the parameters in the reverse order than HLA actually pushes them. So the two calling mechanisms are incompatible on this point. Fortunately, it's easy to work around this problem, just specify the @stdcall procedure option when creating the prototype for a Win32 API function.

The last difference between the two calling mechanisms is that the Pascal interface simply uses the name of the procedure as the external name while the STDCALL prepends an underscore to the function name and uses that combination as the external name. Fortunately, HLA's external procedure declaration syntax provides an easy solution to this problem.

First consider the "C" declaration for the win32 API WriteFile function call:

int WriteFile( dword Handle, byte *buffer, dword len, dword *BytesWritten, dword Overlapped );

As it turns out, "WriteFile" isn't the true name of this procedure. Microsoft appends "@20" to the end of the name to denote that this procedure has 20 bytes of parameters. Along with the STDCALL convention of prepending an underscore, the actual external name is "_WriteFile@20". Therefore, this is the name we'll have to supply as the HLA external name. Since HLA does not allow at-signs ("@") in the middle of an identifier, we'll have to use the auxiliary external declaration syntax that HLA supports when declaring this function. Specifically, the HLA declaration will look something like:

procedure WriteFile( parameters ); @stdcall; external( "_WriteFile@20" );

procedure WriteFile
(
        Handle:       dword
    var buffer:       var;
        len:          dword;
    var BytesWritten: dword;
        overlapped:   dword;
);  external( "_WriteFile@20" );

This declaration will work but there a slight problem with it. The problem with calling the win32 API in this fashion is that it's slightly inefficient; this inefficiency is not specific to HLA, most win32 API calls suffer from this problem, but since this is assembly language it is possible to improve the efficicency of the win32 API call without much effort.

One problem that affects all win32 API calls is the actual machine language interface to the DLLs that process the system calls. Whenever you have an external procedure declaration like the one above, HLA emits a CALL machine instruction whose operand field the linker must fill in with the address of the actual subroutine. Unfortunately, win32 API calls are made indirectly through a variable rather than directly to the actual routine. The direct call that HLA (and other languages) emits will not jump indirect through this pointer variable. To solve this problem, external symbols like "_WriteFile@20" in the kernel32.lib file do not hold the address of the actual API routine or even the indirect jump address. Instead, these names specify the address of a JMP instruction that jumps indirectly through the pointer variable. Therefore, your win32 API calls wind up calling a JMP instruction that jumps indirect through the pointer variable.

It turns out that with a very small change, you can avoid the overhead of this extra JMP instruction on each win32 API call. In addition to exporting the names of the win32 API entry points (which are the addresses of these JMP instructions), the win32 LIB files also export the names of the pointer variables. These pointer variables have the same name as the API routines except they have "__imp__" prepended to the name rather than a single underscore (by the way, that's two underscores followed by "imp" followed by two more underscores). One really slick feature of HLA is that it uses the same syntax for indirect procedure calls as it does for direct procedure calls. This means that by simply changing the external declaration, we can tell HLA to generate an indirect jump rather than a direct jump to the API routines; nothing else changes. Therefore, to remove all the calls to JMPs, all you need to is declare your API routines as follows:

static
    win32_WriteFile: procedure
    (
            Handle:       dword
        var buffer:       var;
            len:          dword;
        var BytesWritten: dword;
            overlapped:   dword;
    );  @external( "__imp__WriteFile@20" );

This declares win32_WriteFile as a procedure pointer rather than a procedure. The external name is the name of the variable containing the address of the actual WriteFile@20 routine. Whenever HLA encounters a call to win32_WriteFile, it emits an indirect CALL instruction that transfers control directly to the WriteFile routine rather than transferring control to the indirect JMP instruction. This saves a small amount of execution time on each API call.

The only real catch is figuring out the API names. Unfortunately, Microsoft's documentation doesn't tell you the (external) name of each API routine. Fortunately, you can use the Microsoft DUMPBIN.EXE utility to extract this information directly from the LIB files. To do this, use the following command syntax:

dumpbin /symbols kernel32.lib

(Obviously, you should supply the appropriate name of the library whose symbols you want to see).

The dumpbin utiltity, with the "/symbols" option displays all the symbols that the specified library module exports. If you're looking for a symbol like "WriteFile" you will probably find a few symbols that have the "WriteFile" substring present. It's a good bet that one of these names (particularly, with the "__imp__" prefix) is the name you want to use in the HLA external declaration.

Many Win32 API export names will have two varieties: one form containing an "A" before the at-sign and one containing a "W" before the at-sign. For example, the "CreateFile" function has two forms: "_CreateFileA@28" and "_CreateFileW@28". The difference between functions whose names end in these two characters is that those with an "A" suffix deal with 8-bit ASCII characters (or, more correctly, ANSI characters). Those with a "W" (for wide) suffix deal with 16-bit UNICODE characters. Feel free to call whichever form is appropriate for your application. Do keep in mind, though, that the HLA Standard Library routines always expect ANSI/ASCII characters and generally don't support UNICODE characters. So if you're working with wide characters, you may not be able to use routines in the HLA Standard Library that work with character and string data.

The following tables list the names that the "kernel32.lib" file exports. This will give you a basic idea of the symbol types that appear in a typical LIB file. This list only presents those symbols found in the "kernel32.lib" file, there are many hundreds of additional names found in other windows LIB files. Here is a list of some of the LIB files that accompany Microsoft's Visual C++ system that may contain useful API routines you can call:

ADVAPI32.LIB

COMCTL32.LIB

COMDLG32.LIB

CRYPT32.LIB

CTL3D32.LIB

CTLFWD32.LIB

CTLFWR32.LIB

DMAPIW32.LIB

FMISTR32.LIB

GDI32.LIB

GLU32.LIB

GTRTST32.LIB

IMM32.LIB

KERNEL32.LIB

LSAPI32.LIB

LZ32.LIB

MAPI32.LIB

MFCUIA32.LIB

MSACM32.LIB

MSIMG32.LIB

MSLSP32.LIB

NETAPI32.LIB

ODBC32.LIB

ODBCCP32.LIB

OLE32.LIB

OLEAUT32.LIB

OLEPRO32.LIB

OPENGL32.LIB

PENWIN32.LIB

PKPD32.LIB

RASAPI32.LIB

RTFLIB32.LIB

SHELL32.LIB

TAPI32.LIB

TH32.LIB

THUNK32.LIB

USER32.LIB

VFW32.LIB

WAPPC32.LIB

WCPIC32.LIB

WINCSV32.LIB

WINRUI32.LIB

WINSLI32.LIB

WLDAP32.LIB

WOW32.LIB

WS2_32.LIB

WSNMP32.LIB

WSOCK32.LIB

Kernel32.lib API Routine Export Names

_AddAtomA@4

_AddAtomW@4

_AddConsoleAliasA@12

_AddConsoleAliasW@12

_AllocConsole@0

_AreFileApisANSI@0

_BackupRead@28

_BackupSeek@24

_BackupWrite@28

_BaseAttachCompleteThunk@0

_Beep@8

_BeginUpdateResourceA@8

_BeginUpdateResourceW@8

_BuildCommDCBA@8

_BuildCommDCBAndTimeoutsA@12

_BuildCommDCBAndTimeoutsW@12

_BuildCommDCBW@8

_CallNamedPipeA@28

_CallNamedPipeW@28

_CancelIo@4

_CancelWaitableTimer@4

_ClearCommBreak@4

_ClearCommError@12

_CloseConsoleHandle@4

_CloseHandle@4

_CloseProfileUserMapping@0

_CmdBatNotification@4

_CommConfigDialogA@12

_CommConfigDialogW@12

_CompareFileTime@8

_CompareStringA@24

_CompareStringW@24

_ConnectNamedPipe@8

_ConsoleMenuControl@12

_ContinueDebugEvent@12

_ConvertDefaultLocale@4

_ConvertThreadToFiber@4

_CopyFileA@12

_CopyFileExA@24

_CopyFileExW@24

_CopyFileW@12

_CreateConsoleScreenBuffer@20

_CreateDirectoryA@8

_CreateDirectoryExA@12

_CreateDirectoryExW@12

_CreateDirectoryW@8

_CreateEventA@16

_CreateEventW@16

_CreateFiber@12

_CreateFileA@28

_CreateFileMappingA@24

_CreateFileMappingW@24

_CreateFileW@28

_CreateIoCompletionPort@16

_CreateMailslotA@16

_CreateMailslotW@16

_CreateMutexA@12

_CreateMutexW@12

_CreateNamedPipeA@32

_CreateNamedPipeW@32

_CreatePipe@16

_CreateProcessA@40

_CreateProcessW@40

_CreateRemoteThread@28

_CreateSemaphoreA@16

_CreateSemaphoreW@16

_CreateTapePartition@16

_CreateThread@24

_CreateVirtualBuffer@12

_CreateWaitableTimerA@12

_CreateWaitableTimerW@12

_DebugActiveProcess@4

_DebugBreak@0

_DefineDosDeviceA@12

_DefineDosDeviceW@12

_DeleteAtom@4

_DeleteCriticalSection@4

_DeleteFiber@4

_DeleteFileA@4

_DeleteFileW@4

_DeviceIoControl@32

_DisableThreadLibraryCalls@4

_DisconnectNamedPipe@4

_DosDateTimeToFileTime@12

_DuplicateConsoleHandle@16

_DuplicateHandle@28

_EndUpdateResourceA@8

_EndUpdateResourceW@8

_EnterCriticalSection@4

_EnumCalendarInfoA@16

_EnumCalendarInfoW@16

_EnumDateFormatsA@12

_EnumDateFormatsW@12

_EnumResourceLanguagesA@20

_EnumResourceLanguagesW@20

_EnumResourceNamesA@16

_EnumResourceNamesW@16

_EnumResourceTypesA@12

_EnumResourceTypesW@12

_EnumSystemCodePagesA@8

_EnumSystemCodePagesW@8

_EnumSystemLocalesA@8

_EnumSystemLocalesW@8

_EnumTimeFormatsA@12

_EnumTimeFormatsW@12

_EraseTape@12

_EscapeCommFunction@8

_ExitProcess@4

_ExitThread@4

_ExitVDM@8

_ExpandEnvironmentStringsA@12

_ExpandEnvironmentStringsW@12

_ExpungeConsoleCommandHistoryA@4

_ExpungeConsoleCommandHistoryW@4

_ExtendVirtualBuffer@8

_FatalAppExitA@8

_FatalAppExitW@8

_FatalExit@4

_FileTimeToDosDateTime@12

_FileTimeToLocalFileTime@8

_FileTimeToSystemTime@8

_FillConsoleOutputAttribute@20

_FillConsoleOutputCharacterA@20

_FillConsoleOutputCharacterW@20

_FindAtomA@4

_FindAtomW@4

_FindClose@4

_FindCloseChangeNotification@4

_FindFirstChangeNotificationA@12

_FindFirstChangeNotificationW@12

_FindFirstFileA@8

_FindFirstFileExA@24

_FindFirstFileExW@24

_FindFirstFileW@8

_FindNextChangeNotification@4

_FindNextFileA@8

_FindNextFileW@8

_FindResourceA@12

_FindResourceExA@16

_FindResourceExW@16

_FindResourceW@12

_FlushConsoleInputBuffer@4

_FlushFileBuffers@4

_FlushInstructionCache@12

_FlushViewOfFile@8

_FoldStringA@20

_FoldStringW@20

_FormatMessageA@28

_FormatMessageW@28

_FreeConsole@0

_FreeEnvironmentStringsA@4

_FreeEnvironmentStringsW@4

_FreeLibrary@4

_FreeLibraryAndExitThread@8

_FreeResource@4

_FreeVirtualBuffer@4

_GenerateConsoleCtrlEvent@8

_GetACP@0

_GetAtomNameA@12

_GetAtomNameW@12

_GetBinaryType@8

_GetBinaryTypeA@8

_GetBinaryTypeW@8

_GetCPInfo@8

_GetCommConfig@12

_GetCommMask@8

_GetCommModemStatus@8

_GetCommProperties@8

_GetCommState@8

_GetCommTimeouts@8

_GetCommandLineA@0

_GetCommandLineW@0

_GetCompressedFileSizeA@8

_GetCompressedFileSizeW@8

_GetComputerNameA@8

_GetComputerNameW@8

_GetConsoleAliasA@16

_GetConsoleAliasExesA@8

_GetConsoleAliasExesLengthA@0

_GetConsoleAliasExesLengthW@0

_GetConsoleAliasExesW@8

_GetConsoleAliasW@16

_GetConsoleAliasesA@12

_GetConsoleAliasesLengthA@4

_GetConsoleAliasesLengthW@4

_GetConsoleAliasesW@12

_GetConsoleCP@0

_GetConsoleCommandHistoryA@12

_GetConsoleCommandHistoryLengthA@4

_GetConsoleCommandHistoryLengthW@4

_GetConsoleCommandHistoryW@12

_GetConsoleCursorInfo@8

_GetConsoleDisplayMode@4

_GetConsoleFontInfo@16

_GetConsoleFontSize@8

_GetConsoleHardwareState@12

_GetConsoleInputExeNameA@8

_GetConsoleInputExeNameW@8

_GetConsoleInputWaitHandle@0

_GetConsoleKeyboardLayoutNameA@4

_GetConsoleKeyboardLayoutNameW@4

_GetConsoleMode@8

_GetConsoleOutputCP@0

_GetConsoleScreenBufferInfo@8

_GetConsoleTitleA@8

_GetConsoleTitleW@8

_GetCurrencyFormatA@24

_GetCurrencyFormatW@24

_GetCurrentConsoleFont@12

_GetCurrentDirectoryA@8

_GetCurrentDirectoryW@8

_GetCurrentProcess@0

_GetCurrentProcessId@0

_GetCurrentThread@0

_GetCurrentThreadId@0

_GetDateFormatA@24

_GetDateFormatW@24

_GetDefaultCommConfigA@12

_GetDefaultCommConfigW@12

_GetDiskFreeSpaceA@20

_GetDiskFreeSpaceExA@16

_GetDiskFreeSpaceExW@16

_GetDiskFreeSpaceW@20

_GetDriveTypeA@4

_GetDriveTypeW@4

_GetEnvironmentStrings@0

_GetEnvironmentStringsA@0

_GetEnvironmentStringsW@0

_GetEnvironmentVariableA@12

_GetEnvironmentVariableW@12

_GetExitCodeProcess@8

_GetExitCodeThread@8

_GetFileAttributesA@4

_GetFileAttributesExA@12

_GetFileAttributesExW@12

_GetFileAttributesW@4

_GetFileInformationByHandle@8

_GetFileSize@8

_GetFileTime@16

_GetFileType@4

_GetFullPathNameA@16

_GetFullPathNameW@16

_GetHandleInformation@8

_GetLargestConsoleWindowSize@4

_GetLastError@0

_GetLocalTime@4

_GetLocaleInfoA@16

_GetLocaleInfoW@16

_GetLogicalDriveStringsA@8

_GetLogicalDriveStringsW@8

_GetLogicalDrives@0

_GetMailslotInfo@20

_GetModuleFileNameA@12

_GetModuleFileNameW@12

_GetModuleHandleA@4

_GetModuleHandleW@4

_GetNamedPipeHandleStateA@28

_GetNamedPipeHandleStateW@28

_GetNamedPipeInfo@20

_GetNextVDMCommand@4

_GetNumberFormatA@24

_GetNumberFormatW@24

_GetNumberOfConsoleFonts@0

_GetNumberOfConsoleInputEvents@8

_GetNumberOfConsoleMouseButtons@4

_GetOEMCP@0

_GetOverlappedResult@16

_GetPriorityClass@4

_GetPrivateProfileIntA@16

_GetPrivateProfileIntW@16

_GetPrivateProfileSectionA@16

_GetPrivateProfileSectionNamesA@12

_GetPrivateProfileSectionNamesW@12

_GetPrivateProfileSectionW@16

_GetPrivateProfileStringA@24

_GetPrivateProfileStringW@24

_GetPrivateProfileStructA@20

_GetPrivateProfileStructW@20

_GetProcAddress@8

_GetProcessAffinityMask@12

_GetProcessHeap@0

_GetProcessHeaps@8

_GetProcessPriorityBoost@8

_GetProcessShutdownParameters@8

_GetProcessTimes@20

_GetProcessVersion@4

_GetProcessWorkingSetSize@12

_GetProfileIntA@12

_GetProfileIntW@12

_GetProfileSectionA@12

_GetProfileSectionW@12

_GetProfileStringA@20

_GetProfileStringW@20

_GetQueuedCompletionStatus@20

_GetShortPathNameA@12

_GetShortPathNameW@12

_GetStartupInfoA@4

_GetStartupInfoW@4

_GetStdHandle@4

_GetStringTypeA@20

_GetStringTypeExA@20

_GetStringTypeExW@20

_GetStringTypeW@16

_GetSystemDefaultLCID@0

_GetSystemDefaultLangID@0

_GetSystemDirectoryA@8

_GetSystemDirectoryW@8

_GetSystemInfo@4

_GetSystemPowerStatus@4

_GetSystemTime@4

_GetSystemTimeAdjustment@12

_GetSystemTimeAsFileTime@4

_GetTapeParameters@16

_GetTapePosition@20

_GetTapeStatus@4

_GetTempFileNameA@16

_GetTempFileNameW@16

_GetTempPathA@8

_GetTempPathW@8

_GetThreadContext@8

_GetThreadLocale@0

_GetThreadPriority@4

_GetThreadPriorityBoost@8

_GetThreadSelectorEntry@12

_GetThreadTimes@20

_GetTickCount@0

_GetTimeFormatA@24

_GetTimeFormatW@24

_GetTimeZoneInformation@4

_GetUserDefaultLCID@0

_GetUserDefaultLangID@0

_GetVDMCurrentDirectories@8

_GetVersion@0

_GetVersionExA@4

_GetVersionExW@4

_GetVolumeInformationA@32

_GetVolumeInformationW@32

_GetWindowsDirectoryA@8

_GetWindowsDirectoryW@8

_GlobalAddAtomA@4

_GlobalAddAtomW@4

_GlobalAlloc@8

_GlobalCompact@4

_GlobalDeleteAtom@4

_GlobalFindAtomA@4

_GlobalFindAtomW@4

_GlobalFix@4

_GlobalFlags@4

_GlobalFree@4

_GlobalGetAtomNameA@12

_GlobalGetAtomNameW@12

_GlobalHandle@4

_GlobalLock@4

_GlobalMemoryStatus@4

_GlobalReAlloc@12

_GlobalSize@4

_GlobalUnWire@4

_GlobalUnfix@4

_GlobalUnlock@4

_GlobalWire@4

_HeapAlloc@12

_HeapCompact@8

_HeapCreate@12

_HeapCreateTagsW@16

_HeapDestroy@4

_HeapExtend@16

_HeapFree@12

_HeapLock@4

_HeapQueryTagW@20

_HeapReAlloc@16

_HeapSize@12

_HeapSummary@12

_HeapUnlock@4

_HeapUsage@20

_HeapValidate@12

_HeapWalk@8

_InitAtomTable@4

_InitializeCriticalSection@4

_InterlockedCompareExchange@12

_InterlockedDecrement@4

_InterlockedExchange@8

_InterlockedExchangeAdd@8

_InterlockedIncrement@4

_InvalidateConsoleDIBits@8

_IsBadCodePtr@4

_IsBadHugeReadPtr@8

_IsBadHugeWritePtr@8

_IsBadReadPtr@8

_IsBadStringPtrA@8

_IsBadStringPtrW@8

_IsBadWritePtr@8

_IsDBCSLeadByte@4

_IsDBCSLeadByteEx@8

_IsDebuggerPresent@0

_IsProcessorFeaturePresent@4

_IsValidCodePage@4

_IsValidLocale@8

_LCMapStringA@24

_LCMapStringW@24

_LeaveCriticalSection@4

_LoadLibraryA@4

_LoadLibraryExA@12

_LoadLibraryExW@12

_LoadLibraryW@4

_LoadModule@8

_LoadResource@8

_LocalAlloc@8

_LocalCompact@4

_LocalFileTimeToFileTime@8

_LocalFlags@4

_LocalFree@4

_LocalHandle@4

_LocalLock@4

_LocalReAlloc@12

_LocalShrink@8

_LocalSize@4

_LocalUnlock@4

_LockFile@20

_LockFileEx@24

_LockResource@4

_MapViewOfFile@20

_MapViewOfFileEx@24

_MoveFileA@8

_MoveFileExA@12

_MoveFileExW@12

_MoveFileW@8

_MulDiv@12

_MultiByteToWideChar@24

_OpenConsoleW@16

_OpenEventA@12

_OpenEventW@12

_OpenFile@12

_OpenFileMappingA@12

_OpenFileMappingW@12

_OpenMutexA@12

_OpenMutexW@12

_OpenProcess@12

_OpenProfileUserMapping@0

_OpenSemaphoreA@12

_OpenSemaphoreW@12

_OpenWaitableTimerA@12

_OpenWaitableTimerW@12

_OutputDebugStringA@4

_OutputDebugStringW@4

_PeekConsoleInputA@16

_PeekConsoleInputW@16

_PeekNamedPipe@24

_PostQueuedCompletionStatus@16

_PrepareTape@12

_PulseEvent@4

_PurgeComm@8

_QueryDosDeviceA@12

_QueryDosDeviceW@12

_QueryPerformanceCounter@4

_QueryPerformanceFrequency@4

_QueryWin31IniFilesMappedToRegistry@16

_QueueUserAPC@12

_RaiseException@16

_ReadConsoleA@20

_ReadConsoleInputA@16

_ReadConsoleInputExA@20

_ReadConsoleInputExW@20

_ReadConsoleInputW@16

_ReadConsoleOutputA@20

_ReadConsoleOutputAttribute@20

_ReadConsoleOutputCharacterA@20

_ReadConsoleOutputCharacterW@20

_ReadConsoleOutputW@20

_ReadConsoleW@20

_ReadDirectoryChangesW@32

_ReadFile@20

_ReadFileEx@20

_ReadProcessMemory@20

_RegisterConsoleVDM@44

_RegisterWaitForInputIdle@4

_RegisterWowBaseHandlers@4

_RegisterWowExec@4

_ReleaseMutex@4

_ReleaseSemaphore@12

_RemoveDirectoryA@4

_RemoveDirectoryW@4

_ResetEvent@4

_ResumeThread@4

_RtlFillMemory@12

_RtlMoveMemory@12

_RtlUnwind@16

_RtlZeroMemory@8

_ScrollConsoleScreenBufferA@20

_ScrollConsoleScreenBufferW@20

_SearchPathA@24

_SearchPathW@24

_SetCommBreak@4

_SetCommConfig@12

_SetCommMask@8

_SetCommState@8

_SetCommTimeouts@8

_SetComputerNameA@4

_SetComputerNameW@4

_SetConsoleActiveScreenBuffer@4

_SetConsoleCP@4

_SetConsoleCommandHistoryMode@4

_SetConsoleCtrlHandler@8

_SetConsoleCursor@8

_SetConsoleCursorInfo@8

_SetConsoleCursorPosition@8

_SetConsoleDisplayMode@12

_SetConsoleFont@8

_SetConsoleHardwareState@12

_SetConsoleIcon@4

_SetConsoleInputExeNameA@4

_SetConsoleInputExeNameW@4

_SetConsoleKeyShortcuts@16

_SetConsoleMaximumWindowSize@8

_SetConsoleMenuClose@4

_SetConsoleMode@8

_SetConsoleNumberOfCommandsA@8

_SetConsoleNumberOfCommandsW@8

_SetConsoleOutputCP@4

_SetConsolePalette@12

_SetConsoleScreenBufferSize@8

_SetConsoleTextAttribute@8

_SetConsoleTitleA@4

_SetConsoleTitleW@4

_SetConsoleWindowInfo@12

_SetCurrentDirectoryA@4

_SetCurrentDirectoryW@4

_SetDefaultCommConfigA@12

_SetDefaultCommConfigW@12

_SetEndOfFile@4

_SetEnvironmentVariableA@8

_SetEnvironmentVariableW@8

_SetErrorMode@4

_SetEvent@4

_SetFileApisToANSI@0

_SetFileApisToOEM@0

_SetFileAttributesA@8

_SetFileAttributesW@8

_SetFilePointer@16

_SetFileTime@16

_SetHandleCount@4

_SetHandleInformation@12

_SetLastConsoleEventActive@0

_SetLastError@4

_SetLocalTime@4

_SetLocaleInfoA@12

_SetLocaleInfoW@12

_SetMailslotInfo@8

_SetNamedPipeHandleState@16

_SetPriorityClass@8

_SetProcessAffinityMask@8

_SetProcessPriorityBoost@8

_SetProcessShutdownParameters@8

_SetProcessWorkingSetSize@12

_SetStdHandle@8

_SetSystemPowerState@8

_SetSystemTime@4

_SetSystemTimeAdjustment@8

_SetTapeParameters@12

_SetTapePosition@24

_SetThreadAffinityMask@8

_SetThreadContext@8

_SetThreadIdealProcessor@8

_SetThreadLocale@4

_SetThreadPriority@8

_SetThreadPriorityBoost@8

_SetTimeZoneInformation@4

_SetUnhandledExceptionFilter@4

_SetVDMCurrentDirectories@8

_SetVolumeLabelA@8

_SetVolumeLabelW@8

_SetWaitableTimer@24

_SetupComm@12

_ShowConsoleCursor@8

_SignalObjectAndWait@16

_SizeofResource@8

_Sleep@4

_SleepEx@8

_SuspendThread@4

_SwitchToFiber@4

_SwitchToThread@0

_SystemTimeToFileTime@8

_SystemTimeToTzSpecificLocalTime@12

_TerminateProcess@8

_TerminateThread@8

_TlsAlloc@0

_TlsFree@4

_TlsGetValue@4

_TlsSetValue@8

_TransactNamedPipe@28

_TransmitCommChar@8

_TrimVirtualBuffer@4

_TryEnterCriticalSection@4

_UnhandledExceptionFilter@4

_UnlockFile@20

_UnlockFileEx@20

_UnmapViewOfFile@4

_UpdateResourceA@24

_UpdateResourceW@24

_VDMConsoleOperation@8

_VDMOperationStarted@4

_VerLanguageNameA@12

_VerLanguageNameW@12

_VerifyConsoleIoHandle@4

_VirtualAlloc@16

_VirtualAllocEx@20

_VirtualBufferExceptionHandler@12

_VirtualFree@12

_VirtualFreeEx@16

_VirtualLock@8

_VirtualProtect@16

_VirtualProtectEx@20

_VirtualQuery@12

_VirtualQueryEx@16

_VirtualUnlock@8

_WaitCommEvent@12

_WaitForDebugEvent@8

_WaitForMultipleObjects@16

_WaitForMultipleObjectsEx@20

_WaitForSingleObject@8

_WaitForSingleObjectEx@12

_WaitNamedPipeA@8

_WaitNamedPipeW@8

_WideCharToMultiByte@32

_WinExec@8

_WriteConsoleA@20

_WriteConsoleInputA@16

_WriteConsoleInputVDMA@16

_WriteConsoleInputVDMW@16

_WriteConsoleInputW@16

_WriteConsoleOutputA@20

_WriteConsoleOutputAttribute@20

_WriteConsoleOutputCharacterA@20

_WriteConsoleOutputCharacterW@20

_WriteConsoleOutputW@20

_WriteConsoleW@20

_WriteFile@20

_WriteFileEx@20

_WritePrivateProfileSectionA@12

_WritePrivateProfileSectionW@12

_WritePrivateProfileStringA@16

_WritePrivateProfileStringW@16

_WritePrivateProfileStructA@20

_WritePrivateProfileStructW@20

_WriteProcessMemory@20

_WriteProfileSectionA@8

_WriteProfileSectionW@8

_WriteProfileStringA@12

_WriteProfileStringW@12

_WriteTapemark@16

__IMPORT_DESCRIPTOR_KERNEL32

__NULL_IMPORT_DESCRIPTOR

__hread@12

__hwrite@12

__lclose@4

__lcreat@8

__llseek@12

__lopen@8

__lread@12

__lwrite@12

_lstrcat@8

_lstrcatA@8

_lstrcatW@8

_lstrcmp@8

_lstrcmpA@8

_lstrcmpW@8

_lstrcmpi@8

_lstrcmpiA@8

_lstrcmpiW@8

_lstrcpy@8

_lstrcpyA@8

_lstrcpyW@8

_lstrcpyn@12

_lstrcpynA@12

_lstrcpynW@12

_lstrlen@4

_lstrlenA@4

_lstrlenW@4

Kernel32.lib Pointer Variable Export Names

__imp__AddAtomA@4

__imp__AddAtomW@4

__imp__AddConsoleAliasA@12

__imp__AddConsoleAliasW@12

__imp__AllocConsole@0

__imp__AreFileApisANSI@0

__imp__BackupRead@28

__imp__BackupSeek@24

__imp__BackupWrite@28

__imp__BaseAttachCompleteThunk@0

__imp__Beep@8

__imp__BeginUpdateResourceA@8

__imp__BeginUpdateResourceW@8

__imp__BuildCommDCBA@8

__imp__BuildCommDCBAndTimeoutsA@12

__imp__BuildCommDCBAndTimeoutsW@12

__imp__BuildCommDCBW@8

__imp__CallNamedPipeA@28

__imp__CallNamedPipeW@28

__imp__CancelIo@4

__imp__CancelWaitableTimer@4

__imp__ClearCommBreak@4

__imp__ClearCommError@12

__imp__CloseConsoleHandle@4

__imp__CloseHandle@4

__imp__CloseProfileUserMapping@0

__imp__CmdBatNotification@4

__imp__CommConfigDialogA@12

__imp__CommConfigDialogW@12

__imp__CompareFileTime@8

__imp__CompareStringA@24

__imp__CompareStringW@24

__imp__ConnectNamedPipe@8

__imp__ConsoleMenuControl@12

__imp__ContinueDebugEvent@12

__imp__ConvertDefaultLocale@4

__imp__ConvertThreadToFiber@4

__imp__CopyFileA@12

__imp__CopyFileExA@24

__imp__CopyFileExW@24

__imp__CopyFileW@12

__imp__CreateConsoleScreenBuffer@20

__imp__CreateDirectoryA@8

__imp__CreateDirectoryExA@12

__imp__CreateDirectoryExW@12

__imp__CreateDirectoryW@8

__imp__CreateEventA@16

__imp__CreateEventW@16

__imp__CreateFiber@12

__imp__CreateFileA@28

__imp__CreateFileMappingA@24

__imp__CreateFileMappingW@24

__imp__CreateFileW@28

__imp__CreateIoCompletionPort@16

__imp__CreateMailslotA@16

__imp__CreateMailslotW@16

__imp__CreateMutexA@12

__imp__CreateMutexW@12

__imp__CreateNamedPipeA@32

__imp__CreateNamedPipeW@32

__imp__CreatePipe@16

__imp__CreateProcessA@40

__imp__CreateProcessW@40

__imp__CreateRemoteThread@28

__imp__CreateSemaphoreA@16

__imp__CreateSemaphoreW@16

__imp__CreateTapePartition@16

__imp__CreateThread@24

__imp__CreateVirtualBuffer@12

__imp__CreateWaitableTimerA@12

__imp__CreateWaitableTimerW@12

__imp__DebugActiveProcess@4

__imp__DebugBreak@0

__imp__DefineDosDeviceA@12

__imp__DefineDosDeviceW@12

__imp__DeleteAtom@4

__imp__DeleteCriticalSection@4

__imp__DeleteFiber@4

__imp__DeleteFileA@4

__imp__DeleteFileW@4

__imp__DeviceIoControl@32

__imp__DisableThreadLibraryCalls@4

__imp__DisconnectNamedPipe@4

__imp__DosDateTimeToFileTime@12

__imp__DuplicateConsoleHandle@16

__imp__DuplicateHandle@28

__imp__EndUpdateResourceA@8

__imp__EndUpdateResourceW@8

__imp__EnterCriticalSection@4

__imp__EnumCalendarInfoA@16

__imp__EnumCalendarInfoW@16

__imp__EnumDateFormatsA@12

__imp__EnumDateFormatsW@12

__imp__EnumResourceLanguagesA@20

__imp__EnumResourceLanguagesW@20

__imp__EnumResourceNamesA@16

__imp__EnumResourceNamesW@16

__imp__EnumResourceTypesA@12

__imp__EnumResourceTypesW@12

__imp__EnumSystemCodePagesA@8

__imp__EnumSystemCodePagesW@8

__imp__EnumSystemLocalesA@8

__imp__EnumSystemLocalesW@8

__imp__EnumTimeFormatsA@12

__imp__EnumTimeFormatsW@12

__imp__EraseTape@12

__imp__EscapeCommFunction@8

__imp__ExitProcess@4

__imp__ExitThread@4

__imp__ExitVDM@8

__imp__ExpandEnvironmentStringsA@12

__imp__ExpandEnvironmentStringsW@12

__imp__ExpungeConsoleCommandHistoryA@4

__imp__ExpungeConsoleCommandHistoryW@4

__imp__ExtendVirtualBuffer@8

__imp__FatalAppExitA@8

__imp__FatalAppExitW@8

__imp__FatalExit@4

__imp__FileTimeToDosDateTime@12

__imp__FileTimeToLocalFileTime@8

__imp__FileTimeToSystemTime@8

__imp__FillConsoleOutputAttribute@20

__imp__FillConsoleOutputCharacterA@20

__imp__FillConsoleOutputCharacterW@20

__imp__FindAtomA@4

__imp__FindAtomW@4

__imp__FindClose@4

__imp__FindCloseChangeNotification@4

__imp__FindFirstChangeNotificationA@12

__imp__FindFirstChangeNotificationW@12

__imp__FindFirstFileA@8

__imp__FindFirstFileExA@24

__imp__FindFirstFileExW@24

__imp__FindFirstFileW@8

__imp__FindNextChangeNotification@4

__imp__FindNextFileA@8

__imp__FindNextFileW@8

__imp__FindResourceA@12

__imp__FindResourceExA@16

__imp__FindResourceExW@16

__imp__FindResourceW@12

__imp__FlushConsoleInputBuffer@4

__imp__FlushFileBuffers@4

__imp__FlushInstructionCache@12

__imp__FlushViewOfFile@8

__imp__FoldStringA@20

__imp__FoldStringW@20

__imp__FormatMessageA@28

__imp__FormatMessageW@28

__imp__FreeConsole@0

__imp__FreeEnvironmentStringsA@4

__imp__FreeEnvironmentStringsW@4

__imp__FreeLibrary@4

__imp__FreeLibraryAndExitThread@8

__imp__FreeResource@4

__imp__FreeVirtualBuffer@4

__imp__GenerateConsoleCtrlEvent@8

__imp__GetACP@0

__imp__GetAtomNameA@12

__imp__GetAtomNameW@12

__imp__GetBinaryType@8

__imp__GetBinaryTypeA@8

__imp__GetBinaryTypeW@8

__imp__GetCPInfo@8

__imp__GetCommConfig@12

__imp__GetCommMask@8

__imp__GetCommModemStatus@8

__imp__GetCommProperties@8

__imp__GetCommState@8

__imp__GetCommTimeouts@8

__imp__GetCommandLineA@0

__imp__GetCommandLineW@0

__imp__GetCompressedFileSizeA@8

__imp__GetCompressedFileSizeW@8

__imp__GetComputerNameA@8

__imp__GetComputerNameW@8

__imp__GetConsoleAliasA@16

__imp__GetConsoleAliasExesA@8

__imp__GetConsoleAliasExesLengthA@0

__imp__GetConsoleAliasExesLengthW@0

__imp__GetConsoleAliasExesW@8

__imp__GetConsoleAliasW@16

__imp__GetConsoleAliasesA@12

__imp__GetConsoleAliasesLengthA@4

__imp__GetConsoleAliasesLengthW@4

__imp__GetConsoleAliasesW@12

__imp__GetConsoleCP@0

__imp__GetConsoleCommandHistoryA@12

__imp__GetConsoleCommandHistoryLengthA@4

__imp__GetConsoleCommandHistoryLengthW@4

__imp__GetConsoleCommandHistoryW@12

__imp__GetConsoleCursorInfo@8

__imp__GetConsoleDisplayMode@4

__imp__GetConsoleFontInfo@16

__imp__GetConsoleFontSize@8

__imp__GetConsoleHardwareState@12

__imp__GetConsoleInputExeNameA@8

__imp__GetConsoleInputExeNameW@8

__imp__GetConsoleInputWaitHandle@0

__imp__GetConsoleKeyboardLayoutNameA@4

__imp__GetConsoleKeyboardLayoutNameW@4

__imp__GetConsoleMode@8

__imp__GetConsoleOutputCP@0

__imp__GetConsoleScreenBufferInfo@8

__imp__GetConsoleTitleA@8

__imp__GetConsoleTitleW@8

__imp__GetCurrencyFormatA@24

__imp__GetCurrencyFormatW@24

__imp__GetCurrentConsoleFont@12

__imp__GetCurrentDirectoryA@8

__imp__GetCurrentDirectoryW@8

__imp__GetCurrentProcess@0

__imp__GetCurrentProcessId@0

__imp__GetCurrentThread@0

__imp__GetCurrentThreadId@0

__imp__GetDateFormatA@24

__imp__GetDateFormatW@24

__imp__GetDefaultCommConfigA@12

__imp__GetDefaultCommConfigW@12

__imp__GetDiskFreeSpaceA@20

__imp__GetDiskFreeSpaceExA@16

__imp__GetDiskFreeSpaceExW@16

__imp__GetDiskFreeSpaceW@20

__imp__GetDriveTypeA@4

__imp__GetDriveTypeW@4

__imp__GetEnvironmentStrings@0

__imp__GetEnvironmentStringsA@0

__imp__GetEnvironmentStringsW@0

__imp__GetEnvironmentVariableA@12

__imp__GetEnvironmentVariableW@12

__imp__GetExitCodeProcess@8

__imp__GetExitCodeThread@8

__imp__GetFileAttributesA@4

__imp__GetFileAttributesExA@12

__imp__GetFileAttributesExW@12

__imp__GetFileAttributesW@4

__imp__GetFileInformationByHandle@8

__imp__GetFileSize@8

__imp__GetFileTime@16

__imp__GetFileType@4

__imp__GetFullPathNameA@16

__imp__GetFullPathNameW@16

__imp__GetHandleInformation@8

__imp__GetLargestConsoleWindowSize@4

__imp__GetLastError@0

__imp__GetLocalTime@4

__imp__GetLocaleInfoA@16

__imp__GetLocaleInfoW@16

__imp__GetLogicalDriveStringsA@8

__imp__GetLogicalDriveStringsW@8

__imp__GetLogicalDrives@0

__imp__GetMailslotInfo@20

__imp__GetModuleFileNameA@12

__imp__GetModuleFileNameW@12

__imp__GetModuleHandleA@4

__imp__GetModuleHandleW@4

__imp__GetNamedPipeHandleStateA@28

__imp__GetNamedPipeHandleStateW@28

__imp__GetNamedPipeInfo@20

__imp__GetNextVDMCommand@4

__imp__GetNumberFormatA@24

__imp__GetNumberFormatW@24

__imp__GetNumberOfConsoleFonts@0

__imp__GetNumberOfConsoleInputEvents@8

__imp__GetNumberOfConsoleMouseButtons@4

__imp__GetOEMCP@0

__imp__GetOverlappedResult@16

__imp__GetPriorityClass@4

__imp__GetPrivateProfileIntA@16

__imp__GetPrivateProfileIntW@16

__imp__GetPrivateProfileSectionA@16

__imp__GetPrivateProfileSectionNamesA@12

__imp__GetPrivateProfileSectionNamesW@12

__imp__GetPrivateProfileSectionW@16

__imp__GetPrivateProfileStringA@24

__imp__GetPrivateProfileStringW@24

__imp__GetPrivateProfileStructA@20

__imp__GetPrivateProfileStructW@20

__imp__GetProcAddress@8

__imp__GetProcessAffinityMask@12

__imp__GetProcessHeap@0

__imp__GetProcessHeaps@8

__imp__GetProcessPriorityBoost@8

__imp__GetProcessShutdownParameters@8

__imp__GetProcessTimes@20

__imp__GetProcessVersion@4

__imp__GetProcessWorkingSetSize@12

__imp__GetProfileIntA@12

__imp__GetProfileIntW@12

__imp__GetProfileSectionA@12

__imp__GetProfileSectionW@12

__imp__GetProfileStringA@20

__imp__GetProfileStringW@20

__imp__GetQueuedCompletionStatus@20

__imp__GetShortPathNameA@12

__imp__GetShortPathNameW@12

__imp__GetStartupInfoA@4

__imp__GetStartupInfoW@4

__imp__GetStdHandle@4

__imp__GetStringTypeA@20

__imp__GetStringTypeExA@20

__imp__GetStringTypeExW@20

__imp__GetStringTypeW@16

__imp__GetSystemDefaultLCID@0

__imp__GetSystemDefaultLangID@0

__imp__GetSystemDirectoryA@8

__imp__GetSystemDirectoryW@8

__imp__GetSystemInfo@4

__imp__GetSystemPowerStatus@4

__imp__GetSystemTime@4

__imp__GetSystemTimeAdjustment@12

__imp__GetSystemTimeAsFileTime@4

__imp__GetTapeParameters@16

__imp__GetTapePosition@20

__imp__GetTapeStatus@4

__imp__GetTempFileNameA@16

__imp__GetTempFileNameW@16

__imp__GetTempPathA@8

__imp__GetTempPathW@8

__imp__GetThreadContext@8

__imp__GetThreadLocale@0

__imp__GetThreadPriority@4

__imp__GetThreadPriorityBoost@8

__imp__GetThreadSelectorEntry@12

__imp__GetThreadTimes@20

__imp__GetTickCount@0

__imp__GetTimeFormatA@24

__imp__GetTimeFormatW@24

__imp__GetTimeZoneInformation@4

__imp__GetUserDefaultLCID@0

__imp__GetUserDefaultLangID@0

__imp__GetVDMCurrentDirectories@8

__imp__GetVersion@0

__imp__GetVersionExA@4

__imp__GetVersionExW@4

__imp__GetVolumeInformationA@32

__imp__GetVolumeInformationW@32

__imp__GetWindowsDirectoryA@8

__imp__GetWindowsDirectoryW@8

__imp__GlobalAddAtomA@4

__imp__GlobalAddAtomW@4

__imp__GlobalAlloc@8

__imp__GlobalCompact@4

__imp__GlobalDeleteAtom@4

__imp__GlobalFindAtomA@4

__imp__GlobalFindAtomW@4

__imp__GlobalFix@4

__imp__GlobalFlags@4

__imp__GlobalFree@4

__imp__GlobalGetAtomNameA@12

__imp__GlobalGetAtomNameW@12

__imp__GlobalHandle@4

__imp__GlobalLock@4

__imp__GlobalMemoryStatus@4

__imp__GlobalReAlloc@12

__imp__GlobalSize@4

__imp__GlobalUnWire@4

__imp__GlobalUnfix@4

__imp__GlobalUnlock@4

__imp__GlobalWire@4

__imp__HeapAlloc@12

__imp__HeapCompact@8

__imp__HeapCreate@12

__imp__HeapCreateTagsW@16

__imp__HeapDestroy@4

__imp__HeapExtend@16

__imp__HeapFree@12

__imp__HeapLock@4

__imp__HeapQueryTagW@20

__imp__HeapReAlloc@16

__imp__HeapSize@12

__imp__HeapSummary@12

__imp__HeapUnlock@4

__imp__HeapUsage@20

__imp__HeapValidate@12

__imp__HeapWalk@8

__imp__InitAtomTable@4

__imp__InitializeCriticalSection@4

__imp__InterlockedCompareExchange@12

__imp__InterlockedDecrement@4

__imp__InterlockedExchange@8

__imp__InterlockedExchangeAdd@8

__imp__InterlockedIncrement@4

__imp__InvalidateConsoleDIBits@8

__imp__IsBadCodePtr@4

__imp__IsBadHugeReadPtr@8

__imp__IsBadHugeWritePtr@8

__imp__IsBadReadPtr@8

__imp__IsBadStringPtrA@8

__imp__IsBadStringPtrW@8

__imp__IsBadWritePtr@8

__imp__IsDBCSLeadByte@4

__imp__IsDBCSLeadByteEx@8

__imp__IsDebuggerPresent@0

__imp__IsProcessorFeaturePresent@4

__imp__IsValidCodePage@4

__imp__IsValidLocale@8

__imp__LCMapStringA@24

__imp__LCMapStringW@24

__imp__LeaveCriticalSection@4

__imp__LoadLibraryA@4

__imp__LoadLibraryExA@12

__imp__LoadLibraryExW@12

__imp__LoadLibraryW@4

__imp__LoadModule@8

__imp__LoadResource@8

__imp__LocalAlloc@8

__imp__LocalCompact@4

__imp__LocalFileTimeToFileTime@8

__imp__LocalFlags@4

__imp__LocalFree@4

__imp__LocalHandle@4

__imp__LocalLock@4

__imp__LocalReAlloc@12

__imp__LocalShrink@8

__imp__LocalSize@4

__imp__LocalUnlock@4

__imp__LockFile@20

__imp__LockFileEx@24

__imp__LockResource@4

__imp__MapViewOfFile@20

__imp__MapViewOfFileEx@24

__imp__MoveFileA@8

__imp__MoveFileExA@12

__imp__MoveFileExW@12

__imp__MoveFileW@8

__imp__MulDiv@12

__imp__MultiByteToWideChar@24

__imp__OpenConsoleW@16

__imp__OpenEventA@12

__imp__OpenEventW@12

__imp__OpenFile@12

__imp__OpenFileMappingA@12

__imp__OpenFileMappingW@12

__imp__OpenMutexA@12

__imp__OpenMutexW@12

__imp__OpenProcess@12

__imp__OpenProfileUserMapping@0

__imp__OpenSemaphoreA@12

__imp__OpenSemaphoreW@12

__imp__OpenWaitableTimerA@12

__imp__OpenWaitableTimerW@12

__imp__OutputDebugStringA@4

__imp__OutputDebugStringW@4

__imp__PeekConsoleInputA@16

__imp__PeekConsoleInputW@16

__imp__PeekNamedPipe@24

__imp__PostQueuedCompletionStatus@16

__imp__PrepareTape@12

__imp__PulseEvent@4

__imp__PurgeComm@8

__imp__QueryDosDeviceA@12

__imp__QueryDosDeviceW@12

__imp__QueryPerformanceCounter@4

__imp__QueryPerformanceFrequency@4

__imp__QueryWin31IniFilesMappedToRegistry@16

__imp__QueueUserAPC@12

__imp__RaiseException@16

__imp__ReadConsoleA@20

__imp__ReadConsoleInputA@16

__imp__ReadConsoleInputExA@20

__imp__ReadConsoleInputExW@20

__imp__ReadConsoleInputW@16

__imp__ReadConsoleOutputA@20

__imp__ReadConsoleOutputAttribute@20

__imp__ReadConsoleOutputCharacterA@20

__imp__ReadConsoleOutputCharacterW@20

__imp__ReadConsoleOutputW@20

__imp__ReadConsoleW@20

__imp__ReadDirectoryChangesW@32

__imp__ReadFile@20

__imp__ReadFileEx@20

__imp__ReadProcessMemory@20

__imp__RegisterConsoleVDM@44

__imp__RegisterWaitForInputIdle@4

__imp__RegisterWowBaseHandlers@4

__imp__RegisterWowExec@4

__imp__ReleaseMutex@4

__imp__ReleaseSemaphore@12

__imp__RemoveDirectoryA@4

__imp__RemoveDirectoryW@4

__imp__ResetEvent@4

__imp__ResumeThread@4

__imp__RtlFillMemory@12

__imp__RtlMoveMemory@12

__imp__RtlUnwind@16

__imp__RtlZeroMemory@8

__imp__ScrollConsoleScreenBufferA@20

__imp__ScrollConsoleScreenBufferW@20

__imp__SearchPathA@24

__imp__SearchPathW@24

__imp__SetCommBreak@4

__imp__SetCommConfig@12

__imp__SetCommMask@8

__imp__SetCommState@8

__imp__SetCommTimeouts@8

__imp__SetComputerNameA@4

__imp__SetComputerNameW@4

__imp__SetConsoleActiveScreenBuffer@4

__imp__SetConsoleCP@4

__imp__SetConsoleCommandHistoryMode@4

__imp__SetConsoleCtrlHandler@8

__imp__SetConsoleCursor@8

__imp__SetConsoleCursorInfo@8

__imp__SetConsoleCursorPosition@8

__imp__SetConsoleDisplayMode@12

__imp__SetConsoleFont@8

__imp__SetConsoleHardwareState@12

__imp__SetConsoleIcon@4

__imp__SetConsoleInputExeNameA@4

__imp__SetConsoleInputExeNameW@4

__imp__SetConsoleKeyShortcuts@16

__imp__SetConsoleMaximumWindowSize@8

__imp__SetConsoleMenuClose@4

__imp__SetConsoleMode@8

__imp__SetConsoleNumberOfCommandsA@8

__imp__SetConsoleNumberOfCommandsW@8

__imp__SetConsoleOutputCP@4

__imp__SetConsolePalette@12

__imp__SetConsoleScreenBufferSize@8

__imp__SetConsoleTextAttribute@8

__imp__SetConsoleTitleA@4

__imp__SetConsoleTitleW@4

__imp__SetConsoleWindowInfo@12

__imp__SetCurrentDirectoryA@4

__imp__SetCurrentDirectoryW@4

__imp__SetDefaultCommConfigA@12

__imp__SetDefaultCommConfigW@12

__imp__SetEndOfFile@4

__imp__SetEnvironmentVariableA@8

__imp__SetEnvironmentVariableW@8

__imp__SetErrorMode@4

__imp__SetEvent@4

__imp__SetFileApisToANSI@0

__imp__SetFileApisToOEM@0

__imp__SetFileAttributesA@8

__imp__SetFileAttributesW@8

__imp__SetFilePointer@16

__imp__SetFileTime@16

__imp__SetHandleCount@4

__imp__SetHandleInformation@12

__imp__SetLastConsoleEventActive@0

__imp__SetLastError@4

__imp__SetLocalTime@4

__imp__SetLocaleInfoA@12

__imp__SetLocaleInfoW@12

__imp__SetMailslotInfo@8

__imp__SetNamedPipeHandleState@16

__imp__SetPriorityClass@8

__imp__SetProcessAffinityMask@8

__imp__SetProcessPriorityBoost@8

__imp__SetProcessShutdownParameters@8

__imp__SetProcessWorkingSetSize@12

__imp__SetStdHandle@8

__imp__SetSystemPowerState@8

__imp__SetSystemTime@4

__imp__SetSystemTimeAdjustment@8

__imp__SetTapeParameters@12

__imp__SetTapePosition@24

__imp__SetThreadAffinityMask@8

__imp__SetThreadContext@8

__imp__SetThreadIdealProcessor@8

__imp__SetThreadLocale@4

__imp__SetThreadPriority@8

__imp__SetThreadPriorityBoost@8

__imp__SetTimeZoneInformation@4

__imp__SetUnhandledExceptionFilter@4

__imp__SetVDMCurrentDirectories@8

__imp__SetVolumeLabelA@8

__imp__SetVolumeLabelW@8

__imp__SetWaitableTimer@24

__imp__SetupComm@12

__imp__ShowConsoleCursor@8

__imp__SignalObjectAndWait@16

__imp__SizeofResource@8

__imp__Sleep@4

__imp__SleepEx@8

__imp__SuspendThread@4

__imp__SwitchToFiber@4

__imp__SwitchToThread@0

__imp__SystemTimeToFileTime@8

__imp__SystemTimeToTzSpecificLocalTime@12

__imp__TerminateProcess@8

__imp__TerminateThread@8

__imp__TlsAlloc@0

__imp__TlsFree@4

__imp__TlsGetValue@4

__imp__TlsSetValue@8

__imp__TransactNamedPipe@28

__imp__TransmitCommChar@8

__imp__TrimVirtualBuffer@4

__imp__TryEnterCriticalSection@4

__imp__UnhandledExceptionFilter@4

__imp__UnlockFile@20

__imp__UnlockFileEx@20

__imp__UnmapViewOfFile@4

__imp__UpdateResourceA@24

__imp__UpdateResourceW@24

__imp__VDMConsoleOperation@8

__imp__VDMOperationStarted@4

__imp__VerLanguageNameA@12

__imp__VerLanguageNameW@12

__imp__VerifyConsoleIoHandle@4

__imp__VirtualAlloc@16

__imp__VirtualAllocEx@20

__imp__VirtualBufferExceptionHandler@12

__imp__VirtualFree@12

__imp__VirtualFreeEx@16

__imp__VirtualLock@8

__imp__VirtualProtect@16

__imp__VirtualProtectEx@20

__imp__VirtualQuery@12

__imp__VirtualQueryEx@16

__imp__VirtualUnlock@8

__imp__WaitCommEvent@12

__imp__WaitForDebugEvent@8

__imp__WaitForMultipleObjects@16

__imp__WaitForMultipleObjectsEx@20

__imp__WaitForSingleObject@8

__imp__WaitForSingleObjectEx@12

__imp__WaitNamedPipeA@8

__imp__WaitNamedPipeW@8

__imp__WideCharToMultiByte@32

__imp__WinExec@8

__imp__WriteConsoleA@20

__imp__WriteConsoleInputA@16

__imp__WriteConsoleInputVDMA@16

__imp__WriteConsoleInputVDMW@16

__imp__WriteConsoleInputW@16

__imp__WriteConsoleOutputA@20

__imp__WriteConsoleOutputAttribute@20

__imp__WriteConsoleOutputCharacterA@20

__imp__WriteConsoleOutputCharacterW@20

__imp__WriteConsoleOutputW@20

__imp__WriteConsoleW@20

__imp__WriteFile@20

__imp__WriteFileEx@20

__imp__WritePrivateProfileSectionA@12

__imp__WritePrivateProfileSectionW@12

__imp__WritePrivateProfileStringA@16

__imp__WritePrivateProfileStringW@16

__imp__WritePrivateProfileStructA@20

__imp__WritePrivateProfileStructW@20

__imp__WriteProcessMemory@20

__imp__WriteProfileSectionA@8

__imp__WriteProfileSectionW@8

__imp__WriteProfileStringA@12

__imp__WriteProfileStringW@12

__imp__WriteTapemark@16

__imp___hread@12

__imp___hwrite@12

__imp___lclose@4

__imp___lcreat@8

__imp___llseek@12

__imp___lopen@8

__imp___lread@12

__imp___lwrite@12

__imp__lstrcat@8

__imp__lstrcatA@8

__imp__lstrcatW@8

__imp__lstrcmp@8

__imp__lstrcmpA@8

__imp__lstrcmpW@8

__imp__lstrcmpi@8

__imp__lstrcmpiA@8

__imp__lstrcmpiW@8

__imp__lstrcpy@8

__imp__lstrcpyA@8

__imp__lstrcpyW@8

__imp__lstrcpyn@12

__imp__lstrcpynA@12

__imp__lstrcpynW@12

__imp__lstrlen@4

__imp__lstrlenA@4

__imp__lstrlenW@4


Exception Handling

Win32 Exception handling for assembler programmers by Jeremy Gordon

JGJorg@cs.com
http://win32assembly.programminghorizon.com/www.godevtool.com/default.htm

CONTENTS (click to go there)
1. Background
2. Exception handling in practice
3. Setting up simple exception handlers
4. Stack unwinds
5. The information sent to the handlers
6. Recovering from and Repairing an exception
7. Continuing execution after final handler called
8. Single-stepping by setting the trap flag within the handler
9. Exception handling in multi-threaded applications
10. Except.Exe

Background

We're going to examine how to make an application more robust by handling its own exceptions, rather than permitting the system to do so. An "exception" is an offence committed by the program, which would otherwise result in the embarrassing appearance of the dreaded closure message box:-
wpe3.gif (5741 bytes)
or its more elaborate counterpart in Windows NT.

What exception handling does ...

The idea of exception handling (often called "Structured Exception Handling") is that your application instals one or more callback routines called "exception handlers" at run-time and then, if an exception occurs, the system will call the routine to let the application deal with the exception. The hope would be that the exception handler may be able to repair the exception and continue running either from the same area of code where the exception occurred, or from a "safe place" in the code as if nothing had happened. No closure message box would then be displayed and the user would be done the wiser. As part of this repair it may be necessary to close handles, close temporary files, free device contexts, free memory areas, inform other threads, then unwind the stack or close down the offending thread. During this process the exception handler may make a record of what it is doing and save this to a file for later analysis.

If a repair cannot be achieved, exception handling allows your application to close gracefully, having done as much clearing up, saving of data, and apologising as it can.

Planned exceptions

The Windows SDK suggests another use for exception handling. It is suggested as a way to keep track of memory usage. The idea is that an exception will occur if you need to commit more memory: you intercept it and carry out the memory allocation. This can be done by intercepting a memory access violation [exception number 0C0000005h], which would occur if your code tries to read from, or write to, memory which had not been committed.

Another way suggested to keep track of memory usage is to set the guard page flag in a call to VirtualAlloc when committing the memory, or later using VirtualProtect. This causes a guard page exception [080000001h] if an attempt was made to read to, or write from a guarded area of memory, after which the guard page flag is released. The exception handler would therefore be kept informed of the memory requirements and could reset the flag if required.

These methods are widely used throughout the system, for example, as more stack is required by a thread, it is automatically enlarged.

An application, however, usually knows what it hopes to do next, so it is much simpler and quicker to keep track of memory requirements by keeping the top of the memory area as a data variable, and to check before the start of each series of memory read/write operations whether the memory area needs to be enlarged or diminished.

This works even if more than one thread uses the same area of memory, since the same data variable can be used by each thread. In that case, handling the 0C0000005h exception might only be a backup in case your code went wrong.

And what exception handling cannot do ...

Apart from divide by zero [exception code 0C0000094h] which can easily be avoided by protective coding, the most common type of exception is an attempt to read from, or write to, an illegal memory address [0C0000005h]. There are several ways that the second (illegal address) can arise. For example:-
  • wrong index register values when addressing memory
  • unexpected continuous loops involving memory access
  • mismatch of PUSHes and POPs so execution continues from the wrong place after return from a CALL
  • unforeseen corruption in input data files
It can be seen from this list that exceptions may occur in unexpected circumstances for a variety of reasons. And it will be precisely this type of exception which may terminate your program despite the best efforts of your exception handler. In these circumstances at the very least, the exception handler should try to save important data which would otherwise be lost, and then retire gracefully, with suitable apologies.

Other program failures

Your program may fail for other reasons which will not result in an exception at all.

The usual cause of this is:-

  • insufficient system resources
  • continuous loops in your program which do not involve memory access
The result is that your program will not be able to respond to system messages it will appear to the user simply to have stopped. Luckily, however, because it runs in its own virtual address space other programs will not be affected, although the whole system may appear to run a little more slowly.

Utterly fatal exceptions

Some errors are so bad that the system cannot even manage to call your exception handler. Then only if the user is lucky will the system's closure message box appear, or the devastating bright blue error screen will appear, showing that a "fatal" error has occurred. Almost inevitably this is a result of a total crash of the system and a reboot is the only remedy. Fortunately in Win32 you have to try quite hard to produce such errors, but they can still occur.

... and where exception handling really scores

Having spent some time on what exception handling cannot do, let’s review the instances where it is invaluable:-
  • During program development, to catch and report on errors as an alternative to debug control.
  • When using code written by others which may not be fully trusted.
  • When reading from, or writing to, memory areas which may be moved without notice. For example, while spelunking around system memory areas (which would be under system control) or memory areas which could possibly be closed by other processes or threads.
  • Using pointers from files which may be corrupted or of the wrong format. Here exception handling would be much quicker than using the IsBadReadPtr or IsBadWritePtr APIs to check each pointer immediately prior to its use.
  • As a general catch-all for all unforeseen bugs.

Exception handling in practice

The Windows sequence

In order to understand what your code can or should do when handling exceptions, you need to know in some more detail what the system does when an exception occurs. If you are new to the subject, the following may not yet be clear. However it is necessary to know these steps to understand the subject. The steps are as follows:-
    1. Windows decides first whether it is an exception which it is willing to send to the program's exception handler. If so, if the program is being debugged, Windows will notify the debugger of the exception by suspending the program and sending EXCEPTION_DEBUG_EVENT (value 1h) to the debugger.
    2. If the program is not being debugged or if the exception is not dealt with by the debugger, the system sends the exception to your per-thread exception handler if you have installed one. A per-thread handler is installed at run-time and is pointed to by the first dword in the Thread Information Block whose address is at FS:[0].
    3. The per-thread exception handler can try to deal with the exception, or it may not do so, leaving it for handlers further up the chain, if there are any more handlers installed.
    4. Eventually if none of the per-thread handlers deal with the exception, if the program is being debugged the system will again suspend the program and notify the debugger.
    5. If the program is not being debugged or if the exception is still not dealt with by the debugger, the system will call your final handler if one is installed. This will be a final handler installed at run-time by the application using the API SetUnhandledExceptionFilter.
    6. If your final handler does not deal with the exception after it returns, the system final handler will be called. Optionally it will show the system’s closure message box. Depending on the registry settings, this box may give the user a chance to attach a debugger to the program. If no debugger can be attached or if the debugger is powerless to assist, the program is doomed and the system will call ExitProcess to terminate the program.
    7. Before finally terminating the program, though, the system will cause a "final unwind" of the stack for the thread in which the exception occurred.

Advantages of using assembler for exception handling

Win32 provides only the framework for exception handling, using a handful of APIs. So most of the code required for exception handling has to be coded by hand.

"C" programmers will use various shortcuts provided by their compilers by including in their source code statements such as _try, _except, _finally, _catch and _throw.

One real disadvantage in relying on the compiler’s code is that it can enlarge the final exe file enormously.

Also most C programmers would have no idea what code is produced by the compiler when exception handling is used, and this is a real disadvantage because to handle exceptions properly you need flexibility, understanding and control. This is because exceptions can be intercepted and handled in various ways and at various different levels in your code. Using assembler you can produce tight, reliable and flexible code which you can tailor closely to your own application.

Multi-threaded applications need particularly careful treatment and assembler provides a simple and versatile way to add exception handling to such programs.

Information about exception handling at a low level is hard to get hold of, and the samples in the Win32 Software Development Kit (SDK) concentrate on how to use the "C" compiler statements rather than how to hard-wire a program to use the Win32 framework itself.

The information in this article was obtained using a test program and a debugger, and by disassembling code produced by "C" compilers. The accompanying program, Except.Exe, demonstrates the techniques described here.

Setting up simple exception handlers

I hope you will be pleasantly surprised to see in practice how easy it is in assembler to add exception handling to your programs.

The two types of exception handlers

As you have seen above, there are two types of exception handlers.

Type 1 – the "final" exception handler

The "final" exception handler is called by the system if your program is doomed to close. Because this handler is process-specific it is called irrespective of which thread caused the exception.

Establishing a final exception handler

Typically, this is established in the main thread as soon as possible after the program entry point by calling the API SetUnhandledExceptionFilter. It therefore covers the whole program from that point until termination. There is no need to remove the handler on termination - this is done automatically by windows.

Example
MAIN:
PUSH OFFSET FINAL_HANDLER
CALL SetUnhandledExceptionFilter
; ...
; ...
; ...
CALL ExitProcess
;************************************
FINAL_HANDLER:
; ...
; ...
; ...
;(eax=-1 reload context and continue)
MOV EAX,1
RET 		;program entry point
;
;
;
;code covered by final handler
;
;
;
;
;
;code to provide a polite exit
;
;
;eax=1 stops display of closure box
;eax=0 enables display of the box

No chaining of final exception handlers

There can only be one application-defined final exception handler in the process at any one time. If SetUnhandledExceptionFilter is called a second time in your code the address of the final exception handler is simply changed to the new value, and the previous one is discarded.

Type 2 – the "per-thread" exception handler

This type of handler is typically used to guard certain areas of code and is established by altering the value held by the system at FS:[0]. Each thread in your program has a different value for the segment register FS, so this exception handler will be thread specific. It will be called if an exception occurs during the execution of code protected by the handler.

The value in FS is a 16-bit selector which points to the "Thread Information Block", a structure which contains important information about each thread. The very first dword in the Thread Information Block points to a structure which we are going to call an "ERR" structure.

The "ERR" structure is at least 2 dwords as follows:-

1st dword +0 | Pointer to next ERR structure
2nd dword +4 | Pointer to own exception handler

Establishing a "per-thread" exception handler

So now we can see how easy it is to establish this type of exception handler:-

Example
PUSH OFFSET HANDLER
PUSH FS:[0]
MOV FS:[0],ESP
...
...
...
POP FS:[0]
ADD ESP,4h
RET
;***********************
HANDLER:
...
...
...
MOV EAX,1
RET 		;
;address of next ERR structure
;give FS:[0] the ERR address just made
;
;the code protected by the handler goes here
;
;restore next ERR structure to FS:[0]
;throw away rest of ERR structure
;
;
;
;
;exception handler code goes here
;
;eax=1 go to next handler
;eax=0 reload context & continue execution

Chaining of per-thread exception handlers

In the above code we can see that the 2nd dword of the ERR structure, which is the address of your handler, is put on the stack first, then the 1st dword of the next ERR structure is put on the stack by the instruction PUSH FS:[0]. Suppose the code which was then protected by this handler called other functions which needed their own individual protection. Then you may create another ERR structure and handler to protect that code in exactly the same way. This is called chaining. In practice this means that when an exception occurs the system will walk the handler chain by first calling the exception handler most recently established before the code where the exception occurred. If that handler does not deal with the exception (returning EAX=1), then the system calls the next handler up the chain. Since each ERR structure contains the address of the next handler up the chain, any number of such handlers can be established in this way. Each handler might guard against or deal with particular types of exceptions depending on what is foreseeable in your code. The stack is used to keep the ERR structure, to avoid write-overs. However there is nothing to stop you using other parts of memory for the ERR structures if you prefer.

Stack unwinds

We’re going to look at with stack unwinds at this point because they shouldn’t keep their mystery any longer! A "stack unwind" sounds very dramatic, but in practice it’s simply all about calling the exception handlers whose local data is held further down the stack and then (probably) continuing execution from another stack frame. In other words the program gets ready to ignore the stack contents between these two positions.

Suppose you have a chain of per-thread handlers established as in this arrangement, where Function A calls Function B which calls Function C:-

wpe5.gif (2556 bytes)
Then the stack will look something like this:-
stackâ +ve
3rd

Stack

Frame
Use of stack by Function C
Handler 3
Local Data Function C
2nd

Stack

Frame
Return address Function C
Use of stack by Function B
Handler 2
Local Data Function B
1st

Stack

Frame
Return address Function B
Use of stack by Function A
Handler 1
Local Data Function A
Return address Function A
Stackâ +ve

Here as each function is called things are PUSHed onto the stack: firstly the return address, then local data, and then the exception handler (this is the "ERR" structure referred to earlier).
Then suppose that an exception occurs in Function C. As we have seen, the system will cause a walk of the handler chain. Handler 3 will be called first. Suppose Handler 3 does not deal with the exception (returning EAX=1), then Handler 2 will be called. Suppose Handler 2 also returns EAX=1 so that Handler 1 is called. If Handler 1 deals with the exception, it may need to cause a clear-up using local data in the stack frames created by Functions B and C.
It can do so by causing an Unwind.

This simply repeats the walk of the handler chain again, causing first Handler 3 then Handler 2, then Handler 1 to be called in turn.

The differences between this type of handler chain walk and the walk initiated by the system when the exception first occurred are as follows:-

    1. This handler walk is initiated by your handler rather than by the system
    2. The exception flag in the EXCEPTION_RECORD should be set to 2h (EH_UNWINDING). This indicates to the per-thread handler that it is being called by another handler higher in the chain to clear-up using local data. It should not attempt to do any more than that and it must return EAX=1.
    3. The handler walk stops at the handler immediately before the caller. For example in the diagram, if Handler 1 initiates the unwind, the last Handler to be called during the unwind is Handler 2. There is no need for Handler 1 to be called from within itself because it has access to its own local data to clear-up.
You can see below ("Providing access to local data") how the handler is able to find local data during the handler walk.

How the unwind is done

The handler can initiate an unwind using the API RtlUnwind or, as we shall see, it can also easily be done using your own code. This API can be called as follows:- PUSH Return value>
PUSH pExceptionRecord
PUSH OFFSET CodeLabel
PUSH LastStackFrame
CALL RtlUnwind

Where:-

Return value is said to give a return value after the unwind (you would probably not use this)

pExceptionRecord is a pointer to the exception record, which is one of the structures sent to the handler when an exception occurs

CodeLabel is a place from which execution should continue after the unwind and is typically the code address immediately after the call to RtlUnwind. If this is not specified the API appears to return in the normal way, however the SDK suggests that it should be used and it is better to play safe with this type of API

LastStackFrame is the stack frame at which the unwind should stop. Typically this will be the stack address of the ERR structure which contains the address of the handler which is initiating the unwind
Unlike other APIs you cannot rely on RtlUnwind saving the EBX, ESI or EDI registers – if you are using these in your code you should ensure that they are saved prior to PUSHing the first parameter and restored after the CodeLabel

Own-code Unwind

The following code simulates the unwind (where ebx holds the address of the EXCEPTION_RECORD structure sent to the handler):-
MOV D[EBX+4],2h
MOV EDI,FS:[0]
>L2:
CMP D[EDI],-1
JZ >L3
PUSH EDI,EBX
CALL [EDI+4]
ADD ESP,8h
MOV EDI,[EDI]
JMP L2
L3: 		;make the exception flag EH_UNWINDING
;get 1st per-thread handler address
;
;see if it’s the last one
;yes, so finish
;push ERR structure, EXCEPTION_RECORD
;call handler to run clear-up code
;remove the two parameters pushed
;get pointer to next ERR structure
;and do next if not at end
;code label when finished
Here each handler is called in turn with the ExceptionFlag set to 2h until the last handler is reached (the system has a value of –1 in the last ERR structure).

The above code does not check for corruption of the values at [EDI] and at [EDI+4]. The first is a stack address and could be checked by ensuring that it is above the thread’s stack base given by FS:[8] and below the thread’s stack top given by FS:[4]. The second is a code address and so you could check that it lies within two code labels, one at the start of your code and one at the end of it. Alternatively you could check that [EDI] and [EDI+4] could be read by calling the API IsBadReadPtr.

Unwind by final handler then continue

It is not just a per-thread handler which can initiate a stack unwind. It can also be done in your final handler by calling either RtlUnwind or an own-code unwind and then returning EAX= –1. (See "Continuing execution after final handler called").

Final unwind then terminate

If a final handler is installed and it returns either EAX=0 or EAX=1, the system will cause the process to terminate. However, before final termination something interesting happens. The system does a final unwind by going back to the very first handler in the chain (that is to say, the handler guarding the code in which the exception occurred). This is the very last opportunity for your handler to execute the clear-up code necessary within each stack frame. You can see this final unwind clearly occurring if you set the accompanying demo program Except.Exe to allow the exception to go to the final handler and press either F3 or F5 when there.

The information sent to the handlers

Clearly sufficient information must be sent to the handlers for them to be able to try to repair the exception, make error logs, or report to the user. As we shall see, this information is sent by the system itself on the stack, when the handlers are called. In addition to this you can send your own information to the handlers by enlarging the ERR structure so that it contains more information.

The information sent to the final handler

The final handler is documented in the Windows Software Development Kit ("SDK") as the API "UnhandledExceptionFilter". It receives one parameter only, a pointer to the structure EXCEPTION_POINTERS. This structure is as follows:-
EXCEPTION_POINTERS +0 | Pointer to structure:- EXCEPTION_RECORD
+4 | Pointer to structure:- CONTEXT record

The structure EXCEPTION_RECORD has these fields:-
EXCEPTION_RECORD +0 | ExceptionCode
+4 | ExceptionFlag
+8 | NestedExceptionRecord
+C | ExceptionAddress
+10 | NumberParameters
+14 | AdditionalData

Where

ExceptionCode gives the type of exception which has occurred. There are a number of these listed in the SDK and header files, but in practice, the types which you may come across are:-

C0000005h – Read or write memory violation

C0000094h – Divide by zero

C00000FDh – The stack went beyond the maximum available size

80000001h – Violation of a guard page in memory set up using Virtual Alloc

The following only occur whilst dealing with exceptions:-

C0000025h – A non-continuable exception – the handler should not try to deal with it

C0000026h – Exception code used the by system during exception handling. This code might be used if the system encounters an unexpected return from a handler. It is also used if no Exception Record is supplied when calling RtlUnwind.

The following are used in debugging:-

80000003h – Breakpoint occurred because there was an INT3 in the code

80000004h – Single step during debugging
The exception codes follow these rules:
Bits 31-30 Bit 29 Bit 28 Bits 27-0
0=success 0=Microsoft Reserved For exception
1=information 1=Application Must be zero code
2=warning
3=error
A typical own exception code sent by RaiseException might therefore be E0000100h (error, application, code=100h).
Own user code – this would be sent by your own application by calling the API RaiseException. This is a quick way to exit code directly into your handler if required.

Exception flag which gives instructions to the handler. The values can be:-

0 – a continuable exception (can be repaired)

1 – a non-continuable exception (cannot be repaired)

2 – the stack is unwinding - do not try to repair

Nested exception record pointing to another EXCEPTION_RECORD structure if the handler itself has caused another exception

Exception address – the address in code where the exception occurred

NumberParameters – number of dwords to follow in Additional information

Additional information – array of dwords with further information

This can either be information sent by the application itself when calling RaiseException, or, if the exception code is C0000005h it will be as follows:-

1st dword - 0=a read violation, 1=a write violation.

2nd dword – address of access violation

The second part of the EXCEPTION_POINTERS structure which is sent to the final handler points to the CONTEXT record structure which contains the processor-specific values of all the registers at the time of the exception. WINNT.H contains the CONTEXT structures for various processors. Your program can find out what sort of processor is being used by calling GetSystemInfo. CONTEXT is as follows for IA32 (Intel 386 and upwards):- +0 context flags
(used when calling GetThreadContext)
DEBUG REGISTERS
+4 debug register #0
+8 debug register #1
+C debug register #2
+10 debug register #3
+14 debug register #6
+18 debug register #7
FLOATING POINT / MMX registers
+1C ControlWord
+20 StatusWord
+24 TagWord
+28 ErrorOffset
+2C ErrorSelector
+30 DataOffset
+34 DataSelector
+38 FP registers x 8 (10 bytes each)
+88 Cr0NpxState
SEGMENT REGISTERS
+8C gs register
+90 fs register
+94 es register
+98 ds register
ORDINARY REGISTERS
+9C edi register
+A0 esi register
+A4 ebx register
+A8 edx register
+AC ecx register
+B0 eax register
CONTROL REGISTERS
+B4 ebp register
+B8 eip register
+BC cs register
+C0 eflags register
+C4 esp register
+C8 ss register

The information sent to the per-thread handlers

At the time of the call to the per-thread handler, ESP points to three structures as follows:-
ESP+4 | Pointer to structure:- EXCEPTION_RECORD
ESP+8 | Pointer to own ERR structure
ESP+C | Pointer to structure:- CONTEXT record
Unlike usual CALLBACKs in Windows, when the per-thread handler is called, the C calling convention is used (caller to remove the arguments from the stack) not the PASCAL convention (function to do so). This can be seen from the actual Kernel32 code used to make the call:-

PUSH Param, CONTEXT record, ERR, EXCEPTION_RECORD
CALL HANDLER
ADD ESP,10h

In practice the first argument, Param, was not found to contain meaningful information

The EXCEPTION_RECORD and CONTEXT record structures have already been described above.

The ERR structure is the structure you created on the stack when the handler was established and it must contain the pointer to the next ERR structure and the code address of the handler now being installed (see "Setting up simple exception handlers", above). The pointer to the ERR structure passed to the per-thread handler is to the top of this structure. It is possible, therefore, to enlarge the ERR structure so that the handler can receive additional information.

In a typical arrangement the ERR structure might look like this, where [ESP+8h] points to the top of this structure when the handler is called:-

ERR +0 | Pointer to next ERR structure
+4 | Pointer to own exception handler
+8 | Code address of "safe-place" for handler
+C | Information for handler
+10 | Area for flags
+14 | Value of EBP at safe-place
As we shall see below ("Continuing execution from a safe-place"), the fields at +8 and +14 may be used by the handler to recover from the exception.

Providing access to local data

Let’s now consider the best position of the ERR structure on the stack relative to the stack frame, which may well hold local data variables. This is important because the handler may well need access to this local data in order to clear-up properly. Here is some typical code which may be used to establish a per-thread handler where there is local data:-
MYFUNCTION:
PUSH EBP
MOV EBP,ESP
SUB ESP,40h
;******** local data now at
;********** install handler
PUSH EBP
PUSH 0
PUSH 0
PUSH OFFSET SAFE_PLACE
PUSH OFFSET HANDLER
PUSH FS:[0]
MOV FS:[0],ESP
...
...
...
JMP >L10
SAFE_PLACE:
L10:
POP FS:[0]
MOV ESP,EBP
POP EBP
RET
;*****************
HANDLER:
RET 		;procedure entry point
;save ebp (used to address stack frame)
;use EBP as stack frame pointer
;make 16 dwords on stack for local data
[EBP-4] to [EBP-40h]
and its ERR structure
;ERR+14h save ebp (being ebp at safe-place)
;ERR+10h area for flags
;ERR+0Ch information for handler
;ERR+8h new eip at safe-place
;ERR+4h address of handler
;ERR+0h keep next ERR up the chain
;point to ERR just made on the stack
;
;code which is protected goes here
;
;normal end if there is no exception
;handler sets eip/esp/ebp for here
;
;restore next ERR up the chain

Using this code, when the handler is called, the following is on the stack, and with [ESP+8h] pointing to the top of the ERR structure (ie. ERR+0):-

 | stackâ +ve
ERR +0 | Pointer to next ERR structure
ERR +4 | Pointer to own exception handler
ERR +8 | Code address of "safe-place" for handler
ERR +C | Information for handler
ERR +10 | Area for flags
ERR +14 | Value of EBP at safe-place
+18 | Local Data
+1C | Local Data
+20 | Local Data
 | more local data â
You can see from this that since the handler is given a pointer to the ERR structure it can also find the address of local data on the stack. This is because the handler knows the size of the ERR structure and also the position of the local data on the stack. If the EBP field is used at ERR+14h as in the above example, that could also be used as a pointer to the local data.

Recovering from and Repairing an exception

Continuing execution from a safe-place

Choosing the safe-place

You need to continue execution from a place in the code which will not cause further problems. The main thing you must bear in mind is that since your program is designed to work within the Windows framework, your aim is to return to the system as soon as possible in a controlled manner, so that you can wait for the next system event. If the exception has occurred during the call by the system to a window procedure, then often a good safe-place will be near the exit point of the window procedure so that control passes back to the system cleanly. In this case it will simply appear to the system that your application has returned from the window procedure in the usual way.

If the exception has occurred, however, in code where there is no window procedure, then you may need to exercise more control. For example, a thread established to do certain tasks will probably need to be terminated, reporting to the main thread that it could not complete the task.

Another major consideration is how easy it is to get the correct EIP, ESP and EBP values at the safe-place. As we can see below, this may not be at all difficult.

There are so many possible permutations here it is probably pointless to postulate them. The precise safe-place will depend on the nature of your code and the use you are making of exception handling.

Example of how to get to safe-place

As an example, though, look again at the code example above in MYFUNCTION. You can see the code label "SAFE-PLACE". This is a code address from which execution could continue safely, the handler having done all necessary clearing up.

In the code example, in order to continue execution successfully, it must be borne in mind that although SAFE-PLACE is within the same stack frame as the exception occurred, the values of ESP and EBP need carefully to be set by the handler before execution continues from EIP.

These 3 registers therefore need to be set and for the following reasons:-

ESP – to enable the POP FS:[0] instruction to work and to POP other values if necessary

EBP – to ensure that local data can be addressed within the handler and to restore the correct ESP value to return from MYFUNCTION

EIP – to cause execution to continue from SAFE-PLACE

Now you can see that each of these values is readily available from within the handler function. The correct ESP value is, in fact, exactly the same as the top of the ERR structure itself (given by [ESP+8h] when the handler is called). The correct EBP value is available from ERR+14h, because this was PUSHed onto the stack when the ERR structure was made. And the correct code address of SAFE-PLACE to give to EIP is at ERR+8h.

Now we are ready to see how the handler can ensure that execution continues from a safe-place, instead of allowing the process to close, should an exception occur.
HANDLER:
PUSH EBP
MOV EBP,ESP
;** now [EBP+8]=pointer
;** [EBP+0Ch]=pointer to
;** [EBP+10h]=pointer to
PUSH EBX,EDI,ESI
MOV EBX,[EBP+8]
TEST D[EBX+4],01h
JNZ >L5
TEST D[EBX+4],02h
JZ >L2
...
...
...
JMP >L5
L2:
PUSH 0
PUSH [EBP+8h]
PUSH OFFSET UN23
PUSH [EBP+0Ch]
CALL RtlUnwind
UN23:
MOV ESI,[EBP+10h]
MOV EDX,[EBP+0Ch]
MOV [ESI+0C4h],EDX
MOV EAX,[EDX+8]
MOV [ESI+0B8h],EAX
MOV EAX,[EDX+14h]
MOV [ESI+0B4h],EAX
XOR EAX,EAX
JMP >L6
L5:
MOV EAX,1
L6:
POP ESI,EDI,EBX
MOV ESP,EBP
POP EBP
RET 		;
;
;
to EXCEPTION_RECORD
ERR structure
CONTEXT record
;save registers as required by windows
;get exception record in ebx
;see if its a non-continuable exception
;yes, so must not deal with it
;see if its EH_UNWINDING (from Unwind)
;no
;
;clear-up code when unwinding
;
;must return 1 to go to next handler
;
;return value (not used)
;pointer to this exception record
;code address for RtlUnwind to return
;pointer to this ERR structure
;
;
;get context record in esi
;get pointer to ERR structure
;use it as new esp
;get safe place given in ERR structure
;insert new eip
;get ebp at safe place given in ERR
;insert new ebp
;reload context & return to system eax=0
;
;
;go to next handler - return eax=1
;ordinary return (no actual arguments)

Repairing the exception

In the above example you saw the context being loaded with the new eip, ebp and esp to cause execution to continue from a safe-place. It may be possible using the same method of replacing the values for some of the registers in the context, to "repair" the exception, permitting execution to continue from near the offending code, so that the current task can be continued.

An obvious example would be a divide by zero, which can be repaired by the handler by substituting the value 1 for the divisor, and then a return with EAX=0 (if a "per-thread" handler) causing the system to reload the context and continue execution.

In the case of memory violations, you can make use of the fact that the address of the memory violation is passed as the second dword in the additional information field of the exception record. The handler can use this very same value to pass to VirtualAlloc to commit more memory starting at that place. If this is successful, the handler can then reload the context (unchanged) and return EAX=0 to continue execution (in the case of a "per-thread" handler).

Continuing execution after final handler called

If you wish you can deal with exceptions in the final handler. You recall that at the beginning of this article I said that the final handler is called by the system when the process is about to be terminated.

This is true.

The returns in EAX from the final handler are not the same as those from the per-thread handler. If the return is EAX=1 the process terminates without showing the system’s closure message box, and if EAX=0 the box is shown.

However, there is also a third return code, EAX= –1 which is properly described in the SDK as "EXCEPTION_CONTINUE_EXECUTION". This return has the same effect as returning EAX=0 from a per-thread handler, that is, it reloads the context record into the processor and continues execution from the eip given in the context. Of course, the final handler may change the context record before returning to the system, in the same way as a per-thread handler might do so. In this way the final handler can recover from the exception by continuing execution from a suitable safe-place or it may try to repair the exception.

If you use the final handler to deal with all exceptions instead of using per-thread handlers you do lose some flexibility, though.

Firstly, you cannot nest final handlers. You can only have one working final handler established by SetUnhandledExceptionFilter in your code at any one time. You could, if you wished, change the address of the final handler as different parts of your code are being processed. SetUnhandledExceptionFilter returns the address of the final handler being replaced so you could make use of this as follows:-
PUSH OFFSET FINAL_HANDLER
CALL SetUnhandledExceptionFilter
PUSH EAX
...
...
...
...
CALL SetUnhandledExceptionFilter 		;
;
;keep address of previous handler
;
;this is the code
;being guarded
;
;restore previous handler
Note here that at the time of the second call to SetUnhandledExceptionFilter the address of the previous handler is already on the stack because of the earlier PUSH EAX instruction.

Another difficulty with using the final handler is that the information sent to it is limited to the exception record and the context record. Therefore you will need to keep the code address of the safe-place, and the values of ESP and EBP at that safe-place, in static memory. This can be done easily at run time. For example, when dealing with the WM_COMMAND message within a window procedure,
PROCESS_COMMAND:
MOV EBPSAFE_PLACE,EBP
MOV ESPSAFE_PLACE,ESP
...
...
...
SAFE_PLACE:
XOR EAX,EAX
RET 		;called on uMsg=111h (WM_COMMAND)
;keep ebp at safe-place
;keep esp at safe-place
;
;protected code here
;
;code-label for safe-place
;return eax=0=message processed
In the above example, in order to repair the exception by continuing execution from the safe-place, the handler would insert the values of EBPSAFE_PLACE at CONTEXT+0B4h (ebp), ESPSAFE_PLACE at CONTEXT+0C4h (esp), and OFFSET SAFE_PLACE into CONTEXT+0B8h (eip) and then return -1.

Note that in a stack unwind forced by the system because of a fatal exit, only the "per-thread" handlers (if any) and not the final handler are called. If there are no "per-thread" handlers, the final handler would have to deal with all clearing-up itself before returning to the system.

Single-stepping by setting the trap flag within the handler

You can make a simple single-step tester for your program while it is under development by using the handler's ability to set the trap flag in the register context before returning to the system. You can arrange for the handler to display the results on the screen, or to dump them to a file. This may be useful if you suspect that results are being altered under debugger control, or if you need to see quickly how a particular piece of code responds to various inputs. Insert the following code fragment where you want single-stepping to begin:-

MOV SSCOUNT,5
INT 3

SSCOUNT is a data symbol and is set to the number of steps the handler should do before returning to normal operation. The INT 3 causes a 80000003h exception, so your handler is called.
The code in your development program should be protected by a per-thread handler using code like this:-.
SS_HANDLER:
PUSH EBP
MOV EBP,ESP
PUSH EBX,EDI,ESI
MOV EBX,[EBP+8]
TEST D[EBX+4],01h
JNZ >L14
TEST D[EBX+4],02h
JNZ >L14
MOV ESI,[EBP+10h]
MOV EAX,[EBX]
CMP EAX,80000004h
JZ >L10
CMP EAX,80000003h
JNZ >L14
L10:
DEC SSCOUNT
JZ >L12
OR D[ESI+0C0h],100h
L12:
...
...
...
XOR EAX,EAX
JMP >L17
L14:
MOV EAX,1
L17:
POP ESI,EDI,EBX
MOV ESP,EBP
POP EBP
RET 		;
;
;
;save registers as required by Windows
;get exception record in ebx
;see if its a non-continuable exception
;yes
;see if EH_UNWINDING
;yes
;get context record in esi
;get ExceptionCode
;see if here because trap flag set
;yes
;see if its own INT 3 inserted to single-step
;no
;
;stop when correct number done
;
;set trap flag in context
;
;
;code here to display results to screen
;
;eax=0 reload context and return to system
;
;
;eax=1 system to go to next handler
Here the first call to the handler is caused by the INT 3 (the system objected strongly to the use of INT 1 when I tried it). On receipt of this exception, which could only come from the code fragment inserted in the code-to-test, the handler sets the trap flag in the context before returning. This causes a 80000004h exception to come back to the handler upon the next instruction. Note that with these exceptions, eip is already at the next instruction ie. one past the INT 3, or past the instruction executed with the trap flag set. Accordingly all you have to do in the handler to continue single-stepping is to set the trap flag again and return to the system.
* Thanks to G.W.Wilhelm, Jr of IBM for this idea

Exception handling in multi-threaded applications

When it comes to exception handling in multi-threaded applications there is little or no help from the system. You will need to plan for likely faults and organise your threads accordingly.

The rules applying to the exception handling provided by the system (in the context of a multi-threaded application) are:-

  1. Only one type 1 (final handler) can be in existence at any one time for each process. If a new thread calls SetUnhandledExceptionFilter, this will simply replace the final handler – there is no chain of final handlers as there is for the type 2 (per-thread) handlers. Therefore the simplest way of using the final handler is still probably the best way in a multi-threaded application – establish it in the main thread as soon as possible after the program start point.
  2. The final handler will be called by the system if the process will be terminating, regardless of which thread caused the exception.
  3. However, there will only be a final unwind (immediately prior to termination) in the per-thread handlers established for the thread which caused the exception. Even if any other (innocent) threads have a window and a message loop, the system will not warn them that the process is about to terminate (no special message will be sent to them other than usual messages arising from the loss of focus of other windows).
  4. Therefore the other (innocent) threads cannot expect a final unwind if the process is to terminate. And they will remain ignorant of the imminent termination.
  5. If, as is likely, these other innocent threads will also need to clear-up on such termination you will need to inform them from the final handler. The final handler will need to wait until these other threads have completed clearing up before returning to the system.
  6. The way in which the innocent threads are informed of the expected termination of the program depends on the precise make-up of your code. If the innocent thread has a window and message loop, then the final handler can use SendMessage to that window to send an application defined message (must be 400h or above), to inform that thread to terminate gracefully. If there is no window and message loop, the final handler could set a public variable flag, polled from time to time by the other thread. Alternatively you could use SetThreadContext to force the thread to execute certain termination code, by setting the value of eip to point to that code. This method would not work if the thread is in an API, for example, waiting for the return from GetMessage. In that case you would need to send a message as well, to make sure the thread returned from the API, so that the new context is set.
  7. RaiseException only works on the calling thread, so this cannot be used as a means of communication between threads to make an innocent thread execute its own exception handler code.
  8. How does the final handler know when it may proceed after informing the other threads that the program is about to terminate? SendMessage will not return until the recipient has returned from its window procedure and the final handler could wait for that return. Alternatively it could poll a flag waiting for a response from the other thread that it has finished clearing up (note you must call the API Sleep in the polling loop to avoid over-using the system). Or better still, the final handler could wait until the other thread has terminated (this can be done using the API WaitForSingleObject or WaitForMultipleObjects if there is more than one thread). Alternatively use could be made of the Event or Semaphore APIs.
  9. For an example of how these procedures could work in practice, suppose a secondary thread has the job of re-organising a database and then writing it to disk. It may be in the middle of this task when the main thread causes an exception which enters your final handler. Here you could either cause the secondary thread to abort its job, by causing it to unwind and terminate gracefully, leaving the original data on disk or alternatively you could permit it to complete the task, and then inform the handler that it had finished so that the handler could then return to the system. You would need to stop the secondary thread starting any further such jobs if your handler had been called. This could be achieved by the handler setting a flag tested by the secondary thread before it started any job, or by using the Event APIs.
  10. If communication between threads is difficult, there is another way for one thread to access the stack of another thread, and thereby cause an unwind. This makes use of the fact that whereas each thread has its own stack, the memory reserved for that stack is within the address space for the process itself. You can check this yourself if you watch a multi-threaded application using a debugger. As you move between threads the values of ESP and EBP will change, but they are all kept within the address space of the process itself. The value of FS will also be different between threads and will point to the Thread Information Block for each thread. So if you take the following steps one thread can access the stack and cause an unwind of another:-

    11. a. As each thread is created record in a static variable the value of its FS register.
    b. As each thread closes it returns the static variables to zero.
    c. The handler which needs to unwind other threads should take all the static variables in turn and for those which have a non-zero value (ie. thread was running at the time of the exception) the handlers should be called with the exception flag of 2 (EH_UNWINDING) and, a user flag of say, 400h to show that the per-thread handler is being called by your final handler. You cannot call a per-thread handler in a different thread using RtlUnwind (which is thread-specific) but it can be done using the following code (where ebx holds the address of the EXCEPTION_RECORD):-
MOV D[EBX+4],402h
L1:
PUSH ES
MOV AX,FS_VALUE
MOV ES,AX
MOV EDI,ES:[0]
POP ES
L2:
CMP D[EDI],-1
JZ >L3
PUSH EDI,EBX
CALL [EDI+4]
ADD ESP,8h
MOV EDI,[EDI]
JMP L2
L3: 		;make the exception flag EH_UNWINDING + 400h
;
;
;get FS value of thread to unwind
;
;get 1st per-thread handler address
;
;
;see if it’s the last one
;yes, so finish
;push ERR structure, EXCEPTION_RECORD
;call handler to run clear-up code
;remove the two parameters pushed
;get pointer to next ERR structure
;and do next if not at end
;code label when finished
;now loop back to L1 with a new FS_VALUE until all threads done

Here you see that the Thread Information Block of each innocent thread is read using the ES register, which is temporarily given the value of the thread’s FS register.

Instead of using FS to find the Thread Information Block you could use the following code to get a 32-bit linear address for it. In this code LDT_ENTRY is a structure of 2 dwords, ax holds the 16-bit selector value (FS_VALUE) to be converted and hThread is any valid thread handle:-
AND EAX,0FFFFh
PUSH OFFSET LDT_ENTRY,EAX,hThread
CALL GetThreadSelectorEntry
OR EAX,EAX
JZ >L300
MOV EAX,OFFSET LDT_ENTRY
MOV DH,[EAX+7]
MOV DL,[EAX+4]
SHL EDX,16D
MOV DX,[EAX+2]
OR EDX,EDX
L300: 		;
;
;
;see if failed
;yes so return zero
;
;get base high
;get base mid
;shift to top of edx
;and get base low
;edx now=linear 32 bit address)
;return nz on success

The reason why it is important (using the flag 400h) to inform the handler being called that it is being called by another thread (the final handler) is that the thread being called is still running because the exception occurred in a different thread. The handler may well need to suspend the thread in these circumstances, so that the clear-up job can be achieved by the calling thread. The innocent thread would then be given a safe-place to go to before calling ResumeThread. All this must be done before the final handler is allowed to return to the system because on return the system will simply terminate all threads by brute force.

Except.Exe

This is the accompanying program which is intended to demonstrate the above explanation of exception handling for assembler programmers.

The source code for Except.Exe (Except.asm and Except.RC) is also provided.

There is a modal dialog for the main window and the final handler is set up very early in the process. When the "Cause Exception" button is clicked, first the dialog procedure is called with the command, then 2 further routines are called, the third routine causing an exception of the type chosen by the radiobuttons. As execution passes through this code, 3 per-thread exception handlers are created.

Seh8.bmp (20494 bytes)
The exception is either repaired in situ if possible, or the program recovers in the chosen handler from a safe-place. If the exception is allowed to go to the final handler you can either exit by pressing F3 or F5, or if you press F7 the final handler will try to recover from the exception.

You can follow events as they occur because each handler displays various messages in the listbox. There is a slight delay between each message so that you can follow more easily what is happening, or you can scroll the messages to get them back into view.

When the program is about to terminate, something interesting happens. The system causes a final unwind with the exception flag set to 2h. The messages sent to the listbox are slowed down even further because the program will be terminating soon!

You will see that the same type of unwind occurs if you specify that execution should continue from a "safe-place" or if F7 is pressed from the final handler. This unwind is initiating by the handler itself.

WB01512_.gif (115 bytes) COPYRIGHT NOTE - this article, Except.ASM, Except.RC and Except.Exe are all
Copyright © Jeremy Gordon 1996-2000
[McDuck Software]
e-mail: JGJorg@cs.com
http://www.godevtool.com/
LEGAL NOTICE - The author accepts no responsibility for losses of any type arising from this article. Whereas the author has used his best endeavours to ensure that the contents of this article are correct, you should not rely on this and you should do your own tests.


Windows Programming in Assembly Language

1 Readme.txt 9

1.1: Chapter Overview 9

1.2: Petzold, Yao, Boling, and Other Acknowledgements 9

1.3: Ground Zero: The Programmer's Challenge 9

1.4: The Tools 11

1.5: Why HLA? 12

1.6: The Ground Rules 13

1.7: Using Make/NMake 14

1.8: The HLA Integrated Development Environment 24

1.9: Debugging HLA Programs Under Windows 25

1.10: Other Tools of Interest 25

1.11: Windows Programming Documentation 25

2 Advanced HLA Programming 26

2.1: Using Advanced HLA Features 26

2.2: HLA's High-Level Data Structure Facilities 26

2.2.1: Basic Data Types 26

2.2.1.1: Eight Bit Data Types 27

2.2.1.2: Sixteen-Bit Data Types 29

2.2.1.3: Thirty-Two-Bit Data Types 29

2.2.1.4: Sixty-Four-Bit Data Types 30

2.2.1.5: Eighty-Bit Data Types 31

2.2.1.6: One Hundred Twenty-Eight Bit Data Types 31

2.2.2: Composite Data Types 31

2.2.2.1: HLA Array Types 32

2.2.2.2: HLA Union Types 36

2.2.2.3: HLA Record (Structure) Types 38

2.2.3: Nested and Anonymous Unions and Records 44

2.2.4: Pointer Types 46

2.2.5: Thunk Types 47

2.2.6: Type Coercion 48

2.3: HLA High-Level Control Structures 49

2.3.1: Boolean Control Expressions 50

2.3.2: The HLA IF..ENDIF Statement 56

2.3.3: The HLA WHILE..ENDWHILE Statement 59

2.3.4: The HLA REPEAT..UNTIL Statement 61

2.3.5: The HLA FOR Loops 62

2.3.6: HLA's BREAK, CONTINUE, and EXIT Statements 64

2.3.7: Exception Handling Statements in HLA 66

2.4: HLA Procedure Declarations and Invocations 73

2.4.1: Disabling HLA's Automatic Code Generation for Procedures 80

2.4.3: Parameter Passing in HLA, Value Parameters 86

2.4.4: Parameter Passing in HLA: Reference Parameters 87

2.4.5: Untyped Reference Parameters 89

2.4.6: Hybrid Parameter Passing in HLA 90

2.4.7: Parameter Passing in HLA, Register Parameters 91

2.4.8: Passing Pointers and Values as Reference Parameters 92

2.5: The HLA Compile-Time Language 96

2.5.1: Compile-Time Assignment Statements 98

2.5.2: Compile-Time Functions 105

2.5.3: Generating Code With a Compile-Time Statement 110

2.5.4: Conditional Assembly (#if..#elseif..#else..#endif) 110

2.5.5: The #for..#endfor Compile-Time Loop 113

2.5.6: The #while..#endwhile Compile-Time Loop 115

2.5.7: Compile-Time I/O and Data Facilities 116

2.5.8: Macros (Compile-Time Procedures and Functions) 119

2.5.9: Performance of the HLA Compile-Time Language 130

2.5.10: A Complex Macro Example: stdout.put 131

2.6: Even More Advanced HLA Programming... 140

3 The C - Assembly Connection 141

3.1: Why are We Reading About C? 141

3.2: Basic C Programming From an Assembly Perspective 141

3.2.1: C Scalar Data Types 142

3.2.1.1: C and Assembler Integer Data Types 142

3.2.1.2: C and Assembly Character Types 144

3.2.1.3: C and Assembly Floating Point (Real) Data Types 145

3.2.2: C and Assembly Composite Data Types 146

3.2.2.1: C and Assembly Array Types 147

3.2.2.2: C and Assembly Record/Structure Types 148

3.2.2.3: C and Assembly Union Types 152

3.2.2.4: C and Assembly Character String Types 153

3.2.2.5: C++ and Assembly Class Types 161

3.2.3: C and Assembly Pointer Types 161

3.2.4: C and Assembly Language Constants 169

3.2.5: Arithmetic Expressions in C and Assembly Language 177

3.2.5.1: Converting Simple Expressions Into Assembly Language 180

3.2.5.2: Operator Precedence 189

3.2.5.3: Associativity 189

3.2.5.4: Side Effects and Sequence Points 190

3.2.5.5: Translating C/C++ Expressions to Assembly Language 194

3.2.6: Control Structures in C and Assembly Language 199

3.2.6.1: Boolean Expressions in HLA Statements 199

3.2.6.2: Converting C/C++ Boolean Expressions to HLA Boolean Expressions 202

3.2.6.3: The IF Statement 203

3.2.6.4: The SWITCH/CASE Statement 209

3.2.6.5: The WHILE Loop 212

3.2.6.6: The DO..WHILE Loop 213

3.2.6.7: The C/C++ FOR Loop 214

3.2.6.8: Break and Continue 216

3.2.6.9: The GOTO Statement 216

3.3: Function Calls, Parameters, and the Win32 Interface 217

3.3.1: C Versus C++ Functions 217

3.3.2: The Intel 80x86 ABI (Application Binary Interface) 218

3.3.2.1: Register Preservation and Scratch Registers in Win32 Calls 218

3.3.2.2: The Stack Pointer (ESP) 218

3.3.2.3: The Direction Flag 219

3.3.2.4: Function Return Results 219

3.3.2.5: Data Alignment and Padding 219

3.3.3: The C, Pascal, and Stdcall Calling Conventions 220

3.3.4: Win32 Parameter Types 222

3.3.5: Pass by Value Versus Pass by Reference 225

3.4: Calling Win32 API Functions 231

3.5: Win32 API Functions and Unicode Data 233

3.6: Win32 API Functions and the Parameter Byte Count 236

3.7: Creating HLA Procedure Prototypes for Win32 API Functions 236

3.7.1: C/C++ Naming Conventions Versus HLA Naming Conventions 236

3.7.1.1: Reserved Word Conflicts 237

3.7.1.2: Alphabetic Case Conflicts 238

3.7.1.3: Common C/C++ Naming Conventions 238

3.7.1.4: Hungarian Notation 241

3.8: The w.hhf Header File 244

3.9: And Now, on to Assembly Language! 245

4 The RadASM IDE for HLA 246

4.1: Integrated Development Environments 246

4.2: Traditional (Command Line) Development in Assembly 246

4.3: HLA Project Organization 247

4.4: Setting Up RadASM to Work With HLA 248

4.4.1: The RADASM.INI Initialization File 248

4.4.2: The HLA.INI Initialization File 251

4.4.3: Additional Support for RadASM on the CD-ROM 258

4.5: Running RadASM 258

4.5.1: The RadASM Project Management Window 259

4.5.2: Creating a New Project in RadASM 267

4.5.3: Working With RadASM Projects 270

4.5.4: Editing HLA Source Files Within RadASM 272

4.6: Creating and Compiling HLA Projects With RadASM 277

4.7: Developing Small Projects with RadASM 284

4.8: Plus More! 285

5 The Event-Oriented Programming Paradigm 286

5.1: Who Dreamed Up This Nonsense? 286

5.2: Message Passing 287

5.3: Handles 289

5.4: The Main Program 290

5.4.1: Filling in the w.WNDCLASSEX Structure and Registering the Window 290

5.4.2: "What is a `Window Class' Anyway?" 295

5.4.3: Creating and Displaying a Window 297

5.4.4: The Message Processing Loop 302

5.4.5: The Complete Main Program 303

5.5: The Window Procedure 304

5.6: Hello World 309

5.7: Compiling and Running HelloWorld From the Command Line 316

5.8: Compiling and Running HelloWorld from RadASM 318

5.9: Goodbye World! 318

6 Text in a GUI World 320

6.1: Text Display Under Windows 320

6.2: Painting 321

6.2.1: Device Contexts 322

6.2.2: Device Context Attributes 324

6.2.3: Painting Text in the Client Area 325

6.2.4: BeginPaint, EndPaint, GetDC, GetWindowDC, and ReleaseDC Macros 347

6.3: Device Capabilities 352

6.4: Typefaces and Fonts 368

6.5: Scroll Bars 404

6.6: The DebugWindow Application 436

6.6.1: Message Passing Under Windows 436

6.6.2: Memory Protection and Messages 441

6.6.3: Coding the DebugWindow Application 443

6.6.4: Using DebugWindow 469

6.7: The Windows Console API 475

6.7.1: Windows' Dirty Secret - GUI Apps Can Do Console I/O! 476

6.7.2: Win32 Console Functions 482

6.7.2.1: w.AllocConsole 482

6.7.2.2: w.CreateConsoleScreenBuffer 483

6.7.2.3: w.FillConsoleOutputAttribute 483

6.7.2.4: w.FillConsoleOutputCharacter 485

6.7.2.5: w.FlushConsoleInputBuffer 486

6.7.2.6: w.FreeConsole 486

6.7.2.7: w.GetConsoleCursorInfo 486

6.7.2.8: w.GetConsoleScreenBufferInfo 487

6.7.2.9: w.GetConsoleTitle 488

6.7.2.10: w.GetConsoleWindow 489

6.7.2.11: w.GetNumberOfConsoleInputEvents 489

6.7.2.12: w.GetStdHandle 490

6.7.2.13: w.PeekConsoleInput 490

6.7.2.14: w.ReadConsole 493

6.7.2.15: w.ReadConsoleInput 494

6.7.2.16: w.ReadConsoleOutput 494

6.7.2.17: w.ReadConsoleOutputAttribute 495

6.7.2.18: w.ReadConsoleOutputCharacter 496

6.7.2.19: w.ScrollConsoleScreenBuffer 496

6.7.2.20: w.SetConsoleActiveScreenBuffer 498

6.7.2.21: w.SetConsoleCursorInfo 498

6.7.2.22: w.SetConsoleCursorPosition 498

6.7.2.23: w.SetConsoleScreenBufferSize 499

6.7.2.24: w.SetConsoleTextAttribute 499

6.7.2.25: w.SetConsoleTitle 500

6.7.2.26: w.SetConsoleWindowInfo 500

6.7.2.27: w.SetStdHandle 501

6.7.2.28: w.WriteConsole 501

6.7.2.29: w.WriteConsoleInput 502

6.7.2.30: w.WriteConsoleOutput 502

6.7.2.31: w.WriteConsoleAttribute 503

6.7.2.32: w.WriteConsoleOutputCharacter 504

6.7.2.33: Plus More! 505

6.7.3: HLA Standard Library Console Support 505

6.8: This Isn't the Final Word! 505

7 Graphics 506

7.1: Graphics in a GUI World 506

7.2: Types of Graphic Objects You Can Draw in Windows 506

7.3: Facilities That the Windows GDI Provides 506

7.4: The Device Context 507

7.5: Obtaining and Using Device Context Information 508

7.6: Line Drawing Under Windows 517

7.6.1: Background Mode and Color for Lines 521

7.6.2: Using the LineTo and MoveToEx API Functions 522

7.6.3: Using the PolyLineTo API Function 533

7.6.4: A Software Engineering Break 540

7.6.5: The FofX.hla Application 550

7.6.6: Pen Drawing Modes 559

7.6.7: Paths 568

7.6.8: Extended Pen Styles 570

7.7: Bounding Boxes and Rectangles in Windows 580

7.8: Drawing Objects in Windows 586

7.8.1: Filling Objects and Windows' Brushes 586

7.8.2: Filling and Framing Paths 589

7.8.3: Drawing Rectangles 593

7.8.4: Drawing Circles and Ellipses 599

7.8.5: Drawing Roundangles, Arcs, Chords, and Pies 605

7.8.6: Bezier Curves 610

7.8.7: Drawing with the Polygon Function 616

7.9: Regions 622

7.10: Clipping 629

7.11: Bitmaps 636

7.12: Metafiles 665

7.13: Additional GDI Functions of Interest 671

7.14: For More Information 674

8 Event-Driven Input 675

8.1: Event-Driven Input Versus "Hurry Up and Wait" 675

8.2: Focus on Focus 675

8.3: Stick and Caret 676

8.4: Keyboard Messages 679

8.5: Mouse Messages 735

8.11: Timer Messages 776

8.13: But Wait! There's More! 785

9 Debugging Windows Applications 786

9.1: 786

10 Controls, Dialogs, Menus, and Windows 787

10.1: 787

11 The Resource Compiler 788

11.1: 788

12 Icons, Cursors, Bitmaps, and Strings 789

12.1: 789

13 File I/O and Other Traditional Kernel Services 790

13.1: 790

1 Windows Programming in Assembly Language 791

rhyde@cs.ucr.edu
TOC PREV NEXT INDEX


Chapter 1: Readme.txt

Note: This text is being made available in alpha form for proofreading and evaluation purposes only. This book contains known problems, use the information in this book at your own discretion. Please do not distribute this book. Send all corrections to rhyde@cs.ucr.edu.

©2003, Randall Hyde, All Rights Reserved

1.1: Chapter Overview

This chapter provides the "ground rules" for this text. It describes the intended audience, the tools, and the intent of this text. This chapter also describes conventions used throughout the rest of these text.

1.2: Petzold, Yao, Boling, and Other Acknowledgements

Much of the material appearing in this book is patterned after Petzold's and Yao's "Programming Windows..." texts and Douglas Boling's "Programming Microsoft Windows CE" book. Of course, this is not a simple translation of those books to assembly language but this book does attempt to present many of the same topics appearing in these books. The reasons for doing this should be obvious - those texts have been around for quite some time and tens of thousands of C programmers have validated their approach. It would be foolish to ignore what what worked so well for C programmers when writing this book for assembly programmers.

As mentioned above, I have not cloned these existing books and swapped assembly instructions for the C statements appearing therein. Besides the obvious copyright violations and other ethical issues, assembly language is fundamentally different than C and Win32 programming in assembly language needs to be different to reflect this. Another issue is the legacy associated with those books. The original edition of Petzold's book appeared sometime around 1988. As such, the text carries a lot of historical baggage that is interesting to read, but is of little practical value to an assembly language programmer in the third millennium. Boling's book, while much better in this is regard, is targeted towards Windows CE, which has several limitations compared to the full Win32 API. I've cut out lots of the anecdotes that contribute little to your knowledge of Win32 assembly. For example, since Petzold's book was written in the very early days of Windows (1.0!), it contains a lot of user interface explanations and other GUI stuff that most people take for granted these days. I've dropped a lot of this material as well (after all, do you really need to be taught what a scroll bar is and what you would use it for?) If you're not comfortable with the Windows user interface, this document is not the place to start your journey into the world of Windows.

I should also note the contribution by Iczelion's Win32 Assembly Tutorials to this text. These nifty programs and documents got me started and taught me how to write "pure" win32 assembly language programs. I could not have started this text without the availability of these tutorials (and several others like them). Also, lots of thanks to Steve Hutchesson for doing the pioneering work on Win32 assembly language with his "MASM32" package, header files, and libraries. Also, lots of thanks to all of those who post answers to the Win32ASM Community messageboard. Your answers have provided a lot of help that made this book possible.

1.3: Ground Zero: The Programmer's Challenge

If you're coming at this with no prior Win32 programming experience, don't expect to be writing Windows applications in a short period of time. There is a tremendous learning curve to mastering the Win32 API (application programmer's interface) set. Petzold estimates this at six months for an established C programmer. I personally don't have a feeling for this because (a) I had some prior experience with systems like Delphi and C++ Builder before attacking the raw Win32 APIs, and (b) I've spread my study of this subject out over several years, learning the material in a piecemeal fashion. Nevertheless, I can certainly vouch for the fact that learning the Win32 API is a lot of work and it is not something you're going to master in a few weekends of work.

Die-hard assembly programmers are going to find Windows a strange place to work. Under DOS, where assembly programmers have complete control of the machine, speed was the hallmark of the assembly language programmer. It was very easy to write programs that ran two to ten times faster than their HLL (high level language) counterparts. Under Windows, this task is much more difficult because a large percentage of the time is spent in Windows itself. Even if you reduce the execution time of your code to zero, Windows still consumes a dramatic portion of the CPU cycles. Hence it is very difficult to optimize whole programs; only those special applications that are compute intensive will see a big benefit in performance when writing in assembly rather than HLLs. For this reason, most programmers who use assembly language under Windows write the bulk of their application in a high level language like C, dropping into assembly for the time-critical functions. There are, however, other benefits to using assembly language besides speed, and the mixed-language approach tends to negate those benefits. So this book will concentrate on applications that are written entirely in assembly language (quite frankly, there really is no need for a book specifically on mixed language programming under Windows, since most of the real "Windows" stuff winds up getting done in the high level language in those environments).

Another reason for writing applications in assembly is the short size of the resulting applications. Some might argue that such efficiency is wasted on today's machines with tens (and hundreds) of gigabytes of disk storage and hundreds of megabytes of RAM. However, it's still impressive to see a 700 byte "Hello World" program blow up to a 100 KByte executable when you implement it in a HLL. Nevertheless, there is little need for the world's smallest "Hello World" program, and real Windows applications tend to be large because they contain lots of data, not because they contain lots of code. Once you get to the point that you're writing real applications, assembly language programs aren't tremendously smaller (i.e., an order of magnitude) than their HLL counterparts. Perhaps half the size, but not amazingly smaller.

There are two real reasons I can immediately think of for writing Win32 programs in assembly language: you don't know one of these other HLLs (nor do you want to learn them), or you want to have full control over the code your program executes. This text does not attempt to evangelize the use of assembly language programming under Windows. If you think people are nuts who attempt this, stop reading right now and go do something more useful. If you have some reason for wanting to write complete applications in assembly language, then this book is for you and I'm not going to question your reasons.

If you are a die-hard assembly programmer that tries to squeeze every last cycle out of your code and you're offended by inefficiencies that exist in commercial code, let me warn you right away that there will be many things that you won't like about this book. I'm a firm believer in writing quality, correct, and easy to maintain assembly code. If that means I lose a few cycles here and there, so be it. I've even been known, now and then, to stick extra code into my programs to verify their proper operation (e.g., asserts). I'm also a big fan of good formatting, decent variable names, and good programming style. If "structured programming" and long variable names drive you nuts, you should put this book down and go back to DOS. Hopefully, you'll find my programming style a refreshing change from a lot of the Win32/assembly stuff that's out there.

Writing good, user-friendly, applications under Windows is a lot of work; a lot more work that is required to write a typical DOS (or UNIX, or other console-based) application. The combination of assembly language (more work for the programmer) and Windows (more work for the programmer) spells a lot more work for the programmer. However, if you take the time to do it properly, assembly language programming under Windows definitely has its rewards.

1.4: The Tools

Although a certain amount of masochism is necessary to want to write assembly language programs under Windows, there's a big difference between those who just want to write assembly code under Windows and those who insist on doing it the most painful way possible. Some people believe that the only "true" way to write assembly code under Windows is to manually push all the parameters themselves and call their Win32 API routines directly. Even MASM doesn't require this level of pain. To reduce your pain considerably, this book uses the High Level Assembler (HLA). HLA is public domain (i.e., free) and readily available (if you're reading this document, you probably have access to HLA). HLA reduces the effort needed to write Windows assembly programs without your giving up the control that assembly provides. If you are truly sick and really want to manually push all the Win32 API parameters on the stack prior to a call, HLA will let you do this. However, this book is not going to teach you how to do it that way because it's a ton of work with virtually no benefit. If you're a masochist and you want to write the "purest" assembly possible, this book is not for you. For the rest of us though, let the journey begin...

This text uses the following tool set:

  • The HLA high level assembler
  • The HLA Standard Library
  • The RadASM Integrated Development Environment (IDE) for HLA
  • The OllyDbg Debugger
  • An assembler capable of processing HLA output to produce PE/COFF files (e.g., MASM or FASM)
  • An appropriate linker that can process the assembler's output (e.g., MS Link)
  • A "resource compiler" such as rc.exe from Microsoft
  • Some sort of make utility for building applications (e.g., Microsoft's nmake or Borland's make)
  • Win32 library modules (either those supplied with the Microsoft SDK or other freely available library modules)

Although the RadASM package provides a complete programmer's text editing system, you may wish to use a text editor with which you're already comfortable. That's fine; most programmer's text editors will work great with HLA and, in fact, certain text editors include an integrated development environment that can support HLA. For example, the UeMake IDE add-in for Ultra-Edit32 supports HLA and is a viable alternative to RadASM. For more details on setting up your text editor as a simple IDE for HLA, please consult your editor's documentation.

A complete set of tools that will let you develop all the code in this book needs to be gathered from several sources. The accompanying CD-ROM contains almost everything you'll need. A few tools you may want to use do not appear on the CD-ROM, for example, you may want to use MASM and the MASM32 resource compiler available by downloading Steve Hutchesson's excellent MASM32 package from his web site at http://www.movsd.com. Between the MASM32 download and the software appearing on the CD-ROM accompanying this book, you'll have all the software you need to develop Win32 assembly language applications.

This book assumes that you have access to a make utility (e.g., Borland's make.exe or Microsoft's nmake.exe). If you do not have a reasonable version of make that runs under Windows, you can obtain one as part of the Borland C++ command-line toolset which is available for free from Borland's website (well, not exactly free, you do have to register with Borland before you can download it, but the cost is free). Go to Borland's web site (www.borland.com) and click on downloads. Then select C++ and download the C++Builder compiler (Borland C++ 5.5). This download includes several useful tools including make.exe, touch.exe, tdump.exe, coff2omf.exe, the Borland C++ compiler, and other software. See the Borland site for more details. The only tool from this package that this book will assume you're using is the make.exe program (or somebody else's version of make, e.g., Microsoft's nmake.exe).

1.5: Why HLA?

Long-time assembly programmers will probably want to know why this book uses the high level assembler (HLA) rather than MASM32 or one of the other assemblers that run under Windows (e.g., Gas, NASM, FASM, SpAsm, TASM, and so on). There are a couple of reasons for this; this section will explain the reasoning.

From the start, it was clear that the primary choice for the assembler would be either Steve Hutchesson's MASM32 package or HLA. Both of these assemblers are "high level" assemblers that dramatically ease the development of assembly language software under Windows (TASM, by the way, is also a high level assembler). MASM32 and HLA both provide the necessary Windows include and library modules (support that is missing in a coherent form in many other assemblers). HLA and MASM are both documented extremely well (I know this, having written much of the HLA documentation myself). So the choice of one of these two assemblers was fairly clear.

So why choose HLA over MASM32? Perhaps the most obvious reason is that I personally developed the HLA system and I've got my own horn to toot here. I certainly wouldn't be so bold as to claim that's not a part of the reason I've selected HLA for the examples appearing in this book. However, that is really a minor reason for selecting HLA. I designed and wrote HLA as a teaching tool; it is more suitable for teaching assembly language programming (at both beginning and advanced levels) than is MASM. Also, HLA source code is quite a bit more readable than MASM (assuming, of course, that the programmer takes care in creating the program in the first place). HLA also comes with the HLA Standard Library that makes assembly programming, in general, much easier. Another important consideration to many people: HLA is free. Totally free. As in public domain. The full source code to the HLA assembler, standard library, and utilities is available and anyone can do anything they want with it. Although you can download MASM free from various sites on the internet, the legalities of doing so are somewhat questionable. By using HLA in this book, I was able to supply almost all of the software you need on the accompanying CD-ROM without having to obtain licenses and (possibly) charge extra for that code. Last, but certainly not least, HLA supports an option to compile HLA code into MASM assembly code. So those who want to see these examples in "pure" MASM code can run the HLA source code through the HLA compiler to produce a MASM assembly source file. Unfortunately, there is no option that will translate MASM code into HLA source code. Therefore, supplying the examples appearing in this book in HLA source form will appeal to the widest audience.

There are another couple of reasons for supporting HLA that is political, rather than technical, in nature. At one time, MASM was unquestionably the standard for 80x86 assembly language programming. Unfortunately, towards the end of the 1990s Microsoft stopped selling MASM as a commercial product and the support for MASM began to wane. Today, by playing a bunch of games, you can manage to (legally) download MASM for free from the Microsoft web site. You can obtain MASM as part of the Microsoft Developer's Network (MSDN) package, and certain versions of MS Visual C++ include it as well. However, it is not available as a separate product and documentation for the assembler is hard to come by. Last, there are certain people who refuse to use MASM for political reasons (open source/free software and all that stuff). HLA is the most powerful and well-supported Win32-capable assembler beyond MASM32, so it makes a lot of sense to use it.

Of course, there is one additional reason for using HLA in the examples appearing in this book: HLA is the most powerful 80x86 assembler around. Originally, I designed HLA as a tool to teach assembly language programming; so some might get the impression that HLA is not going to be as powerful as an assembler like MASM since HLA was designed for beginners and MASM was designed for professionals. This, however, is not true. While I certainly designed HLA to be easy to learn and use, that design required a lot of sophistication behind the scenes. Also, I'm a pretty demanding assembly language programmer and I wanted to make sure that HLA was a suitable tool for programmers like myself. Having written well over a hundred thousand lines of HLA code (at the time I write this), I can assure you that HLA is quite a bit more powerful than MASM (which is, incidentally, quite a bit more powerful than most of the other assemblers out there).

One interesting aspect to the HLA versus MASM32 question, which this book will explore in greater detail a little later, is that it's not an either/or situation. There is nothing stopping you from using both assemblers on a single project. A valid complaint with using HLA to develop Win32 applications in assembly language is that there is an abundance of MASM32 source code and utilities. By using HLA one seems to give up all this existing material. However, this is not the case. You can link HLA and MASM code together exactly as you would link HLA and HLA code or MASM and MASM code (this should be obvious, since in one mode of operation HLA emits MASM source code for processing by MASM into an object file). So if you've got some library routines written for MASM32, they're easily called from HLA. Likewise, if you've got a "wizard" code generator (like the ProStart program that comes with the MASM32 package) then that code can easily invoke HLA functions. Although HLA will not compile MASM source (and vice versa), you can easily merge the object files that these two assemblers produce. Therefore, MASM and HLA coexist quite well when developing Win32 assembly applications.

1.6: The Ground Rules

This text teaches "old-fashioned" Windows programming using direct calls to the Win32 API. Petzold points out that there are benefits to programming this way (rather than using MFC, VB, Delphi, or one of the newer coding systems that make Win32 programming so much easier). I'll leave it up to Petzold to make that argument. We're going to program Win32 the "old-fashioned" way because that's currently the only way to do it in assembly. When tools like Delphi become available for assembly programmers, I'd strongly recommend you take a look at those tools. Until then, the Win32 API interface is the only way to write assembly code in Windows, so talking about these other schemes is a waste of time. The RadASM IDE package does simplify certain things (like creating forms and the like), so you don't have to create everything by hand, but the bottom line is that there is no "Delphi for ASM" at this time.

Before actually writing this book I made several aborted "first attempts" before settling on the organization I've chosen here. Early on, I'd decided that the naming convention that Microsoft uses (with conventions inherited from C) was absolutely horrible. So I designed a new naming convention for Windows API function, data structure, variable, and constant names that was a lot more readable. Unfortunately, I ultimately realized that such an approach would not serve my readers well because there is no way a single book can teach you everything you need to know about Windows programming. Unfortunately, if the Win32 names that this book doesn't match the names that C programmers (and, therefore, the vast amount of Win32 programming literature) use, it's going to be difficult to take advantage of that plethora of standard Windows documentation. Originally, I had thought that I could get away with renaming things because Borland's Delphi did this. Ultimately, however, I realized that although I was gaining something important (a much better naming scheme), I was losing more than I was gaining by not adopting standard Windows names.

Once I had convinced myself that using Windows' identifiers was the right way to go, I went in and started revising a bunch of code I'd written to using the standard Windows naming scheme. I quickly realized that it is not possible to completely take this approach. First of all, several standard Windows identifiers are reserved words in an assembly language program (or conflict with standard identifiers you'll typically find in an assembly language program). Also, there is the issue of case sensitivity. C/C++ is a purely case sensitive language and many programmers have taken advantage of this fact to create several different identifiers whose only lexical difference is the use of upper or lower case in the identifier (e.g., you'd find symbols like "someid" and "SOMEID" in use in the same program). Even ignoring the horrible programming style such usage represents, using such identifiers in a language like HLA isn't even possible. Ultimately, I settled on using standard Windows identifiers whenever possible and practical, and switching to a more reasonable name when there were conflicts (e.g., two different identifiers whose spelling differs only by case). Fortunately, such conflicts don't occur frequently, so most of the Windows identifiers you'll find in this text are the standard symbols the Windows SDK (software development kit) and most of the documentation on this planet use. This book carefully documents the exceptions that occur.

This text does not attempt to teach assembly language programming nor does it attempt to teach the HLA language. See my other books and documentation (e.g., The Art of Assembly Language and the HLA documentation) for that purpose. This book assumes that you've properly set up HLA and you've managed to compile and execute some simple console applications (e.g., a console version of the venerable "Hello World" program).

It would be nice to write a book on Windows assembly language that doesn't rely on the reader knowing anything other than assembly language. With one exception, this book does not assume that you've got experience with any language other than assembly language (and HLA, in particular). However, as noted earlier, there is a tremendous body of Windows programming knowledge out in the real world that is squarely aimed at the C/C++ programmer. Since this book can only hope to present a fraction of the available Windows programming information, there are going to be times when you'll need to seek an answer from some other source. If you've got an experienced Win32 assembly language programmer or two handy, you can ask them (e.g., on the Win32 Assembly Community Board). However, there are going to be times when you need an answer quick and a guru won't be available. In those instances, your best solution is to consult other Windows programming documentation, even if it is directed at C/C++ programmers. In such instances, it's real handy if you know C or C++ so that other documentation will make sense to you. This book spends one chapter (the next chapter, in fact), discussing how to read C/C++-based documentation and mentally translate it into assembly language. Even if you don't know C, C++, Java, or some other C-like language, reading that chapter may prove helpful should you find yourself in a position where you absolutely must consult some C-based Windows programming documentation. However, none of the remaining material in this book depends upon the information in that chapter, so you can skip it if you don't want to work with C (or if you're already well-versed in Win32/C programming).

1.7: Using Make/NMake

Although RadASM provides a true IDE for HLA that supports projects, browsing, and other nice features, the best way to manage your Win32 assembly projects (even within RadASM) is via a makefile. Although The Art of Assembly Language Programming goes into detail about using a makefile, this book does not make the assumption that you've read that book (you could have learned HLA and assembly language programming elsewhere). Since the use of make is going to be a fundamental assumption in this book (e.g., most examples will include a makefile), it's probably wise to discuss the use of make here for those who may be unfamiliar with this program.

The main purpose of a program like make (or nmake, if you're using Microsoft's version of the program) is to automatically manage the compilation and linking of a multi-module project. Although it is theoretically possible to write a single, self-contained, assembly language source file that assembles directly to an executable file, in practice this is rarely done1. Instead, programs are usually broken up into separate source files by logical function. In order to save time during development, you don't always have to recompile every source file that makes up the application. Instead, you need only recompile those source files that have been changed (or depend upon changes in other source files). This can save a considerable amount of time during development if your project consists of many different source files that you're compiling and linking together and you make a single change to one of these source files (because you will only have to recompile the file you've changed rather than all files in the system).

Although using separate compilation reduces assembly time and promotes code reuse and modularity, it is not without its own drawbacks. Suppose you have a program that consists of two modules: pgma.hla and pgmb.hla. Also suppose that you've already compiled both modules so that the files pgma.obj and pgmb.obj exist. Finally, you make changes to pgma.hla and pgmb.hla and compile the pgma.hla file but forget to compile the pgmb.hla file. Therefore, the pgmb.obj file will be out of date since this object file does not reflect the changes made to the pgmb.hla file. If you link the program's modules together, the resulting executable file will only contain the changes to the pgma.hla file, it will not have the updated object code associated with pgmb.hla. As projects get larger they tend to have more modules associated with them, and as more programmers begin working on the project, it gets very difficult to keep track of which object modules are up to date.

This complexity would normally cause someone to recompile all modules in a project, even if many of the object files are up to date, simply because it might seem too difficult to keep track of which modules are up to date and which are not. Doing so, of course, would eliminate many of the benefits that separate compilation offers. Fortunately, the make program can solve this problem for you. The make program, with a little help, can figure out which files need to be reassemble and which files have up to date .OBJ files. With a properly defined make file, you can easily assemble only those modules that absolutely must be assembled to generate a consistent program.

A make file is a text file that lists compile-time dependencies between files. An .EXE file, for example, is dependent on the source code whose assembly produce the executable. If you make any changes to the source code you will (probably) need to reassemble or recompile the source code to produce a new executable file2.

Typical dependencies include the following:

  • An executable file generally depends only on the set of object files that the linker combines to form the executable.
  • A given object code file depends on the assembly language source files that were assembled to produce that object file. This includes the assembly language source files (.HLA) and any files included during that assembly (generally .HHF files).
  • The source files and include files generally don't depend on anything.

A make file generally consists of a dependency statement followed by a set of commands to handle that dependency. A dependency statement takes the following form:

dependent-file : list of files

Example :

pgm.exe: pgma.obj pgmb.obj             --Windows/nmake example

This statement says that "pgm.exe" is dependent upon pgma.obj and pgmb.obj. Any changes that occur to pgma.obj or pgmb.obj will require the generation of a new pgm.exe file.

The make program uses a time/date stamp to determine if a dependent file is out of date with respect to the files it depends upon. Any time you make a change to a file, the operating system will update a modification time and date associated with the file. The make program compares the modification date/time stamp of the dependent file against the modification date/time stamp of the files it depends upon. If the dependent file's modification date/time is earlier than one or more of the files it depends upon, or one of the files it depends upon is not present, then make assumes that some operation must be necessary to update the dependent file.

When an update is necessary, make executes the set of commands following the dependency statement. Presumably, these commands would do whatever is necessary to produce the updated file.

The dependency statement must begin in column one. Any commands that must execute to resolve the dependency must start on the line immediately following the dependency statement and each command must be indented one tabstop. The pgm.exe statement above would probably look something like the following:

pgm.exe: pgma.obj pgmb.obj

    hla -e:pgm.exe pgma.obj pgmb.obj

(The "-e:pgm.exe" option tells HLA to name the executable file "pgm.exe.")

If you need to execute more than one command to resolve the dependencies, you can place several commands after the dependency statement in the appropriate order. Note that you must indent all commands one tab stop. The make program ignores any blank lines in a make file. Therefore, you can add blank lines, as appropriate, to make the file easier to read and understand.

There can be more than a single dependency statement in a make file. In the example above, for example, executable (pgm.exe) depends upon the object files (pgma.obj and pgmb.obj). Obviously, the object files depend upon the source files that generated them. Therefore, before attempting to resolve the dependencies for the executable, make will first check out the rest of the make file to see if the object files depend on anything. If they do, make will resolve those dependencies first. Consider the following make file:

pgm.exe: pgma.obj pgmb.obj

    hla -e:pgm.exe pgma.obj pgmb.obj

pgma.obj: pgma.hla

    hla -c pgma.hla

pgmb.obj: pgmb.hla

    hla -c pgmb.hla

The make program will process the first dependency line it finds in the file. However, the files that pgm.exe depends upon themselves have dependency lines. Therefore, make will first ensure that pgma.obj and pgmb.obj are up to date before attempting to execute HLA to link these files together. Therefore, if the only change you've made has been to pgmb.hla, make takes the following steps (assuming pgma.obj exists and is up to date).

  • The make program processes the first dependency statement. It notices that dependency lines for pgma.obj and pgmb.obj (the files on which pgm.exe depends) exist. So it processes those statements first.
  • The make program processes the pgma.obj dependency line. It notices that the pgma.obj file is newer than the pgma.hla file, so it does not execute the command following this dependency statement.
  • The make program processes the pgmb.obj dependency line. It notes that pgmb.obj is older than pgmb.hla (since we just changed the pgmb.hla source file). Therefore, make executes the command following on the next line. This generates a new pgmb.obj file that is now up to date.
  • Having processed the pgma.obj and pgmb.obj dependencies, make now returns its attention to the first dependency line. Since make just created a new pgmb.obj file, its date/time stamp will be newer than pgm.exe's. Therefore, make will execute the HLA command that links pgma.obj and pgmb.obj together to form the new pgm.exe file.

Note that a properly written make file will instruct the make program to assemble only those modules absolutely necessary to produce a consistent executable file. In the example above, make did not bother to assemble pgma.hla since its object file was already up to date.

There is one final thing to emphasize with respect to dependencies. Often, object files are dependent not only on the source file that produces the object file, but any files that the source file includes as well. In the previous example, there (apparently) were no such include files. Often, this is not the case. A more typical make file might look like the following:

pgm.exe: pgma.obj pgmb.obj

    hla -e:pgm.exe pgma.obj pgmb.obj

pgma.obj: pgma.hla pgm.hhf

    hla -c pgma.hla

pgmb.obj: pgmb.hla pgm.hhf

    hla -c pgmb.hla

Note that any changes to the pgm.hhf file will force the make program to recompile both pgma.hla and pgmb.hla since the pgma.obj and pgmb.obj files both depend upon the pgm.hhf include file. Leaving include files out of a dependency list is a common mistake programmers make that can produce inconsistent executable files.

Note that you would not normally need to specify the HLA Standard Library include files. the Standard Library ".lib" files, or any Windows library files (e.g., kernel32.lib) in the dependency list. True, your resulting executable file does depend on this code, but this code rarely changes, so you can safely leave it out of your dependency list. Should you make a modification to the Standard Library, simply delete any old executable and object files to force a reassembly of the entire system.

The make program, by default, assumes that it will be processing a make file named makefile. When you run the make program, it looks for makefile in the current directory. If it doesn't find this file, it complains and terminates3. Therefore, it is a good idea to collect the files for each project you work on into their own subdirectory and give each project its own makefile. Then to create an executable, you need only change into the appropriate subdirectory and run the make program.

The make program will only execute a single dependency in a make file, plus any other dependencies referenced by that one item (e.g., the pgm.exe dependency line in the previous example depends upon pgma.obj and pgmb.obj, both of which have their own dependencies). By default, the make program executes the first dependency it finds in the makefile plus any dependencies that are subservient to this first item. In particular, if a dependency line exists in the makefile that is not referenced (directly or indirectly) from the main dependency item, then make will ignore that dependency item unless you explicitly request it's execution.

If you want to execute some dependency other than the first dependency in the make file, you can specify the dependency on the make command line when running make from the Windows' command prompt. For example, a common convention in make files is to create a "clean" dependency that cleans up all the files the compile creates. A typical "clean" dependency line for an HLA compilation might look like the following:

clean:

    del *.obj

    del *.inc

    del *.bak

The first thing you'll notice is that the "clean" item doesn't have a dependency list. When an item like "clean" appears without a dependency list, make will always execute the commands that follow. Another peculiarity to the "clean" dependency is that there (usually) isn't a file named clean in the current directory whose date/time stamp the make program can check. If a file doesn't exist, then make will assume that the file is always out of date. A common convention is to specify non-existent filenames (like clean) in a makefile as commands that someone would explicitly execute from within make. Of course, such usage (generally) assumes that you don't actually build a file named "clean" (or whatever name you choose to use).

Since, by default, you typically don't want to execute a command line "clean" when running make, you wouldn't usually place the clean dependency first in the make file (nor would you typically refer to clean within some other dependency list). Since make doesn't normally execute any dependency items that aren't "reachable" from the first dependency item in the make file, you might wonder how you'd tell make to execute the clean command. To specify the execution of some dependency other than the first (default) item in the make file, all you need to is specify the target you want to create (e.g., "clean") on the make command line. For example, to execute the clean command, you'd using a Windows command prompt statement like the following:

    make clean

This command does not tell make to use a different make file. It will still open and use the file named "makefile" in the current directory4; however, instead of executing the first dependency it finds in makefile, it will search for the target "clean" and execute that dependency.

By convention, most programmers use the first dependency in a make file to build the executable based on the current build state of the program (that is, it will compile and link only those files necessary to create an up-to-date executable). Most programmers, by convention, will also include a "clean" target in their make file. The clean command deletes all object and intermediate files that the compiler generates; this ensures that the next build of the program will recompile every source file in the project, even if the original objects (and other targets) were up-to-date already. Doing a clean before building the application is useful when you've changed something that is not listed in the dependency lists but on which the final executable still depends (like the HLA Standard Library). Doing a clean is also a good way to do a sanity check when you're running into problems and you suspect that the dependency lists aren't completely correct.

Beyond clean there aren't too many "standard" target definitions you'll see programmers using in their make files, though it's common for different make files to have some additional commands beyond building the default target and cleaning up temporary compiler files. Throughout this book, the make files associated with each project will generally provide the following dependency/commands (these roughly correspond to make options in the RadASM package):

build: This will be the default command (i.e., the first command appearing in the makefile). It will build an executable by building any out-of-date files and linking everything together. A typical build dependency will look like this:

        build: pgm.exe

This tells make to go execute the dependency for pgm.exe (which would normally be the default dependency in the file).

buildall: This command will rebuild the entire application. It begins by doing a clean, and then it does a build. This command generally takes the following form:

        buildall: clean pgm.exe

compileRC: This command will compile any resource files into .RES files. Though the current example does not have any resource files, a typical entry in the make file might look like the following:

        compileRC: pgm.rc

            rc pgm.rc

syntax: This command will compile any HLA into .ASM files just to check their syntax. Using the pgma.hla/pgmb.hla example given earlier, a typical compile dependency line might look like the following:

        syntax:

            hla -s pgma.hla pgmb.hla

run: This command will build the executable (if necessary) and then run it. The dependency line typically looks like the following:

        run: pgm.exe

            pgm <<any necessary command line parameters>>

clean: This is the command that deletes any compiler/assembler/linker produced temporary files, backup files, and the executable file. A typical clean command is

        clean:

            del *.obj

            del *.inc

            del *.bak

            del pgm.exe

The purpose behind these various make commands will become clear in the chapter on the RadASM integrated development environment. Their main purpose is to interface with RadASM creating an HLA IDE (Integrated Development Environment).

One nice feature that a standard make program provides is variables. The make program allows you to create textual variables in a make file using the following syntax:

identifier=<<text>>

All text beyond the equals sign ("=") to the end of the physical line5 is associated with the identifier and the make program will substitute that text whenever it encounters "$(identifier)" in your text file. This behavior is quite similar to TEXT constants in the HLA language. As an example, consider the following make file fragment:

sources= pgma.hla pgmb.hla

executable= pgm.exe

$(executable): $(sources)

    hla -e:$(executable) $(sources)

Because of the textual substitution that takes place, this is equivalent to the following make file fragment:

pgm.exe: pgma.hla pgmb.hla

    hla -e:pgm.exe pgma.hla pgmb.hla

You can even assign variable names from the make command line using syntax like the following:

make executable=pgm.exe sources="pgma.hla pgmb.hla"

This is an important fact we'll use because it allows us to create a generic makefile that RadASM can use to compile a given project by simply supplying the file names on the command line.

One problem with using makefile variables in this manner is that you lose one of the major advantages of using separate compilation: faster processing. If you specify a variable such as "$(sources)" as a parameter to the HLA.EXE command in a makefile, HLA will assemble all the source files, not just the ones that are older than their corresponding object files. This obviates the whole purpose for using a makefile in the first place (may as well use a batch file if we're going to force the recompilation of all the files in the project). What we need is a smarter generic make file that only compiles those files necessary in a project while allowing a controlling process (i.e., RadASM) to specify exactly what files to use.

Various versions of the make program provide a facility to deal with this problem: implicit rules. Unfortunately, various versions of make have minor syntactical differences that hinder the creation of a universal makefile that will work with every version of make out there. Nonetheless, with a little care and the use of some advanced make features, it is possible to come up with a make file that works reasonably well with a wide range of different make programs. The examples you're about to see work fine with Borland's make.exe program and Microsoft's nmake.exe program, they should also work fine with FSF/GNU's make program6.

Implicit rules in a make file describe how to convert files of one type (e.g., ".hla" files) into files of another type (e.g., ".obj" files). An implicit rule uses the following syntax:

.fromExt.toExt:

    command

Where .fromExt is the file type (extension) of the source file and .toExt is the file type of the destination file to produce. The ".fromExt" must begin in column one. The command is a command line operation that (presumably) translates the source (target) file into the destination (dependent) file. There must be at least one tab in front of this command.

The important thing to note about implicit rules is that they operate on a single file at a time. For example, if you supply the implicit rule ".hla.obj" to tell make how to convert HLA files into object files, and your project includes the files a.hla, b.hla, and c.hla, then this implicit rule will execute the command(s) you specify on each of these files. There is one question, though, "how does the command know what file the implicit rule wants it to process?" The command can determine this by using a couple of special make variables7: "$@" and "$<". Whenever make encounters the special variable "$@" within a command attached to an implicit rule, it substitutes the target name and extension for this variable. So if the implicit rule in this example is processing the files a.hla, b.hla, and c.hla, then make will expand "$@" to a.obj, b.obj, and c.obj, respectively, when processing these three source files. The special make variable "$<"expands into the source filename for the given command. Therefore, when individually processing the a.hla, b.hla, and c.hla files, the "$<" variable expands to a.hla, b.hla, and c.hla, respectively. So consider the following implicit make rule:

.hla.obj:

    hla -c $<

Given the list of files a.hla, b.hla, and c.hla, this single command is equivalent to the following:

a.obj: a.hla

    hla -c a.hla

b.obj: b.hla

    hla -c b.hla

c.obj: c.hla

    hla 0c c.hla

Note that make will only execute an implicit rule if you don't supply an explicit rule to handle the file conversion. Consider the following make file fragment:

.hla.obj:

    hla -c $<

a.obj: a.hla

    hla -s a.hla

    ml -c -Zi a.asm

Given the three files a.hla, b.hla, and c.hla, this makefile sequence will use the implicit rule to process files b.hla and c.hla but it will use the explicit rule given in this example to process the a.hla file.

Most versions of make include a pre-defined set of implicit rules that they will use if you don't explicitly supply some file dependencies in your make file. It is a real good bet, however, that few of these make programs are aware of HLA's existence, so it is unlikely that the make program will recognize HLA files, by default. Worse, many of these make programs do understand various file types (e.g., .asm and .obj) that HLA produces and they may react adversely to the presence of these files in your project directory. Therefore, we need some way of disabling the existing implicit rules while enabling our new ones. The special make .SUFFIXES variable takes care of this. The .SUFFIXES variable is a string that contains a list of all the suffixes (file types) that make recognizes for implicit rules. Our first task is to clear this string variable (to dissociate all the old types) and then add the file types (extensions) that we want make to recognize. This is done in two steps:

# Note: makefile comments begin with a "#" and continue to the end of the line.

#

# Clear out the existing .SUFFIXES list:

.SUFFIXES:

.SUFFIXES: .hla .obj .exe

After clearing and setting up the .SUFFIXES variable, make will be capable of processing .hla files.

If you don't supply any explicit rules in a make file, then make will process every file in the current subdirectory that an implicit rule can handle. If you supply at least one explicit rule, then make will process only that explicit rule (or the first such explicit rule appearing in the file). Because we'll generally want to control which files that make processes by supplying a file list on the command line, we need to add at least one rule to a makefile to give make this kind of control. Consider the following (complete) makefile:

.SUFFIXES:

.SUFFIXES: .hla .obj .exe

.hla.obj:

    hla -c $<

$(executable): $(objects) $(libraries)

    hla -e:$(executable) $(objects) $(libraries)

A typical command-line invocation of make using this makefile might look like this:

make executable=a.exe "objects=a.obj b.obj c.obj"

(Because we don't define the symbol libraries, it expands to an empty string in the makefile.)

There are a couple of problems with this makefile we've created thus far. First of all, make chokes on this file if you don't supply command line definitions for objects and executable. Second of all, it requires that you specify the object file names (e.g., a.obj, b.obj, and c.obj) rather than the source file names; this is a minor point, but the user of the make file is more likely to supply the full source file names rather than the object file names.

The first problem is easily handled by adding a couple of conditional statements to the makefile. Many variants of make (including Borland and Microsoft's versions) support !ifndef and !endif to let you test for a variable definition within a make file (and take some action if the symbol is not defined)8. Here's a possible extension to the current make file we're developing that takes this into account:

.SUFFIXES:

.SUFFIXES: .hla .obj .exe

!ifndef files

files=Error_you_need_to_supply_a_files_command_line_parameter.hla

!endif

!ifdef executable

files=Error_you_need_to_supply_an_executable_command_line_parameter.exe

!endif

.hla.obj:

    hla -c $<

$(executable): $(objects) $(libraries)

    hla -e:$(executable) $(objects) $(libraries)

If you type "make" all by itself on the command line and you fail to supply the "files=..." or "executable=..." command line parameter, this makefile will use the strings appearing in this example as these filenames. This invariably produces an error when HLA cannot find the specified files, or HLA will produce a peculiar executable name. In either case, it should be fairly obvious what went wrong.

You might be tempted to try something like the following:

!ifndef files

files:

    echo Error you need to supply a `files=...' command line parameter

!endif

This approach will always produce an error message and abort execution of the makefile if you do not supply a "files=..." command line parameter to the make program. However, it turns out that there are some times when we don't want to supply such a command line parameter, consider the following makefile:

.SUFFIXES:

.SUFFIXES: .hla .obj .exe

!ifndef files

files=Error_you_need_to_supply_a_files_command_line_parameter.hla

!endif

!ifdef executable

files=Error_you_need_to_supply_an_executable_command_line_parameter.exe

!endif

.hla.obj:

    hla -c $<

$(executable): $(objects) $(libraries)

    hla -e:$(executable) $(objects) $(libraries)

clean:

    del *.obj

    del *.inc

    del *.asm

    del *.link

If the user types "make clean" at the command line hoping to delete all the existing object, assembly, include, and link files, this particular make file will work just fine. However, were you to substitute the !ifndef directive with the type command, then "make clean" would always trigger that !ifndef and make would simply print the error message and quit without processing the clean request. By actually defining files to contain some value (that will generate an error later on), this makefile allows someone to enter a command like "make clean" and make will process the request, as desired.

Although this section discusses the make program in sufficient detail to handle most projects you will be working on, keep in mind that the make program provides considerable functionality that this chapter does not discuss. To learn more about the nmake.exe program, consult the appropriate documentation. This section has briefly touched upon some rather advanced uses of the make program. In fact, make has many other features that you might want to take advantage of. For more details, consult the vendor's documentation accompanying the version of make that you're using. This book will assume that you're using Borland's make (version 4.0 or later) or some version of Microsoft's nmake. Every make file in this book has been tested with both of these versions of make. These make files may work with other versions of make as well, but given the advanced features this makefile uses, it's fairly certain this makefile will not work with all versions of make out there. If you don't already have a copy of make, note that you can download Borland's make as part of the Borland C++ 5.5 compiler (see the directions for downloading this file earlier in this chapter).

Because of the variations in the way different make programs work, the makefiles appearing in this book will be relatively simple, not taking advantage of too many special make features. The generic makefile we'll usually start with looks like this:

# Note: "<<file>>" represents the project/file name. You will replace this text by

# the appropriate name when creating a custom makefile for your project.

build: <<file>>.exe

buildall: clean <<file.exe>>

# The following entry must be modified if the project needs to compile a resource file.

compilerc:

    echo No Resource Files to Process

syntax:

    hla -s <<file>>.hla

run: <<file>>.exe

    <<file>>

clean:

    del /F /Q tmp

    del /F /Q *.exe

    del /F /Q *.obj

    del /F /Q *.link

    del /F /Q *.inc

    del /F /Q *.asm

    del /F /Q *.map

<<file>>.exe: <<file>>.hla wpa.hhf

    hla $(debug) -p:tmp -w -c <<file>>

The $(debug) variable will be defined on the make command line if the makefile is to create a debug version of the software (we'll discuss how to do this later).

1.8: The HLA Integrated Development Environment

This book uses Ketil Olsen's RadASM integrated development environment for HLA/Win32 program development. The RadASM package provides a programmer's text editor, a project manager, a build subsystem, and several other tools you will find useful. Note, however, that you can use another IDE system that works with HLA (e.g., the UeMake add-on for Ultra-Edit32) or you could use your favorite text editor and run HLA from the command line. The choice is up to you. A little bit later, this book will describe the RadASM IDE for HLA for those who want to use an IDE but don't have a particular preference.

1.9: Debugging HLA Programs Under Windows

This book recommends the use of Oleh Yuschuk's OllyDbg debugger for Windows when debugging assembly language programs written in HLA. A whole chapter on the use of this tool will appear later in this book.

1.10: Other Tools of Interest

There are many generic programs that a Windows assembly language programmer might find interesting. For example, Steve Hutchesson's MASM32 package contains several utilities a Windows assembly language programmer will find useful. A quick search on the internet will locate many other utilities you'll find useful as well. This book won't spend too much time on tools other than those that have already been mentioned (there are some space restrictions here!), but that shouldn't stop you from locating and using programs you find useful.

1.11: Windows Programming Documentation

As this chapter mentions, there is no way a single book of any practical size is going to tell you everything you need to know about Windows assembly language programming. Unfortunately, documentation on writing Windows applications in assembly is difficult to come by. There are, however, several sources of information you may find helpful. We'll take a look at a couple of them here.

First, you should get a copy of Petzold and Yao's "Programming Windows..." book. Yes, it's written for C programmers, not assembly programmers. However, much of the information that appears in the book is language-independent and it's easy enough to mentally translate the C descriptions into assembly language (e.g., see the next chapter). No serious Win32 API programmer should be without this text, regardless of which language they use.

Microsoft also provides a large database of Win32 programming information with their MSDN (Microsoft Developer's Network). The MSDN information is available on CD-ROM with nearly every Microsoft programming tool. You can also purchase this information separately from Microsoft and it's also available free on-line at www.msdn.com. Among other things, the MSDN database describes the semantics of each and every Win32 API call that you can make; it's written for C programmers (like Petzold's book), but when you want to know what a particular Win32 API function does, the MSDN data base is a good place to look.

On the CD-ROM accompanying this textbook, you'll find some documentation describing most of the Win32 API class from an HLA perspective. This information is effectively the same as what you'll find in the MSDN system, but it's targeted to HLA users rather than C users.

The Iczelion Tutorials also provide a lot of useful information for Windows assembly language programmers. The Iczelion Tutorials were originally written for MASM32 users, though you can find HLA translations of many of those tutorials on the CD-ROM accompanying this book (the MASM32 versions also appear on the CD-ROM.). There are also several web sites dedicated to Win32 programming in assembly language. Searching the internet via your favorite search engine for "Win32 assembly" should turn up a lot of hits. You should also visit Webster at http://webster.cs.ucr.edu and check on the "links" page. There are links to several good Win32 assembly pages from Webster.

1The SpAsm assembler, for example, works exactly this way. While SpAsm is fast and flexible, it also has some severe limitations that prevent its use for most people who write practical Win32 applications in assembly language; in particular, SpAsm doesn't allow the linking of external library modules. This means that SpAsm forces the programmer to include every little utility routine in their source file when creating Win32 applications. This is unacceptable to most assembly programmers. However, if the thought of not being able to link in third-party code doesn't bother you, you should take a look at the SpAsm system because it does support some other, interesting, features. Unfortunately, this book cannot make use of SpAsm because this book teaches solid software engineering and the ability to link in separately compiled object modules is an important requirement for code reuse and modularity.
2Obviously, if you only change comments or other statements in the source file that do not affect the executable file, a recompile or reassembly will not be necessary. To be safe, though, we will assume any change to the source file will require a reassembly.
3There is a command line option that lets you specify the name of the makefile. See the nmake documentation in the MASM manuals for more details.
4You can tell make to use a different file by specifying the "-f" command line option. Check out make's documentation for more details.
5If you need more text than will physically fit on a single line, place a backslash at the end of the line to tell make that the line continues on the next physical line in the make file. The make program removes the new line characters between the two lines and continues processing.
6If you do not have some version of make available, you can obtain a copy of Borlands' make.exe from their website (http://www.borland.com). You may download a newer version of Borland's make as part of their BC++ v5.5 trial edition.
7By the way, make variables are often called macros by the various make vendors.
8If your version of make doesn't support this feature (or anything like it), you're probably better off using batch files rather than make files to build an HLA project.
TOC PREV NEXT INDEX


Chapter 2: Advanced HLA Programming

Note: This text is being made available in alpha form for proofreading and evaluation purposes only. This book contains known problems, use the information in this book at your own discretion. Please do not distribute this book. Send all corrections to rhyde@cs.ucr.edu.

©2003, Randall Hyde, All Rights Reserved

2.1: Using Advanced HLA Features

This book uses several advanced features in the HLA language. Chances are pretty good that unless you've spent a lot of time studying HLA prior to reading this book, you're not going to be familiar with many of these features. Now before you complain about the choice of using these features, and how this book should have avoided them in order to make the material more accessible to "less than expert HLA programmers," just keep in mind that many of these advanced features were added to the HLA language specifically to support Windows programming. Therefore, avoiding these features would prove to be counter-productive.

Much of the advanced HLA programming we're going to wind up doing involves HLA compile-time language statements (including the macro processor), data declarations, high-level control structures, and high-level procedure call syntax. While The Art of Assembly Language covers each of these topics, this book isn't going to make the assumption that you've carefully read those sections of The Art of Assembly Language (or even read "AoA" at all).Whether or not you've studied this material in that book, you'll definitely want to carefully read this chapter. It contains important information that the rest of this book is going to use, with an emphasis on those HLA features that are important to Windows programmers.

2.2: HLA's High-Level Data Structure Facilities

One of the primary differences between high level languages and "traditional" assemblers is the support for abstract data types in high level languages versus only supporting low-level machine data types (e.g., bytes, words, and double words) in assembly language. One of the hallmarks of a high level assembler (like HLA, MASM, or TASM) is support for abstract data types and user-defined data types. The HLA assembler provides about the richest set of built-in data types you'll find in any assembler plus the ability to easily create new data types as the need arises. Because Windows uses sophisticated data structures in calls to Win32 API functions, you'll want to familiarize yourself with some of HLA's structured data types in order to write code that interfaces better with Windows.

2.2.1: Basic Data Types

Whereas a traditional assembler provides a few machine-level data types based on the sizes of those types, and then leaves it up to the assembly language programmer to manually interpret those types in a program, HLA provides a rich set of basic data types that denote the purpose of the object in addition to its size. For example, with a traditional assembler you might declare a variable to be a byte object; within your assembly language source code it is up to you to decide whether you want to treat that byte as a character variable, an unsigned integer, a boolean object, a signed integer, an enumerated data type, or some other value that you can represent with eight bits. With HLA, you can actually declare your variables to be char, boolean, enum, int8, uns8, etc., and make the intent of that variable's usage much clearer; HLA will even do some type checking when you use these data types to help find logical errors in your programs.

The first thing to note about HLA's abstract data types is that HLA is an assembly language. At the machine instruction level, the 80x86 CPU works with bytes, words, double-words, and other such primitive machine data types. Once you've loaded an int8 object into AL, for example, it is up to you to choose the correct machine instructions to treat that object as a signed integer. There is nothing stopping you from treating this as an unsigned integer object, as a character value, as a boolean value, etc. Do not, however, get the impression that HLA's abstract data typing facilities serve little purpose beyond providing documentative services that make your programs a little easier to read. HLA provides high-level control structures and procedure calling syntax, as well as a built-in compile-time language, that can take advantage of HLA's type-checking facilities. To achieve maximum benefit from HLA, therefore, you should choose the proper data types for your variables, constants, and other objects in HLA.

Although there are some very good reasons for using abstract (high-level) data types in an assembly language program, HLA doesn't lose sight of the fact that it is an assembler. Sometimes the use of high-level data abstractions interferes with the assembly language programming paradigm. In such situations, you'll want to use low-level machine data types to make your code more efficient. To support such situations, HLA supports the full range of low-level machine data types you'll find on the 80x86 CPU, including bytes, words, double words, quad words, ten-byte words, and long words (128-bit objects/16-byte objects). HLA uses the following type names for these primitive data types:

byte Eight-byte (one byte) objects.

word 16-bit (two byte) objects

dword 32-bit (four byte) double word objects

qword 64-bit (eight byte) quad word objects

tbyte 80-bit (ten byte) ten byte objects

lword 128-bit (16 byte) long word objects

Note that some individuals might consider HLA's real32, real64, and real80 data types to be primitive because the 80x86 CPU directly supports these types. However, HLA treats these types as abstract data types and performs type checking on them because of the special nature of their data.

The low-level data types provide a "type-escape" mechanism for HLA. The only type checking that HLA does on these machine level types is to ensure that the size of the object is appropriate for its current use (e.g., HLA will report an error if you try to load a byte object into a word-sized register). Other than the size check, which you can override if you desire, HLA will allow the use of any similarly-sized scalar object in place of one of these data types and you can use an object that has one of these data types in place of a similarly sized scalar object.

Note the use of the term "scalar" in the previous paragraph. What this means is that you can substitute any like-size single variable for a byte, word, dword, qword, tbyte, or lword object in an HLA statement. You may not, however, directly substitute an array, record, union, class, or other composite type in place of one of these data types, even if the size of the composite object is the same as the primitive data type. For example, you cannot directly substitute a two-byte array in place of a word object (though, using coercion, even this is possible in HLA; you'll find out more about coercion in just a little bit).

2.2.1.1: Eight Bit Data Types

HLA supports the following eight-bit data types, all of which are directly compatible with the byte type:

  • boolean
  • enum (HLA, by default, uses a byte to hold enum objects)
  • uns8
  • byte
  • int8
  • char

Directly compatible means that you may supply a byte object anywhere one of these other types is required and you may also supply any one of these other types wherever a byte type is required. Note that these data types are not compatible amongst themselves; that is, you cannot always supply, say, a char data type where an uns8 object is required. The byte type is the only data type that is universally compatible with all of these types.

Note that HLA assigns the byte type to the 80x86 eight-bit registers, AL, AH, BL, BH, CL, CH, DL, and DH. Therefore, these registers are automatically compatible with any byte-sized object using one of HLA's single byte types.

HLA uses the boolean type to represent true/false values. Technically, a boolean variable requires only a single bit to represent the two possible values; however, the 80x86 CPU can only efficiently access objects that are multiples of eight bits. Therefore, HLA sets aside a whole byte for a boolean object. HLA represents false with the value zero and true with the value one. Traditionally, programmers have used zero for false and any non-zero value to represent true; therefore, HLA's definition is upwards compatible to the traditional (i.e., "C") definition. In general, when using boolean values, you should always store a zero or one into a boolean variable and test for zero/not zero when checking boolean values. This will provide the maximum compatibility with all uses of this data type. Note that arithmetic expressions involving boolean operands may produce strange results when combining operands using the AND and OR operators. The next chapter discusses this issue in detail. Just be aware of this for the time being.

Enumerated data types are data types that associate sequential numeric values with symbolic constants. Internally, HLA treats enum objects as unsigned integers (e.g., for purposes of comparison), though enum objects are not directly compatible with uns8 objects. An enumerated data type in HLA consists of a list of identifiers to which HLA assigns sequential unsigned integer values, starting with zero. For example, consider the following HLA type declaration:

type

    color_t :enum{ red, green, blue }:

HLA internally assigns the value zero with red, the value one with green, and the value two with blue. It is important to note, however, that red, green, and blue are not uns8 objects. They are of type color_t and this will make a difference in certain circumstances. For more details on enumerated data types in HLA, please consult the HLA Reference Manual.

HLA uses the uns8 data type to represent unsigned integer values in the range 0..255. This data type is fundamentally equivalent to the byte type except that uns8 objects are not automatically compatible with the other eight-bit data types. In boolean expressions, HLA always defaults to using unsigned comparisons and conditional jump instructions when the operands are uns8 operands.

HLA uses the int8 type to represent signed integers in the range -128..+127. In boolean expressions, HLA always uses signed comparisons and conditional jumps with at least one of the operands is signed (note that if you mix an unsigned and signed operand across a relational operator, HLA defaults to use a signed comparison).

Internally, HLA treats the char data type as an eight-bit unsigned integer value. Though not directly compatible with the uns8 type, HLA uses unsigned comparisons and conditional jumps whenever it encounters a char operand in a run-time boolean expression.

As noted earlier, the byte type is fundamentally compatible with all other scalar eight-bit types. When two byte types appear in a run-time boolean expression around the same operand, HLA compares the two values using an unsigned comparison. If a byte operand appears with another eight-bit type around some relational operator in a run-time boolean expression, then HLA uses whatever type of comparison is appropriate for that other operand (i.e., unsigned for enum, char, boolean, and uns8 operands or signed for int8 operands).

There are three major areas where HLA differentiates the types of eight-bit objects: in run-time boolean expressions, in procedure call parameter lists, and in compile-time (constant) expressions. We'll address each of these areas separately in later sections of this chapter.

2.2.1.2: Sixteen-Bit Data Types

HLA supports the following 16-bit data types, all of which are directly compatible with the word type:

  • int16
  • uns16
  • wchar
  • word

HLA uses int16 objects to represent signed values in the range -32768..+32767. Whenever HLA encounters an int16 object in a run-time boolean expression, it uses signed comparisons and conditional jump instructions, even if the operand opposite the int16 object in a relational expression is an unsigned operand.

HLA uses uns16 objects to represent unsigned values in the range 0..65535. Whenever HLA encounters an uns16 object in a run-time boolean expression, and the corresponding object opposite the relational operator is not an int16 value, HLA uses unsigned comparison and jump instructions.

HLA uses wchar objects to represent unicode characters. These are unsigned objects (though not directly compatible with uns16 values) so HLA uses unsigned comparisons and conditional jump instructions whenever you compare wchar objects in a run-time boolean expression.

Objects of type word are directly compatible with any of the other 16-bit ordinal data types. If an object of type word appears in a run-time boolean expression, then HLA will emit code that uses the type of comparison required by the other operand in the relational expression. If the other operand is also of type word, HLA uses an unsigned comparison and jump to compare the two operands.

2.2.1.3: Thirty-Two-Bit Data Types

HLA supports the following 32-bit data types, all of which are directly compatible with the dword data type:

  • dword
  • int32
  • uns32
  • pointer types
  • procedure pointer types

HLA also supports the following 32-bit data types, though these types are not directly compatible with the dword type:

  • string (this is a pointer type)
  • unicode (this is a pointer type)
  • real32

The behavior of the uns32 and int32 types, with respect to the dword word, is similar to the relationship between uns8/int8 and byte, and uns16/int16 and word. Take a look at the previous sections for more details.

HLA generally treats pointer types as dword objects within most run-time expressions. In particular, HLA will perform an unsigned comparison when comparing two pointer objects.

Procedure pointer types are also treated just like dwords by HLA with one major exception: HLA provides implicit syntax for calling a procedure indirectly using HLA's high level procedure call syntax. So although you can treat a procedure pointer just like a dword object within a run-time boolean/relational expression, you cannot directly call a procedure via a pointer to a procedure in a dword variable using HLA's high-level procedure call syntax. Such activity is only available when using procedure pointers (or when specifying an explicit coercion operator). We'll take another look at HLA's procedure pointer objects in a few sections.

Although HLA string variables are pointers, HLA treats strings as a unique data type for the purposes of type checking. In particular, dword and string objects aren't always interchangeable nor are they even type compatible in all cases. To confuse the issue even farther, certain Win32 API function calls allow you to pass a small integer value (16 bits or less) in place of a string pointer as a function parameter. Therefore, in certain cases, HLA will allow you to substitute a small integer constant in place of an actual string pointer when calling a function (this use, however, is being depreciated, so don't count on it in future versions of HLA). Generally, though, you can use a string variable (not a constant) wherever a dword object is expected; you may not, however, always use a dword object where a string object is expected. This same discussion applies to the unicode data type (which is a string of unicode characters).

Because floating point values on the 80x86 are fundamentally different than integer values, HLA does not allow you to mix real32 and dword objects in the same expression. Indeed, HLA doesn't even allow real32 objects in a boolean/relational expression (though you may pass real32 objects as procedure parameters). For that reason, we'll not consider real32 objects here. See The Art of Assembly Language for more details concerning floating point arithmetic on the 80x86.

2.2.1.4: Sixty-Four-Bit Data Types

HLA supports a couple of 64-bit data types:

  • int64
  • real64
  • uns64
  • qword

As with the 32-bit data types, qword is directly compatible with int64 and uns64 types. The issue of int64 and uns64 compatibility isn't as much of an issue because the 80x86 doesn't directly support 64-bit comparisons with the integer instruction set1. Also like the 32-bit data types, the real64 type is not directly compatible with the qword type, nor may you compare real64 objects within an HLA run-time boolean expression.

2.2.1.5: Eighty-Bit Data Types

HLA supports two 80-bit data types - the tbyte type and the real80 type. Neither type is directly compatible with the other and HLA doesn't allow the comparison of 80-bit objects within a run-time boolean expression. Tbyte objects usually hold BCD values and real80 objects hold floating point values. We'll not consider these two types any farther, here.

2.2.1.6: One Hundred Twenty-Eight Bit Data Types

HLA supports several 128-bit data types, they are:

  • cset
  • int128
  • lword
  • real1282
  • uns128

Though HLA does not support the use of int128, lword, and uns128 objects in a run-time boolean expression (because the 80x86 CPU doesn't support the direct comparison of 128-bit objects using the integer instruction set), you can still pass these objects as procedure parameters and use them in compile-time expressions, so it's worth mentioning that the relationship they share is similar to the dword/uns32/int32 relationship sans the ability to compare them. The cset data type is, technically, a composite data type (character sets can be thought of as an array of 128 bits); therefore, there is no fundamental relationship between an lword and a cset other than their size. similarly, real128 data types are also composite objects (it's an array of two real64 objects) so they aren't directly compatible with lwords, either. Because Windows doesn't use 128-bit data types very often, we'll not consider these data types any further.

2.2.2: Composite Data Types

HLA supports a fair number of composite data types as well as scalar data types. As the name suggests, a composite data type is one that is composed (or made up) of other data types. HLA provides direct support for the following composite data types: arrays, records (structures), unions, classes, pointers, procedure pointers, character sets, and thunks. Character sets, pointers, and procedure pointers are a special case - HLA provides direct support for these composite data types so that you can often treat these types as scalar (though not ordinal) types. Though it's sometimes convenient to treat these objects as scalars, there are times when it's more convenient to think of them as composite objects. Thus we'll consider them again in the following subsections.

2.2.2.1: HLA Array Types

HLA uses a high-level-like syntax for the declaration of arrays in your assembly language programs. A typical (run-time) array variable declaration might look like the following:

static

    runTimeArray :int32[16];

This declaration sets aside storage for a sequence of 16 32-bit integers in memory. The name runTimeArray refers to the address of the first element of this array. The next variable immediately following this array in memory will be at an address that is (at least) 64 bytes later (16 array elements times four bytes for each element is 64 bytes). You can even declare multi-dimensional arrays in HLA using a high level like syntax, e.g.,

type

    two_dim_array : char[ 4, 16 ]; // 64 bytes of storage

    three_dim_array : dword[ 4, 2, 2 ]; // Also 64 bytes of storage

    four_dim_array : real32[ 2, 2, 2, 2 ]; // Also 64 bytes of storage

Note that multi-dimensional arrays consume as much storage as the product of their dimensions, also multiplied by the size of a single array element.

As is true in all assembly languages, indexes into an array start at zero and the last element (of a particular dimension or index) ends at the array bounds minus one. For example, the elements of the runTimeArray example given earlier range from index zero through 15.

One very fundamental point, which is the cause of much confusion to assembly language programmers and especially to HLA programmers, is that you do not index into an array using the same syntax and concepts as you do in a high level language. The fact that HLA uses a high-level language-like syntax for array declarations falsely propagates this idea. The fact that HLA's compile-time language works the way high level languages do, in direct contrast to the run-time (i.e., 80x86 assembly) language also aids in this confusion. Don't get caught in this trap!

Although both The HLA Reference Manual and The Art of Assembly Language Programming discuss accessing elements of arrays, emails and posts to assembly language related newsgroups indicate that this is one area that causes problems for assembly programmers (especially beginning assembly programmers). So it's worthwhile repeating some of that information here.

The most fundamental mistake people make when indexing into array in assembly language code is that they forget that they must multiply the index into the array by the size of an array element. More often than not, a beginning assembly language programmer will translate code like the following:

eax = runTimeArray[ i ]; // "C" example

into assembly code that looks like this:

    mov( i, ebx ); // Must load array index into a 32-bit register to use indexed addressing

    mov( runTimeArray[ ebx ], eax );

Nice try, but completely incorrect. The problem is that the 80x86 indexed addressing modes compute byte offsets from some base address, not element indexes into an array. That is, the "runTimeArray[ebx]" addressing mode computes the effective address obtained by adding the current value in EBX to the address associated with the runTimeArray label. If EBX contains five and the memory address associated with the runTimeArray label is $40_0000, then this addressing mode references the object at address $40_0005 in memory. Note that this is not the address of the sixth element of the runTimeArray array. Each element of the runTimeArray object consumes four bytes, so the elements appear in memory as follows:

runTimeArray[0]- $40_0000

runTimeArray[1]- $40_0004

runTimeArray[2]- $40_0008

runTimeArray[3]- $40_000C

runTimeArray[4]- $40_0010

runTimeArray[5]- $40_0014

runTimeArray[6]- $40_0018

runTimeArray[7]- $40_001C

runTimeArray[8]- $40_0020

runTimeArray[9]- $40_0024

runTimeArray[10]- $40_0028

runTimeArray[11]- $40_002C

runTimeArray[12]- $40_0030

runTimeArray[13]- $40_0034

runTimeArray[14]- $40_0038

runTimeArray[15]- $40_003C

The address $40_0005 (obtained as the effective address of the expression runTimeArray[ebx] when EBX contains five) is actually the address if the second byte of the runTimeArray[1] element. This probably isn't what the programmer had in mind.

As you can see in this list, each element of runTimeArray is located four bytes in memory apart. This makes perfect sense because each element of the array consumes four bytes of memory and we'd like the array elements to occupy adjacent memory cells (that is, each element of the array should immediately follow the previous element of the array in memory). In order to achieve this, you must multiply the array index by the size of each array element (in bytes). Because each element of the array is a double word (four bytes), we have to multiply the index into the array by four and add the base address of the array (that is, the address of the first element) to obtain the actual address of the desired element. In the current example, if we want to access the element at index five (that is, the sixth element of the array), then we need to multiply the index (five) by four before adding in the base address. Five times four is 20 ($14), adding this to the base address produces $40_0014 in the current example. If you'll look at the list for runTimeArray element addresses, you'll find that runTimeArray[5] does, indeed, sit at address $40_0014 in memory.

Because programs commonly access elements of arrays whose element sizes are one, two, four, or eight bytes long, the 80x86 CPU provides a special set of addressing modes, the scaled indexed addressing modes, that let you easily compute the index into these arrays. These addressing modes use syntax like the following:

    byteArray[ eax*1 ] -- note that this is functionally identical to byteArray[ ebx ]

    wordArray[ ebx*2 ]

    dwordArray[ ecx*4 ]

    qwordArray[ edx*8 ]

The register you specify in the scaled indexed addressing mode must be a general-purpose 32-bit integer register; see The Art of Assembly Language for more details.

If you need to access an element of an array whose element sizes are not one, two, four, or eight bytes, then you've got to manually multiply the index by the size of an array element (typically using a shl or intmul instruction). For example, if you've got an array of real80 objects you'll need to multiply the index into the array by 10 (10 bytes = 80 bits) before accessing the particular element. Here's some sample code that demonstrates this:

// Push element "i" of real80Array onto the FPU stack:

    intmul( 10, i, ecx ); // ecx = 10 * i

    fld( real80Array[ ecx ] );

Note that you do not use a scaled indexed addressing mode when you manually multiply the index by the element size. You've already done the multiplication to obtain a byte offset into the array.

Accessing an element of a multidimensional array requires the use of a more complex calculation. In the Windows API world, all two-dimensional arrays are stored in a form known as row-major order. Rather than get into the complexities of row-major (versus column-major) addressing here, we'll just use the HLA Standard Library array.index function that does multi-dimensional array computations for you automatically. Those who are interested in the low-level implementation of multi-dimensional array access should take a look at the chapter covering arrays in The Art of Assembly Language Programming.

The HLA Standard Library array.index macro automatically computes the index into an array (with any number of dimensions). Because this is a macro, it directly generates code that is almost as good as hand-written assembly language code (in a few special cases you might be able to generate slightly better code by hand, but most of the time this macro generates exactly the same code you'd manually write). To use the array.index macro, you must either include stdlib.hhf at the beginning of your source file or, at least, include arrays.hhf. The array.index macro uses the following invocation syntax:

array.index( <32-bit register>, <array name>, <<list of array indices>> );

The first argument must be a 32-bit general purpose integer register (i.e., EAX, EBX, ECX, EDX, ESI, EDI, EBP, or ESP). Generally, you would not use EBP or ESP here (because they have special purposes and they generally aren't available for use in accessing array elements).

The second argument to the array.index macro is the name of the array whose elements you want to access. HLA actually supports dynamically allocated multi-dimensional arrays in the arrays.hhf module, we'll not worry about those here (see the HLA Standard Library documentation for more details); instead, this book will simply assume that you've supplied the name of an array you've declared in a static, readonly, storage, or var declaration section, or as a procedure pass by value parameter. Any of the array examples appearing in this section would work fine, for example.

The third (through nth) parameter(s) specify the constants, variables, or registers that you want to use as the indexes into this array. The index objects must all be 32-bit integer values (presumably unsigned, though the macro accepts signed values too).

The array.index macro computes the address of the specified array element and leaves that address sitting in the 32-bit register you specify as the first argument in the array.index argument list. To access the specified array element, all you need to do is reference the memory location pointed at by that 32-bit register after the array.index invocation. Note that you do not have to multiply this value by the size of the array element (that's already done for you) nor do you have to add in the base address of the array (that's also done for you). Here are some examples of accessing array elements using the array.index macro:

// j = runTimeArray[ i ];

    array.index( ebx, runTimeArray, i );

    mov( [ebx], eax );

    mov( eax, j );

// c = two_dim_array[ i, j ];

    array.index( edx, two_dim_array, i, j );

    mov( [edx], al );

    mov( al, c );

// esi = three_dim_array[ i, j, k ]

    array.index( ecx, three_dim_array, i, j, k );

    mov( [ecx], edx );

// push four_dim_array[ eax, ebx, ecx, edx ] onto the FPU stack:

    array.index( edi, four_dim_array, eax, ebx, ecx, edx );

    fld( (type real32 [edi]) );

The HLA compile-time language provides some built-in compile-time functions that return values of interest to an HLA programmer using arrays in an HLA program. During compilation, HLA replaces each of these compile-time functions with a constant that is the result of computing the compile-time function's result. You may use these compile-time functions anywhere a constant is legal in an HLA program. See Table 2-1 for a list of the applicable functions.

Table 2-1: HLA Compile-Time Functions That Provide Useful Array Information
Function
Sample Call
Description
@size
@size( array_name)
Returns the size, in bytes, of the entire array (the run-time size of the array).
@elements
@elements( array_name)
Returns the total number of elements declared for an array. If the array is a multi-dimensional array, this returns the product of all the array bounds for that array (i.e., the total number of elements).
@elementsize
@elementsize( array_name )
Returns the run-time size, in bytes, of a single element of the array.
@arity
@arity( array_name )
Returns the number of dimensions of the array.
@dim
@dim( array_name )
Returns a compile-time constant array where each element of the array specifies the bounds for one of the dimensions of the array.
@dim( array_name )[index]
Returns the number of elements of a particular dimension of an array (note that index is zero-based).

You can use the compile-time functions in Table 2-1 to automate much of your array calculations. This can make your programs much easier to maintain by avoiding "hard-coded" constants in your program. For example, suppose you have an array of some type (myType_t) whose definition is subject to change as you modify the program. Computing an index into this array is a bit of a problem; for example, if you know that the size of an element in the array is six bytes, you could (manually) compute an index into that array using code like the following:

    intmul( 6, index, ebx );

    lea( eax, myArray[ebx] );  // Load eax with the address of myArray[index].

The problem with this approach is that it fails if you decide to change the definition of myType_t (myArray's type) without manually adjusting this code to deal with the fact that myArray's size is no longer six bytes. Now consider the following implementation of this code sequence:

    intmul( @elementsize( myArray ), index, ebx );

    lea( eax, myArray[ ebx ] );

This code sequence automatically recomputes the appropriate index into the array regardless of myArray's size (because HLA automatically substitutes the correct size for @elementsize). About the only drawback to this scheme is that it hides the fact that certain indexes (1, 2, 4, and 8) might be better computed using an 80x86 scaled indexed addressing mode, though it is possible to fix this problem by using a macro (the HLA Standard Library array.index function, for example, automatically checks for these special element sizes and adjusts the code it outputs in an appropriate fashion for these special cases).

HLA is interesting insofar as it allows you to declare and use compile-time constant arrays as well as run-time memory arrays. Few other languages, much less assemblers, provide the concept of a compile-time array. HLA's compile-time language makes extensive use of these compile-time arrays and you can use compile-time arrays (also known as array constants) to initialize run-time arrays at program load time (i.e., arrays you declare in the static and readonly sections). There is, however, one important issue of which you need to be aware - HLA compile-time arrays/array constants use semantics that are appropriate for the HLA compile-time language (which is a high level language). These semantics are different than the semantics for a run-time (assembly language) array. In particular, indexes into array constants are actual array indexes, not byte offsets. Therefore, you do not multiply the index of a constant array by the element size. Furthermore, HLA automatically computes the row-major index into a constant array for you, you do not have to use array.index or manually compute the index yourself. We'll return to this subject a little later in this chapter when we discuss the HLA compile-time language. Just be aware of the fact that compile-time array constants are fundamentally different that run-time assembly language arrays.

2.2.2.2: HLA Union Types

A discriminant union is a data type that combines several different objects so that they use the same storage in memory. In theory, access to each of the variables in a discriminant union are mutually exclusive; because the variables share the same storage in memory, storing a value into one of these variables disturbs the values of the other variables sharing that same memory address. Some high level language programmers use discriminant union types as a sneaky way to retype data in memory. Obviously, this feature is of little value in an assembly language program where is trivially easy to recast data in memory as some other type. However, there are two primary purposes for using a discriminate union, both of which apply in assembly language as well as in high level languages: memory conservation and the creation of variant types. Because many assembler authors perceive discriminant unions as a type casting trick, very few assemblers support the union data type. Unfortunately (for those assemblers), many Windows data types use unions and this makes using such assemblers inconvenient when working with these Windows data structures. Fortunately, HLA provides full support for discriminant union data types, so this isn't a problem at all in HLA.

HLA implements the discriminant union type using the union..endunion reserved words. The following HLA type declaration demonstrates a union declaration:

type

    allInts:

        union

            i8  :int8;

            i16 :int16;

            i32 :int32;

        endunion;

Each entry variable declaration appearing between the union and endunion keywords is a field of that union. All fields in a union have the same starting address in memory. The size of a union object is the size of the largest field in the union. The fields of a union may have any type that is legal in a variable (HLA var) declaration section.

Given a union object, say i of type allInts, you access the fields of the union by specifying the union variable name, a period ("dot"), and the field name. The following 80x86 mov instructions demonstrate how to access each of the fields of an i variable of type allInts:

    mov( i.i8, al );

    mov( i.i16, ax );

    mov( i.i32, eax );

A good example of a union type in action is the Windows w.CHARTYPE data type:

type

    CHARTYPE:

        union

            UnicodeChar: word;

            AsciiChar: byte;

        endunion;

The Windows w.CHARTYPE is a perfect example of a union data structure that encapsulates a couple of mutually exclusive values (that is, you wouldn't use the UnicodeChar and AsciiChar fields of this union simultaneously). Most Windows applications use either ASCII (ANSI) characters or Unicode characters; few applications would attempt to use both character sets in the same program (other than to convert one form to the canonical form that the application uses). Windows, as a general rule, provides two sets of API functions that deal with characters - one for ASCII and one for Unicode. By playing games with C preprocessor macros and accompanying data types (like w.CHARTYPE), Windows allows C programmers to call either set of functions while passing the same set of parameters. Of course, the programmer has to know which character type to supply (i.e., whether to store their data in the Unicode or AsciiChar fields), but the interface is the same either way.

Most of the unions you'll find in Windows data structures represent mutually exclusive data fields, like the fields belonging to w.CHARTYPE. Depending on your circumstances (e.g., whether your program works with ASCII or Unicode characters) you use one or the other of these two fields in your program and you will probably not use the other field at all. Another common use of discriminant union types is to create variant objects. A variant object is a variable that has a dynamic type, that is, the program determines the type of the object at run-time and, in fact, the variable's type can change under program control. Dynamically typed objects are great when you cannot anticipate exactly what type of data the user will enter while the program is running. The typical implementation of a variant data type consists of two components: a tag value that describes the current (dynamic) type of the object and a discriminant union value that can hold any of the possible types. During program execution, the software uses a switch/case statement (or similar logic) to execute appropriate code based on the current type of the dynamically typed object. Although the use of variant types is interesting and useful, Windows doesn't make much use of this advanced feature, so we won't discuss it any farther here.

2.2.2.3: HLA Record (Structure) Types

Records (structures) are probably the most important high level data structure an assembler can provide for Win32 assembly language programmers. In fact, if an assembler does not provide decent, built-in, capabilities for declaring and using records, you simply don't want to use that assembler for developing Win32 programs. Windows APIs make extensive use of records (C structs) and attempting to write assembly code with an assembler that doesn't properly support records is going to be a burden. Fortunately, HLA is not one of these assemblers - HLA provides excellent support for records. In this section we'll explore HLA's support for this important user-definable data type.

The record data type provides a mechanism for collecting together different variables into a physical unit so the program can treat this disparate collection of objects as a single entity. At the most basic level an array is also such a data type. However, the difference between an array and a record is the fact that all the elements of an array must have the same type, this restriction does not exist for the elements (fields) of a record. Another difference between an array and a record is that you reference the elements of an array using a numeric index whereas you access the fields of a record by the fields' names.

HLA's records allow programmers to create data types whose fields can be different types. The following HLA type declaration defines a simple record with four fields:

type

    Planet:

        record

            x       :int32;

            y       :int32;

            z       :int32;

            density :real64;

        endrecord;

Objects of type Planet will consume 20 bytes of storage at run-time.

The fields of a record may be of any legal HLA data type including other composite data types. Like unions, anything that is legal in a var section is a legal field of a record. Also like unions, you use the dot-notation to access fields of a record object. For example, you could use the following 80x86 instructions to access an object plnt of type Planet:

    mov( plnt.x, eax );

    mov( edx, plnt.y );

    add( plnt.z, ebx );

    fld( plnt.density );

There are several advantages to using records versus simply creating separate variables for each of the fields of the record. First of all, a record type provides a template that makes it very easy to create new instances (copies) of that record type. For example, if you need nine planets, you can easily create them in an HLA static section as follows:

static

    Mercury  :Planet;

    Venus    :Planet;

    Earth    :Planet;

    Mars     :Planet;

    Jupiter  :Planet;

    Saturn   :Planet;

    Uranus   :Planet;

    Neptune  :Planet;

    Pluto    :Planet;

Imagine the mess you would have if you had to create four separate variables for each of these nine planets (e.g., Mercury_x, Mercury_y, Mercury_z, Mercury_density, etc.).

Another solution, of course, is to create an array of records, with one array element for each planet. You can easily do this as follows:

static

    Planets :Planet[9];

Each element of the array will have all the fields associated with the Planet data type. Because HLA treats an array name as though it were the first element of the array, you can access the fields of the Planets array using the same dot-notation syntax you use to access fields of a single Planet object, e.g.,

    mov( Planets.x, eax ); // Moves field "x" of Planets[0] into eax.

If you want to access a field of some element other than the first element of an array of records, you tack on the indexed addressing mode and supply an appropriate index into the array, e.g.,

    intmul( @elementsize( Planets ), index, ebx );

    mov( Planets.x[ebx], eax );

Note that the indexed addressing mode specification follows the entire variable's name, unlike most high level languages, like C, where you would inject the array index into the name immediately after Planets. Keep in mind that you can also use HLA's array.index macro to index into an array of records, though you will need to coerce the resulting pointer to the Planet type in order to access the fields of that record, e.g.,

    array.index( ebx, Planets, index );

    mov( (type Planet [ebx]).x, eax );

In addition to saving you the effort of creating separate variables for each field, records also let you encapsulate related data, making it easier to copy record variables as single entities, manipulate record objects via pointers, and pass records as parameters to procedures. For example, it's much easier to pass a single parameter of type Planet to some procedure rather than passing the four separate fields that the Planet type encapsulates (if you don't think passing four parameters is a big deal, just keep in mind that many Win32 structures, that often appear as procedure parameters, have dozens of fields).

Beyond the issues of convenience and efficiency, there is one other important reason for using records in your assembly language programs: they're easier to read and maintain. Assembly language code is hard enough to read as it is; every opportunity you get to produce more readable assembly code is an opportunity you should take.

Record types may inherit fields from other record types. Consider the following two HLA type declarations:

type

    Pt2D:

        record

            x: int32;

            y: int32;

        endrecord;

    Pt3D:

        record inherits( Pt2D )

            z: int32;

        endrecord;

In this example, Pt3D (point 3-D) inherits all the fields from the Pt2D (point 2-D) type. The inherits keyword tells HLA to copy all the fields from the specified record (Pt2D in this example) to the beginning of the current record declaration (Pt3D in this example). Therefore, the declaration of Pt3D above is equivalent to:

type

    Pt3D:

        record

            x: int32;

            y: int32;

            z: int32;

        endrecord;

In some special situations you may want to override a field from a previous field declaration. For example, consider the following record declarations:

type

    BaseRecord:

        record

            a: uns32;

            b: uns32;

        endrecord;

    DerivedRecord:

        record inherits( BaseRecord )

            b: boolean;  // New definition for b!

            c: char;

        endrecord;

Normally, HLA will report a "duplicate" symbol error when attempting to compile the declaration for DerivedRecord since the b field is already defined via the inherits( BaseRecord ) option. However, in certain cases it's quite possible that the programmer wishes to make the original field inaccessible in the derived class by using a different name. That is, perhaps the programmer intends to actually create the following record:

type

    DerivedRecord:

        record

            a: uns32;    // Derived from BaseRecord

            b: uns32;    // Derived from BaseRecord, but inaccessible here.

            b: boolean;  // New definition for b!

            c: char;

        endrecord;

HLA allows a programmer explicitly override the definition of a particular field by using the overrides keyword before the field they wish to override. So while the previous declarations for DerivedRecord produce errors, the following is acceptable to HLA:

type

    BaseRecord:

        record

            a: uns32;

            b: uns32;

        endrecord;

    DerivedRecord:

        record inherits( BaseRecord )

            overrides b: boolean;  // New definition for b!

            c: char;

        endrecord;

Normally, HLA aligns each field on the next available byte offset in a record. If you wish to align fields within a record on some other boundary, you may use the align directive to achieve this. Consider the following record declaration as an example:

type

    AlignedRecord:

        record

            b:boolean;              // Offset 0

            c:char;                 // Offset 1

            align(4);

            d:dword;                // Offset 4

            e:byte;                 // Offset 8

            w:word;                 // Offset 9

            f:byte;                 // Offset 11

        endrecord;

Note that field d is aligned at a four-byte offset while w is not aligned. We can correct this problem by sticking another align directive in this record:

type

    AlignedRecord2:

        record

            b:boolean;              // Offset 0

            c:char;                 // Offset 1

            align(4);

            d:dword;                // Offset 4

            e:byte;                 // Offset 8

            align(2);

            w:word;                 // Offset 10

            f:byte;                 // Offset 12

        endrecord;

Be aware of the fact that the align directive in a record only aligns fields in memory if the record object itself is aligned on an appropriate boundary. For example, if an object of type AlignedRecord2 appears in memory at an odd address, then the d and w fields will also be misaligned (that is, they will appear at odd addresses in memory). Therefore, you must ensure appropriate alignment of any record variable whose fields you're assuming are aligned.

Note that the AlignedRecord2 type consumes 13 bytes. This means that if you create an array of AlignedRecord2 objects, every other element will be aligned on an odd address and three out of four elements will not be double-word aligned (so the d field will not be aligned on a four-byte boundary in memory). If you are expecting fields in a record to be aligned on a certain byte boundary, then the size of the record must be an even multiple of that alignment factor if you have arrays of the record. This means that you must pad the record with extra bytes at the end to ensure proper alignment. For the AlignedRecord2 example, we need to pad the record with three bytes so that the size is an even multiple of four bytes. This is easily achieved by using an align directive as the last declaration in the record:

type

    AlignedRecord2:

        record

            b:boolean;              // Offset 0

            c:char;                 // Offset 1

            align(4);

            d:dword;                // Offset 4

            e:byte;                 // Offset 8

            align(2);

            w:word;                 // Offset 10

            f:byte;                 // Offset 12

            align(4)                // Ensures we're padded to a multiple of four bytes.

        endrecord;

Note that you should only use values that are integral powers of two in the align directive.

If you want to ensure that all fields are appropriately aligned on some boundary within the record, but you don't want to have to manually insert align directives throughout the record, HLA provides a second alignment option to solve your problem. Consider the following syntax:

type

    alignedRecord3 : record[4]

        << Set of fields >>

    endrecord;

The "[4]" immediately following the record reserved word tells HLA to start all fields in the record at offsets that are multiples of four, regardless of the object's size (and the size of the objects preceding the field). HLA allows any integer expression that produces a value in the range 1..4096 inside these parenthesis. If you specify the value one (which is the default), then all fields are packed (aligned on a byte boundary). For values greater than one, HLA will align each field of the record on the specified boundary. For arrays, HLA will align the field on a boundary that is a multiple of the array element's size. The maximum boundary HLA will round any field to is a multiple of 4096 bytes.

Note that if you set the record alignment using this syntactical form, any align directive you supply in the record may not produce the desired results. When HLA sees an align directive in a record that is using field alignment, HLA will first align the current offset to the value specified by align and then align the next field's offset to the global record align value.

Nested record declarations may specify a different alignment value than the enclosing record, e.g.,

type

    alignedRecord4 : record[4]

        a:byte;

        b:byte;

        c:record[8]

            d:byte;

            e:byte;

        endrecord;

        f:byte;

        g:byte;

    endrecord;

In this example, HLA aligns fields a, b, f, and g on double word boundaries, it aligns d and e (within c) on eight-byte boundaries. Note that the alignment of the fields in the nested record is true only within that nested record. That is, ifc turns out to be aligned on some boundary other than an eight-byte boundary, then d and e will not actually be on eight-byte boundaries; they will, however be on eight-byte boundaries relative to the start of c.

In addition to letting you specify a fixed alignment value, HLA also lets you specify a minimum and maximum alignment value for a record. The syntax for this is the following:

type

    recordname : record[maximum : minimum]

        << fields >>

    endrecord;

Whenever you specify a maximum and minimum value as above, HLA will align all fields on a boundary that is at least the minimum alignment value. However, if the object's size is greater than the minimum value but less than or equal to the maximum value, then HLA will align that particular field on a boundary that is a multiple of the object's size. If the object's size is greater than the maximum size, then HLA will align the object on a boundary that is a multiple of the maximum size. As an example, consider the following record:

type

    r: record[ 4:1 ];

        a:byte;             // offset 0

        b:word;             // offset 2

        c:byte;             // offset 4

        d:dword;[2]         // offset 8

        e:byte;             // offset 16

        f:byte;             // offset 17

        g:qword;            // offset 20

    endrecord;

Note that HLA aligns g on a double word boundary (not quad word, which would be offset 24) since the maximum alignment size is four. Note that since the minimum size is one, HLA allows the f field to be aligned on an odd boundary (since it's a byte).

If an array, record, or union field appears within a record, then HLA uses the size of an array element or the largest field of the record or union to determine the alignment size. That is, HLA will align the field without the outermost record on a boundary that is compatible with the size of the largest element of the nested array, union, or record.

HLA's sophisticated record alignment facilities let you specify record field alignments that match that used by most major high level language compilers. This lets you easily access data types used in those HLLs without resorting to inserting lots of ALIGN directives inside the record. We'll take a look at this feature in the next chapter when we discuss how to translate C structs into HLA records.

By default, the first field of a record is assigned offset zero within that record. If you would like to specify a different starting offset, you can use the following syntax for a record declaration:

type

    Pt3D:

        record := 4;

            x: int32;

            y: int32;

            z: int32;

        endrecord;

The constant expression specified after the assignment operator (":=") specifies the starting offset of the first field in the record. In this example x, y, and z will have the offsets 4, 8, and 12, respectively.

Warning: setting the starting offset in this manner does not add padding bytes to the record. This record is still a 12-byte object. If you declare variables using a record declared in this fashion, you may run into problems because the field offsets do not match the actual offsets in memory. This option is intended primarily for mapping records to pre-existing data structures in memory. Only really advanced assembly language programmers should use this option.

Note: Windows carefully defines the data fields in most of the record data structures so that you don't have to worry about field alignment. As long as you ensure that a record variable is sitting at a double-word aligned address in memory, you can be confident that the record's fields will be aligned at an appropriate address within that record.

2.2.3: Nested and Anonymous Unions and Records

The fields of a record or union may take on any legal data type. Such fields can be primitive types (as has been the case in the examples up to this point), arrays, or even other record and union types. Support for nestable (recursive) data types in an assembly language is very important to Win32 assembly programmers because many Win32 data structures employ this scheme.

Consider, for example, the following record type:

type

    recWrecAndUnion:

        record

            r1Field: byte;

            r2Field: word;

            r3Field: dword;

            rRecField:

                record

                    rr4Field: byte[2];

                    rr5Field: word[3];

                endrecord;

            r6Field: byte;

            rUnionField:

                union

                    r7Field:dword;

                    r8Field:real64;

                endunion;

        endrecord;

To access the nested record and union fields found within the recWrecAndUnion type, you use an extended form of the "dot-notation". For example, if you have a variable, r, of type recWrecAndUnion, you can access the various fields of this variable using the following identifiers:

    r.r1Field

    r.r2Field

    r.r3Field

    r.rRecField.rr4Field

    r.rRecField.rr5Field

    r.r6Field

    r.rUnionField.r7Field

    r.rUnionField.r8Field

In a similar fashion, unions may contain records and other unions as fields, e.g.,

type

    unionWrecord:

        union

            u1Field: byte;

            u2Field: word;

            u3Field: dword;

            urField:

                record

                    u4Field: byte[2];

                    u5Field: word[3];

                endrecord;

            u6Field: byte;

        endunion;

Again, you would use the "extended dot-notation" syntax to access the fields of this union. Assuming that you have a variable u of type unionWrecord, you would access u's fields as follows:

    u.u1Field

    u.u2Field

    u.u3Field

    u.urField.u4Field

    u.urField.u5Field

    u.u6Field

Whenever a record appears within a union, the total size of that record is what HLA uses to determine the largest data object in the union for the purposes of determining the union's size. Also note that the fields of a record within a union all begin at separate offsets; it is the record object in the union that begins at the same offset as the other fields in the union. The fields of the record, however, begin at unique offsets within that record, just as for any record.

Unions also support a special field type known as an anonymous record . The following example demonstrates the syntax for an anonymous record in a union:

type

    unionWrecord:

        union

            u1Field: byte;

            u2Field: word;

            u3Field: dword;

            record

                u4Field: byte[2];

                u5Field: word[3];

            endrecord;

            u6Field: byte;

        endunion;

Fields appearing within the anonymous record do not necessarily start at offset zero in the data structure. In the example above, u4Field starts at offset zero while u5Field immediately follows it two bytes later. The fields in the union outside the anonymous record all start at offset zero. If the size of the anonymous record is larger than any other field in the union, then the record's size determines the size of the union. This is true for the example above, so the union's size is 16 bytes since the anonymous record consumes 16 bytes.

To access a field of an anonymous record in a union, you use the standard dot-notation syntax. Because the anonymous record does not have an explicit field name associated with the record, you access the fields of the record using a single-level dot notation, just like the other fields in the union. For example, if you have a variable named uwr of type unionWrecord, you'd access the fields of uwr as follows:

    uwr.u1Field

    uwr.u2Field

    uwr.u3Field

    uwr.u4Field -- a field of the anonymous record

    uwr.u5Field -- a field of the anonymous record

    uwr.u6Field

You may also declare anonymous unions within a record. An anonymous union is a union declaration without a field name associated with the union, e.g.,

type

    DemoAU:

        record

            x: real32;

            union

                u1:int32;

                r1:real32;

            endunion;

            y:real32;

        endrecord;

In this example, x, u1, r1, and y are all fields of DemoAU. To access the fields of a variable D of type DemoAU, you would use the following names: D.x, D.u1, D.r1, and D.y. Note that D.u1 and D.r1 share the same memory locations at run-time, while D.x and D.y have unique addresses associated with them.

2.2.4: Pointer Types

HLA allows you to declare a pointer to some other type using syntax like the following:

pointer to base_type

The following example demonstrates how to create a pointer to a 32-bit integer within the type declaration section:

type pi32: pointer to int32;

HLA pointers are always 32-bit pointers.

HLA also allows you to define pointers to existing procedures using syntax like the following:

procedure someProc( parameter_list );

<< procedure options, followed by @external, @forward, or procedure body>>

        .

        .

        .

type

    p : pointer to procedure someProc;

The p procedure pointer "inherits" all the parameters and other procedure options associated with the original procedure. This is really just shorthand for the following:

procedure someProc( parameter_list );

<< procedure options, followed by @external, @forward, or procedure body>>

        .

        .

        .

type

    p : procedure ( Same_Parameters_as_someProc ); <<same options as someProc>>

The former version, however, is easier to maintain since you don't have to keep the parameter lists and procedure options in sync.

Note that HLA provides the reserved word null (or NULL, reserved words are case insensitive) to represent the nil pointer. HLA replaces NULL with the value zero. The NULL pointer is compatible with any pointer type (including strings, which are pointers).

2.2.5: Thunk Types

A "thunk" is an eight-byte variable that contains a pointer to a piece of code to execute and an execution environment pointer (i.e., a pointer to an activation record). The code associated with a thunk is, essentially, a small procedure that (generally) uses the activation record of the surround code rather than creating its own activation record. HLA uses thunks to implement the iterator yield statement as well as pass by name and pass by lazy evaluation parameters. In addition to these two uses of thunks, HLA allows you to declare your own thunk objects and use them for any purpose you desire. Windows also uses the term "thunk" but the Windows meaning is different than the HLA meaning. Fortunately, most Windows thunks involve calling 16-bit code in older versions of Windows. In modern Windows systems, you use thunks much less often than in older versions of Windows.

For more information about HLA thunks, please consult the HLA Reference Manual. Thunks are mentioned here only because of the possible confusion that might exist with Windows' thunks. This book will not use HLA thunks, so there is little need to discuss this subject further.

2.2.6: Type Coercion

One HLA feature that many "established" assembly language programmers tend to have a problem with is the fact that HLA enforces a fair amount of type checking on its operands. Traditional assemblers generally treat variable names in a program as a memory address and nothing more; it is up to the programmer to choose an instruction that operates on the data type at that address. More modern assemblers, like MASM and TASM, enforce a small amount of type checking by storing the size of a memory object along with its address in the assembler's symbol table (a database where the assembler keeps track of information related to symbols during assembly). HLA takes this concept even farther. Although HLA is nowhere near as strongly typed as some high level languages (e.g., Ada), HLA does enforce type checking more strongly than most assemblers. While there are some well-established software engineering benefits associated with strong type checking, assembly programmers often need to access a object in a way that may be incompatible with its type. To achieve this in HLA, you use HLA's type coercion operator.

HLA's type coercion operator replaces the type of a memory or register operand. This syntax for this operator is

(type newtype register_or_memory_operand)

where newtype is a valid HLA type specification and register_or_memory_operand is a register name or a memory address. For register operands, the size of the new type must be compatible with the register's size; that is, if the register is an eight-bit register, then the type must be a one-byte type. This restriction does not apply to memory operands3. Here are some examples of HLA's type coercion operator:

    (type byte uns8Var)

    (type int32 eax) // Treat eax as a signed integer rather than unsigned (the default)

    (type string esi) // esi is string object

    (type recType [ebx]) // ebx is a pointer to a record of type recType

    (type recType [ebx]).someField // Access field "someField" pointed at by EBX

    (type procWith2Parms edx)(parm1, parm2); //Call procedure pointed at by edx

Note that there is a big difference between "(type string esi)" and "(type string [esi])". The string type is a pointer type, so "(type string esi)" tells HLA that ESI contains a string variable's value (that is, a pointer to character string data). The construct "(type string [esi])" tells HLA that ESI points at a string variable (which is, itself, a pointer to character string data).

The important thing to note about HLA's type coercion operator is that you can treat everything inside the parentheses as though it were are single memory object (or register object) of that particular type. Therefore, you can use the dot-operator to reference fields of records, unions, and class, you can attach a parameter list to procedure pointer types, etc. Note, by the way, that there is a difference between the following two HLA statements:

    (type procWithTwoParms edx)( parm1, parm2 );

    (type procWithTwoParms [edx])( parm1, parm2 );

The first statement above calls the procedure whose address EDX contains. The second statement calls the procedure whose address is contained in the double word at which EDX points.

HLA's machine types (byte, word, dword, qword, tbyte, and lword) are generally "type-free". The only type checking HLA does on operands of these types is to ensure that the value you store into one of these objects is the same size as the destination operand. The 80x86's integer registers, for example, have the types byte, word, and dword associated with them, so they are compatible with most (ordinal/scalar) objects that are one, two, or four bytes long. HLA's real32 type is a special case - HLA requires that you cast real32 objects to dword if you want manipulate them as a 32-bit integer/ordinal object (e.g., in an integer register).

For those who complain about all the extra type involved with using HLA's type coercion operator, note that you can use an HLA TEXT constant or macro to replace some common type coercion with a single identifier, e.g.,

const

    recEBX :text := "(type recType [ebx])";

        .

        .

        .

    mov( recEBX.field, eax ); // copies (type recType [ebx]).field into EAX

As a general rule, you want to be careful about using HLA's type coercion operator. The overuse of the coercion operator is a sure sign of bad program design, particularly if you're constantly recasting memory variables (as opposed to constructs like "[ebx]" that truly require type casting). If you really need to access an object using one of several different types, you might consider using a union type rather than the type coercion operator. Using a union type is a little bit more structured (and less typing if you pick an appropriate union name) and helps document your intent a little better.

2.3: HLA High-Level Control Structures

An assumption that this book makes is that you are reasonably familiar with HLA and already a competent assembly/HLA programmer. Because HLA's high level control structures were originally intended as a device to help beginners learn assembly language programming via HLA, you might question why a chapter on advanced HLA usage would consider these statements. Indeed, because it's probably safe to assume that Win32 assembly programming is for advanced assembly language programmers, you might argue that a discussion of HLA's high-level control structures doesn't even belong in this book - advanced assembly language programmers already know the low-level way to do control structures.

A typical assembly programmer who has learned assembly language programming via HLA typically goes through three phases in their assembly education. In phase one (beginner), the programmer uses HLA's high-level control structures as a "crutch" to leverage their existing high level language programming knowledge in order to quickly learn assembly language. In phase two, the beginning assembly language programmer discovers that these statements aren't "true" assembly language and they learn the "proper" way to write control flow in assembly language using comparisons, tests, and conditional branches. Once an HLA programmer masters the low-level control flow and gains some experience using assembly language, that programmer enters a third phase where they gain a complete understanding of how HLA converts those high-level control structures into machine instructions and the programmer realizes that sometimes it's beneficial to go ahead and use those high level control statements in order to make their programs more readable.

The vast majority of Win32 assembly code written today is written with a high level assembler and the common convention is to prefer high level control structures over the low-level "compare and branch" sequences that low-level assemblers provide. That is to say, most Win32 assembly programmers have entered that "third phase" of their assembly programming careers where they begin to make intelligent choices concerning the use of high level control structures in order to make their programs more readable.

Windows assembly language programming is sufficiently complex that any tool we can employ to reduce the effort needed to write applications in assembly, that doesn't destroy the efficiency of the resulting application, is well worth using. Tools like HLA's high level control structures (and high level procedure definitions that we'll look at in the next section) provide a tremendous boost to assembly language productivity. So their use is worth serious consideration by Windows assembly programmers.

Long-time die-hard assembly programmers may find the use of anything "high-level" troublesome, even disgusting, in code that claims to be "assembly language." There are two perceived problems with using high-level-like control structures in assembly code: inefficient compilation and inefficient paradigm.

The inefficient compilation issue concerns the fact that few assemblers (if any) provide statement-level optimization facilities such as those found in high-level language compilers (e.g., "C"). This means that high level compilers are theoretically capable of producing better code than a high level assembler because the high level language compiler will attempt to optimize the code it produces whereas no high level assembler does that (today, anyway). This isn't quite as bad as it seems, though, because 80x86 high level assemblers (including HLA) generally place sufficient restrictions on their high level control statements that enable them to generate fairly decent code. Still, do keep in mind that high level assemblers (and HLA in particular) don't optimize the code generated for high level control structures.

Of the two problems, the "inefficient paradigm" problem is probably the bigger problem of the two. Programmers who write assembly code using high level control structures often "think" in high level terms rather than in assembly terms (i.e., they write "C code using mov statements"). While this is a very real problem, this book will make the tacit assumption that a good assembly language programmer always considers the code a high-level control structure will generate and will avoid the use of such code if it leads to something that is overly inefficient.

The purpose of this section is to describe how HLA generates machine code for various high level control structures so that an assembly language programmer can make an educated decision concerning their use. While it's easy enough to get lazy and use a high level control structure where it's inappropriate, this book assumes that someone who cares about efficiency will not succumb to the temptation to overuse these control structures.

One other fact is worth pointing out concerning program efficiency and the need to optimize an assembly language sequence to squeeze out every last cycle - most Win32 programs spend the majority of their time waiting for program events. It is perfectly possible for a Win32 assembly programmer to make the application's code run ten times faster yet the user of that software won't perceive any difference in performance. This can happen because the program spends 95% of its time in the Windows kernel (or some other spot outside of the application) and all you achieve by speeding up the application code by a factor of ten is to shift 4.5% of the time that the application used to consume into Windows. The user will not notice a practical difference in the execution time of such a program improvement4. This is not to suggest that improved program efficiency isn't something to strive for; it simply means that a good software engineer carefully considers the need for optimization and weighs whether the benefits of such optimization are worth the cost (e.g., the production of less readable code by using low-level control structures in assembly language).

2.3.1: Boolean Control Expressions

A big reason why HLA generates reasonably good machine code for its high level control structures is the fact that HLA places several restrictions on the boolean expression you can use with these statements. In this section we'll review HLA's boolean expressions and then discuss how to write the most efficient forms of these expressions.

The primary limitation of HLA's if and other HLL statements has to do with the conditional expressions allowed in these statements. These expressions must take one of the following forms:

    operand1 relop operand2

    register in constant .. constant

    register not in constant .. constant

    memory in constant .. constant

    memory not in constant .. constant

    reg8 in CSet_Constant

    reg8 in CSet_Variable

    reg8 not in CSet_Constant

    reg8 not in CSet_Variable

    register

    !register

    memory

    !memory

    Flag

    ( boolean_expression )

    !( boolean_expression )

    boolean_expression && boolean_expression

    boolean_expression || boolean_expression

For the first form, "operand1 relop operand2", relop is one of:

    =  or ==					(either one, both are equivalent)

    <> or !=					(either one)

    <

    <=

    >

    >=

Operand1 and operand2 must be operands that would be legal for a "cmp(operand1, operand2);" instruction. In fact, HLA simply translates these expressions into a cmp instruction a a conditional jump instruction (the exact conditional jump instruction is chosen by the operator and the type of operands). For the if statement, HLA emits a cmp instruction with the two operands specified and an appropriate conditional jump instruction that skips over the statements following the then reserved word if the condition is false. For example, consider the following code:

if( al = 'a' ) then

    stdout.put( "Option 'a' was selected", nl );

endif;

HLA translates this to:

cmp( al, `a' );

jne skip_if;

    stdout.put( "Option 'a' was selected", nl );

skip_if:

HLA efficiently translates expressions of this form into machine code. As any decent assembly programmer knows, you'll get more efficient code if one (or both) of the operands are an 80x86 register. As the cmp instruction does not allow two memory operands, you may not compare one memory operand to another using the relational expression.

Unlike the conditional branch instructions, the six relational operators cannot differentiate between signed and unsigned comparisons (for example, HLA uses "<" for both signed and unsigned less than comparisons). Since HLA must emit different instructions for signed and unsigned comparisons, and the relational operators do not differentiate between the two, HLA must rely upon the types of the operands to determine which conditional jump instruction to emit.

By default, HLA emits unsigned conditional jump instructions (i.e., ja, jae, jb, jbe, etc.). If either (or both) operands are signed values, HLA will emit signed conditional jump instructions (i.e., jg, jge, jl, jle, etc.) instead. For example, suppose sint32 is a signed integer variable and uint32 is an unsigned 32-bit integer, consider the code that HLA will generate for the following expressions:

// sint32 < 5

    cmp( sint32, 5 );

    jnl skip;

        .

        .

        .

// uint32 < 5

    cmp( uint32, 5 );

    jnb skipu;

HLA considers the 80x86 registers to be unsigned. This can create some problems when using the HLA if statement. Consider the following code:

if( eax < 0 ) then

    << do something if eax is negative >>

endif;

Since neither operand is a signed value, HLA will emit the following code:

        cmp( eax, 0 );

        jnb SkipThenPart;

        << do something if eax is negative >>

SkipThenPart:

Unfortunately, it is never the case that the value in EAX is below zero (since zero is the minimum unsigned value), so the body of this if statement never executes. Clearly, the programmer intended to use a signed comparison here. The solution is to ensure that at least one operand is signed. However, as this example demonstrates, what happens when both operands are intrinsically unsigned?

The solution is to use coercion to tell HLA that one of the operands is a signed value. In general, it is always possible to coerce a register so that HLA treats it as a signed, rather than unsigned, value. The if statement above could be rewritten (correctly) as

if( (type int32 eax) < 0 ) then

    << do something if eax is negative >>

endif;

HLA will emit the jnl instruction (rather than jnb) in this example. Note that if either operand is signed, HLA will emit a signed condition jump instruction. Therefore, it is not necessary to coerce both unsigned operands in this example.

The second form of a conditional expression that the if statement accepts is a register or memory operand followed by "in" and then two constants separated by the ".." operator, e.g.,

    if( al in 1..10 ) then ...

This code checks to see if the first operand is in the range specified by the two constants. The constant value to the left of the ".." must be less than the constant to the right for this expression to make any sense. The result is true if the operand is within the specified range. For this instruction, HLA emits a pair of compare and conditional jump instructions to test the operand to see if it is in the specified range, e.g.,

    cmp( al, 1 );

    jb notInRange;

    cmp( al, 10 );

    ja notInRange;

Once again, HLA uses a signed or unsigned comparison based on the type of the memory or register operand present.

HLA also allows a exclusive range test specified by an expression of the form:

    if( al not in 1..10 ) then ...

In this case, the expression is true if the value in AL is outside the range 1..10. HLA generates code like the following for this expression:

    cmp( al, 1 );

    jb IsInRange;

    cmp( al, 10 );

    jb notInRange;

IsInRange:

In addition to integer ranges, HLA also lets you use the in operator with character set constants and variables. The generic form is one of the following:

        reg8 in CSetConst

        reg8 not in CSetConst

        reg8 in CSetVariable

        reg8 not in CSetVariable

For example, a statement of the form "if( al in {'a'..'z'}) then ..." checks to see if the character in the AL register is a lower case alphabetic character. Similarly,

            if( al not in {'a'..'z', 'A'..'Z'}) then...

checks to see if AL is not an alphabetic character. This form generates some particularly ugly code, so you want to be careful about using it. The code it generates looks something like the following:

    push( eax );

    movzx( al, eax );

    bt( eax, compilerGeneratedCset );

    pop( eax );

    jnc notInSet;

On top of this code, HLA also generates a 16-bit character set constant object containing the members specified by the character set constant in the boolean expression.

The fifth form of a conditional expression that the if statement accepts is a single register name (eight, sixteen, or thirty-two bits). The if statement will test the specified register to see if it is zero (false) or non-zero (true) and branches accordingly. If you specify the not operator ("!") before the register, HLA reverses the sense of this test.

The sixth form of a conditional expression that the if statement accepts is a single memory location. The type of the memory location must be boolean, byte, word, or dword. HLA will emit code that compares the specified memory location against zero (false) and generate an appropriate branch depending upon the value in the memory location. If you put the not operator ("!") before the variable, HLA reverses the sense of the test.

The seventh form of a conditional expression that the if statement accepts is a Flags register bit or other condition code combination handled by the 80x86 conditional jump instructions. The following reserved words are acceptable as if statement expressions:

@c, @nc, @o, @no, @z, @nz, @s, @ns, @a, @na, @ae, @nae, @b, @nb, @be,

@nbe, @l, @nl, @g, @ne, @le, @nle, @ge, @nge, @e, @ne

These items emit an appropriate jump (of the opposite sense) around the then portion of the if statement if the condition is false.

If you supply any legal boolean expression in parentheses, HLA simply uses the value of the internal expression for the value of the whole expression. This allows you to override default precedence for the &&, ||, and ! operators.

The !( boolean_expression ) evaluates the expression and does just the opposite. That is, if the interior expression is false, then !( boolean_expression ) is true and vice versa. This is mainly useful with conjunction and disjunction since all of the other interesting terms already provide a logically negated form. Note that in general, the "!" operator must precede some parentheses. You cannot say "! AX < BX", for example.

Originally, HLA did not include support for the conjunction (&&) and disjunction (||) operators. This was explicitly left out of the design so that beginning students would be forced to rethink their logical operations in assembly language. Unfortunately, it was so inconvenient not to have these operators that they were eventually added.

The conjunction and disjunction operators are the operators && and ||. They expect two valid HLA boolean expressions around the operator, e.g.,

    eax < 5 && ebx <> ecx

Since the above forms a valid boolean expression, it, too, may appear on either side of the && or || operator, e.g.,

    eax < 5 && ebx <> ecx || !dl

HLA gives && higher precedence than ||. Both operators are left-associative so if multiple operators appear within the same expression, they are evaluated from left to right if the operators have the same precedence. Note that you can use parentheses to override HLA's default precedence.

When generating code for these expressions, HLA employs short-circuit evaluation. Short-circuit evaluation means that HLA can generate code that uses control flow to maintain the current state (true or false) of the boolean expression rather than having to save the result somewhere (e.g., in a register). Because HLA promises not to disturb register values in its HLL-like code, short-circuit evaluation is the best way to proceed (it won't require pushing and popping registers as was necessary in the character set test earlier). For the current example, HLA would probably generate code like the following:

//	eax < 5 && ebx <> ecx || !dl

    cmp( eax, 5 );

    jnb tryDL;

    cmp( ebx, ecx );

    jne isTrue;

tryDL:

    test( dl, dl );

    jnz isFalse;

isTrue:

One wrinkle with the addition of && and || is that you need to be careful when using the flags in a boolean expression. For example, "eax < ecx && @nz" hides the fact that HLA emits a compare instruction that affects the Z flag. Hence, the "@nz" adds nothing to this expression since EAX must not equal ECX if eax<ecx. So take care when using && and ||.

If you would prefer to use a less abstract scheme to evaluate boolean expressions, one that lets you see the low-level machine instructions, HLA provides a solution that allows you to write code to evaluate complex boolean expressions within the HLL statements using low-level instructions. Consider the following syntax:

if

(#{

    <<arbitrary HLA statements >>

}#) then

    << "True" section >>

else //or elseif...

    << "False" section >>

endif;

The "#{" and "}#" brackets tell HLA that an arbitrary set of HLA statements will appear between the braces. HLA will not emit any code for the if expression. Instead, it is the programmer's responsibility to provide the appropriate test code within the "#{---}#" section. Within the sequence, HLA allows the use of the boolean constants "true" and "false" as targets of conditional jump instructions. Jumping to the "true" label transfers control to the true section (i.e., the code after the then reserved word). Jumping to the "false" label transfers control to the false section. Consider the following code that checks to see if the character in AL is in the range "a".."z":

    if

    (#{

        cmp( al, 'a' );

        jb false;

        cmp( al, 'z' );

        ja false;

    }#) then

        << code to execute if AL in {'a'..'z'} goes here >>

    endif;

With the inclusion of the #{---}# operand, the if statement becomes much more powerful, allowing you to test any condition possible in assembly language. Of course, the #{---}# expression is legal in the elseif expression as well as the if expression.

2.3.2: The HLA IF..ENDIF Statement

HLA provides a limited if..then..elseif..else..endif statement that can help make your programs easier to read. For the most part, HLA's if statement provides a convenient substitute for a cmp and a conditional branch instruction pair (or chain of such instructions when employing elseif's).

The generic syntax for the HLA if statement is the following:

if( conditional_expression ) then

    << Statements to execute if expression is true >>

endif;

if( conditional_expression ) then

    << Statements to execute if expression is true >>

else

    << Statements to execute if expression is false >>

endif;

if( expr1 ) then

    << Statements to execute if expr1 is true >>

elseif( expr2 ) then

    << Statements to execute if expr1 is false

         and expr2 is true >>

endif;

if( expr1 ) then

    << Statements to execute if expr1 is true >>

elseif( expr2 ) then

    << Statements to execute if expr1 is false

         and expr2 is true >>

else

    << Statements to execute if both expr1 and

         expr2 are false >>

endif;

Note: HLA's if statement allows multiple elseif clauses. All elseif clauses must appear between if clause and the else clause (if present) or the endif (if an else clause is not present).

For simple boolean expressions (see the previous section) HLA generally emits code that is comparable to what an expert assembly language programmer would generate for these statements. There are, however, two issues to keep in mind when using HLA's high-level if..elseif..else..endif control structures: the cost of branching and inefficiencies associated with nested if statements.

High level control structures abstract away the low-level machine code implementation. Most of the time, this is a good thing. The whole purpose of the high level control structures is to hide what's going on underneath and make the code easier to read and understand. Sometimes, however, this abstraction can hide inefficiencies. Consider the following HLA if statement and its low-level implementation:

    if( eax < ebx ) then

        << do this if eax < ebx >>

    else

        << do this if eax >= ebx >>

    endif;

// Low-level equivalent:

    cmp( eax, ebx );

    jnb eaxGEebx;

        << do this if eax < ebx >>

        jmp ifIsDone;

eaxGEebx:

        << do this if eax >= ebx >>

ifIsDone:

This looks relatively straight-forward and simple. Probably the code that most people would have written if HLA's high-level if statement was not available. By carefully studying this code, you will note that there are two execution paths in this code, one path taken if the expression evaluates true and one path taken if the expression evaluates false. There is a transfer of control instruction that is taken along either path (the jmp instruction along the true path and the jnb instruction along the false path). Because jump instructions can execute slowly on modern CPU architectures, this particular code sequence may run slower than necessary because a jump is always taken.

An expert assembly programmer might consider the frequency with which the boolean expression in this if statement evaluates true or false. If the result of this boolean expression is random (that is, there is a 50/50 chance it will be true or false) then there is little you can do to improve the performance. However, in most if statements like this one, it's usually the case that one value occurs more frequently than the other (often, a boolean expression tends to evaluate true more often than false because most programmers tend to put the most common case in the true section of an if..else..endif statement). If that is the case, and the if statement is sitting in some section of time-critical code, an expert assembly programmer will often convert this if statement to something like the following:

    cmp( eax, ebx );

    jnb eaxGEebx;

        << do this if eax < ebx >>

returnHere:

        .

        .

        .

// Somewhere convenient:

eaxGEebx:

    << do this if eax >=ebx >>

    jmp returnHere;

By moving the else section to some other point in the code (presumably, after a control transfer instruction) this code provides a "straight-through" path (i.e., no jmp) for the "true" section of the code. The else section, on the other hand, now has to execute two jump instructions (the condition jump and the jmp at the end of the else sequence). However, if the else section executes less frequently than the true section of the if statement, this code sequence will run a little faster on many CPUs. One problem with the high-level if..else..endif statement is that it obscures the presence of that jmp instruction that transfers control over the else section. So you need to remember this fact if you're writing some highly time-critical code. Most of the time, this type of change ("spaghetti programming") will not make a noticeable difference in the execution time of your program. Under the right circumstances, however, it will make a difference.

In general, any single instance of an inefficiency like these two is going to have a negligible impact on the performance or size of your programs. However, keep in mind that if you write assembly code using a high level language mindset, inefficiencies like this will begin to creep in all over the place and may begin to have an impact on the execution speed of your applications. Therefore, you should carefully consider each high level control statement you use in HLA to verify that HLA will be able to generate reasonable code for the sequence you're supplying.

2.3.3: The HLA WHILE..ENDWHILE Statement

The while..endwhile statement allows the following syntax:

while( boolean_expression ) do

    << while loop body>>

endwhile;

while( boolean_expression ) do

    << while loop body>>

welse

    << Code to execute when boolean_expression is false >>

endwhile;

while(#{ HLA_statements }#) do

    << while loop body>>

endwhile;

while(#{ HLA_statements }#) do

    << while loop body>>

welse

    << Code to execute when expression is false >>

endwhile;

The while statement allows the same boolean expressions as the HLA if statement. Like the HLA if statement, HLA allows you to use the boolean constants "true" and "false" as labels in the #{...}# form of the while statement above. Jumping to the true label executes the body of the while loop, jumping to the false label exits the while loop.

For the "while( expr ) do" forms, HLA moves the test for loop termination to the bottom of the loop and emits a jump at the top of the loop to transfer control to the termination test. For example, consider the following while statement:

    while( eax < ebx ) do

        << statements to execute while eax < ebx >>

    endwhile;

Typical "hand" conversion of this while loop:

whlLabel:

        cmp( eax, ebx );

        jnb endWhileLabel;

            << statements to execute while eax < ebx >>

        jmp whlLabel;

endWhileLabel:

Here's the code that HLA actually emits for this while loop:

        jmp doWhile;

whlLabel:

            << statements to execute while eax < ebx >>

doWhile:

        cmp( eax, ebx );

        jb whlLabel;

The advantage of rotating the test to the bottom of the loop is that it eliminates a jump out of the body's path. That is, each iteration of this while loop executes one less jmp instruction than the typical conversion. As long as the loop's body executes one or more times (on the average), this conversion scheme is slightly more efficient. Note, however, that if the loop body doesn't execute the majority of the time, then the former conversion runs slightly faster.

For the "while(#{stmts}#)" form, HLA compiles the termination test at the top of the emitted code for the loop (i.e., the standard conversion). Therefore, the standard while loop may be slightly more efficient (in the typical case) than the hybrid form; just something to keep in mind.

The HLA while loop supports an optional welse (while-else) section. The while loop will execute the code in this section only when then the expression evaluates false. Note that if you exit the loop via a "break" or "breakif" statement the welse section does not execute. This provides logic that is sometimes useful when you want to do something different depending upon whether you exit the loop via the expression going false or by a break statement. Here's a quick example that demonstrates how HLA translates a while..welse..endwhile statement into low-level code:

    while( eax < ebx ) do

        << statements to execute while eax < ebx >>

        breakif( ecx = 0 );

        << more statements to execute >>

    welse

        << statements to execute when the boolean expression evaluates false >>

    endwhile;

// Typical HLA conversion to low-level code:

        jmp whlExpr;

whlLoop:

            << statements to execute while eax < ebx >>

            // breakif conversion:

            test( ecx, ecx );

            jz whileDone;

            << more statements to execute >>

whlExpr:

        cmp( eax, ebx );

        jb whlLoop;

// code for the welse section:

        << statements to execute when the boolean expression evaluates false >>

whileDone:

2.3.4: The HLA REPEAT..UNTIL Statement

HLA's repeat..until statement uses the following syntax:

repeat

    << statements to execute repeatedly >>

until( boolean_expression );

repeat

    << statements to execute repeatedly >>

until(#{ HLA_statements }#);

The body of the loop always executes at least once and the test for loop termination occurs at the bottom of the loop. The repeat..until loop (unlike C/C++'s do..while statement) terminates loop execution when the expression is true (that is, repeat..until repeats while the expression is false).

As you can see, the syntax for this is very similar to the while loop. About the only major difference is the fact that jump to the "true" label in the #{---}# sequence exits the loop while jumping to the "false" label in the #{---}# sequence transfers control back to the top of the loop. Also note that there is no equivalent to the welse clause in a repeat..until loop.

If you take away the jmp that HLA emits at the top of a (standard) while loop, you'll have a pretty good idea of the type of code that HLA generates for a repeat..until loop.

As a general rule, repeat..until loops are slightly more efficient than while loops because they don't execute that extra jmp instruction. Otherwise, the performance characteristics are virtually identical to that for the while loop.

2.3.5: The HLA FOR Loops

The HLA for..endfor statement is very similar to the C/C++ for loop. The for clause consists of three components:

for( initialize_stmt; boolean_expression; increment_statement ) do

The initialize_statement component is a single machine instruction. This instruction typically initializes a loop control variable. HLA emits this statement before the loop body so that it executes only once, before the test for loop termination.

The boolean_expression component is a simple boolean expression. This expression determines whether the loop body executes. Note that the for statement tests for loop termination before executing the body of the loop, just like the while statement (though the compiler will move this test to the bottom of the loop for efficiency reasons, the semantics are the same as though the test were physically at the beginning of the loop).

The increment_statement component is a single machine instruction that HLA emits at the bottom of the loop, just before jumping back to the top of the loop. This instruction is typically used to modify the loop control variable.

The syntax for the HLA for statement is the following:

for( initStmt; BoolExpr; incStmt ) do

    << loop body >>

endfor;

-or-

for( initStmt; BoolExpr; incStmt ) do

    << loop body >>

felse

    << statements to execute when BoolExpr evaluates false >>

endfor;

Semantically, this statement is identical to the following while loop:

initStmt;

while( BoolExpr ) do

    << loop body >>

    incStmt;

endwhile;

-or-

initStmt;

while( BoolExpr ) do

    << loop body >>

    incStmt;

welse

    << statements to execute when BoolExpr evaluates false >>

endwhile;

Note that HLA does not include a form of the for loop that lets you bury a sequence of statements inside the boolean expression. Use the while loop if you want to do that. If this is inconvenient, you can always create your own version of the for loop using HLA's macro facilities.

The felse section in the for..felse..endfor loop executes when the boolean expression evaluates false. Note that the felse section does not execute if you break out of the for loop with a break or breakif statement. You can use this fact to do different logic depending on whether the code exits the loop via the boolean expression going false or via some sort of break.

The code that HLA generates for the for loop is identical to the code that HLA generates for the converted while example. Therefore, efficiency concerns about the for loop are identical to those for the while loop.

HLA's forever..endfor loop creates an infinite loop5. The endfor efficiently compiles into a single jmp instruction that transfers control to the top of the loop (i.e., to the forever clause). Typically, you'd use a break or breakif statement to exit a forever..endfor at some point in the middle of the loop. This makes forever..endfor loops slightly less efficient that a standard while..endwhile or repeat..until loop because you'll always execute two jump instructions in such a loop - one that breaks out of the loop and one at the bottom of the loop that transfers control back to the top of the loop. For example, consider the following:

    forever

        << Code to execute before the test >>

        breakif( eax = ecx );

        << Code to execute after the test >>

    endfor;

// conversion to "pure" assembly language

    forLabel:

        << Code to execute before the test >>

        cmp( eax, ecx );

        jne exitFor;

        << Code to execute after the test >>

    jmp forLabel;

    exitFor:

An experienced assembly language programmer would probably rewrite this as a loop that tests for loop termination at the bottom of the loop using code like the following:

// conversion to slightly more efficient assembly language

    jmp IntoLoop

    forLabel:

        << Code to execute after the test >>

    IntoLoop:

        << Code to execute before the test >>

        cmp( eax, ecx );

        je forLabel;

HLA provides a third for loop, the foreach loop, that lets you create user-defined loops via HLA's iterator facility. We're not going to discuss the code generation for the foreach loop here, but be aware that HLA generates some fairly sophisticated code for the foreach loop and the corresponding iterator. Though the code is not particularly inefficient, you should note that using a foreach loop where a simple for or while loop will suffice is always going to be less efficient. Please see the HLA documentation for more details concerning the use of iterators and the foreach statement.

2.3.6: HLA's BREAK, CONTINUE, and EXIT Statements

The break and breakif statements allow you to exit a loop at some point other than the normal test for loop termination. These two statements allow the following syntax:

break;

breakif( boolean_expression );

breakif(#{ stmts }#);

The continue and continueif statements allow you to restart a loop. These two statements allow the following syntax:

continue;

continueif( boolean_expression );

continueif(#{ stmts }#);

"Restarting a loop" means jumping to the loop termination test for while, repeat..until, and for loops, it means jumping to the (physical) top of the loop for forever and foreach loops.

Note that the break, breakif, continue, and continueif statements are legal only inside while, for, forever, foreach, and repeat loops. HLA does not recognize loops you've coded yourself using discrete assembly language instructions (of course, you can probably write a macro to provide a break function for your own loops). Note that the foreach loop pushes data on the stack that the break statement is unaware of. Therefore, if you break out of a foreach loop, garbage will be left on the stack. The HLA break statement will issue a warning if this occurs. It is your responsibility to clean up the stack upon exiting a foreach loop if you break out of it. Using a continue within a foreach loop is perfectly fine.

The begin..end statement block provides a structured goto statement for HLA. The begin and end clauses surround a group of statements; the exit and exitif statements allow you to exit such a block of statements in much the same way that the break and breakif statements allow you to exit a loop. Unlike break and breakif, which can only exit the loop that immediately contains the break or breakif, the exit statements allow you to specify a begin label so you can exit several nested contexts at once. The syntax for the begin..end, exit, and exitif statements is as follows:

begin contextLabel ;

    << statements within the specified context >>

end contextLabel;

exit contextLabel;

exitif( boolean_expression ) contextLabel;

exitif(#{ stmts }#) contextLabel;

The begin..end clauses do not generate any machine code (although end does emit a label to the assembly output file). The exit statement simply emits a jmp to the first instruction following the end clause. The exitif statement emits a compare and a conditional jump to the statement following the specified end.

If you break out of a foreach loop using the exit or exitif statements, there will be garbage left on the stack. It is your responsibility to be aware of this situation (i.e., HLA doesn't warn you about it) and clean up the stack, if necessary.

You can nest begin..end blocks and exit out of any enclosing begin..end block at any time. The begin label provides this capability. Consider the following example:

program ContextDemo;

#include( "stdio.hhf" );

static

    i:int32;

begin ContextDemo;

    stdout.put( "Enter an integer:" );

    stdin.get( i );

    begin c1;

        begin c2;

            stdout.put( "Inside c2" nl );

            exitif( i < 0 ) c1;

        end c2;

        stdout.put( "Inside c1" nl );

        exitif( i = 0 ) c1;

        stdout.put( "Still inside c1" nl );

    end c1;

    stdout.put( "Outside of c1" nl );

end ContextDemo;

The exit and exitif statements let you exit any begin..end block; including those associated with a program unit such as a procedure, iterator, method, or even the main program. Consider the following (unusable) program:

program mainPgm;

    procedure LexLevel1;

        procedure LexLevel2;

        begin LexLevel2;

            exit LexLevel2;						// Returns from this procedure.

            exit LexLevel1;						// Returns from this procedure and

                                    //  and the LexLevel1 procedure

                                    //  (including cleaning up the stack).

            exit mainPgm;						// Terminates the main program.

        end LexLevel2;

    begin LexLevel1;

        .

        .

        .

    end LexLevel1;

begin mainPgm;

    .

    .

    .

end mainPgm;

Note: You may only exit from procedures that have a display and all nested procedures from the procedure you wish to exit from through to the exit statement itself must have displays. In the example above, both LexLevel1 and LexLevel2 must have displays if you wish to exit from the LexLevel1 procedure from inside LexLevel2. By default, HLA emits code to build the display unless you use the "@nodisplay" procedure option.

Note that to exit from the current procedure, you must not have specified the "@noframe" procedure option. This applies only to the current procedure. You may exit from nesting (lower lex level) procedures as long as the display has been built. For more information on displays and stack frames, see the upcoming section on procedures and procedure invocations.

2.3.7: Exception Handling Statements in HLA

HLA's exception handling system is an interesting example for those seeking to write efficient code. While using HLA's try..endtry and raise statements is quite a bit more efficient than manually checking for problems in your code, it's also the case that these statements generate a fair number of instructions and their overuse can lead to the creation of some inefficient code. Therefore, it's important to understand the code that HLA generates for these statements so you can write code efficiently by using these statements properly.

HLA uses the try..exception..endtry and raise statements to implement exception handling. The syntax for these statements is as follows:

try

    << HLA Statements to execute >>

<<  unprotected // Optional unprotected section.

    << HLA Statements to execute >>

>>

exception( const1 )

    << Statements to execute if exception const1 is raised >>

<< optional exception statements for other exceptions >>

<<  anyexception //Optional anyexception section.

    << HLA Statements to execute >>

>>

endtry;

raise( const2 );

Const1 and const2 must be unsigned integer constants. Usually, these are values defined in the excepts.hhf header file. Some examples of predefined values include the following:

ex.StringOverflow

ex.StringIndexError

ex.ValueOutOfRange

ex.IllegalChar

ex.ConversionError

ex.BadFileHandle

ex.FileOpenFailure

ex.FileCloseError

ex.FileWriteError

ex.FileReadError

ex.DiskFullError

ex.EndOfFile

ex.MemoryAllocationFailure

ex.AttemptToDerefNULL

ex.WidthTooBig

ex.TooManyCmdLnParms

ex.ArrayShapeViolation

ex.ArrayBounds

ex.InvalidDate

ex.InvalidDateFormat

ex.TimeOverflow

ex.AssertionFailed

ex.ExecutedAbstract

Windows Structured Exception Handler exception values:

ex.AccessViolation

ex.Breakpoint

ex.SingleStep

ex.PrivInstr

ex.IllegalInstr

ex.BoundInstr

ex.IntoInstr

ex.DivideError

ex.fDenormal

ex.fDivByZero

ex.fInexactResult

ex.fInvalidOperation

ex.fOverflow

ex.fStackCheck

ex.fUnderflow

ex.InvalidHandle

ex.StackOverflow

ex.ControlC

This list is constantly changing as the HLA Standard Library grows, so it is impossible to provide a compete list of standard exceptions here. Please see the excepts.hhf header file for a complete list of standard exceptions.

The HLA Standard Library currently reserves exception numbers zero through 1023 for its own internal use. User-defined exceptions should use an integer value greater than or equal to 1024 and less than or equal to 65535 ($FFFF). Exception value $10000 and above are reserved for use by Windows Structured Exception Handler and Linux signals.

The try..endtry statement contains two or more blocks of statements. The statements to protect immediately follow the try reserved word. During the execution of the protected statements, if the program encounters the first exception block, control immediately transfers to the first statement following the endtry reserved word. The program will skip all the statements in the exception blocks.

If an exception occurs during the execution of the protected block, control is immediate transferred to an exception handling block that begins with the exception reserved word and the constant that specifies the type of exception.

Example:

repeat

    mov( false, GoodInput );

    try

        stdout.put( "Enter an integer value:" );

        stdin.get( i );

        mov( true, GoodInput );

    exception( ex.ValueOutOfRange )

        stdout.put( "Numeric overflow, please reenter ", nl );

    exception( ex.ConversionError )

        stdout.put( "Conversion error, please reenter", nl );

    endtry;

until( GoodInput = true );

In this code, the program will repeatedly request the input of an integer value as long as the user enters a value that is out of range (+/- 2 billion) or as long as the user enters a value containing illegal characters.

Multiple try..endtry statements can be nested. If an exception occurs within a nested try protected block, the exception blocks in the innermost try block containing the offending statement get first shot at the exceptions. If none of the exception blocks in the enclosing try..endtry statement handle the specified exception, then the next innermost try..endtry block gets a crack at the exception. This process continues until some exception block handles the exception or there are no more try..endtry statements.

If an exception goes unhandled, the HLA run-time system will handle it by printing an appropriate error message and aborting the program. Generally, this consists of printing "Unhandled Exception" (or a similar message) and stopping the program. If you include the excepts.hhf header file in your main program, then HLA will automatically link in a somewhat better default exception handler that will print the number (and name, if known) of the exception before stopping the program.

Note that try..endtry blocks are dynamically nested, not statically nested. That is, a program must actually execute the try in order to activate the exception handler. You should never jump into the middle of a protected block, skipping over the try. Doing so may produce unpredictable results.

You should not use the try..endtry statement as a general control structure. For example, it will probably occur to someone that one could easily create a switch/case selection statement using try..endtry as follows:

try

    raise( SomeValue );

    exception( case1_const)

        <code for case 1>

    exception( case2_const)

        <code for case 2>

    etc.

endtry

While this might work in some situations, there are two problems with this code.

First, if an exception occurs while using the try..endtry statement as a switch statement, the results may be unpredictable. Second, HLA's run-time system assumes that exceptions are rare events. Therefore, the code generated for the exception handlers doesn't have to be efficient. You will get much better results implementing a switch/case statement using a table lookup and indirect jump (see the Art of Assembly) rather than a try..endtry block.

Warning: As you'll see in a moment, the try statement pushes data onto the stack upon initial entry and pops data off the stack upon leaving the try..endtry block. Therefore, jumping into or out of a try..endtry block is an absolute no-no. As explained so far, then, there are only two reasonable ways to exit a try statement, by falling off the end of the protected block or by an exception (handled by the try statement or a surrounding try statement).

The unprotected clause in the try..endtry statement provides a safe way to exit a try..endtry block without raising an exception or executing all the statements in the protected portion of the try..endtry statement. An unprotected section is a sequence of statements, between the protected block and the first exception handler, that begins with the keyword unprotected. E.g.,

try

    << Protected HLA Statements >>

  unprotected

    << Unprotected HLA Statements >>

  exception( SomeExceptionID )

    << etc. >>

endtry;

control flows from the protected block directly into the unprotected block as though the unprotected keyword were not present. However, between the two blocks HLA compiler-generated code removes the data pushed on the stack. Therefore, it is safe to transfer control to some spot outside the try..endtry statement from within the unprotected section.

If an exception occurs in an unprotected section, the try..endtry statement containing that section does not handle the exception. Instead, control transfers to the (dynamically) nesting try..endtry statement (or to the HLA run-time system if there is no enclosing try..endtry).

If you're wondering why the unprotected section is necessary (after all, why not simply put the statements in the unprotected section after the endtry?), just keep in mind that both the protected sequence and the handled exceptions continue execution after the endtry. There may be some operations you want to perform after exceptions are released, but only if the protected block finished successfully. The unprotected section provides this capability. Perhaps the most common use of the unprotected section is to break out of a loop that repeats a try..endtry block until it executes without an exception occurring. The following code demonstrates this use:

forever

    try

            stdout.put( "Enter an integer: " );

            stdin.geti8();   // May raise an exception.

        unprotected

            break;

        exception( ex.ValueOutOfRange )

            stdout.put( "Value was out of range, reenter" nl );

        exception( ex.ConversionError )

            stdout.put( "Value contained illegal chars" nl );

    endtry;

endfor;

This simple example repeatedly asks the user to input an int8 integer until the value is legal and within the range of valid integers.

Another clause in the try..except statement is the anyexception clause. If this clause is present, it must be the last clause in the try..except statement, e.g.,

try

    << protected statements >>

<<

    unprotected

        Optional unprotected statements

>>

<<	exception( constant ) // Note: may be zero or more of

                             of these.

        Optional exception handler statements

>>

    anyexception

        << Exception handler if none of the others execute >>

endtry;

Without the anyexception clause present, if the program raises an exception that is not specifically handled by one of the exception clauses, control transfers to the enclosing try..endtry statement. The anyexception clause gives a try..endtry statement the opportunity to handle any exception, even those that are not explicitly listed. Upon entry into the anyexception block, the EAX register contains the actual exception number.

The HLA raise statement generates an exception. The single parameter is an 8, 16, or 32-bit ordinal constant. Control is (ultimately) transferred to the first (most deeply nested) try..endtry statement that has a corresponding exception handler (including anyexception).

If the program executes the raise statement within the protected block of a try..endtry statement, then the enclosing try..endtry gets first shot at handling the exception. If the raise statement occurs in an unprotected block, or in an exception handler (including anyexception), then the next higher level (nesting) try..endtry statement will handle the exception. This allows cascading exceptions; that is, exceptions that the system handles in two or more exception handlers. Consider the following example:

try

    << Protected statements >>

  exception( someException )

    << Code to process this exception >>

    // The following re-raises this exception, allowing

    // an enclosing try..endtry statement to handle

    // this exception as well as this handler.

    raise( someException );

  << Additional, optional, exception handlers >>

endtry;

To understand the cost of using HLA's exception handling statements, it's wise to take a look at the code the compiler generates for these statements. Consider the following HLA code and it's conversion to "pure" assembly language:

    try

        << Protected Code >>

    exception( ex.ValueOutOfRange )

        << Code to execute if system raises "out of range" exception >>

    exception( ex.StringIndexError )

        << Code to execute if there is a string index exception >>

    endtry;

// "Pure" assembly code HLA produces for the above statements:

    push( &exceptionLabel );

    push( ebp );

    mov( ExceptionPtr__hla_, ebp ); // This is a global symbol HLA defines

    push( &HWexcept__hla_ );        //  "   "   " "  "   "  "   "   "    "

    mov( esp, ExceptionPtr__hla_ );

        << Protected Code >>

    mov( ExceptionPtr__hla_, esp ); // Code that cleans up the stack after

    pop( ExceptionPtr__hla_ );      //  executing the protected code.

    add( 8, esp );

    pop( ebp );

    add( 4, esp );

    jmp endTryLabel;

exceptionLabel:

    cmp( eax, 3 ); // ex.ValueOutOfRange = 3

    jne tryStrIndexEx

        << Code to execute if system raises "out of range" exception >>

    jmp endTryLabel;

tryStrIndexEx:

    cmp( eax, 2 ); // ex.StringIndexError = 2

    jne Raise__hla_; // Re-raises exception in surrounding try..endtry block.

endTryLabel:

As you can see by reading through this code, HLA emits five instructions upon encountering the "try" clause and emits six instructions after the protected block (i.e., before the first exception statement). Assuming exceptions don't occur very frequently (that is, they are the exceptions rather than the rule), most of the other statements that HLA emits will not execute frequently. These 11 statements before and after the protected code, however, execute everything the program encounters and leaves a try..endtry block. While 11 statements isn't a horrendous number, if you've only got a few statements within the try..endtry statement and you execute this sequence within a loop, this 11 statements can have an impact on your program's performance.

If the performance of the try..endtry statement is an issue, and you would still like to protect a sequence of statements with try..endtry, you can reduce the overhead by placing more statements inside the try..endtry protected region. For example, if you've got a try..endtry block inside a loop you'll have to execute that try..endtry sequence on each iteration of the loop. However, if you place these statements around the loop, you only have to execute the 11 statements that the try..endtry block generates once for each execution of the loop. The drawback to "widening the scope" of the try..endtry statement is that it becomes more difficult to pinpoint the cause of the exception to just a few statements. For example, if you're reading a set of values into an array within a loop and you place the try..endtry block around the loop, you may not be able to determine which iteration of the loop triggered the exception (note that you cannot count on the registers containing reasonable values whenever someone raises an exception, so if your loop index is in a register, it's lost).

The big problem with exceptions is that they shouldn't occur under normal circumstances; therefore, the execution of those 11 statements is pure overhead, most of the time. Therefore, you want to be judicious with your use of the try..endtry block, especially in nested loops and other code sequences that execute frequently. It goes without saying that you should not use exception handling as a normal flow control sequence (that is, you shouldn't attempt to use the try..endtry block as a fancy form of a switch/case statement). The 11 statement overhead applies to code sequences that execute normally, without raising any exceptions. Processing an exception can require several dozen (or more) instructions. Normally, the overhead associated with processing an actual exception isn't an issue in most applications because exceptions rarely occur. However, if you attempt to use exceptions as a normal execution path, you'll probably be disappointed with the performance (note that the code that executes when an exception occurs is part of the HLA run-time system, it doesn't appear in the code that HLA emits for the try..endtry block shown earlier).

2.4: HLA Procedure Declarations and Invocations

HLA provides a very sophisticated system for declaring and calling procedures using a high level syntax. Indeed, HLA's procedure syntax, in many ways, is higher-level than most high level programming languages. We aren't going to get into all the gory details here because, quite frankly, all these features aren't necessary for writing Win32 applications (because C doesn't support these features). Nevertheless, HLA does have many useful features you won't normally find in an assembly language that help make writing Win32 assembly code much easier. We'll discuss those features in this section.

The general syntax for an HLA procedure declaration is:

procedure identifier ( optional_parameter_list );  procedure_options

    declarations

begin identifier;

    statements

end identifier;

The optional parameter list consists of a list of var-type declarations taking the form:

    optional_access_keyword  identifier1 : identifier2 optional_in_reg

optional_access_keyword, if present, must be val, var, valres, result, name, or lazy and defines the parameter passing mechanism (pass by value, pass by reference, pass by value/result [or value/returned], pass by result, pass by name, or pass by lazy evaluation, respectively). The default is pass by value (val) if an access keyword is not present. For pass by value parameters, HLA allocates the specified number of bytes according to the size of that object in the activation record. For pass by reference, pass by value/result, and pass by result, HLA allocates four bytes to hold a pointer to the object. For pass by name and pass by lazy evaluation, HLA allocates eight bytes to hold a pointer to the associated thunk and a pointer to the thunk's execution environment. Because Win32 applications typically only use pass by value and pass by reference, those are the two parameter passing mechanisms we'll discuss in this book. For details on the other HLA parameter passing mechanisms, please consult the HLA Reference Manual.

The optional_in_reg clause, if present, corresponds to the phrase "in reg" where reg is one of the 80x86's general purpose 8-, 16-, or 32-bit registers. This clause tells HLA to pass the parameter in the specified register rather than on the stack.

HLA also allows a special parameter of the form:

    var identifier : var

This creates an untyped reference parameter. You may specify any memory variable as the corresponding actual parameter and HLA will compute the address of that object and pass it on to the procedure without further type checking. Within the procedure, the parameter is given the dword type.

The procedure_options component above is a list of keywords that specify how HLA emits code for the procedure. There are several different procedure options available:@noalignstack, @alignstack, @pascal, @stdcall, @cdecl, @align( int_const), @use reg32, @leave, @noleave, @enter, @noenter, and @returns( "text" ). See Table 2-2 for a description of each of these procedure options.

Table 2-2: Procedure Options in an HLA Program

@noframe,
@frame
By default, HLA emits code at the beginning of the procedure to construct a stack frame. The @noframe option disables this action. The @frame option tells HLA to emit code for a particular procedure if stack frame generation is off by default. HLA also uses these two special identifiers as a compile-time variable to set the default display generation for all procedures. Setting @frame to true (or @noframe to false) turns on frame generation by default; setting @frame to false (or @noframe to true) turns off frame generation.
@nodisplay,
@display
By default, HLA emits code at the beginning of the procedure to construct a display within the frame. The @nodisplay option disables this action. The @display option tells HLA to emit code to generate a display for a particular procedure if display generation is off by default. Note that HLA does not emit code to construct the display if '@noframe' is in effect, though it will assume that the programmer will construct this display themselves. HLA also uses these two special identifiers as a compile-time variable to set the default display generation for all procedures. Setting @display to true (or @nodisplay to false) turns on display generation by default; setting @display to false (or @nodisplay to true) turns off display generation. Because most Win32 applications do not typically use nested procedures (and, in particular, none of the examples in this book use them), you'll usually see the following statement near the top of most example programs appearing in this book:

?nodisplay := true;

@noalignstack,
@alignstack
By default (assuming frame generation is active), HLA will an instruction to align ESP on a four-byte boundary after allocating local variables. Win32, Linux, and other 32-bit OSes require the stack to be dword-aligned (hence this option). If you know the stack will be dword-aligned, you can eliminate this extra instruction by specifying the @noalignstack option. Conversely, you can force the generation of this instruction by specifying the @alignstack procedure option. HLA also uses these two special identifiers as a compile-time variable to set the default display generation for all procedures. Setting @alignstack to true (or @noalignstack to false) turns on stack alignment generation by default; setting @alignstack to false (or @noalignstack to true) turns off stack alignment code generation. Because Windows guarantees that the stack will be aligned when it transfers control to a procedure in your application, and because programs in this book never force a misalignment of the stack, you'll commonly see the following statement near the beginning of the HLA example programs in this book:

?@noalignstack := true;
@pascal,
@cdecl,
@stdcall
These options give you the ability to specify the parameter passing mechanism for the procedure. By default, HLA uses the @pascal calling sequence. This calling sequence pushes the parameters on the stack in a left-to-right order (i.e., in the order they appear in the parameter list). The @cdecl procedure option tells HLA to pass the parameters from right-to-left so that the first parameter appears at the lowest address in memory and that it is the user's responsibility to remove the parameters from the stack. The @stdcall procedure option is a hybrid of the @pascal and @cdecl calling conventions. It pushes the parameters in the right-to-left order (just like @cdecl) but @stdcall procedures automatically remove their parameter data from the stack (just like @pascal). Win32 API calls use the @stdcall calling convention.

In the Win32 examples appearing in this book, you'll see the following conventions adhered to:

  • All Win32 API functions use the @stdcall calling convention (there are some Windows API functions that require the @cdecl calling convention, but this book does not call those routines).
  • All functions in the application that Windows calls (the "window procedure" and any "callback functions") use the @stdcall calling convention.
  • Local procedures in the program (those that only HLA code calls) use the @pascal calling convention.
@align( int_constant )
The @align( int_const ) procedure option aligns the procedure on a 1, 2, 4, 8, or 16 byte boundary. Specify the boundary you desire as the parameter to this option. The default is @align(1), which is unaligned; HLA also uses this special identifier as a compile-time variable to set the default procedure alignment . Setting @align := 1 turns off procedure alignment while supplying some other value (which must be a power of two) sets the default procedure alignment to the specified number of bytes.
@use reg32
When passing parameters, HLA can sometimes generate better code if it has a 32-bit general purpose register for use as a scratchpad register. By default, HLA never modifies the value of a register behind your back; so it will often generate less than optimal code when passing certain parameters on the stack. By using the @use procedure option, you can specify one of the following 32-bit registers for use by HLA: EAX, EBX, ECX, EDX, ESI, or EDI. By providing one of these registers, HLA may be able to generate significantly better code when passing certain parameters. Note that it is not legal for you to specify the EBP or ESP registers after an @use option. Procedures only require a single 32-bit register, so if you specify multiple @use options, HLA only uses the last register you specify. Although HLA allows the use of ESI and EDI, HLA generates better code in certain circumstances if you specify one of EAX, EBX, ECX or EDX.
@returns( "text" )
This option specifies the compile-time return value whenever a function name appears as an instruction operand. For example, suppose you are writing a function that returns its result in EAX. You should probably specify a "returns" value of "EAX" so you can compose that procedure just like any other HLA machine instruction. For more details on this option, check out the discussion of "instruction composition" in the HLA Reference Manual.
@leave, @noleave
These two options control the code generation for the standard exit sequence. If you specify the @leave option then HLA emits the x86 leave instruction to clean up the activation record before the procedure returns. If you specify the @noleave option, then HLA emits the primitive instructions to achieve this, e.g.,
mov( ebp, esp );
pop( ebp );

The manual sequence is faster on some architectures, the leave instruction is always shorter.

Note that @noleave occurs by default if you've specified @noframe. By default, HLA assumes @noleave but you may change the default using these special identifiers as a compile-time variable to set the default leave generation for all procedures. Setting @leave to true (or @noleave to false) turns on leave generation by default; setting @leave to false (or @noleave to true) turns off the use of the leave instruction.
@enter, @noenter
These two options control the code generation for a procedure's standard entry sequence. If you specify the @enter option then HLA emits the x86 enter instruction to create the activation record. If you specify the @noenter option, then HLA emits the primitive instructions to achieve this.

The manual sequence is always faster, using the enter instruction is usually shorter.

Note that @noenter occurs by default if you've specified @noframe. By default, HLA assumes @noenter but you may change the default using these special identifiers as a compile-time variable to set the default enter generation for all procedures. Setting @enter to true (or @noenter to false) turns on enter generation by default; setting @enter to false (or @noenter to true) turns off the use of the enter instruction.

The enter instruction is primarily of interest to programmers using nested procedures and displays (see the @nodisplay option). Because most Win32 applications don't use nested procedures and a display, you'll rarely see this option used in a Win32 program.

The @returns option and "instruction composition" in HLA deserve a special mention. Instruction composition is a feature in HLA that allows you to substitute an HLA instruction (or other statement) in place of certain instruction operands. For example, consider the following perfectly legal HLA statement:

    mov( mov( 0, eax ), ebx );

To process a statement like this, HLA associates a compile-time "return" value with every instruction. The return value is a string (text) value and is usually the destination operand of the instruction. For example, the destination operand of the nested instruction in this example is "eax" so HLA substitutes "EAX" for the nested instruction after emitting the code for that instruction. In other words, this particular sequence is equivalent to the following:

    mov( 0, eax );

    mov( eax, ebx );  // First operand was the result of instruction composition.

You may also supply procedure invocations as instruction operands. When HLA encounters a procedure invocation in an instruction operand, it emits the code for that procedure call and then substitutes the "returns" string for that procedure invocation (the "returns" string is the string operand you supply in the @returns procedure option). The following example demonstrates how the @returns option works:

program returnsDemo;

#include( "stdio.hhf" );

    procedure eax0; @returns( "eax" );

    begin eax0;

        mov( 0, eax );

    end eax0;

begin returnsDemo;

    mov( eax0(), ebx );

    stdout.put( "ebx=", ebx, nl );

end returnsDemo;

If you do not explicitly supply an @returns procedure option, then HLA will use the empty string as the @returns value for that procedure. Obviously, if you compose such a procedure invocation inside some other instruction you'll probably create a syntax error.

You'll rarely see code that uses procedure invocations in an instruction as in these examples. However, instruction composition can take place in other areas too. For example, the operands of a boolean expression in an HLA if statement also support instruction composition, allowing you to specify statements like the following:

    if( eax0() <> 0 ) then

        .

        .

        .

    endif;

HLA will emit the code for the call to the procedure (eax0 in this example) and then substitute the procedure's @returns value for the procedure invocation in the boolean expression (i.e., "eax" in this example).

To help those who insist on constructing the activation record themselves, HLA declares two local constants within each procedure: _vars_ and _parms_. The _vars_ symbol is an integer constant that specifies the number of local variables declared in the procedure. This constant is useful when allocating storage for your local variables. The _parms_ constants specifies the number of bytes of parameters. You would normally supply this constant as the parameter to a ret() instruction to automatically clean up the procedure's parameters when it returns. Here's an example of these two constants in use:

    procedure hasParmsAndLocals( p1:dword; p2:int32; p3:uns32 ); @noframe; @nodisplay;

    var

        localA :dword;

        localB :dword;

        localC :dword;

        localD :dword;

    begin hasParmsAndLocals;

        push( ebp ); // we have to build the activation record ourselves!

        mov( esp, ebp );

        sub( _vars_, esp ); // make room for local variables (16 bytes, as it turns out).

            .

            .

            .

        mov( ebp, esp );  // Clean up the activation record on return

        pop( ebp );       // Note that we could also use "leave" here.

        ret( _parms_ );   // Return and remove parameters from the stack (12 bytes).

If you do not specify @nodisplay, then HLA defines a run-time variable named _display_ that is an array of pointers to activation records. As Win32 applications generally do not use nested procedures and displays, we will not discuss the use of the @nodisplay option (or displays) in this book. For more details on displays and the @nodisplay (or @display) procedure option, please consult the HLA Reference Manual.

You can also declare @external procedures (procedures defined in other HLA units or written in languages other than HLA) using the following syntaxes:

procedure externProc1 (optional parameters) ; @returns( "text" ); @external;

procedure externProc2 (optional parameters) ;

    @returns( "text" ); @external( "different_name" );

As with normal procedure declarations, the parameter list and @returns clause are optional.

The first form is generally used for HLA-written functions. HLA will use the procedure's name (externProc1 in this case) as external name.

The second form lets you refer to the procedure by one name (externProc2 in this case) within your HLA program and by a different name ("different_name" in this example) in the MASM generated code. This second form has two main uses: (1) if you choose an external procedure name that just happens to be a MASM reserved word, the program may compile correctly but fail to assemble. Changing the external name to something else solves this problem. (2) When calling procedures written in external languages you may need to specify characters that are not legal in HLA identifiers. For example, Win32 API calls often use names like "WriteFile@24" containing illegal (in HLA) identifier symbols. The string operand to the external option lets you specify any name you choose. Of course, it is your responsibility to see to it that you use identifiers that are compatible with the linker and MASM, HLA doesn't check these names.

By default, HLA does the following:

  • Creates a display for every procedure.
  • Emits code to construct the stack frame for each procedure.
  • Emits code to align ESP on a four-byte boundary upon procedure entry.
  • HLA assumes that it cannot modify any register values when passing (non-register) parameters.
  • The first instruction of the procedure is unaligned.

These options are the most general and "safest" for beginning assembly language programmers. However, the code HLA generates for this general case may not be as compact or as fast as is possible in a specific case. For example, few procedures will actually need a display data structure built upon procedure activation. Therefore, the code that HLA emits to build the display can reduce the efficiency of the program. Most Win32 application programmers, of course, will want to use procedure options like "@nodisplay" to tell HLA to skip the generation of this code. However, if a program contains many procedures and none of them need a display, continually adding the "@nodisplay" option can get really old. Therefore, HLA allows you to treat these directives as "pseudo-compile-time-variables" to control the default code generation. E.g.,

    ? @display := true; // Turns on default display generation.

    ? @display := false; // Turns off default display generation.

    ? @nodisplay := true; // Turns off default display generation.

    ? @nodisplay := false; // Turns on default display generation.

    ? @frame := true; // Turns on default frame generation.

    ? @frame := false; // Turns off default frame generation.

    ? @noframe := true; // Turns off default frame generation.

    ? @noframe := false; // Turns on default frame generation.

    ? @alignstack := true; // Turns on default stk alignment code generation.

    ? @alignstack := false; // Turns off default stk alignment code generation.

    ? @noalignstack := true; // Turns off default stk alignment code generation.

    ? @noalignstack := false; // Turns on default stk alignment code generation.

    ? @enter := true; // Turns on default ENTER code generation.

    ? @enter := false; // Turns off default ENTER code generation.

    ? @noenter := true; // Turns off default ENTER code generation.

    ? @noenter := false; // Turns on default ENTER code generation.

    ? @leave := true; // Turns on default LEAVE code generation.

    ? @leave := false; // Turns off default LEAVE code generation.

    ? @noleave := true; // Turns off default LEAVE code generation.

    ? @noleave := false; // Turns on default LEAVE code generation.

    ?@align := 1; // Turns off procedure alignment (align on byte boundary).

    ?@align := int_expr; // Sets alignment, must be a power of two.

These directives may appear anywhere in the source file. They set the internal HLA default values and all procedure declarations following one of these assignments (up to the next, corresponding assignment) use the specified code generation option(s). Note that you can override these defaults by using the corresponding procedure options mentioned earlier.

2.4.1: Disabling HLA's Automatic Code Generation for Procedures

Before jumping in and describing how to use the high level HLA features for procedures, the best place to start is with a discussion of how to disable these features and write "plain old fashioned" assembly language code. This discussion is important because procedures are the one place where HLA automatically generates a lot of code for you and many assembly language programmers prefer to control their own destinies; they don't want the compiler to generate any excess code for them. Though most Win32 assembly language programmers use HLA's high level procedure features, many may want to disable some or all of HLA's code generation features for procedures. So disabling HLA's automatic code generation capabilities is a good place to start this discussion.

By default, HLA automatically emits code at the beginning of each procedure to do five things: (1) Preserve the pointer to the previous activation record (EBP); (2) build a display in the current activation record; (3) allocate storage for local variables; (4) load EBP with the base address of the current activation record; (5) adjust the stack pointer (downwards) so that it points at a dword-aligned address.

When you return from a procedure, by default HLA will deallocate the local storage and return, removing any parameters from the stack.

To understand the code that HLA emits, consider the following simple procedure:

procedure p( j:int32 );

var

    i:int32;

begin p;

end p;

Here is a dump of the symbol table that HLA creates for procedure p6:

p   <0,proc>:Procedure type (ID=?1_p)

    --------------------------------

    _vars_          <1,cons>:uns32, (4 bytes)  =4

    i               <1,var >:int32, (4 bytes, ofs:-12)

    _parms_         <1,cons>:uns32, (4 bytes)  =4

    _display_       <1,var >:dword, (8 bytes, ofs:-4)

    j               <1,valp>:int32, (4 bytes, ofs:8)

    p               <1,proc>:

------------------------------------

The important thing to note here is that local variable "i" is at offset -12 and HLA automatically created an eight-bit local variable named "_display_" which is at offset -4.

HLA emits code similar to the following for the procedure above7:

procedure p; @nodisplay; @noframe; @nostackalign;

begin p;

        push( ebp );        //Dynamic link (pointer to previous activation record)

        pushd( [ebp-4] );   //Display for lex level 0

        lea( ebp, [esp+04] );  //Get frame ptr (point EBP at current activation record)

        push( ebp );           //Ptr to this proc's A.R. (part of display construction)

        sub( 4, esp );         //Local storage.

        and( $fffffffc, esp ); //dword-align stack

// Exit point for the procedure:

        mov( ebp, esp );  //Deallocate local variables.

        pop( ebp );       //Restore pointer to previous activation record.

        ret( 4 );         //Return, popping parameters from the stack.

end p;

Building the display data structure is not very common in standard assembly language programs. This is only necessary if you are using nested procedures and those nested procedures need to access non-local variables. Because this is a rare situation, many programmers will immediately want to tell HLA to stop emitting the code to generate the display. This is easily accomplished by adding the "@nodisplay" procedure option to the procedure declaration. Adding this option to procedure p produces the following:

procedure p( j:int32 ); @nodisplay;

var

    i:int32;

begin p;

end p;

Compiling this procedures the following symbol table dump:

p               <0,proc>:Procedure type (ID=?1_p)

    --------------------------------

    _vars_          <1,cons>:uns32, (4 bytes)  =4

    i               <1,var >:int32, (4 bytes, ofs:-4)

    _parms_         <1,cons>:uns32, (4 bytes)  =4

    j               <1,valp>:int32, (4 bytes, ofs:8)

    p               <1,proc>:

------------------------------------

Note that the _display_ variable is gone and the local variable i is now at offset -4. Here is the code that HLA emits for this new version of the procedure:

procedure p; @nodisplay; @noframe; @nostackalign;

begin p;

        push( ebp );           //Dynamic link (pointer to previous activation record)

        mov( esp, ebp );       //Point EBP at current activation record.

        sub( 4, esp );         //Local storage.

        and( $fffffffc, esp ); //dword-align stack

// Exit point for the procedure:

        mov( ebp, esp );  //Deallocate local variables.

        pop( ebp );       //Restore pointer to previous activation record.

        ret( 4 );         //Return, popping parameters from the stack.

end p;

As you can see, this code is smaller and a bit less complex. Unlike the code that built the display, it is fairly common for an assembly language programmer to construct an activation record in a manner similar to this. Indeed, about the only instruction out of the ordinary in this example is the "and" instruction that dword-aligns the stack (Win32 calls require the stack to be double word aligned, and the system performance is much better if the stack is double word aligned).

This code is still relatively inefficient if you don't pass parameters on the stack and you don't use automatic (non-static, local) variables. Many assembly language programmers pass their few parameters in machine registers and also maintain local values in the registers. If this is the case, then the code above is pure overhead. You can inform HLA that you wish to take full responsibility for the entry and exit code by using the "@noframe" procedure option. Consider the following version of p:

procedure p( j:int32 ); @nodisplay; @noframe;

var

    i:int32;

begin p;

end p;

(this produces the same symbol table dump as the previous example).

HLA emits the following code for this version of p:

procedure p; @nodisplay; @noframe; @nostackalign;

begin p;

end p;

Whoa! There's nothing there! But this is exactly what the advanced assembly language programmer wants. With both the @nodisplay and @noframe options, HLA does not emit any extra code for you. You would have to write this code yourself.

By the way, you can specify the @noframe option without specifying the @nodisplay option. HLA still generates no extra code, but it will assume that you are allocating storage for the display in the code you write. That is, there will be an eight-byte _display_ variable created and i will have an offset of -12 in the activation record. It will be your responsibility to deal with this. Although this situation is possible, it's doubtful this combination will be used much at all.

Note a major difference between the two versions of p when @noframe is not specified and @noframe is specified: if @noframe is not present, HLA automatically emits code to return from the procedure. This code executes if control falls through to the "end p;" statement at the end of the procedure. Therefore, if you specify the @noframe option, you must ensure that the last statement in the procedure is a ret() instruction or some other instruction that causes an unconditional transfer of control. If you do not do this, then control will fall through to the beginning of the next procedure in memory, probably with disastrous results.

The ret() instruction presents a special problem. It is dangerous to use this instruction to return from a procedure that does not have the @noframe option. Remember, HLA has emitted code that pushes a lot of data onto the stack. If you return from such a procedure without first removing this data from the stack, your program will probably crash. The correct way to return from a procedure without the @noframe option is to jump to the bottom of the procedure and run off the end of it. Rather than require you to explicitly put a label into your program and jump to this label, HLA provides the "exit procname;" instruction. HLA compiles the exit instruction into a jmp that transfers control to the clean-up code HLA emits at the bottom of the procedure. Consider the following modification of p and the resulting assembly code produced:

procedure p( j:int32 ); @nodisplay;

var

    i:int32;

begin p;

    exit p;

    nop();

end p;

procedure p( j:int32 ); @noframe; @nodisplay;

var

    i:int32;

begin p;

    push( ebp )

    mov( esp, ebp )l

    sub( 4, esp );

    and( $fffffffc, esp );

    jmp     _x_2_p;          // This is what the exit statement compiles to

    nop;

_x_2_p:

    mov( ebp, esp );

    pop( ebp );

    ret( 4 );

end p;

As you can see, HLA automatically emits a label to the assembly output file ("_x_2_p" in this instance) at the bottom of the procedure where the clean-up code starts. HLA translates the "exit p;" instruction into a jmp to this label.

If you look back at the code emitted for the version of p with the @noframe option, you'll note that HLA did not emit a label at the bottom of the procedure. Therefore, HLA cannot generate a jump to this nonexistent label, so you cannot use the exit statement in a procedure with the @noframe option (HLA will generate an error if you attempt this).

Of course, HLA will not stop you from putting a ret() instruction into a procedure without the @noframe option (some people who know exactly what they are doing might actually want to do this). Keep in mind, if you decide to do this, that you must deallocate the local variables (that's what the "mov esp, ebp" instruction is doing), you need to restore EBP (via the "pop ebp" instruction above), and you need to deallocate any parameters pushed on the stack (the "ret 4" handles this in the example above). The following code demonstrates this:

procedure p( j:int32 ); @nodisplay;

var

    i:int32;

begin p;

    if( j = 0 ) then

        // Deallocate locals.

        mov( ebp, esp );

        // Restore old EBP

        pop( ebp );

        // Return and pop parameters

        ret( 4 );

    endif;

    nop();

end p;

procedure p; @nodisplay; @noframe;

begin p;

    push( ebp );

    mov( esp, ebp );

    sub( 4, esp );

    and( $fffffffc, esp );

    cmp( (type dword [ebp+8]), 0 );

    jne _2_false;

    mov( ebp, esp );

    pop( ebp );

    ret( 4 );

_2_false:

    nop;

    mov( ebp, esp );

    pop( ebp );

    ret( 4 );

end p;

If "real" assembly language programmers would generally specify both the @noframe and @nodisplay options, why not make them the default case (and use "@frame" and "@display" options to specify the generation of the activation record and display)? Well, keep in mind that HLA was originally designed as a tool to teach assembly language programming to beginning students and this default behavior is safer for beginning programmers. Also, most Win32 applications use these defaults (that is, most Win32 application programmers do not specify @noframe, regardless of how advanced they are) so these defaults make a lot of sense for Win32 applications.

If you are absolutely certain that your stack pointer is aligned on a four-byte boundary upon entry into a procedure, you can tell HLA to skip emitting the and( $ffff_fffc, esp ); instruction by specifying the @noalignstack procedure option. Because Win32 apps always keep the stack dword aligned, most Win32 applications set the stack alignment to "off" via a statement like this one near the beginning of the source file:

    ?@nostackalign := true;

Note that specifying @noframe also specifies @noalignstack.

Note that HLA does provide a few additional procedure options. Please consult the HLA Reference Manual for more details on these options. They are not of immediate interest to us here, so this book will ignore them.

2.4.2 Calling HLA Procedures

There are two standard ways to call an HLA procedure: use the call instruction or simply specify the name of the procedure as an HLA statement. Both mechanisms have their pluses and minuses.

To call an HLA procedure using the call instruction is exceedingly easy. Simply use either of the following syntaxes:

call( procName );

call procName;

Either form compiles into an 80x86 call instruction that calls the specified procedure. The difference between the two is that the first form (with the parentheses) returns the procedure's "returns" value, so this form can appear as an operand to another instruction. The second form above always returns the empty string, so it is not suitable as an operand of another instruction. Also, note that the second form requires a statement or procedure label, you may not use memory addressing modes in this form; on the other hand, the second form is the only form that lets you "call" a statement label (as opposed to a procedure label); this form is useful on occasion.

If you use the call statement to call a procedure, then you are responsible for passing any parameters to that procedure. In particular, if the parameters are passed on the stack, you are responsible for pushing those parameters (in the correct order) onto the stack before the call. This is a lot more work than letting HLA push the parameters for you, but in certain cases you can write more efficient code by pushing the parameters yourself.

The second way to call an HLA procedure is to simply specify the procedure name and a list of actual parameters (if needed) for the call. This method has the advantage of being easy and convenient at the expense of a possible slight loss in efficiency and flexibility. This calling method should also prove familiar to most HLL programmers. As an example, consider the following HLA program:

program parameterDemo;

#include( "stdio.hhf" );

procedure PrtAplusB( a:int32; b:int32 ); @nodisplay;

begin PrtAplusB;

    mov( a, eax );

    add( b, eax );

    stdout.put( "a+b=", (type int32 eax ), nl );

end PrtAplusB;

static

    v1:int32 := 25;

    v2:int32 := 5;

begin parameterDemo;

    PrtAplusB( 1, 2 );

    PrtAplusB( -7, 12 );

    PrtAplusB( v1, v2 );

    mov( -77, eax );

    mov( 55, ebx );

    PrtAplusB( eax, ebx );

end parameterDemo;

This program produces the following output:

a+b=3

a+b=5

a+b=30

a+b=-22

As you can see, the calls to PrtAplusB in HLA are very similar to calling procedures (and passing parameters) in a high level language like C/C++ or Pascal. There are, however, some key differences between and HLA call and a HLL procedure call. The next section will cover those differences in greater detail. The important thing to note here is that if you choose to call a procedure using the HLL syntax (that is, the second method described here), you can pass the parameters in the parameter list and let HLA push the parameters for you. If you want to take complete control over the parameter passing code, one way to do this is to use the call instruction.

2.4.3: Parameter Passing in HLA, Value Parameters

The previous section probably gave you the impression that passing parameters to a procedure in HLA is nearly identical to passing those same parameters to a procedure in a high level language. The truth is, the examples in the previous section were rigged. There are actually many restrictions on how you can pass parameters to an HLA procedure. This section discusses the parameter passing mechanism in detail.

The most important restriction on actual parameters in a call to an HLA procedure is that HLA only allows memory variables, registers, constants, and certain other special items as parameters. In particular, you cannot specify an arithmetic expression that requires computation at run-time (although a constant expression, computable at compile time is okay). The bottom line is this: if you need to pass the value of an expression to a procedure, you must compute that value prior to calling the procedure and pass the result of the computation; HLA will not automatically generate the code to compute that expression for you.

The second point to mention here is that HLA is a strongly typed language when it comes to passing parameters. This means that with only a few exceptions, the type of the actual parameter must exactly match the type of the formal parameter. If the actual parameter is an int8 object, the formal parameter had better not be an int32 object or HLA will generate an error. The only exceptions to this rule are the byte, word, and dword types. If a formal parameter is of type byte, the corresponding actual parameter may be any one-byte data object. If a formal parameter is a word object, the corresponding actual parameter can be any two-byte object. Likewise, if a formal parameter is a dword object, the actual parameter can be any four-byte data type. Conversely, if the actual parameter is a byte, word, or dword object, it can be passed without error to any one, two, or four-byte actual parameter (respectively). Programmers who are really lazy make all their parameters bytes, words, or dwords (at least, wherever possible). Programmers who care about the quality of their code use untyped parameters cautiously.

If you want to use the high level calling sequence, but you don't like the inefficient code HLA sometimes produces when generating code to pass your parameters, you can always use the #{...}# sequence parameter to override HLA's code generation and substitute your own code for one or two parameters. Of course, it doesn't make any sense to pass all the parameters is a procedure using this trick, it would be far easier just to use the call instruction. Here's an example of using HLA's manual parameter passing facility within a HLL style procedure call:

    PrtAplusB

    (

        #{

            mov( i, eax );   // First parameter is "i+5"

            add( 5, eax );

            push( eax );

        }#,

        5

    );

HLA will automatically copy an actual value parameter into local storage for the procedure, regardless of the size of the parameter. If your value parameter is a one million byte array, HLA will allocate storage for 1,000,000 bytes and then copy that array in on each call. C/C++ programmers may expect HLA to automatically pass arrays by reference (as C/C++ does) but this is not the case. If you want your parameters passed by reference, you must explicitly state this.

The code HLA generates to copy value parameters, while not particularly bad, certainly isn't optimal. If you need the fastest possible code when passing parameters by value on the stack, it would be better if you explicitly pushed the data yourself. Another alternative that sometimes helps is to use the "use reg32" procedure option to provide HLA with a hint about a 32-bit scratchpad register that it can use when building parameters on the stack. For more details on this facility, please see the HLA Reference Manual or The Art of Assembly Language.

2.4.4: Parameter Passing in HLA: Reference Parameters

The one good thing about pass by reference parameters is that they are always four byte pointers, regardless of the size of the actual parameter. Therefore, HLA has an easier time generating code for these parameters than it does generating code for pass by value parameters.

Like high level languages, HLA places a whopper of a restriction on pass by reference parameters: they can only be memory locations. Constants and registers are not allowed since you cannot compute their address. Do keep in mind, however, that any valid memory addressing mode is a valid candidate to be passed by reference; you do not have to limit yourself to static and local variables. For example, "[eax]" is a perfectly valid memory location, so you can pass this by reference (assuming you type-cast it, of course, to match the type of the formal parameter). The following example demonstrate a simple procedure with a pass by reference parameter:

program refDemo;

#include( "stdio.hhf" );

    procedure refParm( var a:int32 );

    begin refParm;

        mov( a, eax );

        mov( 12345, (type int32 [eax]));

    end refParm;

    static

        i:int32:=5;

begin refDemo;

    stdout.put( "(1) i=", i, nl );

    mov( 25, i );

    stdout.put( "(2) i=", i, nl );

    refParm( i );

    stdout.put( "(3) i=", i, nl );

end refDemo;

The output produced by this code is

(1) i=5

(2) i=25

(3) i=12345

As you can see, the parameter a in refParm exhibits pass by reference semantics since the change to the value a in refParm changed the value of the actual parameter (i) in the main program.

Note that HLA passes the address of i to refParm, therefore, the "a" parameter contains the address of i. When accessing the value of the i parameter, the refParm procedure must deference the pointer passed in "a". The two instructions in the body of the refParm procedure accomplish this.

Take a look at the code that HLA generates for the call to refParm:

            pushd( &i );

            call refParm;

As you can see, this program simply computed the address of i and pushed it onto the stack. Now consider the following modification to the main program:

program refDemo;

#include( "stdio.hhf" );

    procedure refParm( var a:int32 );

    begin refParm;

        mov( a, eax );

        mov( 12345, (type int32 [eax]));

    end refParm;

    static

        i:int32:=5;

    var

        j:int32;

begin refDemo;

    mov( 0, j );

    refParm( j );

    refParm( i );

    lea( eax, j );

    refParm( [eax] );

end refDemo;

This version emits the following code:

    mov( 0, (type dword [ebp-8]) );

    push( eax );

    lea( eax, (type dword [ebp-8]));

    xchg( eax, [esp] );

    call refParm;

    pushd( &i );

    call refParm;

    lea( eax, (type dword [ebp-8]));

    push( eax );

    push( eax );

    lea( eax, (type dword [eax+0]));

    mov( eax, [esp+4] );

    pop( eax );

    call refParm;

As you can see, the code emitted for the last call is pretty ugly (we could easily get rid of three of the instructions in this code). This call would be a good candidate for using the call instruction directly. Also see "Hybrid Parameters" a little later in this book. Another option is to use the "@use reg32" option to tell HLA it can use one of the 32-bit registers as a scratchpad. Consider the following:

procedure refParm( var a:int32 ); use eax;

        .

        .

        .

    lea( ebx, j );

    refParm( [ebx] );

This sequence generates the following code (which is a little better than the previous example):

    lea( ebx, j );

    lea( eax, (type dword [ebx+0]));

    push( eax );

    call refParm;

As a general rule, the type of an actual reference parameter must exactly match the type of the formal parameter. There are a couple exceptions to this rule. First, if the formal parameter is dword, then HLA will allow you to pass any four-byte data type as an actual parameter by reference to this procedure. Second, you can pass an actual dword parameter by reference if the formal parameter is a four-byte data type. There are some other exceptions that we'll explore in a later section of this chapter.

2.4.5: Untyped Reference Parameters

HLA provides a special formal parameter syntax that tells HLA that you want to pass an object by reference and you don't care what its type is. Consider the following HLA procedure:

procedure zeroObject( var object:byte; size:uns32 );

begin zeroObject;

    << code to write "size" zeros to "object" >

end zeroObject;

The problem with this procedure is that you will have to coerce non-byte parameters to a byte before passing them to zeroObject. That is, unless you're passing a byte parameter, you've always got to call zeroObject thusly:

    zeroObject( (type byte NotAByte), sizeToZero );

For some functions you call frequently with different types of data, this can get painful very quickly.

The HLA untyped reference parameter syntax solves this problem. Consider the following declaration of zeroObject:

procedure zeroObject( var object:var; size:uns32 );

begin zeroObject;

    << code to write "size" zeros to "object" >

end zeroObject;

Notice the use of the reserved word "var" instead of a data type for the object parameter. This syntax tells HLA that you're passing an arbitrary variable by reference. Now you can call zeroObject and pass any (memory) object as the first parameter and HLA won't complain about the type, e.g.,

    zeroObject( NotAByte, sizeToZero );

Note that you may only pass untyped objects by reference to a procedure.

Note, and this is very important, when you pass an actual parameter to a procedure that has an untyped reference parameter in that slot, HLA will always take the address of the object you pass it. This is true even if the actual parameter is a pointer to some data (which is in direct contrast to what HLA normally does with pointer objects that you pass as reference parameters). For more details, read the section on "Passing Pointers and Values as Reference Parameters" on page 92.

2.4.6: Hybrid Parameter Passing in HLA

HLA's automatic code generation for parameters specified using the high level language syntax isn't always optimal. In fact, sometimes it is downright inefficient. This is because HLA makes very few assumptions about your program. For example, suppose you are passing a word parameter to a procedure by value. Since all parameters in HLA consume an even multiple of four bytes on the stack, HLA will zero extend the word and push it onto the stack. It does this using code like the following:

    pushw( 0 );

    pushw( Parameter );

Clearly you can do better than this if you know something about the variable. For example, if you know that the two bytes following "Parameter" are in memory (as opposed to being in the next page of memory that isn't allocated, and access to such memory would cause a protection fault), you could get by with the single instruction:

    push( (type dword Parameter) );

Unfortunately, HLA cannot make these kinds of assumptions about the data because doing so could create malfunctioning code (admittedly, in very rare circumstances).

One solution, of course, is to forego the HLA high level language syntax for procedure calls and manually push all the parameters yourself and call the procedure via the call instruction. However, this is a major pain that involves lots of extra typing and produces code that is difficult to read and understand. Therefore, HLA provides a hybrid parameter passing mechanism that lets you continue to use a high level language calling syntax yet still specify the exact instructions needed to pass certain parameters. This hybrid scheme works out well because HLA actually does a good job with most parameters (e.g., if they are an even multiple of four bytes, HLA generates efficient code to pass the parameters; it's only those parameters that have a weird size that HLA generates less than optimal code for).

If a parameter consists of the "#{" and "}#" brackets with some corresponding code inside the brackets, HLA will emit the code inside the brackets in place of any code it would normally generate for that parameter. So if you wanted to pass a 16-bit parameter efficiently to a procedure named "Proc" and you're sure there is no problem accessing the two bytes beyond this parameter, you could use code like the following:

    Proc( #{ push( (type dword WordVar) ); }# );

Whenever you pass a non-static8 variable as a parameter by reference, HLA generates the following code to pass the address of that variable to the procedure:

    push( eax );

    push( eax );

    lea( eax, Variable );

    mov( eax, [esp+4] );

    pop( eax );

It generates this particular code to ensure that it doesn't change any register values (after all, you could be passing some other parameter in the EAX register). If you have a free register available, you can generate slightly better code using a calling sequence like the following (assuming EBX is free):

    HasRefParm

    (

        #{

                lea( ebx, Variable );

                push( ebx );

        }#

    );

2.4.7: Parameter Passing in HLA, Register Parameters

HLA provides a special syntax that lets you specify that certain parameters are to be passed in registers rather than on the stack. The following are some examples of procedure declarations that use this feature:

procedure a( u:uns32 in eax ); forward;

procedure b( w:word in bx ); forward;

procedure d( c:char in ch ); forward;

Whenever you call one of these procedures, the code that HLA automatically emits for the call will load the actual parameter value into the specified register rather than pushing this value onto the stack. You may specify any general purpose 8-bit, 16-bit, or 32-bit register after the "in" keyword following the parameter's type. Obviously, the parameter must fit in the specified register. You may only pass reference parameters in 32-bit registers; you cannot pass parameters that are not one, two, or four bytes long in a register. Also note that HLA does not support passing parameters in FPU, MMX, or XMM registers (at least, using the high level syntax).

You can get in to trouble if you're not careful when using register parameters, consider the following two procedure definitions:

procedure one( u:uns32 in eax; v:dword in ebx ); forward;

procedure two( a:uns32 in eax );

begin two;

    one( 25, a );

end two;

The call to "one" in procedure "two" looks like it passes the values 25 and whatever was passed in for "a" in procedure two. However, if you study the HLA output code, you will discover that the call to "one" passes 25 for both parameters. They reason for this is because HLA emits the code to load 25 into EAX in order to pass 25 in the "u" parameter. Unfortunately, this wipes out the value passed into "two" in the "a" variable, hence the problem. Be aware of this if you use register parameters often.

2.4.8: Passing Pointers and Values as Reference Parameters

When it comes to pass by reference parameters, HLA relaxes the type checking rules a little bit in order to make calling procedures with reference parameters a bit more convenient. For the most part, this relaxation (which you'll see in a moment) is intuitive and works well. However, there are a few special cases where the lack of strict type checking creates some surprising results; so we'll discuss this issue in detail here.

To understand the motivation for HLA's relaxation of strict type-checking, consider the following procedure declaration:

    procedure uns8RefParm( var b:uns8 );

    begin uns8RefParm;

        .

        .

        .

    end uns8RefParm;

Whenever you call this procedure, you have to pass it an actual parameter that is either an uns8 typed object or a byte object (remember, you can pass a byte object as a parameter whenever a byte-sized reference parameter is required). HLA will emit the code to take the address of that one-byte object in memory and pass this address on to the procedure. Note that within the uns8RefParm procedure, like any reference parameter, HLA treats b as though it were a dword object (because pointers always require 32 bits, regardless of the size of the object at which they point). Here is a typical call to the uns8RefParm procedure:

static

    uns8Var :uns8;

        .

        .

        .

    uns8RefParm( uns8Var );

Here's the code HLA might generate for this procedure call:

    pushd( &uns8Var );

    call uns8RefParm;

Now consider the following code fragments:

type

    pUns8 : pointer to uns8;

static

    pUns8Var :pUns8;

    uns8Var  :uns8;

        .

        .

        .

    mov( &uns8Var, pUns8Var ); // Initialize pUns8Var with the address of uns8Var

        .

        .

        .

    uns8RefParm( pUns8Var );

Where HLA to strictly enforce type checking of reference parameters, this call to uns8RefParm would generate an error. After all, this code is trying to pass a four-byte pointer object by reference to a procedure that is expecting an uns8 reference parameter. The two variables in memory are not at all compatible even though the pointer object, itself, points at an uns8 variable in memory. In a typical high level language, it makes a lot of sense to report this as an error and reject the source file as uncompilable. In assembly language, however, it is very common to refer to objects (even static ones) using pointers to those objects that you generate on the fly (e.g., pointer values that you load into registers and possibly manipulate in those registers). As a result, you've often got the address of a variable sitting around in a register or a pointer and you'd like to pass that address on through to the procedure rather than taking the address of the object. After all, any assembly language programmer realizes that pass by reference parameters are just syntactical sugar for saying "take the address of the actual parameter because the procedure is expecting us to pass it an address." If you've already got the address somewhere, why bother computing it again?

If the address is sitting in a 32-bit register, you can always pass the address using a memory operand like "[ebx]". HLA will, nicely, assume that an untyped memory operand like this actually points at a correct object. So you could call uns8RefParm as follows:

    uns8RefParm( [ebx] );

Theoretically, it would be better for you to write this as

    uns8RefParm( (type uns8 [ebx]) );

However, experience shows that requiring the type coercion here is just too inconvenient to consider. So HLA relaxes the requirement a bit and allows the use of anonymous memory references (those involving an indirect address in a 32-bit register without any other type or name information) as legal arguments for any pass by reference parameter.

Now consider the following call to uns8RefParm:

    uns8RefParm( pUns8Var );

The pUns8Var argument should, technically, be illegal. This is not an object of byte uns8 (or byte) and, therefore, HLA should generate a "type mismatch error" in response to this procedure invocation. As it turns out, however, it is very common in assembly language programs (especially in Win32 assembly programs) to want to pass the pointer to a memory object in a reference parameter slot. One could easily do this by explicitly passing the value of the pointer as the parameter, e.g.,

    push( pUns8Var );

    call uns8RefParm;

// or

    uns8RefParm( #{ push( pUns8Var ); }# );

However, the need to pass such a pointer variable's value in place of taking the address of some variable occurs so frequently that HLA relaxes the type checking of a reference parameter to allow any pointer variable that points at the base type of the formal parameter's type. That is, it is legal to pass a pointer to an uns8 variable as a pass by reference parameter whose type is uns8. Rather than take the address of the pointer variable, such a procedure invocation would copy the value of that pointer variable (which is the address of some other object) as the actual parameter data, e.g.,

    uns8RefParm( uns8Var );  // Legal, of course

    uns8RefParm( pUns8Var ); // Also legal

// The above two statements emit the following code:

    push( &uns8Var );

    call uns8RefParm;

    push( pUns8Var );  // Pushes the value of pUns8Var, not it's address!

    call uns8RefParm;

Note that untyped reference parameters always take the address of the actual parameter to pass on to the procedure, even if the actual parameter is a pointer. This is an unfortunately "gotcha" that you're going to have to remember. Consider the following code as an example of this problem:

procedure untypedParm( var v:var );

        .

        .

        .

procedure typedParm( var v:char );

        .

        .

        .

static

    p :pointer to char;

        .

        .

        .

    typedParm( p ); // Legal, passes value of p to typedParm.

    untypedParm( p ); // Legal passes the *address* of p to untypedParm.

// code equivalent to the above two calls:

    pushd( p );   // Note that this pushes p's value, not its address.

    call typedParm;

    pushd( &p );  // Notice that this pushes the address of p, not p's value.

    call untypedParm;

This behavior isn't entirely intuitive. As a result, many programmers who've gotten in the habit of passing a pointer variable as a reference parameter wind up getting burned when the formal parameter is an untyped reference parameter. This problem crops up enough that it's quite possible that HLA will drop support for automatically passing the value of a pointer variable through a reference parameter in some future version of the compiler. Therefore, you should avoid passing a pointer variable as a reference parameter, expecting HLA to automatically push the value (rather than the address) of the pointer variable onto the stack.

Although you should avoid having HLA automatically pass a pointer variable's value through a reference parameter, the need to pass such a value exists. Recent versions of HLA have made use of HLA's val reserved word as a prefix to an actual parameter to tell HLA to pass that parameter's value as the address the reference parameter requires. To do this, simply prefix the actual parameter with the val keyword, e.g.,

        untypedParm( ptrVar );      // Passes the address of ptrVal

        untypedParm( val ptrVar );  // Passes ptrVar's value.

The val keyword actually works with any dword or pointer variable type, not just pointers to the underlying type, e.g.,

            typedParm( val dwordVar );

In HLA, strings are special cases of pointer objects. If you pass a string variable as a parameter that is an untyped reference parameter, HLA will actually pass the address of the string variable (that is, the address of the pointer) rather than the address of the string data (which is where the string is pointing). Several Win32 API functions, for example, expect you to pass them the address of a sequence of characters (i.e., a string) and you'll probably have that data sitting in an HLA string variable in your code. However, if you directly pass the string variable as the parameter to the Win32 API function, HLA will actually take the address of the string pointer variable rather than pass the address of the character data on through to the procedure. You can counteract this behavior by prefixing the string variable with the val keyword in the actual procedure call, e.g.,

    procWithUntypedParm( val strVariable );

Because strings are pointers, you may also use the val keyword to pass an arbitrary 32-bit numeric value for a string parameter. This is useful in certain Win32 API calls where you pass either a pointer to a zero-terminated sequence of characters (i.e., a string) or a small integer "ATOM" value, e.g.,

    proceWithStrParm( val 12345 );  // Normally requires a string parameter.

Whenever you're in doubt as to what HLA is doing with your code, you should take a look at the assembly language output file that HLA produces. Although the syntax is different than HLA, you should be able to figure out what code the compiler is generating for your procedure calls. If this isn't the code you want, you can always pass the parameters manually or apply some operator like val to an actual parameter.

2.5: The HLA Compile-Time Language

One of HLA's more powerful features, and a feature we'll use quite a bit in this book, is the HLA compile-time language. The concept of a compile-time language may seem pretty weird when you first consider it, but the truth is this term is really just a new label for something that's been around a long time: a built-in macro processor. The main reason HLA calls its macro processor a compile-time language is because HLA's macro facilities are quite a bit more powerful than those available with most assemblers.

Actually, to equate the term "compile-time language" with "macro processor" is a bit of an injustice. A compile-time language includes several things that many people often think of as being separate from macro processing, including conditional assembly/compilation, string processing (e.g., of macro operands), repetitive code emission, and so on. Most languages that provide compile-time language features provide a minimal set of such constructs in the overall language. Not so with HLA. It's actually possible to write complete applications using nothing more than the HLA compile-time language; such usage isn't entirely appropriate or efficient, but it is possible to create simple applications within HLA's compile-time language facility.

The real purpose of the HLA compile-time language is to automate the creation of certain common code sequences. A simple macro is a good, well-understood, example of this. By invoking a macro, you can expand some sequence of machine instructions over and over again in an assembly language file, thus "automating" the creation of the code sequences those macros produce. Macros aren't the only constructs that can automate the generation of code at compile-time, however. HLA also provides loops, declarations, assignments, I/O, and other facilities we can use to automate code generation.

Before explaining how to use HLA's compile-time language, perhaps it's a good idea to explicitly define what a compile-time language and a run-time language are and how you would use them. The run-time language is the language that you use to create programs that run after compilation completes; in the case of HLA, the run-time language is 80x86 assembly language and a run-time program is the 80x86 machine code resulting from an HLA compilation. The run-time language includes all the 80x86 machine instructions, data declaration statements, procedure declarations, and so on). It is not physically possible for the run-time program to execute concurrently with its production by the HLA compiler (obviously, you can run a previous instance of that program while HLA is running, but we're talking about the machine code that HLA produces during compilation here). The compile-time language, on the other hand, consists of a handful of statements that HLA executes, via interpretation, while it is compiling the run-time program. The two languages are not entirely distinct. For example, constant and value declarations in the HLA declaration section are usable by both the run-time and compile-time programs. Despite the overlap, it's important to realize that the two languages are distinct and the presence of compile-time language statements in an HLA source file may not produce equivalent execution semantics in a run-time program.

The best place to start when discussing the compile-time language is with the HLA declaration section. The compile-time and run-time languages share the HLA declaration sections (though the two languages sometimes interpret the declarations differently). So let's look at each of the declaration sections that appear in an HLA source file and compare their effects on both the compile-time and run-time programs in that source file.

The HLA const declaration section defines constant values in both the compile-time and run-time languages. Any declaration appearing in an HLA const section is available in both languages in the same source file (subject, of course, to the scoping rules that apply to that identifier). The only issue is that HLA text constants are only usable at compile-time.

The HLA val declaration section also declares constants as far as the run-time language is concerned. However, this is where you declare compile-time variables. The difference between HLA const values and HLA val values is the fact that (during assembly) you can change the value of a val object. Hence, at compile-time, val objects are variables. Note that the HLA compile-time "?" (assignment) statement also creates val objects that the run-time program can treat as a constant value during the application's execution. We'll return to this statement in a moment.

The HLA type declaration section is also applicable to both compile-time and run-time programs. That is, you can define types that are usable by both compile-time and run-time programs in this declaration section. Note, however, that there are a couple of types that don't make sense at compile-time. Specifically you cannot create compile-time class objects, thunks, and a few other types. As noted for const and val objects, if you define a new type based on HLA's built-in text type, that type is only usable in a compile-time program.

The static, readonly, storage, and var sections create run-time variables. HLA's compile-time language provides compile-time function that let you test the compile-time attributes of these variable declarations (e.g., determine their type, the number of array elements, stuff like that), but the compile-time program cannot access the run-time values of these variables (because "run-time" occurs long after the compile-time program terminates). For static and readonly objects the compile-time language can specify the initial value loaded into memory for these run-time variables, but the compile-time language really has no idea what value a run-time variable will have at a given point during program execution.

HLA procedures, methods, and iterators are purely run-time entities. Like run-time variables, the HLA compile-time language can test certain procedure/iterator/method name attributes, but the compile-time language cannot "call" this subroutines nor determine much else about them.

An HLA namespace is an encapsulation of a declaration section. Namespaces may encapsulate run-time as well as compile-time declarations, so namespaces are equally applicable to both types of programs. Accessing fields of the namespace is done the same way in both compile-time and run-time programs (e.g., using "dot-notation").

HLA classes are mainly a run-time construct. However, classes, like namespaces, open up a new declaration section in which you can declare constants, values, and variables. So although you cannot create class constants at compile-time, a class may contain several compile-time language elements (e.g., const, val, and macro declarations). Though this book use classes and objects in a few sample programs, such usage is going to be rare enough that we can effectively ignore classes in our discussion here.

So what does a "compile-time program" look like, anyway? That's a good question and there isn't really a specific definition of a compile-time program. Some might consider an entire HLA source file containing compile-time statements to be a single compile-time program. Other programmers might consider a short sequence of compile-time statements in a source file to be a single compile-time program (so a single HLA source file could contain several compile-time program-ettes). HLA doesn't really define what constitutes a single compile-time program, so we'll just work on the assumption that any sequence of compile-time statements used to compute some compile-time value or create a single run-time sequence of statements constitutes a single compile-time program (i.e., there can be multiple compile-time "programs" in a single HLA source file). Well, enough of the philosophy, let's look at the statements that are available to HLA compile-time language programmers.

2.5.1: Compile-Time Assignment Statements

One of the more important compile-time statements is the compile-time assignment statement. This statement takes one of these forms:

    ? compile_time_variable := compile_time_expression ;

    ? compile_time_variable : type_id := compile_time_expression ;

    ? compile_time_variable : type_id;

    ? compile_time_array_variable[ dimension_list ] := compile_time_expression ;

    ? @built_in_variable := compile_time_expression ;

Here are some examples:

    ? i := 5;

    ? b :boolean := true;

    ? a :int32[4];  // Default initialization is all zeros.

    ? a[2] := 5;

    ? @nodisplay := true;

Note that the compile-time assignment statement is almost semantically identical to a val section declaration. In fact, the internal HLA code is almost identical for handling the val section versus the "?" statement. The only semantic difference between the two has to do with identifier scope. Consider the following code fragment:

val

    i :int32;

    j :uns32;

    procedure nestMe;

    val

        i :real := 3.0; // Creates a new "i" in this procedure's scope.

        i := 2.2;       // Resets the local i's value to 2.2, does not affect value of i

                        //  in the outer scope.

            .

            .

            .

        ?j := 5;        // Stores 5 into the outer scope's "j".

            .

            .

            .

    end nestMe;

As you can see in this example, a val declaration will always create a new instance of a compile-time variable if that variable does not already exist in the current scope, then HLA will create a new variable in the current scope (though if the compile-time variable does exist in the current scope, then HLA will use that variable). The compile-time assignment operator will always attempt to use a predefined version of the compile-time variable, even if that variable was declared in an outer scope. The compile-time assignment statement will only create a new compile-time variable if it is not visible at all at the point in the program where the assignment occurs.

HLA compile-time variables use a dynamic typed system. This means that you can (usually) change the type of a compile-time variable after you've declared it as some other type. E.g.,

? i:int32;  // Declare i as an int32 compile-time object

    .

    .   // compile-time code that uses i as an int32 compile-time variable.

    .

? i :string := "i is now a string"; //From this point forward, i is a string object.

The one point we've not really discussed here is exactly what is legal for an expression in an HLA compile-time assignment statement. The truth is, HLA provides a very sophisticated expression syntax that is very similar to what you would find in a high level language like C/C++ or Pascal. In fact, with all the built-in compile-time functions, one could easily argue that HLA's compile-time language is even more sophisticated that what you'll find in many high level languages. The bottom line is that HLA constant expressions are a bit too complex to do justice here; you'll have to read the HLA Reference Manual to get the full picture. Nevertheless, it's worth listing the arithmetic and relational operators because you'll frequently use them in HLA compile-time programs.

! (unary not), - (unary negation)

*, div, mod, /, <<, >>

+, -

=, = =, <>, !=, <=, >=, <, >

&, |, &, in

One of HLA's more powerful features is the ability to support structured constants in the compile-time language. Because we'll be using structured constants throughout this book, it's worthwhile to review character set, record, and union constants.

A character set literal constant consists of several comma delimited character set expressions within a pair of braces. The character set expressions can either be individual character values or a pair of character values separated by an ellipse (".."). If an individual character expression appears within the character set, then that character is a member of the set; if a pair of character expressions, separated by an ellipse, appears within a character set literal, then all characters between the first such expression and the second expression are members of the set.

Examples:

    {`a','b','c'}            // a, b, and c.

    {`a'..'c'}               // a, b, and c.

    {`A'..'Z','a'..'z'}      //Alphabetic characters.

    {` `,#$d,#$a,#$9}        //Whitespace (space, return, linefeed, tab).

HLA character sets are currently limited to holding characters from the 128-character ASCII character set.

HLA lets you specify an array literal constant by enclosing a set of values within a pair of square brackets. Since array elements must be homogenous, all elements in an array literal constant must be the same type or conformable to the same type. Examples:

[ 1, 2, 3, 4, 9, 17 ]

[ 'a', 'A', 'b', 'B' ]

[ "hello", "world" ]

Note that each item in the list of values can actually be a constant expression, not a simple literal value.

HLA array constants are always one dimensional. This, however, is not a limitation because if you attempt to use array constants in a constant expression, the only thing that HLA checks is the total number of elements. Therefore, an array constant with eight integers can be assigned to any of the following arrays:

const

    a8:			int32[8]					:= [1,2,3,4,5,6,7,8];

    a2x4:			int32[2,4]					:= [1,2,3,4,5,6,7,8];

    a2x2x2:			int32[2,2,2]					:= [1,2,3,4,5,6,7,8];

Although HLA doesn't support the notation of a multi-dimensional array constant, HLA does allow you to include an array constant as one of the elements in an array constant. If an array constant appears as a list item within some other array constant, then HLA expands the interior constant in place, lengthening the list of items in the enclosing list. E.g., the following three array constants are equivalent:

[ [1,2,3,4], [5,6,7,8] ]

[ [ [1,2], [3,4] ], [ [5,6], [7,8] ] ]

[1,2,3,4,5,6,7,8]

Although the three array constants are identical, as far as HLA is concerned, you might want to use these three different forms to suggest the shape of the array in an actual declaration, e.g.,

const

    a8:			int32[8]					:= [1,2,3,4,5,6,7,8];

    a2x4:			int32[2,4]					:= [ [1,2,3,4], [5,6,7,8] ];

    a2x2x2:			int32[2,2,2]					:= [[[1,2], [3,4] ], [[5,6], [7,8]]];

Also note that symbol array constants, not just literal array constants, may appear in a literal array constant. For example, the following literal array constant creates a nine-element array holding the values one through nine at indexes zero through eight:

const Nine:				int32[ 9 ]					:= [ a8, 9 ];

This assumes, of course, that "a8" was previously declared as above. Since HLA "flattens" all array constants, you could have substituted a2x4 or ax2x2x for a8 in the example above and obtained identical results.

You may also create an array constant using the HLA dup operator. This operator uses the following syntax:

expression dup [expression_to_replicate]

Where expression is an integer expression and expression_to_replicate is a some expression, possibly an array constant. HLA generates an array constant by repeating the values in the expression_to_replicate the number of times specified by the expression. (Note: this does not create an array with expression elements unless the expression_to_replicate contains only a single value; it creates an array whose element count is expression times the number of items in the expression_to_replicate). Examples:

10 dup [1]   -- equivalent to [1,1,1,1,1,1,1,1,1,1]

5 dup [1,2]  -- equivalent to [1,2,1,2,1,2,1,2,1,2]

Please note that HLA does not allow class constants, so class objects may not appear in array constants.

HLA supports record constants using a syntax very similar to array constants. You enclose a comma-separated list of values for each field in a pair of square brackets. To further differentiate array and record constants, the name of the record type and a colon must precede the opening square bracket, e.g.,

type

    Planet:

        record

            x:				int32;

            y:				int32;

            z:				int32;

            density:				real64;

        endrecord;

const

    somePlanet : Planet := Planet:[ 1, 12, 34, 1.96 ]

HLA associates the items in the list with the fields as they appear in the original record declaration. In this example, the values 1, 12, 34, and 1.96 are associated with fields x, y, z, and density, respectively. Of course, the types of the individual constants must match (or be conformable to) the types of the individual fields.

Note that you may not create a record constant for a particular record type if that record includes data types that cannot have compile-time constants associated with them. For example, if a field of a record is a class object, you cannot create a record constant for that type since you cannot create class constants.

Union constants allow you to initialize static union data structures in memory as well as initialize union fields of other data structures (including anonymous union fields in records). There are some important differences between HLA compile-time union constants and HLA run-time unions (as well as between the HLA run-time union constants and unions in other languages). Therefore, it's a good idea to begin the discussion of HLA's union constants with a description of these differences.

There are a couple of different reasons people use unions in a program. The original reason was to share a sequence of memory locations between various fields whose access is mutually exclusive. When using a union in this manner, one never reads the data from a field unless they've previous written data to that field and there are no intervening writes to other fields between that previous write and the current read. The HLA compile-time language fully (and only) supports this use of union objects.

A second reason people use unions (especially in high level languages) is to provide aliases to a given memory location; particularly, aliases whose data types are different. In this mode, a programmer might write a value to one field and then read that data using a different field (in order to access that data's bit representation as a different type). HLA does not support this type of access to union constants. The reason is quite simple: internally, HLA uses a special "variant" data type to represent all possible constant types. Whenever you create a union constant, HLA lets you provide a value for a single data field. From that point forward, HLA effectively treats the union constant as a scalar object whose type is the same as the field you've initialized; access to the other fields through the union constant is no longer possible. Therefore, you cannot use HLA compile-time constants to do type coercion; nor is there any need to since HLA provides a set of type coercion operators like @byte, @word, @dword, @int8, etc. As noted already, the main purpose for providing HLA union constants is to allow you to initialize static union variables; since you can only store one value into a memory location at a time, union constants only need to be able to represent a single field of the union at one time (of course, at run-time you may access any field of the static union object you've created; but at compile-time you may only access the single field associated with a union constant).

An HLA literal union constant takes the following form:

        typename.fieldname:[ constant_expression ]

The typename component above must be the name of a previously declared HLA union data type (i.e., a union type you've created in the type section). The fieldname component must be the name of a field within that union type. The constant_expression component must be a constant value (expression) whose type is the same as, or is automatically coercible to, the type of the fieldname field. Here is a complete example:

type

    u:union

        b:byte;

        w:word;

        d:dword;

        q:qword;

    endunion;

static

    uVar		:u		:= u.w:[$1234];

The declaration for uVar initializes the first two bytes of this object in memory with the value $1234. Note that uVar is actually eight bytes long; HLA automatically zeros any unused bytes when initializing a static memory object with a union constant.

Note that you may place a literal union constant in records, arrays, and other composite data structures. The following is a simple example of a record constant that has a union as one of its fields:

type

    r	:record

            b:byte;

            uf:u;

            d:dword;

    endrecord;

static

    sr	:r := r:[0, u.d:[$1234_5678], 12345];

In this example, HLA initializes the sr variable with the byte value zero, followed by a dword containing $1234_5678 and a dword containing zero (to pad out the remainder of the union field), followed by a dword containing 12345.

You can also create records that have anonymous unions in them and then initialize a record object with a literal constant. Consider the following type declaration with an anonymous union:

type

    rau :record

            b:byte;

            union

                c:char;

                d:dword;

            endunion;

            w:word;

    endrecord;

Since anonymous unions within a record do not have a type associated with them, you cannot use the standard literal union constant syntax to initialize the anonymous union field (that syntax requires a type name). Instead, HLA offers you two choices when creating a literal record constant with an anonymous union field. The first alternative is to use the reserved word union in place of a typename when creating a literal union constant, e.g.,

static

    srau :rau := rau:[ 1, union.d:[$12345], $5678 ];

The second alternative is a shortcut notation. HLA allows you to simply specify a value that is compatible with the first field of the anonymous union and HLA will assign that value to the first field and ignore any other fields in the union, e.g.,

static

    srau2 :rau := rau:[ 1, 'c', $5678 ];

This is slightly dangerous since HLA relaxes type checking a bit here, but when creating tables of record constants, this is very convenient if you generally provide values for only a single field of the anonymous union; just make sure that the commonly used field appears first and you're in business.

Although HLA allows anonymous records within a union, there was no syntactically acceptable way to differentiate anonymous record fields from other fields in the union; therefore, HLA does not allow you to create union constants if the union type contains an anonymous record. The easy workaround is to create a named record field and specify the name of the record field when creating a union constant, e.g.,

type

    r	:record

            c:char;

            d:dword;

    endrecord;

    u	:union

            b:byte;

            x:r;

            w:word;

    endunion;

static

    y :u := u.x:[ r:[ 'a', 5]];

You may declare a union constant and then assign data to the specific fields as you would a record constant. The following example provides some samples of this:

type

    u_t :union

            b:byte;

            x:r;

            w:word;

    endunion;

val

    u :u_t;

        .

        .

        .

    ?u.b := 0;

        .

        .

        .

    ?u.w := $1234;

The two assignments above are roughly equivalent to the following:

    ?u := u_t.b:[0];

and

    ?u := u_t.w:[$1234];

However, to use the straight assignment (the former example) you must first declare the value u as a u_t union.

To access a value of a union constant, you use the familiar "dot notation" from records and other languages, e.g.,

    ?x := u.b;

        .

        .

        .

    ?y := u.w & $FF00;

Note, however, that you may only access the last field of the union into which you've stored some value. If you store data into one field and attempt to read the data from some other field of the union, HLA will report an error. Remember, you don't use union constants as a sneaky way to coerce one value's type to another (use the coercion functions for that purpose).

HLA allows a very limited form of a pointer constant. If you place an ampersand ("&") in front of a static object's name (i.e., the name of a static variable, readonly variable, uninitialized variable, segment variable, procedure, method, or iterator), HLA will compute the run-time offset of that variable. Pointer constants may not be used in arbitrary constant expressions. You may only use pointer constants in expressions used to initialize static or readonly variables or as constant expressions in 80x86 instructions. The following example demonstrates how pointer constants can be used:

program pointerConstDemo;

static

    t:int32;

    pt: pointer to int32 := &t;

begin pointerConstDemo;

    mov( &t, eax );

end pointerConstDemo;

Also note that HLA allows the use of the reserved word NULL anywhere a pointer constant is legal. HLA substitutes the value zero for NULL.

You may assign any of these structured literal constants to a compile-time variable or constant using an HLA compile-time assignment statement.

2.5.2: Compile-Time Functions

HLA provides the ability to create your own compile-time "functions" using macros, and we'll discuss how to do that in a little bit. In this section, however, we'll take a look at a small sample of the built-in compile-time functions that HLA automatically provides for you. HLA actually provides a large number of compile-time functions, too many to present here, so in this section we'll take a look at the ones that Win32 programmers commonly use. Please see the HLA Reference manual for more details on the HLA compile-time function facilities.

Though it is perfectly possible to write an assembly language program without ever calling an HLA compile-time function (or using the compile-time language at all, for that matter), the HLA compile-time functions can make it easier to develop certain macros and they can help make your programs easier to write and maintain. Therefore, it's a good idea to familiarize yourself with the HLA compile-time functions.

Note: don't confuse HLA's compile-time functions with the functions available in the HLA Standard Library. Although there are many similarities, Standard Library functions are run-time functions, compile-time functions execute during compilation.

The first set of compile-time functions to look at are the type-conversion functions. These compile-time function convert a constant expression you pass as an argument to the corresponding data type (or create a compile-time error if the conversion is not possible). These conversion functions are unusual insofar as they are the only set of HLA compile-time functions that don't begin with "@" (this functions just use the built-in HLA type names).

boolean( const_expr )

The expression must be an ordinal or string expression. If const_expr is numeric, this function returns false for zero and true for everything else. If const_expr is a character, this function returns true for "T" and false for "F". It generates an error for any other character value. If const_expr is a string, the string must contain "true" or "false" else HLA generates an error.

int8( const_expr ), int16( const_expr ), int32( const_expr )

int64( const_expr ), int128( const_expr )

uns8( const_expr ), uns16 const_expr ), uns32( const_expr )

uns64( const_expr ), uns128( const_expr )

byte( const_expr ), word( const_expr ), dword( const_expr )

qword( const_expr ), lword( const_expr )

These functions convert their parameter to the specified integer. For real operands, the result is truncated to form a numeric operand. For all other numeric operands, the result is ranged checked. For character operands, the ASCII code of the specified character is returned. For boolean objects, zero or one is returned. For string operands, the string must be a sequence of decimal characters which are converted to the specified type. Note that byte, word, and dword types are synonymous with uns8, uns16, and uns32 for the purposes of range checking.

real32( const_expr ), real64( const_expr ), real80( const_expr )

Similar to the integer functions above, except these functions produce the obvious real results. Only numeric and string parameters are legal.

char( const_expr )

Const_expr must be a ordinal or string value. This function returns a character whose ASCII code is that ordinal value. For strings, this function returns the first character of the string.

string( const_expr )

This function produces a reasonable string representation of the parameter. Almost all data types are legal.

cset( const_expr )

The parameter must be a character, string, or cset. For character parameters, this function returns the singleton set containing only the specified character. For strings, each character in the string is unioned into the set and the function returns the result. If the parameter is a cset, this function makes a copy of that character set.

The type conversion functions just described will automatically convert their operands from the source type to the destination type. Sometimes you might want to change the type of some object without changing its value. For many "conversions" this is exactly what takes place. For example, when converting and uns8 object to an uns16 value using the uns16(---) function, HLA does not modify the bit pattern at all. For other conversions, however, HLA may completely change the underlying bit pattern when doing the conversion. For example, when converting the real32 value 1.0 to a dword value, HLA completely changes the underlying bit pattern ($3F80_0000) so that the dword value is equal to one. On occasion, however, you might actually want to copy the bits straight across so that the resulting dword value is $3F80_0000. The HLA bit-transfer type conversion compile-time functions provide this facility.

The HLA bit-transfer type conversion functions are the following:

@int8( const_expr ), @int16( const_expr ), @int32( const_expr )

@int64( const_expr ), @int128( const_expr )

@uns8( const_expr ), @uns16 const_expr ), @uns32( const_expr )

@uns64( const_expr ), @uns128( const_expr )

@byte( const_expr ), @word( const_expr ), @dword( const_expr )

@qword( const_expr ), @lword( const_expr )

@real32( const_expr ), @real64( const_expr ), @real80( const_expr )

@char( const_expr )

@cset( const_expr )

The above functions extract eight, 16, 32, 64, or 128 bits from the constant expression for use as the value of the function. Note that supplying a string expression as an argument isn't particularly useful since the functions above will simply return the address of the string data in memory while HLA is compiling the program. The @byte function provides an additional syntax with two parameters, we'll describe that in a moment.

@string( const_expr )

HLA string objects are pointers (in both the language as well as within the compiler). So simply copying the bits to the internal string object would create problems since the bit pattern probably is not a valid pointer to string data during the compilation. With just a few exceptions, what the @string function does is takes the bit data of its argument and translates this to a string (up to 16 characters long). Note that the actual string may be between zero and 16 characters long since the HLA compiler (internally) uses zero-terminated strings to represent string constants. Note that the first zero byte found in the argument will end the string.

If you supply a string expression as an argument to @string, HLA simply returns the value of the string argument as the value for the @string function. If you supply a text object as an argument to the @string function, HLA returns the text data as a string without first expanding the text value (similar to the @string:identifier token). If you supply a pointer constant as an argument to the @string function, HLA returns the string that HLA will substitute for the static object when it emits the assembly file.

HLA provides a fair set of functions that let you determine attributes of various symbols and expressions during compilation. Here are some of these compile-time functions you'll commonly use:

]@name( identifier )

This function returns a string of characters that corresponds to the name of the identifier (note: after text/macro expansion). This is useful inside macros when attempting to determine the name of a macro parameter variable (e.g., for error messages, etc.). This function returns the empty string if the parameter is not an identifier.

@typename( identifier_or_expression )

This function returns the string name of the type of the identifier or constant expression. Examples include "int32", "boolean", and "real80".

@size( identifier_or_expression )

This function returns the size, in bytes, of the specified object.

@elementsize( identifier_or_expression )

This function returns the size, in bytes, of an element of the specified array. If the parameter is not an array identifier, this function generates an assembly-time error.

@offset( identifier )

For VAR, PARM, METHOD, and class ITERATOR objects only, this function returns the integer offset into the activation record (or object record) of the specified symbol.

@Is External( identifier )

This function returns true if the specified identifier is an external symbol.

@arity( identifier_or_expression )

This function returns zero if the specified identifier is not an array. Otherwise it returns the number of dimension of that array.

@dim( array_identifier_or_expression )

This function returns a single array of integers with one element for each dimension of the array passed as a parameter. Each element of the array returned by this function gives the number of elements in the specified dimension. For example, given the following code:

val threeD: int32[ 2, 4, 6];

    tdDims:= @dim( threeD );

The tdDims constant would be an array with the three elements [2, 4, 6];

@elements( array_identifier_or_expression )

This function returns the total number of elements in the specified array. For multi-dimensional array constants, this function returns the number of all elements, not just a particular row or column.

@defined( identifier )

This function returns true if the specified identifier is has been previously defined in the program and is currently in scope.

@isconst( expr ), @isreg( expr ), @isreg8( expr ), @isreg16( expr ), @isreg32( expr )

@isfreg( expr ),@ismem( expr ), @istype( identifier )

These functions return true if their argument is a constant, register, memory address, or type ID. These functions are useful in classifying macro parameters. This is not a complete list of the HLA compile-time classification functions, please consult the HLA Reference Manual to see the complete set.

@linenumber

This function returns the current line number in the source file.

@filename

This function returns the name of the current source file.

HLA provides several general purpose numeric functions that are great for initializing tables and doing other compile-time calculations. Here are some of these functions:

@abs( numeric_expr )

Returns the absolute equivalent of the numeric value passed as a parameter.

@byte( integer_expr, which ), @byte( real32_expr, which ), @byte( real64_expr, which )

@byte( real80_expr, which )

The @byte function extracts a single byte from a multi-byte data type. For integer/ordinal expressions, the which parameter is a value in the range 0..15. For real32 operands, the which parameter is a value in the range 0..3. This function extracts the specified byte from the value of the real32_expression parameter. For real64 operands, the which parameter is a value in the range 0..7. For real80 operands, the which parameter is a value in the range 0..9. These functions extract the specified byte from the value of their second operand.

@ceil( real_expr ), @floor( real_expr )

The @ceil function returns the smallest integer value larger than or equal to the expression passed as a parameter. The @floor function returns the largest integer value less than or equal to the supplied expression. Note that although the result will be an integer, these functions return a real80 value.

@cos( real_expr ), @sin( real_expr ), @tan( real_expr )

The real parameter is an angle in radians. These functions return the usual trigonometric values for the parameter you pass them.

@date

This function returns a string of the form "YYYY/MM/DD" containing the current date.

@exp( real_expr )

This function returns a real80 value that is the result of the computation ereal_expr.

@extract( cset_expr )

This function returns a character from the specified character set constant. Note that this function doesn't actually remove the character from the set, if you want to do that, then you will need to explicitly remove the character yourself.

@isalpha( char_expr ), @isalphanum( char_expr ), @isdigit( char_expr )

@islower( char_expr ), @isspace( char_expr ), @isupper( char_expr )

@isxdigit( char_expr )

These predicate functions return true or false based on whether there arguments belong to a certain set of characters. Note that "isxdigit" returns true if its argument is a hexadecimal character.

@log( real_expr ), @log10( real_expr )

@log returns the natural (base-e) logarithm of its argument, @log10 returns the base-10 logarithm of the supplied parameter.

@max( comma_separated_list_of_ordinal_or_real_values ),

@min( comma_separated_list_of_ordinal_or_real_values )

These functions return the maximum or minimum of the values in the specified list.

@odd( int_expr )

This function returns true if the integer expression is an odd number.

@random( int_expr )

This function returns a random uns32 value.

@randomize( int_expr )

This function uses the integer expression passed as a parameter as the new seed value for the random number generator.

@sqrt( real_expr )

This function returns the square root of the parameter.

@time

This function returns a string of the form "HH:MM:SS xM" (x= A or P) denoting the time at the point this function was called (according to the system clock).

HLA provides a large number of compile-time string functions. These functions are especially useful for processing macro parameters and other string constants in an HLA compile-time program. Here are some of the compile-time string functions that HLA provides (see the HLA Reference Manual for the complete list):

@delete( str_expr, int_start, int_len )

This function returns a string consisting of the str_expr passed as a parameter with ( possibly) some characters removed. This function removes int_len characters from the string starting at index int_start (note that strings have a starting index of zero).

@index( str_expr1, int_start, str_expr2 )

This function searches for str_expr2 within str_expr1 starting at character position int_start within str_expr1. If the string is found, this function returns the index into str1_expr1 of the first match (starting at int_start). This function returns -1 if there is no match.

@insert( str_expr1, int_start, str_expr2 ), @index( str_expr1, int_start, str_expr2 )

The insert function inserts str_expr2 into str_expr1 just before the character at index int_start. The rindex function does the same, except int_start is from the end of the string rather than the start of the string. These functions return the converted string (note that these functions do not modify the original string).

@length( str_expr )

This function returns the length of the specified string.

@lowercase( str_expr, int_start ), @uppercase( str_expr, int_start )

These functions return a string of characters from str_expr with all uppercase alphabetic characters converted to lower case or vice versa. Only those characters from int_start on are copied into the result string.

@substr( str_expr, int_start, int_len )

This function returns the substring specified by the starting position and length in str_expr.

HLA also provides a large number of string/pattern matching functions. Because of the large number of these functions, we'll not regurgitate their description here. Please see the HLA Reference Manual for details on the compile-time pattern matching functions.

HLA provides a large number of additional compile-time functions and "pseudo-variables" that you may find useful when creating compile-time programs. Space limitations prevent the complete exposition of these functions here. This chapter just high-lights some of the more common functions in order to give you a taste of what is possible when using the HLA compile-time language. As a Win32 programmer, you might not use all of the functions this chapter describes and you might also use several of HLA's compile-time functions that this chapter does not describe. The set of functions you might use in an application are as varied as the number of applications that have been written.

2.5.3: Generating Code With a Compile-Time Statement

Up to this point, the HLA compile-time statements and functions we've seen generate constant results. They're useful for embedded within machine instructions or in data declaration statements (e.g., to initialize tables); for example "mov( @length( someString), eax );" automatically substitutes the length of a compile-time string constant as the source operand of this instruction, the program reflects any changes made to the string by adjusting the value of the source operand each time you recompile the program. However, we haven't looked at how you can actually select which statements in your program to compile or how to iteratively process statements (that is, inject multiple copies of a statement into a program).

Why would you want to do this? Well, consider a data table with 2,000 elements where each entry of the table is initialized with it's index plus the sum of the previous two elements (assuming elements before zero contain the value zero). You could manually create this table in HLA as follows:

uns32 0, 1, 3, 7, 14, 26, ...

Of course, filling in a table like this one, with 2,000 elements, is laborious. Furthermore, chances are pretty good you'd make a mistake somewhere along the line (quick, can you spot any mistakes in this example?). Fortunately, generating a table like this one is very easy to do by writing a short HLA compile-time program. Writing such HLA compile time programs can save you considerable effort, help you write correct code, and make it easier to verify the correctness of your programs. So let's take a look at some of the statements in the HLA compile-time language that allow you to do this.

2.5.4: Conditional Assembly (#if..#elseif..#else..#endif)

HLA's compile-time language provides an "if" statement that allows you to decide, at compile-time, whether certain statements will be present in the actual object code. Many languages (including various assemblers) provide this facility and they call it "conditional assembly" or "conditional compilation". The conditional compilation statements in HLA use the following syntax:

#if( constant_boolean_expression )

    << Statements to compile if the >>

    << expression above is true.    >>

#elseif( constant_boolean_expression )

    << Statements to compile if the >>

    << expression immediately above >>

    << is true and the first expres->>

    << sion above is false.         >>

#else

    << Statements to compile if both    >>

    << the expressions above are false. >>

#endif

The #elseif and #ELSE clauses are optional. Like the HLA run-time elseif clause, there may be more than one #elseif clause in the same conditional #if sequence.

Unlike some other assemblers and high level languages, HLA's conditional compilation directives are legal anywhere whitespace is legal. You could even embed them in the middle of an instruction! While directly embedding these directives in an instruction isn't recommended (because it would make your code very hard to read), it's nice to know that you can place these directives in a macro and then replace an instruction operand with a macro invocation.

The constant expression in the #if and #elseif clauses must be of type boolean or HLA will emit an error. Any legal compile-time expression that produces a boolean result is legal here. Keep in mind that conditional compilation directives are executed at compile-time, not at run-time. You would not use these directives to (attempt to) make decisions while your program is actually running.

Programmers use conditional assembly for a variety of purposes in HLA programs. A very common use of conditional assembly is to compile different code sequences based on the environment in which the program is going to run. For example, some programmers use conditional assembly to embed non-portable code sequences into their programs and then they select between various code sequences by some boolean expression. For example, HLA programmers who are writing code that is to be portable between Linux and Windows might use conditional compilation sequences like the following:

    #if( os.win32 ) // os.win32 and os.linux are boolean constants provided by the

                         // HLA standard library that specifies the OS you're using.

        << code that is Windows specific >>

    #elseif( os.linux )

        << code that is Linux specific >>

    #endif

Of course, this book isn't dealing with code that could run under Windows or Linux, so this feature is probably of little use to the average reader of this book. However, this same idea is useful for writing code that makes advantage of features available only in certain versions of Windows. For example, if you want to make an API call that is available only in Windows XP or later, but you still want to be able to create a version of your application that runs under earlier versions of Windows, you can use this same trick, e.g.,

    #if( WinXPorLater )

        << Code that uses WinXP-specific API functions >>

    #else

        << code that deals with the fact that this API is missing >>

    #endif

In this example, WinXPorLater is a compile-time boolean value that you've defined in your program. A programmer will typically set this variable true or false depending upon whether they want to compile a version of their program for WinXP or an earlier version of Windows.

A traditional trick, inherited from the C/C++ programming community, is to test to see whether a symbol is defined or not defined to determine if code should be assembled one way or another. This technique is easy to use in HLA by employing the @defined compile-time variable, e.g.,

    #if( @defined( WinXPorLater ))

        << Code that uses WinXP-specific API functions >>

    #else

        << code that deals with the fact that this API is missing >>

    #endif

Note that WinXPorLater's type and value are irrelevant. In fact, WinXPorLater doesn't even have to be a constant or compile-time variable (i.e., you can use any legal HLA identifier here, the @defined function returns true if the symbol is defined, false if it is not defined, and it completely ignores the other attributes the symbol may possess). HLA programmers who commonly use @defined in this manner generally define such symbols near the beginning of their programs and, although the type and value is irrelevant, they tend to define such compile-time variables as boolean with the value true.

Specifically to support the @defined compile-time function, the HLA provides a compile-time option that lets you define symbols. This command option takes the following form:

-dSymbolToDefine

This creates an HLA const definition for SymbolToDefine, setting the type to boolean and the value to false.

Here's a suggestion if you're going to use HLA's command-line feature to support compilation options via conditional assembly. Rather than use @defined all over your code, just put a short compile-time sequence like the following near the beginning of your source file:

    #if( !@defined( SymbolToDefine ))

        ?SymbolToDefine := false;

    #endif

This sequence guarantees that SymbolToDefine is defined in your source file. It's value (true or false) determines how conditional assembly will take place later on by simply using the SymbolToDefine symbol as a boolean expression in your code. The advantage of using this technique, rather than just using @defined, is that you can turn the feature on and off throughout the source code by injecting compile-time assignments like "?SymbolToDefine := true;" and "SymbolToDefine := false;" into your source file.

Of course, conditional assembly has far more uses than simply letting you incorporate different program options in the source file that you can control via simple boolean expressions. Conditional assembly, of course, is what you use to make decisions in an HLA compile-time program. The examples thus far have used conditional assembly to select between one of several sequences of instructions to compile into the final run-time program. In fact, many HLA #if..#endif sequences don't contain any code emitting statements at all, instead they just contain sequence of other HLA compile-time language statements. Combined with HLA's compile-time functions, it's quite possible to do some very sophisticated processing. Here's a simple example that might be used to prevent problems when generating a table of log values:

    #if( r > 0 )

        ?logVal := @log( r );

    #else

        #error( "Illegal value passed to @log" )

    #endif

Here's another example that you might find in a macro that tests a parameter's type to determine how to process the parameter:

    #if( @typename( macroParm ) = "int32" )

        << do something if the macro parameter is a 32-bit integer >>

    #elseif( @typename( macroParm ) = "int16" )

        << do something if the macro parameter is a 16-bit integer >>

    #elseif( @typename( macroParm ) = "int88" )

        << do something if the macro parameter is an 8-bit integer >>

    #else

        #error( "Expected an int32, int16, or int8 parameter" )

    #endif

To see some complex examples of HLA's #if..#elseif..#else..#endif statement in action, check out the source code to many of the HLA Standard Library routines (especially in several of the HLA Standard Library header files). The HLA Standard Library stdout.put macro is an extreme example that you might want to take a look at.

2.5.5: The #for..#endfor Compile-Time Loop

The #for..#endfor loop can take one of the following forms:

#for( loop_control_var := Start_expr to end_expr )

    << Statements to execute as long as the loop control variable's >>

    << value is less than or equal to the ending expression.        >>

#endfor

#for( loop_control_var := Start_expr downto end_expr )

    << Statements to execute as long as the loop control variable's >>

    << value is greater than or equal to the ending expression.     >>

#endfor

The HLA compile-time #for..#endfor statement is very similar to the for loops found in languages like Pascal and BASIC. This is a definite loop that executes some number of times determine when HLA first encounters the #for directive (this can be zero or more times, but HLA computes the number only once when HLA first encounters the #for). The loop control variable must be a val object or an undefined identifier (in which case, HLA will create a new val object with the specified name). Also, the number control variable must be an eight, sixteen, or thirty-two bit integer value (uns8, uns16, uns32, int8, int16, or int32). Also, the starting and ending expressions must be values that an int32 val object can hold.

The#for loop with the to clause initializes the loop control variable with the starting value and repeats the loop as long as the loop control variable's value is less than or equal to the ending expression's value. The #for..to..#endfor loop increments the loop control variable on each iteration of the loop.

The#for loop with the downto clause initializes the loop control variable with the starting value and repeats the loop as long as the loop control variable's value is greater than or equal to the ending expression's value. The #for..downto..#endfor loop decrements the loop control variable on each iteration of the loop.

Note that the #for..to/downto..#endfor loop only computes the value of the ending expression once, when HLA first encounters the #for statement. If the components of this expression would change as a result of the execution of the #for loop's body, this will not affect the number of loop iterations. If you need this capability, you will need to use HLA's compile-time indefinite loop (the #while loop, see the next section).

The #for..#endfor loop can also take the following form:

#for( loop_control_var in composite_expr )

    << Statements to execute for each element present in the expression >>

#endfor

The composite_expr in this syntactical form may be a string, a character set, an array, or a record constant.

This particular form of the #for loop repeats once for each item that is a member of the composite expression. For strings, the loop repeats once for each character in the string and the loop control variable is set to each successive character in the string. For character sets, the loop repeats for each character that is a member of the set; the loop control variable is assigned the value of each character found in the set (you should assume that the extraction of characters from the set is arbitrary, even though the current implementation extracts them in order of their ASCII codes). For arrays, this #for loop variant repeats for each element of the array and assigns each successive array element to the loop control variable. For record constants, the #for loop extracts each field and assigns the fields, in turn, to the loop control variable.

Examples:

    #for( c in "Hello" )

        #print( c )  // Prints the five characters 'H', 'e', ..., 'o'

    #endfor

    // The following prints a..z and 0..9 (not necessarily in that order):

    #for( c in {'a'..'z', '0'..'9'} )

        #print( c )

    #endfor

    // The following prints 1, 10, 100, 1000

    #for( i in [1, 10, 100, 1000] )

        #print( i )

    #endfor

    // The following prints all the fields of the record type r

    // (presumably, r is a record type you've defined elsewhere):

    #for( rv in r:[0, 'a', "Hello", 3.14159] )

        #print( rv )

    #endfor

The HLA compile-time #for loop is really useful for processing variable parameter lists in a macro. HLA creates an array of strings with each array element containing the text for each macro parameter. By using this latter form of the #for loop you can process each element of the macro in order. You'll see this feature used when we discuss HLA's macro facilities in a few sections.

One very useful purpose for the #for..#endfor loop is to construct data tables at compile time. For example, suppose you want to create a table of sine values for each of the angles in the range 0..359 degrees. You could easily do this in HLA as follows:

static

    sineTable: real32[ 360 ]; @nostorage;

        #for( angle := 0 to 359 )

            // Note: the HLA compile-time @sin function requires an angle in radians.

            // Conversion from degrees to radians is via the formula:

            // radians = angle * 2 * pi / 360

            real32 @sin( angle * 3.14159 / 180.0 );

        #endfor

HLA's #for..#endfor compile-time statement is sufficiently powerful that you can generate almost any type of data table you need at compile-time without having to enter in the data for the entire table yourself (or compute the table entries at run-time, consuming both space for the machine instructions and time to execute those instructions). For the few cases where the #for loop isn't entirely appropriate, you'll probably want to consider using the HLA #while loop that the next section describes.

2.5.6: The #while..#endwhile Compile-Time Loop

HLA provides an indefinite looping mechanism in the compile-time language via the #while..#endwhile loop. The #while..#endwhile compile-time loop takes the following form:

#while( constant_boolean_expression )

    << Statements to emit repeatedly as long >>

    << as the expression is true.            >>

#endwhile

While processing the #while..#endwhile loop, HLA evaluates the constant boolean expression. If it is false, HLA immediately skips to the first statement beyond the #endwhile directive.

If the expression is true, then HLA proceeds to compile the body of the#while loop. Upon encountering the #endwhile directive, HLA jumps back up to the #while clause in the source code and repeats this process until the expression evaluates false.

Warning: since HLA allows you to create loops in your source code that evaluation during the compilation process, HLA also allows you to create infinite loops that will lock up the system during compilation. If HLA seems to have gone off into la-la land during compilation and you're using #while loops in your code, it might not be a bad idea to put some #print directives9 into your loop(s) to see if you've created an infinite loop.

The HLA #while..#endwhile loop is great for repeatedly emitting sections of code whose loop control expression varies while processing the loop (unlike the #for..#endfor loop, for which HLA can compute the number of iterations when HLA first encounters the #for clause). Though nowhere near as popular in modern HLA code as the #for..#endfor loop, the #while..#endwhile loop is still quite useful in many situations. The #while..#endwhile loop, for example, is quite useful when you want to increment or decrement a non-integer loop control variable, e.g.,

    ? r :real64 := 10.2;

    #while( r > 0 )

        << Statements to process while r is greater than zero >>

        ? r := r - 0.1;

    #endwhile

If you look at some older HLA code (e.g., in the HLA Standard Library), you find the #while..#endwhile loop used in many cases where a #for..#endfor loop might be more appropriate. This is because the #for..#endfor loop is a fairly recent addition to the HLA language, appearing long after the creation of the HLA Standard library.

2.5.7: Compile-Time I/O and Data Facilities

HLA's compile-time language provides several facilities for printing messages during compilation, reading and writing text file data, executing system commands, and processing blocks of text as string data in the HLA source file. Though some of these features are rather esoteric, they are quite useful in some circumstances. We'll take a look at many of these HLA features in this section.

The #system directive requires a single string parameter. It executes this string as an operating system (shell/command interpreter) operation via the C "system" function call. This call is useful, for example, to run a program during compilation that dynamically creates a text file that an HLA program may include immediately after the #system invocation.

Example of #system usage:

#system( "dir" )

Note that the "#system" directive is legal anywhere white space is allowable and doesn't require a semicolon at the end of the statement.

Do keep in mind that HLA does not have any control over the programs you run via the #system compile-time language statement. It is your responsibility to ensure that you don't run some program that interferes with the current assembly in process (e.g., deleting the current source file that HLA is compiling would be an example of interference that might leave the current compilation in an undefined state).

The #print directive displays its parameter values during compilation. The basic syntax is the following:

#print( comma, separated, list, of, constant, expressions, ... )

The #print statement is very useful for displaying messages during assembly (e.g., when debugging complex macros or compile-time programs). The items in the #print list must evaluate to constant (const or val) values at compile time. This directive is great for displaying status information during compilation. It's also great for tracking down bugs in a compile-time program (e.g., if HLA hangs up and you suspect that this is due to an infinite #while loop in your code, you can use #print to track down exactly where HLA is hanging up).

The #error directive behaves like #print insofar as it prints its parameter to the console device during compilation. However, this instruction also generates an HLA error message and does not allow the creation of an object file after compilation. This statement only allows a single string expression as a parameter. If you need to print multiple values of different types, use string concatenation and the @string function to achieve this. Example:

#error( "Error, unexpected value.  Value = " + #string( theValue ))

Notice that neither the #print nor the #error statements end with a semicolon.

The #openwrite, #write, and #closewrite compile-time statements let you do simple file output during compilation. The #openwrite statement opens a single file for output, #write writes data to that output file, and #closewrite closes the file when output is complete. These statements are useful for automatically generating include files that the source file will include later on during the compilation. These statements are also useful for storing bulk data for later retrieval or generating a log during assembly.

The #openwrite statement uses the following syntax:

#openwrite( string_expression )

This call opens a single output file using the filename specified by the string expression. If the system cannot open the file, HLA emits a compilation error. Note that #openwrite only allows one output file to be active at a time. HLA will report an error if you execute #openwrite and there is already an output file open. If the file already exists, HLA deletes it prior to opening it (so be careful!). If the file does not already exist, HLA creates a new one with the specified name.

The #write statement uses the same syntax as the #print directive. Note, however, that #write doesn't automatically emit a newline after writing all its operands to the file; if you want a newline output you must explicitly supply it as the last parameter to #write.

The #closewrite statement closes the file opened via #openwrite. HLA automatically closes this file at the end of assembly if you leave it open. However, you must explicitly close this file before attempting to use the data (via #include or #openread) in your program. Also, since HLA allows only one open output file at a time, you must use #closewrite to close the file before you can open another with #openwrite.

Here is an example of the #openwrite, #write, and #closewrite statements in action. This short compile-time program creates an array of integer values for inclusion elsewhere in the source file:

    #openwrite( "myDataInclude.hhf" )

    #write( "[" )

    #for( i := 0 to 2047 )

        #write( i )

        #if( i <> 2047 )

            #write( ", " )

        #elseif( ( i mod 16) == 15 )

            #write( nl )

        #endif

    #endfor

    #write( "];" )

    #closewrite

        .

        .

        .

const

    constArray :dword[2048] :=

        #include( "myDataInclude.hhf" )

Of course, initializing an array in this manner is rather silly, because we could have (more easily) initialized the array directly with an HLA compile-time program (e.g., the #for loop above with a few minor modifications would have been sufficient). However, it's easy enough to imagine building up the include file at various points throughout the code, thus making the use of #openwrite/#write/#closewrite more applicable.

The #openread, @read, and #closeread compile-time statements and function let you do simple file input during compilation. The #openread statement opens a single file for input, @read is a compile-time function that reads a line of text from the file, and #closeread closes the file when input is complete. These statements are useful for reading files produced by #openwrite/#write/#closewrite during compilation, or any other text file for that matter.

The #openread statement uses the following syntax:

#openread( filename )

The filename parameter must be a string expression or HLA reports an error. HLA attempts to open the specified file for reading; HLA prints an error message if it cannot open the file.

The @read function uses the following call syntax:

@read( val_object )

The val_object parameter must either be a symbol you've defined in a val section (or via "?") or it must be an undefined symbol (in which case @read defines it as a val object). @read is an HLA compile-time function (hence the "@" prefix rather than "#"; HLA uses "#" for compile-time statements). It returns either true or false, true if the read was successful, false if the read operation encountered the end of file. Note that if any other read error occurs, HLA will print an error message and return false as the function result. If the read operation is successful, then HLA stores the string it read (up to 4095 characters) into the val object specified by the parameter. Unlike #openread and #closeread, the @read function may not appear at an arbitrary point in your source file. It must appear within a constant expression since it returns a boolean result (and it is your responsibility to check for EOF).

The #closeread statement closes the input file. Since you may only have one open input file at a time, you must close an open input file with #closeread prior to opening a second file. Syntax:

#closeread

Example of using compile-time file I/O:

#openwrite( "hw.txt" )

#write( "Hello World", nl )

#closewrite

#openread( "hw.txt" )

?goodread := @read( s );

#closeread

#print( "data read from file = ", s )

The #text and #endtext directives surround a block of text in an HLA program from which HLA will create an array of string constants. The syntax for these directives is:

    #text( identifier )

        << arbitrary lines of text >>

    #endtext

The identifier must either be an undefined symbol or an object declared in the val section.

This directive converts each line of text between the #text and #endtext directives into a string and then builds an array of strings from all this text. After building the array of strings, HLA assigns this array to the identifier symbol. This is a val constant array of strings. The #text..#endtext directives may appear anywhere in the program where white space is allowed.

These directives provide an easy way to initialize a constant array of strings and they provide a convenient alternative to using #openread/@read/#closeread when you simply want to inject a fair amount of textual data into your HLA source file for further processing by the compile-time language. Here is an example that uses the #text..#endtext to initialize an array of strings in a program.

#text( constStringArray )

This goes into the first string.

This line goes into the second string.

The third string will contain this line.

And so on...

#endtext

static

    strArray :string[] := constStringArray;

By using #text..#endtext or the #openread..@read..#closeread compile-time statements, it is actually possible to create your own languages using the HLA compile-time language. Processing the text appearing in a #text..#endtext block, or the text read from a file via the #openread..@read..#closeread statements is one place where HLA's compile-time string and pattern matching functions come in real handy.

2.5.8: Macros (Compile-Time Procedures and Functions)

HLA provides one of the most sophisticated macro processing facilities of any assembler, indeed, any language, available. Macros are a great tool for Win32 programmers because a considerable amount of Win32 programming is the repetitive application of some common programming template. Learning how to properly use macros can help you reduce the drudgery often associated with assembly language programming. Unfortunately, HLA's macro sophistication comes with a price tag: if you want to learn the macros inside and out, there is a steep learning curve associated with them. Fortunately, you don't have to learn everything there is to know about HLA macros in order to effectively employ them in your source files. This section will concentrate on those features of the HLA macro subsystem that a Win32 programmer will commonly use.

You can declare macros almost anywhere HLA allows whitespace in a program using the following syntax:

#macro identifier ( optional_parameter_list ) ;

    statements

#endmacro

Note that a semicolon does not follow the #endmacro clause.

The optional parameter list must be a list of one or more identifiers separated by commas. Unlike procedure declarations, you do not associate a type with macro parameters. HLA automatically associates the type "text" with all macro parameters (except for one special case noted below). Example:

    #macro MacroWParms( a, b, c );

        ?a = b + c;

    #endmacro

Optionally, the last (or only) name in the identifier list may take the form "identifier[]". This syntax tells the macro that it may allow a variable number of parameters and HLA will create an array of string objects to hold all the parameters (HLA uses a string array rather than a text array because text arrays are illegal).

Example:

    #macro MacroWVarParms( a, b, c[] );

        ?a = b + text(c[0]) + text(c[1]);

    #endmacro

If the macro does not allow any parameters, then you follow the identifier with a semicolon (i.e., no parentheses or parameter identifiers). See the first example in this section for a macro without any parameters.

Occasionally you may need to define some symbols that are local to a particular macro invocation (that is, each invocation of the macro generates a unique symbol for a given identifier). The local identifier list allows you to do this. To declare a list of local identifiers, simply following the parameter list (after the parenthesis but before the semicolon) with a colon (":") and a comma separated list of identifiers, e.g.,

        #macro ThisMacro(parm1):id1,id2;

        ...

HLA automatically renames each symbol appearing in the local identifier list so that the new name is unique throughout the program. HLA creates unique symbols of the form "_XXXX_" where XXXX is some hexadecimal numeric value. To guarantee that HLA can generate unique symbols, you should avoid defining symbols of this form in your own programs (in general, symbols that begin and end with an underscore are reserved for use by the compiler and the HLA standard library). Example:

    #macro LocalSym : i,j;

    j:	cmp(ax, 0)

        jne( i )

        dec( ax )

        jmp( j )

    i:

    #endmacro

Without the local identifier list, multiple expansions of this macro within the same procedure would yield multiple statement definitions for "i" and "j". With the local statement present, however, HLA substitutes symbols similar to "_0001_" and "_0002_" for i and j for the first invocation and symbols like "_0003_" and "_0004_" for i and j on the second invocation, etc. This avoids duplicate symbol errors if you do not use (poorly chosen) identifiers like "_0001_" and "_0004_" in your code.

The statements section of the macro may contain any legal HLA statements (including definitions of other macros). However, the legality of such statements is controlled by where you expand the macro.

To invoke a macro, you simply supply its name and an appropriate set of parameters. Unless you specify a variable number of parameters (using the array syntax) then the number of actual parameters must exactly match the number of formal parameters. If you specify a variable number of parameters, then the number of actual parameters must be greater than or equal to the number of formal parameters (not counting the array parameter).

During macro expansion, HLA automatically substitutes the text associated with an actual parameter for the formal parameter in the macro's body. The array parameter, however, is a string array rather than a text array so you will have force the expansion yourself using the "@text" function:

    #macro example( variableParms[] );

        ?@text(variableParms[0]) := @text(variableParms[1]);

    #endmacro

Actual macro parameters consist of a string of characters up to, but not including a separate comma or the closing parentheses, e.g.,

    example( v1, x+2*y )

"v1" is the text for parameter #1, "x+2*y" is the text for parameter #2. Note that HLA strips all leading whitespace and control characters before and after the actual parameter when expanding the code in-line. The example immediately above would expand do the following:

    ?v1 := x+2*y;

If (balanced) parentheses appear in some macro's actual parameter list, HLA does not count the closing parenthesis as the end of the macro parameter. That is, the following is perfectly legal:

    example(  v1, ((x+2)*y) )

This expands to:

    ?v1 := ((x+2)*y);

If you need to embed commas or unmatched parentheses in the text of an actual parameter, use the HLA literal quotes "#(" and ")#" to surround the text. Everything (except surrounding whitespace) inside the literal quotes will be included as part of the macro parameter's text. Example:

    example( v1, #( array[0,1,i] )# )

The above expands to:

    ?v1 := array[0,1,i];

Without the literal quote operator, HLA would have expanded the code to

    ?V1 := array[0;

and then generated an error because (1) there were too many actual macro parameters (four instead of two) and (2) the expansion produces a syntax error.

Of course, HLA's macro parameter parser does not consider commas appearing inside string or character constants as parameter separators. The following is perfectly legal, as you would expect:

    example( charVar, `,' )

As you may have noticed in these examples, a macro invocation does not require a terminating semicolon. Macro expansion occurs upon encountering the closing parenthesis of the macro invocation. HLA uses this syntax to allow a macro expansion anywhere in an HLA source file. Consider the following:

#macro funny( dest )

    , dest );

#endmacro

    mov( 0 funny( ax )

This code expands to "mov( 0, ax );" and produces a legal machine instruction. Of course, the this is a truly horrible example of macro use (the style is really bad), but it demonstrates the power of HLA macros in your program. This "expand anywhere" philosophy is the primary reason macro invocations do not end with a semicolon.

HLA macros provide some very powerful facilities not found in other macro assemblers. One of the really unique features that HLA macros provides is support for multi-part (or context-free) macro invocations. This feature is accessed via the #terminator and #keyword reserved words. Consider the following macro declaration:

program demoTerminator;

#include( "stdio.hhf" );

#macro InfLoop:TopOfLoop, LoopExit;

    TopOfLoop:

#terminator endInfLoop;

    jmp TopOfLoop;

    LoopExit:

#endmacro;

static

    i:int32;

begin demoTerminator;

    mov( 0, i );

    InfLoop

        stdout.put( "i=", i, nl );

        inc( i );

    endInfLoop;

end demoTerminator;

The #terminator keyword, if it appears within a macro, defines a second macro that is available for a one-time use after invoking the main macro. In the example above, the "endInfLoop" macro is available only after the invocation of the "InfLoop" macro. Once you invoke the EndInfLoop macro, it is no longer available (though the macro calls can be nested, more on that later). During the invocation of the #terminator macro, all local symbols declared in the main macro (InfLoop above) are available (note that these symbols are not available outside the macro body. In particular, you could not refer to either "TopOfLoop" nor "LoopExit" in the statements appearing between the InfLoop and endInfLoop invocations above). The code above, by the way, emits code similar to the following:

_0000_:

        stdout.put( "i=", i, nl );

        inc( i );

        jmp _0000_;

_0001_:

The macro expansion code appears in italics. This program, therefore, generates an infinite loop that prints successive integer values.

These macros are called multi-part macros for the obvious reason: they come in multiple pieces (note, though, that HLA only allows a single #terminator macro). They are also referred to as Context-Free macros because of their syntactical nature. Earlier, this document claimed that you could refer to the #terminator macro only once after invoking the main macro. Technically, this should have said "you can invoke the terminator once for each outstanding invocation of the main macro." In other words, you can nest these macro calls, e.g.,

    InfLoop

        mov( 0, j );

        InfLoop

            inc( i );

            inc( j );

            stdout.put( "i=", i, " j=", j, nl );

        endInfLoop;

    endInfLoop;

The term Context-Free comes from automata theory; it describes this nestable feature of these macros.

As should be painfully obvious from this InfLoop macro example, it would be really nice if one could define more than one macro within this context-free group. Furthermore, the need often arises to define limited-scope scope macros that can be invoked more than once (limited-scope means between the main macro call and the terminator macro invocation). The #keyword definition allows you to create such macros.

In the InfLoop example above, it would be really nice if you could exit the loop using a statement like "brkLoop" (note that "break" is an HLA reserved word and cannot be used for this purpose). The #keyword section of a macro allows you to do exactly this. Consider the following macro definition:

#macro InfLoop:TopOfLoop, LoopExit;

    TopOfLoop:

#keyword brkLoop;

    jmp LoopExit;

#terminator endInfLoop;

    jmp TopOfLoop;

    LoopExit:

#endmacro;

As with the "#terminator" section, the "#keyword" section defines a macro that is active after the main macro invocation until the terminator macro invocation. However, #keyword macro invocations to not terminate the multi-part invocation. Furthermore, #keyword invocations may occur more that once. Consider the following code that might appear in the main program:

    mov( 0, i );

    InfLoop

        mov( 0, j );

        InfLoop

            inc( j );

            stdout.put( "i=", i, " j=", j, nl );

            if( j >= 10 ) then

                brkLoop;

            endif

        endInfLoop;

        inc( i );

        if( i >= 10 ) then

            brkLoop;

        endif;

    endInfLoop;

The "brkLoop" invocation inside the "if( j >= 10)" statement will break out of the inner-most loop, as expected (another feature of the context-free behavior of HLA's macros). The "brkLoop" invocation associated with the "if( i >= 10 )" statement breaks out of the outer-most loop. Of course, the HLA (run-time) language provides the forever..endfor loop and the break and breakif statements, so there is no need for this InfLoop macro, nevertheless, this example is useful because it is easy to understand. If you are looking for a challenge, try creating a statement similar to the C/C++ switch/case statement; it is perfectly possible to implement such a statement with HLA's macro facilities, see the HLA Standard Library for an example of the switch statement implemented as a macro.

The discussion above introduced the "#keyword" and "#terminator" macro sections in an informal way. There are a few details omitted from that discussion. First, the full syntax for HLA macro declarations is actually:

#macro identifier ( optional_parameter_list ) :optional_local_symbols;

    statements

#keyword identifier ( optional_parameter_list ) :optional_local_symbols;

    statements

note: additional #keyword declarations may appear here

#terminator identifier ( optional_parameter_list ) :optional_local_symbols;

    statements

#endmacro

There are three things that should immediately stand out here: (1) You may define more than one #keyword within a macro. (2) #keywords and #terminators allow optional parameters. (3) #keyword and #terminator allow their own local symbols.

The scope of the parameters and local symbols isn't particularly intuitive (although it turns out that the scope rules are exactly what you would want). The parameters and local symbols declared in the main macro declaration are available to all statements in the macro (including the statements in the #keyword and #terminator sections). The InfLoop macro used this feature because the jmp instructions in the brkLoop and endInfLoop sections referred to the local symbols declared in the main macro. The InfLoop macro did not declare any parameters, but had they been present, the brkLoop and endInfLoop sections could have used those symbols as well.

Parameters and local symbols declared in a #keyword or #terminator section are local to that particular section. In particular, parameters and/or local symbols declared in a #keyword section are not visible in other #keyword sections or in the #terminator section.

One important issue is that local symbols in a multipart macro are visible in the main code between the start of the multipart macro and the terminating macro. That is, if you have some sequence like the following:

        InfLoop

            jmp LoopExit;

        endInfLoop;

Then HLA substitutes the appropriate internal symbol (e.g., "_xxxx_") for the LoopExit symbol. This is somewhat unintuitive and might be considered a flaw in HLA's design. Future versions of HLA may deal with this issue; in the meantime, however, some code takes advantage of this feature (to mask global symbols) so it's not easy to change without breaking a lot of code. Be forewarned before taking advantage of this "feature", however, that it will probably change in HLA v2.x. An important aspect of this behavior is that macro parameter names are also visible in the code section between the initial macro and the terminator macro. Therefore, you must take care to choose macro parameter names that will not conflict with other identifiers in your program. E.g., the following will probably lead to some problems:

static

    i:int32;

#macro parmi(i);

    mov( i, eax );

#terminator endParmi;

    mov( eax, i );

#endmacro

    .

    .

    .

    parmi( xyz );

    mov( i, ebx );					// actually moves xyz into ebx, since the parameter i

                        // overrides the global variable i here.

    endParmi;

As mentioned earlier, HLA treats all non-array macro parameters as text constants that are assigned a string corresponding to the actual parameter(s) passed to the macro. I.e., consider the following:

#macro SetI( v );

    ?i := v;

#endmacro

SetI( 2 );

The above macro and invocation is roughly equivalent to the following:

const

    v : text := "2";

    ?i := v;

When utilizing variable parameter lists in a macro, HLA treats the parameter object as a string array rather than a text array (because HLA v1.x does not currently support text arrays). For example, consider the following macro and invocation:

#macro SetI2( v[] );

    ?i := v[0];

#endmacro

SetI2( 2 );

Although this looks quite similar to the previous example, there is a subtle difference between the two. The former example will initialize the constant (value) i with the int32 value two. The second example will initialize i with the string value "2".

If you need to treat a macro array parameter as text rather than as a string object, use the HLA "@text" function that expands a string parameter as text. E.g., the former example could be rewritten as:

#macro SetI2( v[] );

    ?i := @text( v[0]);

#endmacro

SetI2( 2 );

In this example, the @text function tells HLA to expand the string value v[0] (which is "2") directly as text, so the "SetI2( 2 )" invocation expands as

?i := 2;

rather than as

?i := "2";

On occasion, you may need to do the converse of this operation. That is, you may want to treat a standard (non-array) macro parameter as a string object rather than as a text object. Unfortunately, text objects are expanded by the lexer in-line upon initial processing; the compiler never sees the text variable name (or parameter name, in this particular case). To overcome this problem, the lexer has a special feature to avoid expanding text constants if they appear inside an @string compile-time function. The following example demonstrates one possible use of this feature:

program demoString;

#macro seti3( v );

    #print( "i is being set to " + @string( v ))

    ?i := v;

#endmacro

begin demoString;

    seti3( 4 )

    #print( "i = " + string( i ) )

    seti3( 2 )

    #print( "i = " + string( i ) )

end demoString;

If an identifier is a text constant (e.g., a macro parameter or a const/value identifier of type text), special care must be taken to modify the string associated with that text object. A simple val expression like the following won't work:

?textVar:text := "SomeNewText";

The reason this doesn't work is subtle: if textVar is already a text object, HLA immediately replaces textVar with its corresponding string; this includes the occurrence of the identifier immediately after the "?" in the example above. So were you to execute the following two statements:

?textVar:text := "x";

?textVar:text := "1";

the second statement would not change textVar's value from "x" to "1". Instead, the second statement above would be converted to:

?x:text := "1";

and textVar's value would remain "x". To overcome this problem, HLA provides a special syntactical entity that converts a text object to a string and then returns the text object ID. The syntax for this special form is "@tostring:identifier". The example above could be rewritten as:

?textVar:text := "x";

?@tostring:textVar:text := "1";

In this example, textVar would be a text object that expands to the string "1".

As described earlier, HLA processes as parameters all text between a set of matching parentheses after the macro's name in a macro invocation. HLA macro parameters are delimited by the surrounding parentheses and commas. That is, the first parameter consists of all text beyond the left parenthesis up to the first comma (or up to the right parenthesis if there is only one parameter). The second parameter consists of all text just beyond the first comma up to the second comma (or right parenthesis if there are only two parameters). Etc. The last parameter consists of all text from the last comma to the closing right parenthesis.

Note that HLA will strip away any white space at the beginning and end of the parameter's text (though it does not remove any white space from the interior of the parameter's text).

If a single parameter must contain commas or parentheses, you must surround the parameter with the literal text macro quotes "#(" and ")#". HLA considers everything but leading and trailing space between these macro quote symbols as a single parameter. Note that this applies to macro invocations appearing within a parameter list. Consider the following (erroneous) code:

CallToAMacro( 5, "a", CallToAnotherMacro( 6,7 ), true );

Presumably, the "( 6,7 )" text is the parameter list for the "CallToAnotherMacro" invocation. When HLA encounters a macro invocation in a parameter list, it defers the expansion of the macro. That is, the third parameter of "CallToAMacro" should expand to "CallToAnotherMacro( 6,7 )", not the text that "CallToAnotherMacro" would expand to. Unfortunately, this example will not compile correctly because the macro processor treats the comma between the 6 and the 7 as the end of the third parameter to CallToAMacro (in other words, the third parameter is actually "CallToAnotherMacro( 6" and the fourth parameter is "7 )". If you really need to pass a macro invocation as a parameter, use the "#(" and ")#" macro quotes to surround the interior invocation:

CallToAMacro( 5, "a", #( CallToAnotherMacro( 6,7 ) )#, true );

In this example, HLA passes all the text between the "#(" and ")#" markers as a single parameter (the third parameter) to the "CallToAMacro" macro.

This example demonstrates another feature of HLA's macro processing system - HLA uses deferred macro parameter expansion. That is, the text of a macro parameter is expanded when HLA encounters the formal parameter within the macro's body, not while HLA is processing the actual parameters in the macro invocation (which would be eager evaluation).

There are three exceptions to the rule of deferred parameter evaluation: (1) text constants are always expanded in an eager fashion (that is, the value of the text constant, not the text constant's name, is passed as the macro parameter). (2) The @text function, if it appears in a parameter list, expands the string parameter in an eager fashion. (3) The @eval function immediately evaluates its parameter; the discussion of @eval appears a little later.

In general, there is very little difference between eager and deferred evaluation of macro parameters. In some rare cases there is a semantic difference between the two. For example, consider the following two programs:

program demoDeferred;

#macro two( x, y ):z;

    ?z:text:="1";

    x+y

#endmacro

const

    z:string := "2";

begin demoDeferred;

    ?i := two( z, 2 );

    #print( "i=" + string( i ))

end demoDeferred;

In the example above, the code passes the actual parameter "z" as the value for the formal parameter "x". Therefore, whenever HLA expands "x" it gets the value "z" which is a local symbol inside the "two" macro that expands to the value "1". Therefore, this code prints "3" ( "1" plus y's value which is "2") during assembly. Now consider the following code:

program demoEager;

#macro two( x, y ):z;

    ?z:text:="1";

    x+y

#endmacro

const

    z:string := "2";

begin demoEager;

    ?i := two( @text( z ), 2 );

    #print( "i=" + string( i ))

end demoEager;

The only differences between these two programs are their names and the fact that demoEager invocation of "two" uses the @text function to eagerly expand z's text. As a result, the formal parameter "x" is given the value of z's expansion ("2") and HLA ignores the local value for "z" in macro "two". This code prints the value "4" during assembly. Note that changing "z" in the main program to a text constant (rather than a string constant) has the same effect:

program demoEager;

#macro two( x, y ):z;

    ?z:text:="1";

    x+y

#endmacro

const

    z:text := "2";

begin demoEager;

    ?i := two( z, 2 );

    #print( "i=" + string( i ))

end demoEager;

This program also prints "4" during assembly.

One place where deferred vs. eager evaluation can get you into trouble is with some of the HLA built-in functions. Consider the following HLA macro:

    #macro DemoProblem( Parm );

        #print( string( Parm ) )

    #endmacro

        .

        .

        .

    DemoProblem( @linenumber );

(The @linenumber function returns, as an uns32 constant, the current line number in the file).

When this program fragment compiles, HLA will use deferred evaluation and pass the text "@linenumber" as the parameter "Parm". Upon compilation of this fragment, the macro will expand to "#print( string( @linenumber ))" with the intent, apparently, being to print the line number of the statement containing the DemoProblem invocation. In reality, that is not what this code will do. Instead, it will print the line number, in the macro, of the "#print( string (Parm));" statement. By delaying the substitution of the current line number for the "@linenumber" function call until inside the macro, deferred execution produces the wrong result. What is really needed here is eager evaluation so that the @linenumber function expands to the line number string before being passed as a parameter to the DemoProblem macro. The @eval built-in function provides this capability. The following coding of the DemoProblem macro invocation will solve the problem:

        DemoProblem( @eval( @linenumber ) );

Now the compiler will execute the @linenumber function and pass that number as the macro parameter text rather than the string "@linenumber". Therefore, the #print statement inside the macro will print the actual line number of the DemoProblem statement rather than the line number of the #print statement.

Of course, always having to type "@eval( @linenumber )" whenever you want to pass @linenumber as a macro parameter can get rather burdensome. Fortunately, you can create a text constant to ease this problem for you:

const

    evalLnNum :text := "@eval( @linenumber )";

        .

        .

        .

    DemoProblem( evalLnNum );

Because text constants expand in place (before any evaluation takes place), this code properly substitutes the current line number (of the DemoProblem statement) for the macro parameter.

2.5.9: Performance of the HLA Compile-Time Language

The HLA compile-time language was written to save development time by giving the assembly language programmer the ability to automate the creation of long or complex sequences of instructions. Because of HLA's design and the languages used to implement HLA (Flex, Bison, and C), HLA uses a "pure interpreter" implementation for the compile-time language. This means that HLA processes the text in your source file directly when executing a compile-time program. Unfortunately, "pure interpretation" is one of the slowest forms of program execution in common use. For the most part, the fact that HLA interprets compile-time language relatively slowly is a moot issue. Modern machines are sufficiently fast that even a pure interpretation scheme won't normally introduce a noticeable delay in the compilation time of your programs. However, the overuse of HLA's compile-time facilities can lead to slower development times, but consuming several seconds (or even minutes in some extreme cases) to interpret all the compile-time language statements in a source file.

Before discussing this subject any further, it's important for you to realize that we are talking about compilation times here, not the run-time of your actual application. The speed of HLA's compile-time language has nothing to do with the execution time of an application you write in HLA. So don't confuse the two and be concerned that HLA's compile-time language is somehow slowing down your applications.

As noted, modern machines are sufficiently fast that even HLA's "pure interpretation" execution model for compile-time programs won't have a tremendously significant impact on the compilation times of your programs. However, don't forget one thing - whenever HLA expands a macro the system isn't processing a single line of text in the source file, it's actually processing every line of text in that macro for each invocation of the macro. Likewise, whenever HLA processes a #while loop or a #for loop it isn't simply processing the number of lines in the loop's body, it's processing that number of lines times the number of loop iterations. It is very easy to create a very short HLA source file that takes a tremendous amount of time to compile. For example, on a 2GHz Pentium IV machine, the following trivial HLA program requires about a minute to compile:

program t;

    #for( i:= 0 to 100_000_000 )

    #endfor

begin t;

end t;

The fact that HLA is actually processing 1.7 million lines per second while compiling this program (a phenomenal number) is of little relief to the programmer waiting around a minute for this program to compile. The average programmer sees a short source file with only five statements and questions why the assembler would take nearly a minute to compile this file. They don't see the fact that as far as HLA is concerned, this source file is actually over one hundred million lines long. Compiling 100,000,000 lines of source code in a minute is very impressive. It's just not obvious to most people looking at this source code that this is what is going on.

Though examples like this one are rather rare, it is possible to create HLA macros that consume a fair amount of time, particularly if you invoke those macros many times within a source file. For example, the HLA stdout.put macro is between 400 and 500 source lines long (not counting all the loops present in the macro). If you stick a thousand invocations of stdout.put (each with multiple parameters) into a source file, you will measure compilation time in minutes, not seconds, on typical PCs. Counting loops and other features in the stdout.put macro, an invocation of this macro typically requires the interpretation of 500 or so HLA compile-time language statements per macro argument. A source file with 1,000 stdout.put macro invocations can easily expand to between 1,000,000 and 2,000,000 compile-time statements that HLA must interpret (which is slow, much slower than processing the empty #for loop in the previous example).

The moral of this story is that if you intend to write (or use) some extremely complex macros, and you intend to invoke that macro many, many, times in your source file, be prepared for slower than usual compilation times. If this loss of performance hinders your development, you might consider using separate compilation and splitting up the macro invocations across as many source files as possible so you don't have to recompile the entire program every time you make a minor change.

2.5.10: A Complex Macro Example: stdout.put

Before concluding this chapter, it would be a good idea to provide an example of a relatively complex HLA compile-time program. Probably the most common example of just such a compile-time program is the HLA Standard Library's stdout.put macro. The stdout.put macro is interesting because it parses the list of arguments you supply, determines the type of the argument, extracts optional formatting information (if present), and calls an appropriate HLA Standard Library routine to actually print the value to the standard output device. Using stdout.put is far more convenient than calling all the individual routines in the HLA Standard Library.

Note: although we won't use the stdout.put macro much in this book, it's mainly useful for writing console applications, not GUI apps, we will make use of the fileio.put and str.put macros which work in a similar fashion to stdout.put. Even if these other macros weren't useful to Win32 programmers, the concepts that stdout.put employs are quite useful for Win32-based macros.

To begin with, you should note that the HLA stdout.put macro is really the "put" macro that just happens to be defined in the stdout name space. We'll continue to call it stdout.put in this discussion to differentiate it from other "put" macros that appear in the HLA Standard Library. This macro's definition appears in the stdout.hhf header file; Those who would like to see the original source code can find stdout.hhf in the HLA include subdirectory.

The stdout.put macro allows zero or more arguments. To handle this, the put macro declaration defines a variable parameter list (using HLA's variable parameter list syntax -- an open ended array). In addition to this varying parameter list, the put macro uses several local symbols within the macro, they are all part of the stdout.put macro declaration:

    #macro put( _parameters_[] ):

        _curparm_, _pType_, _arg_, _width_,

        _decpts_, _parmArray_, _id_, _tempid_, _fieldCnt_;

In order to process each parameter the caller supplies, the stdout.put macro uses a compile-time variable and a #while loop to step through each of the elements of the _parameters_ array. This could actually be done more conveniently with a #for loop, but the stdout.put macro was written long before the #for loop was added to the HLA language, hence the use of the #while loop to simulate a #for loop:

        ?_curparm_:uns32 := 0;

        // The following loop repeats once for each PUT parameter

        // we process.

        #while( _curparm_ < @elements( _parameters_ ))

The stdout.put macro allows operands to take the following form: operand, operand:n, or operand:n:m. The "n" and "m" items provide a minimum field width (n) and positions after the decimal point (m, for real values). In order to properly process each argument, HLA needs to split up each parameter into one, two, or three separate strings, depending on the presence of the field width and decimal point options. HLA maintains this information in an array of strings and uses the local symbol _parmArray_ to hold these strings. On each iteration of the loop, HLA redefines _parmArray_ as an uns32 object in order to free the string storage used by the previous iteration of the loop. After doing this, the macro calls the @tokenize compile-time function to break up the current parameter string into various parts (see the HLA Reference Manual for a complete description of the @tokenize function; for our purposes, just assume that it puts the actual operand name into the first element of the _parmArray_ array of strings, the field width into the second element, and the decimal point position into the third element).

            ?_parmArray_:uns32 := 0;

            ?_parmArray_ := @tokenize

                            (

                                _parameters_[ _curparm_ ],

                                0,

                                {':'},

                                {

                                    '"',

                                    '''',

                                    '[',

                                    ']',

                                    '(',

                                    ')',

                                    '{',

                                    '}'

                                }

                            );

Next, the stdout.put macro does some processing on the first portion of the current parameter to determine if we've got an identifier or an expression. First, this code strips away any leading and trailing spaces and then checks to see if the string begins with an identifier. If so, the code invokes the stdio._GetID_ macro that extracts the identifier from the start of the string.

            ?_arg_ := @trim( _parmArray_[ 0 ], 0 );

            #if( char( _arg_ ) in stdio._idchars_ )

                // If this parameter begins with an id character,

                // then strip away any non-ID symbols from the

                // end of the string.  Then determine if we've

                // got a constant or some other class (e.g.,

                // variable or procedure).  If not a constant,

                // keep only the name.  If a constant, we need

                // to keep all trailing characters as well.

                ?_id_ := stdio._GetID_( _arg_ );

                #if

                (

                        @class( _id_ ) = hla.cConstant

                    |   @class( _id_ ) = hla.cValue

                )

                    ?_id_ := _arg_;

                #endif

            #else

                // If it's not an ID, we need to keep everything.

                ?_id_ := _arg_;

            #endif

After extracting the first operand, the next step is to process the optional field width and fractional width components. The stdout.put macro determines if these fields are present by checking the number of elements in the _parmArray_ object. If the number of elements is two or greater, then stdout.put assumes that the second array element holds the minimum field width. If the number of elements is three or greater, then stdout.put assumes that the third element contains the fractional width information.

            // Okay, determine if the caller supplied a field width

            // value with this parameter.

            ?_fieldCnt_ := @elements( _parmArray_ );

            #if( _fieldCnt_ > 1 )

                ?_width_ := @trim( _parmArray_[ 1 ], 0 );

            #else

                ?_width_ := "-1";       // Default width value.

            #endif

            // Determine if the user supplied a fractional width

            // value with this parameter.

            #if( _fieldCnt_ > 2 )

                ?_decpts_ := @trim( _parmArray_[ 2 ], 0 );

            #else

                ?_decpts_ := "-1";  // Default fractional value.

            #endif

            // Quick check to see if the user supplied too many

            // width fields with this parameter.

            #if( _fieldCnt_ > 3 )

                #error

                (

                    "<<" + _parameters_[ _curparm_ ] + ">>" +

                    " has too many width fields"

                );

            #endif

After extracting the components of the current parameter, the stdout.put macro now goes about its business of determining the type of the current parameter so it can determine which HLA Standard Library function to call to actually print the value. This code sequence uses the HLA @pType and @typename compile-time functions to determine the symbol's type (see the HLA Reference Manual for more details). Note that this code also handles arrays of these objects by determining the base address of the array object (this is what the #while loop is doing in the following code sequence).

            // Determine the type of this parameter so we can

            // call the appropriate routine to print it.

            ?_pType_ := @pType( @text( _id_ ));

            ?_tempid_ := _id_;

            #while( _pType_ = hla.ptArray )

                ?_tempid_ := @typename( @text( _tempid_ ));

                ?_pType_ := @pType( @text( _tempid_ ));

            #endwhile

Once stdout.put has a handle on the parameter's type, it invokes the stdout._put_ macro (which we'll describe in a moment) to actually emit the code. Note that the @pType compile-time function returns an integer value that specifies a "primitive HLA type" (hence the name "pType") and the corresponding type values have been given meaningful names in the hla.hhf header file (which this code is using). Another interesting aspect to this code is that if the argument is a class object, then it will automatically call a "put" method for that class, if such a method exists. This is how stdout.put extends its ability to print user-defined data types.

            // Based on the type, call the appropriate library

            // routine to print this value.

            #if( _pType_ = hla.ptBoolean )

                stdout._put_( stdout.putbool, boolean )

            #elseif( _pType_ = hla.ptUns8 )

                stdout._put_( stdout.putu8, uns8 )

            #elseif( _pType_ = hla.ptUns16 )

                stdout._put_( stdout.putu16, uns16 )

            #elseif( _pType_ = hla.ptUns32 )

                stdout._put_( stdout.putu32, uns32 )

            #elseif( _pType_ = hla.ptUns64 )

                stdout._put_( stdout.putu64, uns64 )

            #elseif( _pType_ = hla.ptUns128 )

                stdout._put_( stdout.putu128, uns128 )

            #elseif( _pType_ = hla.ptByte )

                stdout._put_( stdout.putb, byte )

            #elseif( _pType_ = hla.ptWord )

                stdout._put_( stdout.putw, word )

            #elseif( _pType_ = hla.ptDWord )

                stdout._put_( stdout.putd, dword )

            #elseif( _pType_ = hla.ptQWord )

                stdout._put_( stdout.putq, qword )

            #elseif( _pType_ = hla.ptLWord )

                stdout._put_( stdout.putl, lword )

            #elseif( _pType_ = hla.ptInt8 )

                stdout._put_( stdout.puti8, int8 )

            #elseif( _pType_ = hla.ptInt16 )

                stdout._put_( stdout.puti16, int16 )

            #elseif( _pType_ = hla.ptInt32 )

                stdout._put_( stdout.puti32, int32 )

            #elseif( _pType_ = hla.ptInt64 )

                stdout._put_( stdout.puti64, int64 )

            #elseif( _pType_ = hla.ptInt128 )

                stdout._put_( stdout.puti128, int128 )

            #elseif( _pType_ = hla.ptChar )

                stdout._put_( stdout.putc, char )

            #elseif( _pType_ = hla.ptCset )

                stdout._put_( stdout.putcset, cset )

            #elseif( _pType_ = hla.ptReal32 )

                stdout._put_( stdout.putr32, real32 )

            #elseif( _pType_ = hla.ptReal64 )

                stdout._put_( stdout.putr64, real64 )

            #elseif( _pType_ = hla.ptReal80 )

                stdout._put_( stdout.putr80, real80 )

            #elseif( _pType_ = hla.ptString )

                stdout._put_( stdout.puts, string )

            #elseif( @isclass( @text( _parameters_[ _curparm_ ] )))

                #if

                (

                    @defined

                    (

                        @text( _parameters_[ _curparm_ ] + ".toString" )

                    )

                )

                    push( eax );

                    push( esi );

                    push( edi );

                    @text

                    (

                        _parameters_[ _curparm_ ] +

                        ".toString()"

                    );

                    puts( eax );

                    strfree( eax );

                    pop( edi );

                    pop( esi );

                    pop( eax );

                #else

                    #error

                    (

                        "stdout.put: Class does not provide a toString " +

                        "method or procedure"

                    );

                #endif

            #else

                #error

                (

                    "stdout.put: Unknown data type (" +

                    _parameters_[ _curparm_ ] +

                    ":" +

                    @typename( @text( _id_ )) +

                    ")"

                );

            #endif

            ?_curparm_ := _curparm_ + 1;

        #endwhile

        // The following is stuck here just to require

        // that the user end the stdout.put(--) invocation

        // with a semicolon.

        static

            ;

        endstatic

    #endmacro;

The stdout.put macro actually invokes a couple of macros. One of these is the stdio._GetID_ macro (which we'll not cover here, see the source code in the stdio.hhf header file found in the HLA include subdirectory for details). The second macro that stdout.put invokes is the stdout._put_ macro that expands into a call to a specific HLA Standard Library function depending on the type of the parameter that stdout.put is processing. This macro calculates the size information and fills in the parameter to the actual HLA Standard Library call.

    #macro _put_( _routine_, _typename_ ):

                            _func_, sizeParms, _realsize_, _typ_;

        ?_func_:string := @string(_routine_);

        ?sizeParms:string := "";

        ?_typ_:string := @string(_typename_)

        // Real values allow two size parameters (width & decpts).

        #if( @substr( _typ_, 0, 4 ) = "real" )

            // Note: on entry, typename = real32, real64, or real80 and

            // routine = putr32, putr64, putr80, fputr32, fputr64, or

            // fputr80.

            ?_realsize_:string := @substr( _typ_, 4, 2 );

            #if( _width_ <> "-1" )

                // If decpts is <> -1, print in dec notation,

                // else print in sci notation.

                #if( _decpts_ <> "-1" )

                    ?sizeParms:string := "," + _width_ + "," + _decpts_;

                #else

                    ?_func_:string := "stdout.pute" + _realsize_;

                    ?sizeParms:string := "," + _width_;

                #endif

            #else

                // If the user did not specify a format size,

                // then use the puteXX routines with default

                // sizes of: real32=15, real64=22, real80=28.

                ?_func_:string := "stdout.pute" + _realsize_;

                #if( _realsize_ = "32" )

                    ?sizeParms:string := ",15";

                #elseif( _realsize_ = "64" )

                    ?sizeParms:string := ",20";

                #else

                    ?sizeParms:string := ",23";

                #endif

            #endif

        #else //It's not a real type.

            #if( _decpts_ <> "-1" )

                #error

                (

                    "Fractional width specification is not supported here"

                )

            #elseif( _width_ <> "-1" )

                // Width specifications are only supported for

                // certain types.  Check for that here.

                #if

                (

                        _typ_ <> "uns8"

                    &   _typ_ <> "uns16"

                    &   _typ_ <> "uns32"

                    &   _typ_ <> "uns64"

                    &   _typ_ <> "uns128"

                    &   _typ_ <> "int8"

                    &   _typ_ <> "int16"

                    &   _typ_ <> "int32"

                    &   _typ_ <> "int64"

                    &   _typ_ <> "int128"

                    &   _typ_ <> "char"

                    &   _typ_ <> "string"

                    &   _typ_ <> "byte"

                    &   _typ_ <> "word"

                    &   _typ_ <> "dword"

                    &   _typ_ <> "qword"

                    &   _typ_ <> "lword"

                )

                    #error

                    (

                        "Type " +

                        _typ_ +

                        " does not support width format option"

                    )

                #else

                    ?_func_:string := _func_ + "Size";

                    ?sizeParms:string := "," + _width_ + ", ' '";

                #endif

            #endif

        #endif

        // Here's the code that calls the appropriate function based on the parameter's

        // type and format information:

        #if

        (

                @isconst( @text( _arg_ ))

            &   _typ_ = "string"

            &   _arg_ = "#13 #10"

        )

            stdout.newln();

        #elseif( @isconst( @text( _arg_ )) )

            @text( _func_ )( @text( _arg_ ) @text( sizeParms ));

        #else

            @text( _func_ )

                ( (type _typename_ @text( _arg_ )) @text( sizeParms ));

        #endif

    #endmacro;

2.6: Even More Advanced HLA Programming...

This chapter could go on and on forever. HLA is a very sophisticated assembly language and provides tons of features that this chapter doesn't even touch upon. However, we do need to get on with the real purpose of this book, learning Win32 assembly language programming. We've covered enough advanced HLA programming to deal with just about everything you will encounter the need for when writing Win32 assembly applications. Of course, if you want to learn more about HLA, here are two suggestions: (1) Read the HLA Reference manual and (2) write lots of HLA code and experiment.

1We'll ignore MMX and SSE operands because they require special instructions that HLA's run-time boolean expressions do not support.
2Real128 is actually a composite data type, specifically a record, that HLA supports for SSE/2 compatibility. In theory, it is not a scalar data type. However, we'll mention it here just for completeness.
3The restriction on registers exists because it doesn't make sense to cast a register as a size that is different than the register's actual size. Memory operands, on the other hand, may consume fewer or more bytes that the variable's original declaration because the 80x86 CPU can access addresses after the variable without any problems.
4Obviously, this does not apply in every case, some applications are "compute bound" and consume an inordinate amount of CPU time within the application. Most Win32 applications, however, are not compute bound.
5Though you may use HLA's break, breakif, exit, or exitif statements to escape from such a loop.
6Note that the symbol table output routines in HLA change from time to time, so this output may not exactly match what HLA produces in the particular version that you're using.
7Again, code generation is subject to change in HLA, so this may not exactly match what the particular version of HLA that you're using produces.
8Static variables are those you declare in the static, readonly, and storage sections. Non-static variables include parameters, VAR objects, and anonymous memory locations.
9These will be described a little later in this chapter.
TOC PREV NEXT INDEX


Chapter 3: The C - Assembly Connection

3.1: Why are We Reading About C?

You probably purchased this book to learn assembly language programming under Windows (after all, that's what the title promises). This chapter is going to spend considerable time talking about the C programming language. Now assembly language programmers fall into two camps with respect to the C programming language: those who already know it and don't really need to learn a whole lot more about C, and those who don't know C and probably don't want to learn it, either. Unfortunately, as the last chapter points out, the vast majority of Windows programming documentation assumes that the reader is fluent in C. This book cannot begin to provide all the information you may need to write effective Win32 applications; therefore, this chapter does the next best thing - it describes how you can translate that C documentation for use in your assembly language programs.

This chapter contains two main sections. The first section provides a basic description of the C programming language for those readers who are not familiar with the C/C++ programming language. It describes various statements in C/C++ and provides their HLA equivalents. Though far from a complete course on the C programming language, this section will provide sufficient information to read some common Win32 programming examples in C and translate them into assembly language. Experienced C/C++ programmers can elect to skip this section (though if you're not comfortable with HLA, you may want to skim over this section because it will help you learn HLA from a C perspective). The second portion of this chapter deals with the Win32 interface and how C passes parameter data to and from Windows. Unless you're well-versed in compiler construction, mixed language calling sequences, and you've examined a lot of compiler code, you'll probably want to take a look at this material.

3.2: Basic C Programming From an Assembly Perspective

The C programming language is a member of the group of programming languages known as the imperative or procedural programming languages. Languages in this family include FORTRAN, BASIC, Pascal (Delphi/Kylix), Ada, Modula-2, and, of course, C. Generally, if you've learned to write programs in one of these languages, it's relatively easy to learn one of the other languages in the same category. When you attempt to learn a new language from a different class of languages (i.e., you switch programming paradigms), it's almost like you're learning to program all over again; learning a new language that is dissimilar to the one(s) you already know is a difficult task. A recent trend in programming language design has been the hybrid language. A hybrid language bridges the gap between two different programming paradigms. For example, the C++ language is a hybrid language that shares attributes common to both procedural/imperative languages and object-oriented languages. Although hybrid languages often present some compromises on one side or the other of the gulf they span, the advantage of a hybrid language is that it is easy to learn a new programming paradigm if you're already familiar with one of the programming methodologies that the language presents. For example, programmers who already know find it much easier to learn object-oriented programming via C++ rather than learning the object-oriented programming paradigm from scratch, say by learning Smalltalk (or some other "pure" object-oriented language). So hybrid languages are good in the sense that they help you learn a new way of programming by leveraging your existing knowledge.

The High Level Assembler, HLA, is a good example of a hybrid programming language. While a true assembly language, allowing you to do just about anything that is possible with a traditional (or low-level) assembler, HLA also inherits some syntax and many other features from various high-level imperative programming languages. In particular, HLA borrows several control and data structures from the C, Pascal, Ada, and Modula-2 programming languages. The original intent for this design choice was to make it easier to learn assembly language if you already knew an high level language like Pascal or C/C++. By borrowing heavily from the syntax of these high-level programming languages, a new assembly language programmer could learn assembly programming much more rapidly by leveraging their C/C++/Pascal knowledge during the early phase of their assembly education.

Note, however, that the reverse is also true. Someone who knows HLA well but doesn't know C can use their HLA knowledge to help them learn the C programming language. HLA's high level control structures are strongly based on languages like C and Modula-2 (or Ada); therefore, if you're familiar with HLA's high level control structures, then learning C's control structures will be a breeze. The sections that immediately follow use this concept to teach some basic C syntax. For those programmers who are not comfortable or familiar with HLA's high level control structures, the following subsections will also describe how to convert between "pure" assembly language and various C control structures. The ultimate goal here is to show you how to convert C code to HLA assembly code; after all, when reading some Win32 programming documentation, you're going to need to convert the examples you're reading in C into assembly language. Although it is always possible (and very easy) to convert any C control structure directly into assembly language, the reverse is not true. That is, it is possible to devise some control flow scheme in assembly language that does not translate directly into a high level language like C. Fortunately, for our purposes, you generally won't need to go in that direction. So even though you're learning about C from an assembly perspective (that is, you're being taught how to read C code by studying the comparable assembly code), this is not a treatise on converting assembly into C (which can be a very difficult task if the assembly code is not well structured).

3.2.1: C Scalar Data Types

The C programming language provides three basic scalar data types1: integers, and a couple floating point types. Other data types you'd find in a traditional imperative programming language (e.g., character or boolean values) are generally implemented with integer types in C. Although C only provides three basic scalar types, it does provide several variations of the integer and floating point types. Fortunately, every C data type maps directly to an HLA structured data type, so conversion from C to HLA data types is a trivial process.

3.2.1.1: C and Assembler Integer Data Types

The C programming language specifies (up to) four different integer types: char (which, despite its name, is a special case of an integer value), short, int, and long. A few compilers support a fifth size, "long long". In general, the C programming language does not specify the size of the integer values; that decision is left to whomever implements a specific compiler. However, when working under Windows (Win32), you can make the following assumptions about integer sizes:

  • char - one byte
  • short - two bytes
  • int, long - four bytes

The C programming language also specifies two types of integers: signed and unsigned. By default, all integer values are signed. You can explicitly specify unsigned by prefacing one of these types with the keyword unsigned. Therefore, C's integral types map to HLA's types as shown in Table 3-1.

Table 3-1: Integer Type Correspondence Between HLA and C
C Type
Corresponding HLA Types
char
char, byte, int81
short
word, int16
int
dword, int32
long
dword, int32
long long
qword, int64
unsigned char
char, byte, uns8
unsigned short
word, uns16
unsigned
dword, uns32
unsigned int
dword, uns32
unsigned long
dword, uns32
unsigned long long
qword, uns64
1Some compilers have an option that lets you specify the use of unsigned char as the default. In this case, the corresponding HLA type is uns8.

Generic integer literal constants in C take several forms. C uses standard decimal representation for base 10 integer constants, just like most programming languages (including HLA). For example, the sequence of digits:

128

represents the literal integer constant 128.

If a literal integer constant begins with a zero (followed by one or more octal digits in the range 0..7), then C treats the literal constant as a base-8 (octal) value. HLA doesn't support octal constants, so you will have to manually convert such constants to decimal or hexadecimal prior to using them in an assembly language program. Fortunately, you rarely see octal constants in modern C programs (especially in Win32 programs).

C integer literal constants that begin with "0x" are hexadecimal (base-16) constants. You will replace the "0x" prefix with a "$" prefix when converting the value from C to HLA. For example, the C literal constant "0x1234ABCD" becomes the HLA literal constant "$1234ABCD".

C also allows the use of an "L" suffix on a literal integer constant to tell the compiler that this should be a long integer value. HLA automatically adjusts all literal constants to the appropriate size, so there is no need to tell HLA to extend a smaller constant to a long (32-bit) value. If you encounter an "L" suffix in a C literal constant, just drop the suffix when translating the value to assembly.

3.2.1.2: C and Assembly Character Types

As the previous section notes, C treats character variables and constants as really small (one-byte) integer values. There are some non-intuitive aspects to using C character variables that can trip you up; hence the presence of this section.

The first place to start is with a discussion of C and HLA literal character constants. The two literal forms are quite similar, but there are just enough differences to trip you up if you're not careful. The first thing to note is that both HLA and C treat a character constant differently than a string containing one character. We'll cover character strings a little later in this chapter, but keep in mind that character objects are not a special case of a string object.

A character literal constant in C and HLA usually consists of a single character surrounded by apostrophe characters. E.g., `a' is a character constant in both of these languages. However, HLA and C differ when dealing with non-printable (i.e., control) and a couple of other characters. C uses an escape character sequence to represent the apostrophe character, the backslash character, and the control characters. For example, to represent the apostrophe character itself, you'd use the C literal constant `\''. The backslash tells C to treat the following value specially; in this particular case, the backslash tells C to treat the following apostrophe character as a regular character rather than using it to terminate the character constant. Likewise, you use `\\' to tell C that you want a single backslash character constant. C also uses a backslash followed by a single lowercase alphabetic character to denote common control characters. Table 3-2 lists the escape character sequences that C defines.

Table 3-2: C Escape Character Sequences
C Escape Sequence
Control Character
`\n'
New line (carriage return/line feed under Windows, though C encodes this as a single line feed)
`\r'
Carriage return
`\b'
Backspace
`\a'
Alert (bell character, control-G)
`\f'
Form Feed (control-L)
`\t'
Tab character (control-I)
`\v'
Vertical tab character (control-k)

C also allows the specification of the character's numeric code by following a backslash with an octal or hexadecimal constant in the range 0..0xff, e.g., `\0x1b'.

HLA does not support escape character sequences using the backslash character. Instead, HLA uses a pound sign (`#') followed immediately by a numeric constant to specify the ASCII character code. Table 3-3 shows how to translate various C escape sequences to their corresponding HLA literal character constants.

Table 3-3: Converting C Escape Sequences to HLA Character Constants
C Escape Sequence
HLA Character Constant
Description
`\n'
#$a #$d
Note that the end of line sequence under Windows is not a character, but rather a string consisting of two characters. If you need to represent newline with a single character, using a linefeed (as see does) whose ASCII code is $A; linefeed is also defined in the HLA Standard Library as stdio.lf. Note that the "nl" symbol an HLA user typically uses for newline is a two-character string containing line feed followed by carriage return.
`\r'
#$d
Carriage return character. This is defined in the HLA Standard Library as stdio.cr.
`\b'
#8
Backspace character. This is defined in the HLA Standard Library as stdio.bs.
`\a'
#7
Alert (bell) character. This is defined in the HLA Standard Library as stdio.bell.
`\f'
#$c
Form feed character.
`\t'
#9
Tab character. This is defined in the HLA Standard Library as stdio.tab.
`\v'
#$b
Vertical tab character.

Because C treats character values as single-byte integer values, there is another interesting aspect to character values in C - they can be negative. One might wonder what "minus `z'" means, but the truth is, there really is no such thing as a negative character; C simply uses signed characters to represent small integer values in the range -128..+127 (versus unsigned characters that represent values in the range 0..255). For the standard seven-bit ASCII characters, the values are always positive, regardless of whether you're using a signed character or an unsigned character object. Note, however, that many C functions return a signed character value to specify certain error conditions or other states that you cannot normally represent within the ASCII character set. For example, many functions return the character value minus one to indicate end of file.

3.2.1.3: C and Assembly Floating Point (Real) Data Types

The C programming language defines three different floating point sizes: float, double, and long double 2. Like the integer data type sizes, the C language makes no guarantees about the size or precision of floating point values other than to claim that double is at least as large as float and long double is at least as large as double. However, while nearly every C/C++ compiler that generates code for Windows uses the same sizes for integers (8, 16, and 32 bits for char, short, and int/long), there are differences in the sizes of floating objects among compilers. In particular, some compilers use a 10-byte extended precision format for long double (e.g., Borland) while others use an eight-byte double precision format (e.g., Microsoft). Fortunately, all (reasonable) compilers running under Windows use the IEEE 32-bit single-precision format for float and the IEEE 64-bit double-precision format for double. If you encounter a long double object in C code, you will have to check with the compiler's vendor to determine the size of the object in order to convert it to assembly language. Of course, for many applications it won't really matter if you go ahead and use a 10-byte real value whenever you encounter a long double object. After all, the original programmer is probably expecting something bigger than a double object anyway. Do keep in mind, however, that some algorithms involving real arithmetic may not be stable when run with a different floating point size other than the sized used when they were developed.

T shows the correspondence between C/C++ floating point data types and HLA's floating point data types.

Table 3-4: Real Type Correspondence Between C and Assembly
C Real Type
Corresponding HLA Type
Comment
float
real32
32-bit IEEE format floating point value.
double
real64
64-bit IEEE format floating point value.
long double
real64
64-bit IEEE format floating point value on certain compilers (e.g., Microsoft).
real80
80-bit IEEE format floating point value on certain compilers (e.g., Borland)

C and HLA floating point literal constants are quite similar. They may begin with an optional sign, followed by one or more decimal digits. Then you can have an optional decimal point followed by another optional sequence of decimal digits; following that, you can have an optional exponent specified as an `e' or `E', an optional sign, and one or more decimal digits. The final result must not look like an integer constant (i.e., the decimal point or an exponent must be present).

C allows an optional "F" suffix on a floating point constant to specify single precision, e.g., 1.2f. Similarly, you can attach an "L" suffix to a float value to indicate long double, e.g., 1.2L. By default, C literal floating point constants are always double precision values. HLA always maintains all constants as 80-bit floating point values internally and converts them to 64 or 32 bits as necessary, so there is no need for such a suffix. So by dropping the "F" or "L" suffix, if it is present, you can use any C floating point literal constant as-is in the HLA code.

3.2.2: C and Assembly Composite Data Types

C provides four major composite (or aggregate) data types: arrays, structures, unions, and pointers. C++ adds the class types well. A composite type is a type that is built up from smaller types (e.g., an array is a collection of smaller objects, each element having the same type as all other elements in the array). In the following sub-sections, we'll explore these composite data types in C and provide the corresponding type in HLA.

As is the case throughout this chapter, this section assumes that you already know assembly language and may not know C. This section provides the correspondence between C and HLA, but doesn't attempt to teach you how to do things like access an array in assembly language; the previous chapter already covered that material.

3.2.2.1: C and Assembly Array Types

Since HLA's syntax was based on the C and Pascal programming languages, it should come as no surprise that array declarations in HLA are very similar to those in C. This makes the translation from C to HLA very easy.

The syntax for an array declaration in C is the following:

elementType  arrayName[ elements ] <<additional dimension infor-
mation>>;

elementType is the type of an individual element of the array. arrayName is the name of the array object. elements is an integer constant value the specifies the number of array elements. Here are some sample C array declarations:

    int intArray[4];

    char grid[3][3];  // 3x3 two-dimensional array

    double flts[16];

    userType userData[2][2][2]

In HLA, multiple dimension arrays use a comma-delimited list to separate each of the maximum bounds (rather than using separate sets of brackets). Here are the corresponding declarations in HLA:

    intArray :int32[ 4 ];

    grid     :char[3,3];

    flts     :real64[16];

    userData :userType[2,2,2];

Both C and HLA index arrays from zero to n-1, where n is the value specified as the array bound in the declaration.

C stores arrays in memory using row-major ordering. Therefore, when accessing elements of a multi-dimensional array, always use the row-major ordering algorithm (see The Art of Assembly Language for details if you're unfamiliar with accessing elements of a multi-dimensional array in assembly language).

In C, it is possible to provide an initial value for an array when declaring an array. The following C example demonstrates the syntax for this operation:

    int iArray[4] = {1,2,3,4};

HLA also allows initialization of array variables, but only for static objects. Here's the HLA version of this C code:

static

    iArray :int32[4] := [1,2,3,4];

C allows the same syntax for an array initialization to automatic variables. However, you cannot initialize an automatic variable at compile time (this is true for C and HLA); therefore, the C compiler automatically emits code to copy the data from some static memory somewhere into the automatic array, e.g., a C declaration like the following appearing in a function (i.e., as an automatic local variable):

    int autoArray[4] = {1,2,3,4};

gets translated into machine code that looks something like the following:

readonly

    staticInitializeData :dword := [1,2,3,4];

        .

        .

        .

var

    autoArray: int32[4];

        .

        .

        .

    mov( &staticInitializerData, esi );

    lea( edi, autoArray );

    mov( 4, ecx );

    rep.movsd();

(yes, compilers really do generate code like this). This code is pretty disgusting. If you see an automatic array variable with an initializer, it's probably a better idea to try and figure out if you really need a new copy of the initial data every time you enter the function.

3.2.2.2: C and Assembly Record/Structure Types

C implements the equivalent of HLA'S records using the struct keyword. Structure declarations in C look just like standard variable declarations sandwiched inside a "struct {...}" block. Conversion to HLA is relatively simple: just stick the HLA equivalent of those field declarations in a record..endrecord block. The only point of confusion is C's syntax for declaring tags, types, and variables of some struct type.

C allows you to declare structure variables and types several different ways. First, consider the following structure variable declaration:

    struct

    {

        int fieldA;

        float fieldB;

        char fieldC;

    }

        structVar;

Assuming this is a global variable in C (i.e., not within a function) then this creates a static variable structVar that has three fields: structVar.fieldA, structVar.fieldB, and structVar.fieldC. The corresponding HLA declaration is the following (again, assuming a static variable):

static

    structVar:

        record

            fieldA  :int32;

            fieldB  :real32;

            fieldC  :char;

        endrecord;

In an HLA program, you'd access the fields of the structVar variable using the exact same syntax as C, specifically: structVar.fieldA, structVar.fieldB, and structVar.fieldC.

There are two ways to declare structure types in C: using typedef and using tag fields. Here's the version using C's typedef facility (along with a variable declaration of the structure type):

typedef struct

    {

        int fieldA;

        float fieldB;

        char fieldC;

    }

        structType;

    structType structVar;

Once again, you access the fields of structVar as structVar.fieldA, structVar.fieldB, and structVar.fieldC.

The typedef keyword was added to the C language well after it's original design. In the original C language, you'd declare a structure type using a structure tag as follows:

struct structType

    {

        int fieldA;

        float fieldB;

        char fieldC;

    } /* Note: you could actually declare structType variables here */ ;

    struct structType strucVar;

HLA provides a single mechanism for declaring a record type - by declaring the type in HLA's type section. The syntax for an HLA record in the type section takes this form:

type

    structType:

        record

            fieldA  :int32;

            fieldB  :real32;

            fieldC  :char;

        endrecord;

static

    structVar  :structType;

C also allows the initialization of structure variables by using initializers in a variable declaration. The syntax is similar to that for an array (i.e., a list of comma separated values within braces). C associates the values in the list with each of the fields by position. Here is an example of the structVar declaration given earlier with an initializer:

    struct structType structVar = { 1234, 5.678, `9' };

Like the array initializers you read about in the previous section, the C compiler will initialize these fields at compile time if the variable is a static or global variable. however, if it's an automatic variable, then the C compiler emits code to copy the data into the structure upon each entry into the function defining the initialized structure (i.e., this is an expensive operation).

Like arrays, HLA allows the initialization of static objects you define in a static, storage, or readonly section. Here is the previous C example translated to HLA:

static

    structVar  :structType := structType:[1234, 5.678, `9' ];

One major difference between HLA and C with respect to structures is the alignment of the fields within the structure. By default, HLA packs all the fields in a record so that there are no extra bytes within a structure. The structType structure, for example, would consume exactly nine bytes in HLA (four bytes each for the int32 and real32 fields and one byte for the char field). C structures, on the other hand, generally adhere to certain alignment and padding rules. Although the rules vary by compiler implementation, most Win32 C/C++ compilers align each field of a struct on an offset boundary that is an even multiple of that field's size (up to four bytes). In the current structType example, fieldA would fall at offset zero, fieldB would fall on offset four within the structure, and FieldC would appear at offset eight in the structure. Since each field has an offset that is an even multiple of the object's size, C doesn't manipulate the field offsets for the record. Suppose, however, that we have the following C declaration in our code:

typedef struct

    {

        char fieldC;

        int fieldA;

        short fieldD;

        float fieldB;

    }

        structType2;

If C, like HLA, didn't align the fields by default, you'd find fieldC appearing at offset zero, fieldA at offset 1, fieldD at offset five, and fieldB at offset seven. Unfortunately, these fields would appear at less than optimal addresses in memory. C, however, tends to insert padding between fields so that each field is aligned at an offset within the structure that is an even multiple of the object's size (up to four bytes, which is the maximum field alignment that compilers support under Win32). This padding is generally transparent to the C programmer; however, when converting C structures to assembly language records, the assembly language programmer must take this padding into consideration in order to correctly communicate information between an assembly language program and Windows. The traditional way to handle this (in assembly language) has been to explicitly add padding fields to the assembly language structure, e.g.,

type

    structType2:

        record

            fieldC  :char;

            pad0    :byte[3]; // Three bytes of padding.

            fieldA  :int32;

            fieldD  :int16;

            pad1    :byte[2]; // Two bytes of padding.

            fieldB  :real32;

        endrecord;

HLA provides a set of features that automatically and semi-automatically align record fields. First of all, you can use the HLA align directive to align a field to a given offset within the record; this mechanism is great for programs that need absolute control over the alignment of each field. However, this kind of control is not necessary when implementing C records in assembly language, so a better approach is to use HLA's automatic record field alignment facilities3. The rules for C/C++ (under Windows, at least) are pretty standard among compilers; the rule is this: each object (up to four bytes) is aligned at a starting offset that is an even multiple of that object's size. If the object is larger than four bytes, then it gets aligned to an offset that is an even multiple of four bytes. In HLA, you can easily do this using a declaration like the following:

type

    structType2:

        record[4:1];

            fieldC  :char;

            pad0    :byte[3]; // Three bytes of padding.

            fieldA  :int32;

            fieldD  :int16;

            pad1    :byte[2]; // Two bytes of padding.

            fieldB  :real32;

        endrecord;

The "[4:1];" appendage to the record keyword tells HLA to align objects between one and four bytes on their natural boundary and larger objects on a four-byte boundary, just like C. This is the only record alignment option you should need for C/C++ code (and, in fact, you should always use this alignment option when converting C/C++ structs to HLA records). If you would like more information on this alignment option, please see the HLA reference manual.

Specifying the field alignment via the "[4:1];" option only ensures that each field in the record starts at an appropriate offset. It does not guarantee that the record's length is an even multiple of four bytes (and most C/C++ compilers will add padding to the end of a struct to ensure the length is an even multiple of four bytes). However, you can easily tell HLA to ensure that the record's length is an even multiple of four bytes by adding an align directive to the end of the field list in the record, e.g.,

type

    structType2:

        record[4:1];

            fieldC  :char;

            pad0    :byte[3]; // Three bytes of padding.

            fieldA  :int32;

            fieldD  :int16;

            pad1    :byte[2]; // Two bytes of padding.

            fieldB  :real32;

            align(4);

        endrecord;

Another important thing to remember is that the fields of a record or structure are only properly aligned in memory if the starting address of the record or structure is aligned at an appropriate address in memory. Specifying the alignment of the fields in an HLA record does not guarantee this. Instead, you will have to use HLA's align directive when declaring variables in your HLA programs. The best thing to do is ensure that an instance of a record (i.e., a record variable) begins at an address that is an even multiple of four bytes4. You can do this by declaring your record variables as follows:

static

    align(4);  // Align following record variable on a four-byte address in memory.

    recVar   :structType2;

3.2.2.3: C and Assembly Union Types

A C union is a special type of structure where all the fields have the same offset (in C, the offset is zero, in HLA you can actually select the offset though the default is zero). That is, all the fields of an instance of a union overlay one another in memory. Because the fields of a union all have the same starting address, there are no issues regarding field offset alignment between C and HLA. Therefore, all you need really do is directly convert the C syntax to the HLA syntax for a union declaration.

In C, union type declarations can take one of two forms:

union unionTag

{

    <<fields that look like variable declarations>>

} <<optional union variable declarations>>;

typedef union

{

    <<fields that look like variable declarations>>

}

    unionTag2; /* The type declaration */

You may also declare union variables directly using syntax like the following:

union

{

    << fields that look like variable declarations >>

}

    unionVariable1, unionVariable2, unionVariable3;

    union unionTag unionVar3, unionVar4;

    unionTag2 unionVar5;

In HLA, you declare a union type in HLA's type section using the union/endunion reserved words as follows:

type

    unionType:

        union

            << fields that look like HLA variable declarations >>

        endunion;

You can declare actual HLA union variables using declarations like the following:

storage

    unionVar1:

        union

            << fields that look like HLA variable declarations >>

        endunion;

    unionvar2: unionType;

The size of a union object in HLA is the size of the largest field in the union declaration. You can force the size of the union object to some fixed size by placing an align directive at the end of the union declaration. For example, the following HLA type declaration defines a union type that consumes an even multiple of four bytes:

type

    union4_t:

        union

            b    :boolean;

            c    :char[3];

            w    :word;

            align(4);

        endunion;

Without the "align(4);" field in this union declaration, HLA would only allocate three bytes for a object of type union4_t because the single largest field is c, which consumes three bytes. The presence of the "align(4);" directive, however, tells HLA to align the end of the union on a four-byte boundary (that is, make the union's size an even multiple of four bytes). You'll need to check with your compiler to see what it will do with unions, but most compilers probably extend the union so that it's size is an even multiple of the largest scalar (e.g., non-array) object in the field list (in the example above, a typical C compiler would probably ensure that and instance of the union4_t type is an even multiple of two bytes long).

As for records, the fact that a union type is an even multiple of some size does not guarantee that a variable of that type (i.e., an instance) is aligned on that particular boundary in memory. As with records, if you want to ensure that a union variable begins at a desired address boundary, you need to stick an align directive before the declaration, e.g.,

var

    align(4);

    u  :union4_t;

3.2.2.4: C and Assembly Character String Types

The C/C++ programming language does not support a true string type. Instead, C/C++ uses an array of characters with a zero terminating byte to represent a character string. C/C++ uses a pointer to the first character of the character sequence as a "string object". HLA defines a true character string type, though it's internal representation is a little different than C/C++'s. Fortunately, HLA's string format is upwards compatible with the zero-terminated string format that C/C++ uses, so it's very easy to convert HLA strings into the C/C++ format (indeed, the conversion is trivial). There are a few problems going in the other direction (at least, at run time). So a special discussion of HLA versus C/C++ strings is in order.

Character string declarations are somewhat confused in C/C++ because C/C++ often treats pointers to an object and arrays of that same object type equivalently. For example, in an arithmetic expression, C/C++ does not differentiate between the use of a char* object (a character pointer) and an array of characters. Because of this syntactical confusion in C/C++, you'll often see strings in this language declared one of two different ways: as an array of characters or as a pointer to a character. For example, the following are both typical string variable declarations in C/C++:

    char stringA[ 256 ]; // Holds up to 255 characters plus a zero terminating byte.

    char *stringB;       // Must allocate storage for this string dynamically.

The big difference between these two declarations is that the stringA declaration actually reserves the storage for the character string while the stringB declaration only reserves storage for a pointer. Later, when the program is running, the programmer must allocate storage for the string associated with stringB (or assign the address of some previously allocated string to stringB). Interestingly enough, once you have two declarations like stringA and stringB in this example, you can access characters in either string variable using either pointer or array syntax. That is, all of the following are perfectly legal given these declarations for stringA and stringB (and they all do basically the same thing):

    char ch;

    ch = stringA[i];       // Access the char at position i in stringA.

    ch = *(stringB + i);   // Access the char at position i in stringB.

    ch = stringB[i];       // Access the char at position i in stringB.

    ch = *(stringA + i);   // Access the char at position i in stringA.

String literal constants in C/C++ are interesting. Syntactically, a C/C++ string literal looks very similar to an HLA string literal: it consists of a sequence of zero or more characters surrounded by quotes. The big difference between HLA string literals and C/C++ string literals is that C/C++ uses escape sequences to represent control characters and non-graphic characters within a string (as well as the backslash and quote characters). HLA does not support the escape character sequences (see the section on character constants for more details on C/C++ escape character sequences). To convert a C/C++ string literal that contains escape sequences into an HLA character string, there are four rules you need follow:

  • Replace the escape sequence \" with "". HLA uses doubled-up quotes to represent a single quote within a string literal constant.
  • Replace the escape sequence \\ with a single backslash. Since HLA doesn't use the backslash as a special character within a string literal, you need only one instance of it in the string to represent a single backslash character
  • Convert special control-character escape sequences, e.g., \n, \r, \a, \b, \t, \f, and \v, to their corresponding ASCII codes (see Table 3-3) and splice that character into the string using HLA's #nn character literal, e.g., the C/C++ string "hello\nworld\n" becomes the following:
"hello" #$d #$a "world" #$d #$a    //HLA automatically splices all
this together.
  • Whenever a C/C++ numeric escape sequence appears in a string (e.g., \0nn or \0Xnn) then simply convert the octal constant to a hexadecimal constant (or just use the hexadecimal constant as-is) along with the HLA #$nn literal constant specification and splice the object into the string as before. For example, the C/C++ string "hello\0xaworld0xa" becomes:
"hello" #$a "world" #$a

Whenever a C/C++ compiler sees a string literal constant in a source file, the compiler will allocate storage for each character in that constant plus one extra byte to hold the zero terminating byte5 and, of course, the compiler will initialize each byte of this storage with the successive characters in the string literal constant. C/C++ compilers will typically (though not always) place this string they've created in a read-only section of memory. Once the compiler does this, it replaces the string literal constant in the program with the address of the first character it has created in this read-only memory segment. To see this in action, consider the following C/C++ code fragment:

    char* str;

    str = "Some Literal String Constant";

This does not copy the character sequence "Some Literal String Constant" into str. Instead, the compiler creates a sequence of bytes somewhere in memory, initializes them with the characters in this string (plus a zero terminating byte), and then stores the address of the first character of this sequence into the str character pointer variable. This is very efficient, as C/C++ needs to only move a four-byte pointer around at run-time rather than moving a 29 byte sequence.

The HLA language provides an explicit string data type. Internally, however, HLA represents strings using a four-byte pointer variable, just as C/C++ does. There is an important difference, however, between the type of data that an HLA string variable references and a C/C++ character pointer references: HLA includes some additional information with its string data: specifically, a length field and a maximum length field. Like C/C++ string objects, the address held in an HLA string variable points at the first character of the character string (and HLA strings also end with a zero terminating byte). Unlike C/C++ however, the four bytes immediately preceding the first character of the string contain the current length of the string (as an uns32 value). The four bytes preceding the length field contain the maximum possible length of the string (that is, how much memory is reserved for the string variable, which can be larger than the current number of characters actually found in the string). Figure 3-1 shows the HLA string format in memory.

Figure 3-1: HLA String Format in Memory

The interesting thing to note about HLA strings is that they are downwards compatible with C/C++ strings. That is, if you've got a function, procedure, or some other piece of code that operates on a C/C++ string, you can generally pass it a pointer to an HLA string and the operation will work as expected6. This was done by design and is one of the reasons HLA interfaces so well with the Win32 API. The Win32 API generally expects C/C++ zero terminated strings when you pass it string data. Usually, you can pass an HLA string directly to a Win32 API function and everything will work as expected (few Win32 API function calls actually change the string data).

Going the other direction, converting a C/C++ string to an HLA string, is not quite as trivial (though still easy to accomplish using appropriate code in the HLA Standard Library). Before describing how to convert C/C++ strings to the HLA format, however, it is important to point out that HLA is assembly language and assembly language is capable of working with any string format you can dream up (including C/C++ strings). If you've got some code that produces a zero-terminated C/C++ string, you don't have to convert that string to an HLA string in order to manipulate it within your HLA programs. Although the HLA string format is generally more efficient (faster) because the algorithms that process HLA strings can be written more efficiently, you can write your own zero-terminated string functions and, in fact, there are several common zero-terminated string functions found in the HLA Standard Library. However, as just noted, HLA strings are generally more efficient, so if you're going to be doing a bit of processing on a zero terminated string, it's probably wise to convert it to an HLA string first. On the other hand, if you're only going to do one or two trivial operations on a zero-terminated string that the Win32 API returns, you're better off manipulating it directly as a zero-terminated string and not bothering with the conversion (of course, if convenience is your goal rather than efficiency, it's probably always better to convert the string to the HLA string format upon return from a Win32 API call just so you don't have to work with two different string formats in your assembly code). Of course, the other option open to you is to work exclusively with zero-terminated strings in your assembly code (though the HLA Standard Library doesn't provide anywhere near the number of zero-terminated string functions).

The purpose of this chapter is to explain how to convert C/C++ based Windows documentation to a form suitable for assembly language programmers and describe how assembly language programmers can call the C-based functions that make up the Win32 API set. Therefore, we won't get into the details of converting string function calls in C/C++ to their equivalent (or similar) HLA Standard Library calls. Rest assured, the HLA Standard Library provides a much richer set of string functions than does the C Standard Library; so anything you find someone doing in C/C++ can be done just as easily (and usually more efficiently) using an HLA Standard Library function. Please consult the HLA Standard Library documentation for more details. In this section, we'll simply cover those routines that are necessary for converting between the two different string formats and copying string data from one location to another.

In C/C++ code, it is very common to see the program declare a string object (character pointer) and initialize the pointer with the address of a literal string constant all in one operation, e.g.,

    char *str = "Literal String Constant";

Remember, this doesn't actually copy any character data; it simply loads the character pointer variable str with the address of the `L' character in this string literal constant. The compiler places the literal string data somewhere in memory and either initializes str with the address of the start of that character data (if str is a global or static variable) or it generates a short (generally one instruction) machine code sequence to initialize a run-time variable with the address of this string constant. Note, and this is very important, many compilers will allocate the actual string data in read-only memory. Therefore, as long as str continues to point at this literal string constant's data in memory, any attempt to store data into the string will likely produce a memory protection fault. Some very old C programs may have made the assumption that literal string data appears in read/write memory and would overwrite the characters in the string literal constant at run-time. This is generally not allowed by modern C/C++ compilers. HLA, by default, also places string literal data in write-protected memory. So if you encounter some older C code that overwrites the characters in a string literal constant, be aware of the fact that you will not (generally) be able to get away with this in HLA.

If str (in the previous example) is a static or global variable, converting this declaration to assembly language is very easy, you can use a declaration like the following:

static

    HLAstr  :string := "Literal String Constant";

Remember, like C/C++, HLA implements string objects using pointers. So HLAstr is actually a four-byte pointer variable and this code initializes that pointer with the address of the first character in the literal string constant.

Because HLA string variables are pointers that (when properly initialized) point at a sequence of characters that end with a zero byte (among other things), you can use an HLA string variable just about anywhere C/C++ expects a string object. So, for example, if you want to make some Win32 API call that requires a string parameter, you can pass in HLAstr as that parameter, e.g.,

Win32APIcall( HLAstr );

Of course, you don't have to assign the address of a string literal constant to an HLA string variable to make this function call, HLA allows you to pass the literal string constant directly:

Win32APIcall( "Literal String Constant" );

Note that this call does not pass the string data directly to the function; like C/C++, HLA continues to allocate and initialize the literal string constant in write-protected memory somewhere else and simply passes the address of the literal string constant as the function's parameter.

For actual string variable objects (that is, strings whose character values you can change at run-time), you'll typically find two different ways of variable declaration/allocation in a C/C++ program: the programmer will either create a character array with sufficient storage to hold the characters in the string, or the programmer will simply declare a character pointer variable and allocate storage for the string at run-time using a function like malloc (C) or new (C++). We'll look at both of these mechanisms in HLA in the following paragraphs.

One way to allocate storage for a C string variable is to simply declare a character array with sufficient room to hold the longest possible string value you will assign plus an extra byte for the zero terminator. To do this, a C/C++ programmer simply declares a character array with n+1 elements, where n is the maximum number of characters they expect to put into the string. The following is a typical "string" declaration in C/C++ that creates a string capable of holding up to 255 characters:

char myStr[256];  // 255 chars plus a zero terminating byte

Although HLA most certainly allows you to create arrays of characters, such arrays are not directly compatible with HLA strings (because they don't reserve extra storage prior to the string object for the length and maximum length fields that are present in the HLA string format). Therefore, you cannot allocate storage for an HLA string variable by simply doing the following:

static

        myStr :char[256];  // Does NOT create an HLA string!

You can use this technique to pre-allocate storage for a zero-terminated string in HLA, but this does not create the proper storage for an HLA string variable.

In general, HLA expects you to allocate storage for string objects dynamically (that is, at run-time). However, for static strings (i.e., non-automatic string variables) the HLA Standard Library does provide a macro, str.strvar, that lets you declare string variable and allocate storage for the string variable at compile-time. Here is an example of the use of this macro:

static  //Note: str.strvar only makes sense in the STATIC section

    preAllocated :str.strvar( 255 ); // Allows strings up to 255 characters long.

There are a couple of important things to note about this declaration. First of all, note that you surround the size of the string with parentheses, not square brackets. This is because str.strvar is actually a macro, not a type and the 255 that appears in this example is a macro argument. The second thing to note is that the use of the str.strvar macro only makes sense in the static declaration section. It is illegal in a var or storage section because this macro initializes the variable you're declaring (something you can't do in a var or storage declaration section). While the use of the str.strvar macro is technically legal in HLA's readonly declaration section, it doesn't make sense to use it there since doing so would allocate the string data storage in write-protected memory, so you'd never be able to store any character data into the string. Also note that the str.strvar macro doesn't provide any mechanism for initializing the character data in the string within the declaration (not that it would be that hard to create a new macro that does this). Finally, note that you specify the maximum number of characters you want to allow in the string as the str.strvar argument. You do not have to account for any zero-terminating bytes, the current string length field, or the maximum length field.

Although the str.strvar macro is very convenient to use, it does have a couple of serious limitations. Specifically, it only makes sense to use it in the static section of an HLA program and you must know the maximum length of the string before you can use the str.strvar macro to declare a string object. Both of these issues are easy to resolve by using dynamically allocated string objects. C/C++ programmers can also create dynamically allocated strings by declaring their string variables as pointers to characters, e.g.,

    char *strAsPtr;

        .

        .

        .

    // Allocate storage for a 255 character string at run-time

    strAsPtr = malloc( 256 );  // 255 chars + zero terminating byte

    // Or (in C++):

    strAsPtr = new char[256];

In HLA, the declaration and allocation is very similar:

var  // Could also be static or storage section

    strAsPtr :string;

        .

        .

        .

    strmalloc( 255 );

    mov( eax, strAsPtr );

Do note a couple of things about the HLA usage. First, you should note that the specify the actual number of characters you wish to allow in the string; you don't have to worry about the zero terminating byte. Second, you should call the HLA Standard Library stralloc function (rather than malloc, which HLA also provides) in order to allocate storage for a string at run time; in addition to allocating storage for the character data in the string, stralloc also allocates (additional) storage for the zero terminating byte, the maximum length value, and the current length value. The stralloc function also initializes the string to the empty string (by setting the current length to zero and storing a zero in the first character position of the string data area). Finally, note that stralloc returns the address of the first character of the string in EAX so you must store that address into the string variable. HLA does support a feature known as instruction composition, so you could actually write these two instructions as follows:

mov( stralloc( 255 ), strAsPtr );

The drawback to this form, however, is that it hides the fact that this code sequence wipes out the EAX register. So be careful if you decide to use this form.

Once you've initialized an HLA string variable so that it points at valid string data, you can generally pass that string variable to a C/C++ function (i.e., a Win32 API function) that expects a string value. The only restriction is that if the C/C++ function modifies the string data in such a way that it changes the length of the string, HLA will not recognize the change if you continue to treat the string as an HLA type string upon return from the C/C++ function. Likewise, if a C/C++ function returns a pointer to a string that the function has created itself, your HLA functions cannot treat that string as an HLA string because it's probably just a zero-terminated string. Fortunately, there are only a small number of Win32 API functions that return a string or modify an existing string, so you don't have to worry about this very often. However, that's a problem in and of itself; you may have to deal with this problem so infrequently that it slips your mind whenever you call a function that returns such a string. As a concrete example, consider the following (HLA) prototype for the Win32 GetFullPathName function:

static

    GetFullPathName: procedure

    (

           lpFileName    : string;

           nBufferLength : dword;

       var lpBuffer      : var;

       var lpFilePart    : var

    );

    @stdcall; @returns( "eax" ); @external( "__imp__GetFullPathNameA@16" );

This function, given the address of a zero-terminated string containing a filename (lpFileName), the length length of a buffer (nBufferLength), the address of a buffer (lpBuffer), and the address of a pointer variable (lpFilePart) will convert the filename you pass in to a full drive letter/path/filename string. Specifically, this function returns a string in the buffer whose address you specify in the lpbuffer parameter. However, this function does not create an HLA-compatible string; it simply creates a zero-terminated string.

Fortunately, most functions in the Win32 API that return string data do two things for you that will help make your life easier. First of all, Win32 API functions that store string data into your variables generally require that you pass a maximum length for the string (e.g., the nBufferLength parameter in the prototype above). The function will not write any characters to the buffer beyond this maximum length (doing so could corrupt other data in memory). The other important thing this function does (which is true for most Win32 API functions that return string data) is that it returns the length of the string in the EAX register; the function returns zero if there was some sort of error. Because of the way this function works, converting the return result to an HLA string is nearly trivial. Consider the following call to GetFullPathName:

static

    fullName :string;

    namePtr  :pointer to char;

        .

        .

        .

    stralloc( 256 );       // Allocate sufficient storage to hold the string data.

    mov( eax, fullName );

        .

        .

        .

    mov( fullName, edx );

    GetFullPathName

    (

        "myfile.exe",                          // File to get the full path for.

        (type str.strRec [edx]).MaxStrLen,     // Maximum string size

        [edx],                                 // Pointer to buffer

        namePtr                                // Address of base name gets stored here

    );

    mov( fullName, edx );                     // Note: Win32 calls don't preserve EDX

    mov( eax, (type str.strRec [edx]).length  // Set the actual string length

The nice thing about most Win32 API calls that return string data is that they guarantee that they won't overwrite a buffer you pass in (you also pass in the maximum string length, which is available in the MaxStrLen field of the string object, that is, at offset -8 from the string pointer). These string functions also return the string length as the function's result, so you can shove this into the HLA string's length field (at offset -4 from the string's base address) immediately upon return. This is a very efficient way to convert C/C++ strings to HLA format strings upon return from a function.

Of course, converting a C/C++ string to an HLA string is only easy if the C/C++ function you're calling returns the length of the string it has processed. It also helps if the function guarantees that it won't overstep the bounds of the string variable you've passed it (i.e., it accepts a MaxStrLen parameter and won't write any data beyond the maximum buffer size you've specified). Although most Win32 API functions that return string data operate this way (respect a maximum buffer size and return the length of the actual string), there are many C/C++ functions you may need to call that won't do this. In such a case, you've got to compute the length of the string yourself (and guarantee that your character buffer is large enough to hold the maximum possible string the function will produce). Fortunately, there is a function in the HLA Standard Library, str.zlen, that will compute the length of a zero terminated string so you can easily update the length field of an HLA string object that a C/C++ function has changed (without respect to the HLA string's length field). For example, suppose you have a C/C++ function named fstr that expects the address of a character buffer where it can store (or modify) a zero-terminated string. Since HLA strings are zero-terminated, you can pass an HLA string to this function. However, if fstr changes the length of the string, the function will not update the HLA string's length field and the result will be inconsistent. You can easily correct this by computing the length of the string yourself and storing the length into the HLA string's length field, e.g.,

static

    someStr  :str.strvar( 255 );  // Allow strings up to 255 characters in length.

        .

        .

        .

    fstr( someStr );     // Assume this changes the length of someStr.

    mov( someStr, edx ); // Get the pointer to the string data structure.

    str.zlen( edx );     // Compute the new length of someStr.

    mov( eax, (type str.strRec [edx]).length );  // Update the length field.

One thing to keep in mind is that str.zlen has to search past every character in the string to find the zero-terminating byte in order to compute the string's length. This is not a particularly efficient operation, particularly if the string is long. Although str.zlen uses a fairly decent algorithm to search for the terminating zero byte, the amount of time it takes to execute is still proportional to the length of the string. Therefore, you want to avoid calling str.zlen if at all possible. Fortunately, many C/C++ string functions (that modify string data) return the length as the function result, so calling str.zlen isn't always necessary.

Although not explicitly shown in the example above, do keep in mind that many string functions (especially Win32 API functions) assign double-duty to the function return result; they'll return a positive value if the function successfully produces a string and they'll return a negative result (or sometimes a zero result) if there was an error. For example, the GetFullPathName function in the earlier example returns zero if it there was a problem producing the string. You code should check for errors on return from these functions to prevent problems. While shoving a zero into a string length field isn't cause for concern (indeed, that's perfectly reasonable), a negative number will create all kinds of problems (since the HLA string functions treat the length field as an uns32 value, those functions will interpret a negative number as a really large positive value).

3.2.2.5: C++ and Assembly Class Types

C++ and HLA both support classes. Since HLA is an assembly language, it should be obvious that anything you can do with a C++ class you can do with in HLA (since C++ compilers convert C++ source code into x86 machine code, it should be obvious that you can achieve anything possible in C++ using assembly language). However, directly translating C++ classes into HLA classes is not a trivial matter. Many constructs transfer across directly. Some constructs in C++, however, do not have a direct correspondence in HLA. Quite honestly, the conversion of C++ classes to HLA classes is beyond the scope of this book; fortunately, we're dealing with the Win32 API in this book so we won't see much Windows source code that uses classes in typical documentation we're interested it. There is quite a bit of C++-based object-oriented code out there for Windows but most of it uses the Microsoft Foundation Classes (MFC) class library, and that's not applicable to assembly language programming (at least, not today; someday there might be an "Assembly Language Foundation Class" library but until that day arrives we don't have to worry about MFC). Since this book covers Win32 API programming, there really is no need to worry about converting C++ classes into assembly - the Win32 API documentation doesn't use classes. This book may very well use HLA classes and objects in certain programming examples, but that will be pure assembly language, not the result of translating C++ code into HLA code.

3.2.3: C and Assembly Pointer Types7

When running under 32-bit versions of the Windows operating system, C pointers are always 32-bit values and map directly to 32-bit flat/linear machine addresses. This is true regardless of what the pointer references (including both data and functions in C). At one level, the conversion of a pointer from C to HLA/assembly is fairly trivial, just create a dword variable in you HLA program and keep the pointer there. However, HLA (like C) supports typed pointers and procedure pointers; generally, it's a good idea to map C pointers to their HLA typed-pointer equivalent.

In C, you can apply the address-of operator, `&' to an object (e.g., a static variable or a function name) to take the address of that object. In HLA, you may use this same operator to take the address of a static object (a static/storage/readonly variable, statement label, or procedure name). The result is a 32-bit value that you may load into a register or 32-bit memory variable. For example, if in C you have a statement like the following (pi is a pointer to an integer and i is a static integer variable):

pi = &i;

You convert this to HLA syntax as follows:

mov( &i, pi );

If the object you're taking the address of with the `&' operator is not a function name or a static (non-indexed) variable, then you must compute the address of the object at run-time using the lea instruction. For example, automatic variables (non-static local variables) fall into this class. Consider the following C function fragment:

    int f( void )

    {

        int *pi;   // Declares a variable that is a pointer to an integer.

        int i;     // Declares a local integer variable using automatic allocation.

        pi = &i;

            .

            .

            .

    }

Because i is an automatic variable (the default storage class for local variables in a function) HLA cannot static compute this address for you at compile time. Instead, you would use the lea instruction as follows:

    procedure f;

    var

        pi: pointer to int32;  // Declare HLA pointers this way.

        i:  int32;             // Declares an automatic variable in HLA (in the VAR section)

            .

            .

            .

        lea( eax, i );    // Compute the run-time address of i

        mov( eax, pi );   // Save address in pi.

            .

            .

            .

You should also note that you need to use the lea instruction when accessing an indexed object, even if the base address is a static variable. For example, consider the following C/C++ code that takes the address of an array element:

    int *pi;

    static int array[ 16 ];

        .

        .

        .

    pi = &array[i];

In order to access an array element in assembly language, you will need to use a 32-bit register as an index register and compute the actual element address at run-time rather than at compile-time (assuming that i in this example is a variable rather than a constant). Therefore, you cannot use the `&' operator to statically compute the address of this array element at compile-time, instead you should use the lea instruction as follows:

    mov( i, ebx );               // Move index into a 32-bit register.

    lea( eax, array[ ebx*4 ] );  // int objects are four bytes under Win32

    mov( eax, pi );

As you've seen in these simple examples, C uses the unary `*' operator to declare pointer objects. Anytime you see a declaration like the following in C:

    typename *x;

you can convert it to a comparable HLA declaration as follows:

    x:pointer to hla_typename;  // hla_typename is the HLA equivalent of C's typename

Of course, since all pointers are simply 32-bit objects, you can also convert to assembly language using a statement like the following:

    x:dword;

Both forms are equivalent in assembly language, though the former version is preferable since it's a little more descriptive of what's going on here.

Function pointers in C is one area where the C syntax can get absolutely weird. A simple C function pointer declaration takes the following form:

    returnType (*functionPtrName) ( parameters );

For example,

    int (*ptrToFuncReturnsInt) ( int i, int j );

This example declares a pointer to a function that takes two integer parameters and returns an integer value. Note that the following is not equivalent to this example:

    int *ptrToFuncReturnsInt( int i, int j );  //Not a function pointer declaration!

This example is a prototype for a function that returns a pointer to an integer, not a function pointer. C's syntax is a little messed up (i.e., it wasn't thought out properly during the early stages of the design), so you can get some real interesting function pointer declarations; some are nearly indecipherable.

Of course, at the assembly language level all pointers are just dword variables. So all you really need to implement the C function pointer in HLA is a statement like the following:

    ptrToFuncReturnsInt: dword;

You can all this function in HLA using the call instruction as follows:

    call( ptrToFuncReturnsInt );

As for data pointers, however, HLA provides a better solution: procedure variables. A procedure variable is a pointer object that (presumably) contains the address of some HLA procedure. The advantage of a procedure variable over a dword variable is that you can use HLA's high-level syntax calling convention with procedure variables; something you cannot do with a dword variable. Here's an example of a procedure variable declaration in HLA:

    ptrToFuncReturnsInt: procedure( i:int32; j:int32 );

HLA allows you to call the code whose address this variable contains using the standard HLA procedure call syntax, e.g.,

    ptrToFuncReturnsInt( 5, intVar );

To make this same call using a dword variable8, you'd have to manually pass the parameters yourself as follows (assuming you pass the parameters on the stack):

        push( 5 );       // Pass first parameter (i)

        push( intVar );  // Pass second parameter.

        call( ptrToFuncReturnsInt );

In C, any time a function name appears without the call operator attached to it (the "call operator" is a set of parenthesis that may contain optional parameters), C substitutes the address of the function in place of the name. You do not have to supply the address-of operator (`&') to extract the function's address (though it is legal to go ahead and do so). So if you see something like the following in a C program:

ptrToFuncReturnsInt = funcReturnsInt;

where funcReturnsInt is the name of a function that is compatible with ptrToFuncReturnsInt's declaration (e.g., it returns an integer result and has two integer parameters in our current examples), this code is simply taking the address of the function and shoving it into ptrToFuncReturnsInt exactly as though you'd stuck the `&' operator in front of the whole thing. In HLA, you can use the `&' operator to take the address of a function (they are always static objects as far as the compiler is concerned) and move it into a 32-bit register or variable (e.g., a procedure variable). Here's the code above rewritten in HLA:

mov( &funcReturnsInt, ptrToFuncReturnsInt );

Both C and HLA allow you to initialize static variables (including pointer variables) at compile time. In C, you could do this as follows:

static int (*ptrToFuncReturnsInt) ( int i, int j) = funcRe-
turnsInt;

The comparable statement in HLA looks like this:

procedure funcReturnsInt( i:int32; j:int32 );

begin funcReturnsInt;

        .

        .

        .

end funcReturnsInt;

        .

        .

        .

static

    ptrToFuncReturnsInt:procedure( i:int32; j:int32 ) := &funcReturnsInt;

Especially note that in HLA, unlike C, you still have to use the `&' operator when taking the address of a function name.

Note that you cannot initialize HLA automatic variables (var variables) using a statement like the one in this example. Instead, you must move the address of the function into the pointer variable using the mov instruction given a little earlier.

Arrays are another object that C treats specially with respect to pointers. Like functions, C will automatically supply the address of an array if you specify the name of an array variable without the corresponding index operator (the square brackets). HLA requires that you explicitly take the address of the array variable. If the array is a static object (static/readonly/storage) then you may use the (static) address-of operator, `&'; however, if the variable is an automatic (var) object, then you have to take the address of the object at run-time using the lea instruction:

static

    staticArray  :byte[10];

var

    autoArray    :byte[10];

        .

        .

        .

    mov( &staticArray, eax );   // Can use `&' on static objects

    lea( ebx, autoArray );      // Must use lea on automatic (VAR) objects.

C doesn't automatically substitute the address of a structure or union whenever it encounters a struct or union variable. You have to explicitly take the address of the object using the `&' operator. In HLA, taking the address of a structure or union operator is very easy - if it's a static object you can use the `&' operator, if it's an automatic (var) object, you have to use the lea instruction to compute the address of the object at run-time, just like other variables in HLA.

There are a couple of different ways that C/C++ allows you to dereference a pointer variable9. First, as you've already seen, to dereference a function pointer you simply "call" the function pointer the same way you would directly call a function: you append the call operator (parenthesis and possible parameters) to the pointer name. As you've already seen, you can do the same thing in HLA as well. Technically, you could also dereference a function pointer in C/C++ as follows:

(*ptrToFuncReturnsInt)( 5, intVar );

However, you'll rarely see this syntax in an actual C/C++ source file. You may convert an indirect function call in C/C++ to either HLA's high-level or low-level syntax, e.g., the following two calls are equivalent:

        push( 5 );

        push( intVar );

        call( ptrToFuncReturnsInt );

// -or-

        ptrToFuncReturnsInt( 5, intVar );

Dereferencing a pointer to a data object is a bit more exciting in C/C++. There are several ways to dereference pointers depending upon the underlying data type that the pointer references. If you've got a pointer that refers to a scalar data object in memory, C/C++ uses the unary `*' operator. For example, if pi is a pointer that contains the address of some integer in memory, you can reference the integer value using the following C/C++ syntax:

        *pi = i+2;  // Store the sum i+2 into the integer that pi points at.

        j = *pi;    // Grab a copy of the integer's value and store it into j.

Converting an indirect reference to assembly language is fairly simple. The only gotcha, of course, is that you must first move the pointer value into an 80x86 register before dereferencing the pointer. The following HLA examples demonstrate how to convert the two C/C++ statements in this example to their equivalent HLA code:

    // *pi = i + 2;

    mov( pi, ebx );    // Move pointer into a 32-bit register first!

    mov( i, eax );     // Compute i + 2 and leave sum in EAX

    add( 2, eax );

    mov( eax, [ebx] ); // Store i+2's sum into the location pointed at by EBX.

    // j = *pi;

    mov( pi, ebx );    // Only necessary if EBX no longer contains pi's value!

    mov( [ebx], eax ); // Only necessary if EAX no longer contains *pi's value!

    mov( eax, j );     // Store the value of *pi into j.

If you've got a pointer that holds the address of a sequence of data values (e.g., an array), then there are two completely different (but equivalent) ways you can indirectly access those values. One way is to use C/C++'s pointer arithmetic syntax, the other is to use array syntax. Assuming pa is a pointer to an array of integers, the following example demonstrates these two different syntactical forms in action:

    *(pa+i) = j;   // Stores j into the ith object beyond the address held in pa.

    pa[i] = j;     // Stores j into the ith element of the array pointed at by pa.

The important thing to note here is that both forms are absolutely equivalent and almost every C/C++ compiler on the planet generates exactly the same code for these two statements. Since compilers generally produce exactly the same code for these two statements, it should come as no surprise that you would manually convert these two statements to the same assembly code sequence. Conversion to assembly language is slightly complicated by the fact that you must remember to multiply the index into an array (or sequence of objects) by the size of each array element. You might be tempted to convert the statements above to something like the following:

    mov( j, XXX );          // XXX represents some register that will hold j's value.

    mov( pa, ebx );         // Get base address of array/sequence in memory

    mov( i, ecx );          // Grab index

    mov( XXX, [ebx][ecx] ); // XXX as above

The problem with this sequence is that it only works properly if each element of the array is exactly one byte in size. For larger objects, you must multiply the index by the size of an array element (in bytes). For example, if each element of the array is six bytes long, you'd probably use code like the following to implement these two C/C++ statements:

    mov( j, eax );            // Get the L.O. four bytes of j.

    mov( j[4], dx );          // Get the H.O. two bytes of j.

    mov( pa, ebx );           // Get the base address of the array into EBX

    mov( i, ecx );            // Grab the index

    intmul( 6, ecx );         // Multiply the index by the element size (six bytes).

    mov( eax, [ebx][ecx] );   // Store away L.O. four bytes

    mov( dx, [ebx][ecx][4] ); // Store away H.O. two bytes.

Of course, if the size of your array elements is one of the four magic sizes of one, two, four, or eight bytes, then you don't need to do an explicit multiplication. You can get by using the 80x86 scaled indexed addressing mode as the following HLA example demonstrates:

    mov( j, eax );

    mov( pa, ebx );           // Get base address of array/sequence in memory

    mov( i, ecx );            // Grab index

    mov( eax, [ebx][ecx*4] ); // Store j's value into pa[i].

C uses yet another syntax when accessing fields of a structure indirectly (that is, you have a pointer to some structure in memory and you want to access a field of that structure via the pointer). The problem is that C's unary `*' (dereference) operator has a lower precedence than C's `.' (field access) operator. In order to access a field of some structure to which you have a pointer, you'd have to write an expression like the following when using the `*' operator:

(*ptrToStruct).field

Avoid the temptation to write this as follows:

*ptrToStruct.field

The problem with this latter expression is that `.' has a higher precedence than `*', so this expression tells the C compiler to dereference the thing that ptrToStruct.field points at. That is, ptrToStruct must be an actual struct object and it must have a field, field, that is a pointer to some object. This syntax indirectly references the value whose address field contains. An expression of the form "*(ptrToStruct).field" tells the compiler to first dereference the pointer ptrToStruct and then access field at the given offset from that indirect address.

Because accessing fields of a structure object indirectly is a common operation, using the syntax "(*ptrToStruct).field" tends to clutter up a program and make it less readable. In order to reduce the clutter the C/C++ programming language defines a second dereferencing operator that you use specifically to access fields of a structure (or union) via a pointer: the "->" operator. The "->" operator has the same precedence as the field selection operator (".") and they are both left associative. This allows you to write the following expression rather than the ungainly one given earlier:

ptrToStruct->field

Regardless of which syntax you find in the C/C++ code, in assembly language you wind up using the same code sequence to access a field of a structure indirectly. The first step is to always load the pointer into a 32-bit register and then access the field at some offset from that indirect address. In HLA, the best way to do this is to coerce an indirect expression like "[ebx]" to the structure type (using the "(type XXX [ebx])" syntax) and then use the "." field reference operator. For example,

type

    Struct:

        record

            field  :int32;

                .

                .

                .

        endrecord;

            .

            .

            .

static

    ptrToStruct :pointer to Struct;

        .

        .

        .

    // Access field "field" indirectly via "ptrToStruct"

    mov( ptrToStruct, ebx );

    mov( (type Struct [ebx]).field, eax );

For more details on the HLA type coercion operator, please consult the HLA language reference manual. Note that you use this same technique to indirectly access fields of a union or a class in HLA.

You may combine structure, array, and pointer types in C/C++ to form recursive and nested types. That is, you can have an array of structs, a struct may contain a field that is an array (or a struct), or you could even have a structure that has a field that is an array of structs whose fields are arrays of pointers to structs whose fields... In general, a C/C++ programmer can create an arbitrarily complex data structure by nesting array, struct, union, class, and pointer data types. Translating such objects into assembly language is equally complex, often taking a half dozen or more instructions to access the final object. Although such constructs rarely appear in real-world C/C++ programs, they do appear every now an then, so you'll need to know how to translate them into assembly language.

Although a complete description of every possible data structure access conversion would require too much space here, an example that demonstrates the process you would go through is probably worthwhile. For our purposes, consider the following complex C/C++ expression:

per->field[i].p->x[j].y

This expression uses three separate operators: "->", "[ ]", and ".". These three operators all have the same precedence and are left associative, so we process this expression strictly on a left-to-right basis10. The first object in this expression, per, is a pointer to some structure. So the first step in the conversion to HLA is to get this pointer value into a 32-bit register so the code can refer to the indirect object:

mov( per, ebx );

The next step is to access a field of the structure that per references. In this example, field is an array of structures and the code accesses element i of this array. To access this particular object, we need to compute the index into the array (by multiplying the index by the size of an array element). Assuming field is an array of fieldpiece objects, you'd use code like the following to reference the ith object of field:

        mov( i, ecx );                      // Get the index into the field array.

        intmul( @size( fieldpiece ), ecx );  // Multiply index by the size of an element

The next step in the conversion of this expression is to access the p field of the ith array element of field. The following code does this:

    mov( (type ptrsType[ebx]).p[ecx], edx );

The interesting thing to note here is that the index into the field array is tacked on to the end of the HLA address expression we've created, i.e., we write "(type ptrsType[ebx]).p[ecx]" rather than "(type ptrsType[ebx])[ecx].p". This is done simply because HLA doesn't allow this latter syntax. Because the "." and "[ ]" operators both involve addition and addition is commutative, it doesn't matter which syntax we use. Note that HLA would allow an address expression of the form "(type ptrsType [ebx][ecx]).p" but this tends to (incorrectly) imply that EBX points at an array of pointers, so we'll not use this form here11.

The array element that "(type ptrsType [ebx]).p[ecx]" access is a pointer object. Therefore, we have to move this pointer into a 32-bit register in order to dereference the pointer. That's why the previous HLA statement moved this value into the EDX register. Now this pointer points at an array of array objects (the x field) and the code then accesses the jth element of this array (of structures). To do this, we can use the following HLA statements:

    mov( j, esi );                  // Get the index into the array.

    intmul( @size( xType ), esi );  // Multiply by the size of an array element.

   // Now [edx][esi] references the jth element of the x field

The last step in our code sequence is to reference the y field of the jth element of the x array. Assuming that y is a double-word object (just to make things easy for this example), here's the code to achieve this:

mov( (type xType [edx]).y[esi], eax );

Here's the complete code sequence:

    // ptr->field[i].p->x[j].y

        mov( ptr, ebx );

        mov( i, ecx );                      // Get the index into the field array.

        intmul( @size( fieldType ), ecx );  // Multiply index by the size of an element

        mov( (type ptrsType [ebx]).p[ecx], edx );

        mov( j, esi );                  // Get the index into the array.

        intmul( @size( xType ), esi );  // Multiply by the size of an array element.

        mov( (type xType [edx]).y[esi], eax );

3.2.4: C and Assembly Language Constants

Although we've already looked at literal constants in C/C++ and assembly language, we must still consider symbolic constants in C/C++ and their equivalents in assembly language. A symbolic constant is one that we refer to by name (an identifier) rather than the constant's value (i.e., a literal constant). Syntactically, a symbolic constant is an identifier and you use it as you would a variable identifier; semantically of course, there are certain limitations on symbolic constants (such as you cannot assign the value of an expression to a symbol constant). There are two types of symbolic constants you'll find in the C and C++ languages: manifest constants and storage constants.

The first type of constant to consider is a manifest constant. A manifest constant is a symbolic identifier to which you've bound (assigned) some value. During compilation, the compiler simply substitutes the actual value of the constant everywhere it encounters the manifest constant's identifier. From the standpoint of the compiler's code generation algorithms, there is no difference between a manifest constant and a literal constant. By the time the code generator sees the constants, it's a literal value. In C/C++, you can use the #define preprocessor directive to create manifest constants. Note that you can use a manifest constant anywhere a literal constant is legal.

There are two ways to convert a C/C++ manifest constant into assembly language. The first way, of course, is to manually do the translation from symbolic to literal constant. That is, whenever you encounter a manifest constant in C/C++, you simply translate it to the corresponding literal constant in your assembly language code. E.g.,

#define someConst 55

        .

        .

        .

    i = someConst;

in assembly language becomes:

    mov( 55, i );

Obviously, manually expanding manifest constants when converting C/C++ code into assembly language is not a good idea. The reasons for using symbolic constants in C/C++ apply equally well to assembly language programs. Therefore, the best solution is to keep the constants symbolic when translating the C/C++ code to assembly language. In HLA, the way to create manifest constants is by using the const declaration section. The exact form of the translation depends how the C/C++ code uses the #define preprocessor directive. Technically, the #define preprocessor directive in C/C++ doesn't define a constant, it defines a macro. There are two basic forms of the #define directive:

#define someID  some text...

#define someID(parameter list) some text...

We'll not consider the second form here, since that's a true macro declaration. We'll return to macros and how to convert this second example to HLA a little later in this chapter.

The first #define directive in this example defines a textual substitution macro. The C/C++ preprocessor will substitute the text following someID for each occurrence of someID appearing after this declaration in the source file. Note that the text following the declaration can be anything, it isn't limited to being a literal constant. For the moment, however, let's just consider the case where the text following the #define directive and the identifier is a single literal constant. This begin the case, you can create an equivalent HLA manifest constant declaration using code like the following:

// #define fiftyFive 55

const

    fiftyFive := 55;

Like C/C++, HLA will substitute the literal value 55 for the identifier fiftyFive everywhere it appears in the HLA source file.

There is a subtle difference between HLA manifest constants you define in a const section and manifest constants you define with C/C++'s #define directive: HLA's constants are typed while C/C++ #define constants are untyped textual substitutions. Generally, however, you will not notice a difference between the two. However, one special case does deserve additional comment: the case where a program specifies a constant expression rather than a single literal constant (which both languages allow). Consider the following statements in C/C++ and in HLA:

// C/C++ constant expression:

#define constExpr i*2+j

// HLA constant expression:

const

    constExpr := i*2+j;

The difference between these two is that the C/C++ preprocessor simply saves up the text "i*2+j" and emits that string whenever it encounters constExpr in the source file. C/C++ does not require that i and j be defined prior to the #define statement. As long as these identifiers have a valid definition prior to the first appearance of constExpr in the source file, the C/C++ code will compile correctly. HLA, on the other hand, evaluates the constant expression at the point of the declaration. So i and j must be defined at the point of the constant declaration (another difference is that HLA requires i and j to both be constants whereas C/C++ doesn't require this; though if i and j are not constant objects then this isn't really a manifest constant declaration as we're defining it here, so we won't worry about that).

Beyond the fact that C/C++ relaxes the requirement that i and j be defined before the manifest constant declaration, there is another subtle difference between C/C++ constant declarations and HLA constant declarations: late binding. HLA evaluates the value of the expression at the point you declare the constant in your source file (which is why i and j have to be defined at that point in the previous example). C/C++, on the other hand, only evaluates the constant expression when it actually expands the symbolic identifier in your source file. Consider the following C/C++ source fragment:

#define constExpr i*2 + j

#define i 2

#define j 3

    printf( "1:%d\n", constExpr );

#define i 4  //The compiler may issue a warning about this

    printf( "2:%d\n", constExpr );

The first printf statement in this example will display the value seven (2*2+3) whereas the second example will display 11 (4*2+3). Were you to do the equivalent in HLA (using val constants and the "?" statement in HLA, see the HLA reference manual for more details), you would get a different result, e.g.,

program t;

#include( "stdlib.hhf" )

? i := 2; // Defines i as a redefinable constant

? j := 3;

const

    constExpr := i*2 + j;

begin t;

    stdout.put( "1:", constExpr, nl );

    ? i := 4;

    stdout.put( "2:", constExpr, nl );

end t;

The HLA code prints the strings "1:7" and "2:7" since HLA only computes the expression "i*2+j" once, when you define the constExpr manifest constant.

HLA does allow the definition of textual substitution constants using the text data type in the const section. For example, consider the following HLA const declaration:

const

    constExpr :text := "i*2+j";

This declaration is totally equivalent to the C/C++ #define declaration. However, as you cannot drop in arithmetic expressions into assembly code at arbitrary points in your source file, this textual substitution isn't always legal in an assembly file as it might be in a C/C++ source file. So best not to attempt to use textual substitution constants like this. For completeness' sake, however, the following HLA example demonstrates how to embed textual substitution constant expressions in an HLA source file (and have the compiler calculate the expression at the point of expansion):

program t;

#include( "stdlib.hhf" )

? i := 2; // Defines i as a redefinable constant

? j := 3;

const

    // Note: "@eval" tells HLA to evaluate the constant expression inside

    //       the parentheses at the point of expansion.  This avoids some

    //       syntax problems with the stdout.put statements below.

    constExpr :text := "@eval(i*2 + j)";

begin t;

    stdout.put( "1:", constExpr, nl );

    ? i := 4;

    stdout.put( "2:", constExpr, nl );

end t;

This HLA example prints "1:7" and "2:11" just like the C/C++ example. Again, however, if the C/C++ manifest constant expansion depends upon late binding (that is, computing the value of the expression at the point of use rather than the point of declaration in the source file) then you should probably expand the text manually at each point of use to avoid problems.

The other way to define constant objects in C/C++ is to use the const keyword. By prefacing what looks like an initialized C/C++ variable declaration with the const keyword, you can create constant (immutable at run-time) values in your program, e.g.,

const int cConst = 4;

Although C/C++ lets you declare constant objects using the const keyword, such constants possess different semantics than manifest and literal constants. For example, in C/C++ you may declare an array as follows:

    #define maxArray 16

    int array[ maxArray ];

    int anotherArray[ maxArray ];

However, the following is generally not legal in C/C++:

    const int maxBounds = 8;

    int iArray[ maxBounds ];

The difference between manifest constants and const objects in C/C++ has to do with how the program treats the constant object at run-time. Semantically, C++ treats const objects as read-only variables. If the CPU and operating system support write-protected memory, the compiler may very well place the const object's value in write-protected memory to enforce the read-only semantics at run-time. Other than the compiler doesn't allow you to store the result of some expression into a const object, there is little difference between const and static variable declarations in C++. This is why a declaration like the one for iArray earlier is illegal. C/C++ does not allow you to specify an array bounds using a variable and (with the exception of the read-only attribute) const objects are semantically equivalent to variables. To understand why C/C++ const objects are not manifest constants and why such declarations are even necessary in C/C++ (given the presence of the #define preprocessor directive), we need to look at how CPUs encode constants at the machine code level.

The 80x86 provides special instructions that can encode certain constants directly in a machine instruction. Consider the following two 80x86 assembly language instructions:

    mov( maxBound, eax );  // Copy maxBound's value into eax

    mov( 8, eax );         // Copy the value eight into eax

In both cases the machine code the CPU executes consists of three components: an opcode that tells the processor that it needs to move data from one location to another; an addressing mode specification that specifies the register, whether the register is a destination or source register, and the format the other operand takes (it could be a register, a memory location, or a constant); and the third component is the encoding of the memory address or the actual constant. The instruction that copies maxBound's value into EAX encodes the address of the variable as part of the instruction whereas the instruction that copies the value eight into EAX encodes the 32-bit value for eight into the instruction. At the machine level, there is a fundamental difference between the execution of these two instructions - the CPU requires an extra step to fetch maxBound's value from memory (and this fact remains true even if you initialize maxBound to eight and place maxBound in write-protected memory). Therefore, the CPU treats certain types of literal and manifest constants differently than it does other types of constants.

Note that a constant (literal, manifest, or otherwise) object in a high level language does not imply that the language encodes that constant as part of a machine instruction. Most CPUs only allow you to encode integer constants (and in some cases, small integer constants) directly in an instruction's opcode. The 80x86, for example, does not allow you to encode a floating point constant within a machine instruction. Even if the CPU were capable of encoding floating point and all supported integer values as immediate constants within an opcode, high level languages like C/C++ support the declaration of large data objects as constant data. For example, you could create a constant array in C/C++ as follows:

const int constArray[4] = {0,1,2,3};

Few processors, if any, support the ability to encode an arbitrary array constant as immediate data within a machine instruction. Similarly, you'll rarely find structure/record constants or string constants encoded directly within an instruction. Support for large structured constants is the main reason C/C++ adds a another class of constants to the language.

A high level language compiler may encode a literal constant or a manifest constant as an instruction's immediate operand. There is no guarantee, however, that the compiler might actually do this; the CPU must support immediate constants of the specified type and the compiler write must choose to emit the appropriate immediate addressing mode along with the instruction. On the other hand, constants that are not manifest constants (e.g., const objects in C/C++) are almost always encoded as memory references rather than as immediate data to an instruction.

So why would you care whether the compiler emits a machine instruction that encodes a constant as part of the opcode versus accessing that constant value appearing in memory somewhere? After all, since machine instructions appear in memory, an immediate constant encoded as part of an instruction also appears in memory. So what's the difference? Well, the principle difference is that accessing a constant value appearing elsewhere in memory (i.e., not as immediate data attached to the instruction) requires twice as much memory to encode. First, you need the constant itself, consuming memory somewhere; second, you need the address that constant encoded as part of the instruction. Since the address of a constant value typically consumes 32-bits, it typically takes twice as much memory to encode the access to the constant. Of course, if you reference the same constant value throughout your code, the compiler should only store one copy of the constant in memory and every instruction that references that constant would reference the same memory location. However, even if you amortize the size of the constant access over any number of instructions, the bottom line is that encoding constants as memory location still takes more room than encoding immediate constants.

Another difference between manifest/literal constants and read-only objects is that decent compilers will compute the result of constant expressions at compile-time, something that it may not be able to do with read-only objects. Consider the following C++ code:

#define one 1

#define two 2

const int three = 3;

const int four = 4;

int i;

int j;

    i = one + two;

    j = three + four;

Most decent C/C++ compilers will replace the first assignment statement above with the following:

i = 3;  // Compiler compute 1+2 at compile-time

On the 80x86 processor this statement takes a single machine instruction to encode (this is generally true for most processors). Some compilers, however, may not precompute the value of the expression "three+four" and will, instead, emit machine instructions to fetch these values from their memory locations and add them at run-time.

HLA provides a mechanism whereby you can create immutable "variables" in your code if you need the storage semantics of a variable that the program must not change at run-time. You can use HLA's readonly declaration section to declare such objects, e.g.,

readonly

    constValue :int32 := -2;

For all intents and purposes, HLA treats the readonly declaration like a static declaration. The two major differences are than HLA requires an initializer associated with all readonly objects and HLA attempts to place such objects in read-only memory at run time. Note that HLA doesn't prevent you from attempting to store a value into a readonly object. That is, the following is perfect legal and HLA will compile the program without complaint:

mov( 56, constValue );

Of course, if you attempt to execute the program containing this statement, the program will probably abort with an illegal access violation when the program attempts to execute this statement. This is because HLA will place this object in write-protected memory and the operating system will probably raise an exception when you attempt to execute this statement.

A compiler may not be able to efficiently process a constant simply because it is a literal constant or a manifest constant. For example most CPUs are not capable of encoding a string constant in an instruction. Using a manifest string constant may actually make your program less efficient. Consider the following C code:

#define strConst "A string constant"

        .

        .

        .

    printf( "string: %s\n", strConst );

        .

        .

        .

    sptr = strConst;

        .

        .

        .

    result = strcmp( s, strConst );

        .

        .

        .

Because the compiler (actually, the C preprocessor) expands the macro strConst to the string literal "A string constant" everywhere the identifier strConst appears in the source file, the above code is actually equivalent to:

        .

        .

        .

    printf( "string: %s\n", "A string constant" );

        .

        .

        .

    sptr = "A string constant";

        .

        .

        .

    result = strcmp( s, "A string constant" );

        .

        .

        .

The problem with this code is that the same string constant appears at different places throughout the program. In C/C++, the compiler places the string constant off in memory somewhere and substitutes a pointer to that string for the string literal constant. A naive compiler would wind up making three separate copies of the string in memory, thus wasting space since the data is exactly the same in all three cases. Compiler writers figured this out a couple of decades ago and modified their compilers to keep track of all the strings the compiler had already emitted; when the program used the same literal string constant again, the compiler would not allocate storage for a second copy of the string, it would simply return the address of the earlier string appearing in memory. Such an optimization could reduce the size of the code the compiler produced by a fair amount if the same string appears through some program. Unfortunately, this optimization probably lasted about a week before the compiler vendors figured out that there were problems with this approach. One major problem with this approach is that a lot of C programs would assign a string literal constant to a character pointer variable and then proceed to change the characters in that literal string, e.g.,

    sptr = "A String Constant";

        .

        .

        .

    *(sptr+2) = `s';

        .

        .

        .

    printf( "string: `%s'\n", sptr ); /* displays "string: `A string Constant'" */

        .

        .

        .

    printf( "A String Constant" );    /* Prints "A string Constant"! */

Compilers that used the same string constant in memory for multiple occurrences of the same string literal appearing in the program quickly discovered that this trick wouldn't work if the user stored data into the string object, as the code above demonstrates. Although this is a bad programming practice, it did occur frequently enough that the compiler vendors could not use the same storage for multiple copies of the same string literal. Even if the compiler vendor were to place the string literal constant into write-protected memory to prevent this problem, there are other semantic issues that this optimization raise. Consider the following C/C++ code:

    sptr1 = "A String Constant";

    sptr2 = "A String Constant";

    s1EQs2 = sptr1 == sptr2;

Will s1EQs2 contain true (1) or false (0) after executing this instruction sequence? In programs written before C compilers had strong optimizers available, this sequence of statements would leave false in s1EQs2 because the compiler created two different copies of the same string data and placed those strings at different addresses in memory (so the addresses the program assigns to sptr1 and sptr2 would be different). In a later compiler, that kept only a single copy of the string data in memory, this code sequence would leave true sitting in s1EQs2 since both sptr1 and sptr2 would be pointing at the same address in memory; this difference exists regardless of whether the string data appears in write-protected memory.

Of course, when converting the C/C++ code to assembly language, it is your responsibility to determine whether you can merge strings and use a common copy of the data or whether you will have to use a separate copy of the string data for each instance of the symbolic constant throughout your assembly code.

C/C++ supports other composite constant types as well (e.g., arrays and structures/records). This discussion of string constants in a program applies equally well to these other data types. Large data structures that the CPU cannot represent as a primitive data type (i.e., hold in a general purpose register) almost always wind up stored in memory and the program access the constant data exactly as it would access a variable of that type. On many modern systems, the compiler may place the constant data in write-protected memory to prevent the program from accidentally overwriting the constant data, but otherwise the "constant" is structurally equivalent to a variable in every sense except the ability to change its value. You can place such "constant" declarations in an HLA readonly declaration section to achieve the HLA equivalent to the C/C++ code.

HLA also allows the declaration of composite constants in the const section. For example, the following is perfect legal in HLA:

const

    constArray :int32[4] := [1,2,3,4];

However, you should note that HLA maintains this array strictly at compile-time within the compiler. You cannot, for example, write HLA code like the following once you have the above declaration:

    for( mov( 0, ebx ); ebx < 4; inc( ebx )) do

        mov( constArray[ ebx*4 ], eax );

        stdout.puti32( eax );

    endfor;

The problem with this code is that constArray is not a memory location so you cannot refer to it using an 80x86 addressing mode. In order for this array constant to be accessible (as an array) at run time, you have to make a copy of it in memory. You can do this with an HLA declaration like the following:

readonly

    rtConstArray :int32[4] := constArray; //Assuming the declaration given earlier.

Please consult the HLA documentation for more details on structured (composite) constants. Although these are quite useful for HLA programmers, they aren't generally necessary when converting C/C++ code to HLA. As such, they're a bit beyond the scope of this chapter so we won't deal any farther with this issue here.

3.2.5: Arithmetic Expressions in C and Assembly Language

One of the major advances that high level languages provided over low level languages was the use of algebraic-like expressions. High level language arithmetic expressions are an order of magnitude more readable than the sequence of machine instructions the compiler converts them into. However, this conversion process (from arithmetic expressions into machine code) is also one of the more difficult transformation to do efficiently and a fair percentage of a typical compiler's optimization phase is dedicated to handling this transformation.

Computer architects have made extensive studies of typical source files and one thing they've discovered is that a large percentage of assignment statements in such programs take one of the following forms:

    var = var2;

    var = constant;

    var = op var2;

    var = var op var2;

    var = var2 op var3;

Although other assignments do exist, the set of statements in a program that takes one of these form is generally larger than any other group of assignment statements. Therefore, computer architects have generally optimized their CPUs to efficiently handle one of these forms.

The 80x86 architecture is what is known as a two-address machine. In a two-address machine, one of the source operands is also the destination operand. Consider the following 80x86/HLA add instruction:

add( ebx, eax );  ; computes eax := eax + ebx;

Two-address machines, like the 80x86, can handle the first four forms of the assignment statement given earlier with a single instruction. The last form, however, requires two or more instructions and a temporary register. For example, to compute "var1 = var2 + var3;" you would need to use the following code (assuming var2 and var3 are memory variables and the compiler is keeping var1 in the EAX register):

    mov( var2, eax );

    add( var3, eax );  //Result (var1) is in EAX.

Once your expressions get more complex than the five forms given earlier, the compiler will have to generate a sequence of two or more instructions to evaluate the expression. When compiling the code, most compilers will internally translate complex expressions into a sequence of "three address statements" that are semantically equivalent to the more complex expression. The following is an example of a more complex expression and a sequence of three-address instructions that are representative of what a typical compiler might produce:

    // complex = ( a + b ) * ( c - d ) - e/f;

    temp1 = a + b;

    temp2 = c - d;

    temp1 = temp1 * temp2;

    temp2 = e / f;

    complex = temp1 - temp2;

If you study the five statements above, you should be able to convince yourself that they are semantically equivalent to the complex expression appearing in the comment. The major difference in the computation is the introduction of two temporary values (temp1 and temp2). Most compilers will attempt to use machine registers to maintain these temporary values (assuming there are free registers available for the compiler to use).

Table 3-5 lists most of the arithmetic operators used by C/C++ programs as well as their associativity.

Table 3-5: C/C++ Operators, Precedence, and Associativity
Precedence
Operator Classification
Associativity
C/C++ Operators
1 (highest)
Primary Scope Resolution (C++)
left to right
::
2
Primary
left to right
( ) [ ] . ->
3
Unary
(monadic1)
right to left
++ -- + - ! ~ & *
(type) sizeof new delete
4
Multiplicative
(dyadic2)
left to right
* / %
5
Additive
(dyadic)
left to right
+ -
6
Bitwise Shift
(dyadic)
left to right
<< >>
7
Relational
(dyadic)
left to right
< > <= >=
8
Equality
(dyadic)
left to right
== !=
9
Bitwise AND (dyadic)
left to right
&
10
Bitwise Exclusive OR (dyadic)
left to right
^
11
Bitwise Inclusive OR (dyadic)
left to right
|
12
Logical AND
(dyadic)
left to right
&&
13
Logical OR
(dyadic)
left to right
||
14
Conditional
(triadic3)
right to left
? :
15
Assignment
right to left
= += -= *= /= <<= >>= %= &= ^= |=
16 (lowest)
Comma
left to right
,
1Monadic means "single operand".
2Dyadic means "two operand".
3Triadic means "three operands".

3.2.5.1: Converting Simple Expressions Into Assembly Language

In this section we will consider the case where we need to convert a simple C/C++ expressions into assembly language. We've already discussed the conversion of the primary operators (in Table 3-5) into assembly language, so we won't bother repeating that discussion here. Likewise, we've already discussed the address-of operator ("&") and the dereferencing operator ("*") so we'll skip their discussions here as well.

Although the conversion of the remaining operators into assembly language is generally obvious, there are a few peculiarities. So it's worthwhile to quickly discuss how to convert a simple expression of the form @X, X@, or X@Y (where `@' represents one of the operators found in Table 3-5) into assembly language. Note that the discussion that follows deals with integer (signed or unsigned) only. The conversion of floating point expressions into assembly language is actually easier than converting integer expressions. This book will not deal with converting floating point expressions into assembly language. There are two reasons for this: (1) once you see how to convert integer expressions to assembly, you'll discover than floating point expression conversion is very similar; (2) the Win32 API uses very few floating point values. The whole reason for this chapter is to describe the C/C++ to assembly conversion process so you can read and understand existing C/C++ documentation when writing Windows assembly code. Since you won't find much Windows programming documentation that involves the use of floating point arithmetic, there is little need to present that information here. If you're interested in the subject, be sure to check out the discussion of this process in The Art of Assembly Language Programming.

Each of the examples appearing in this section will assume that you're operating on 32-bit integers producing a 32-bit result (except in the case of boolean results, where this book will assume an 8-bit result is probably sufficient). If you need to operate on eight or sixteen bit values, so sweat, just substitute the 8-bit or 16-bit registers in place of the 32-bit registers you'll find in these examples. If you need to deal with larger values (e.g., long long ints), well, that's beyond the scope of this book; please see the section on extended precision arithmetic in The Art of Assembly Language for details on those operations.

Translating the ++ and -- (increment and decrement) operators from C/C++ to assembly language looks, at first, like a trivial operation. You simply substitute an inc or dec instruction for these operators. However, there are two details that complicate this conversion by a slight amount: pre- and post- increment/decrement operations and pointer increment/decrement operations.

Normally, when you apply the ++ (increment) operator to an integer variable, the ++ operator increments the value of that variable by one. Similarly, when you apply the -- (decrement) operator to an integer variable, the -- operator decrements that variable by one. However, C/C++ also allows you to apply the ++ and -- operators to pointer variables as well as integers (pointers and integer variables are the only legal objects to which you may apply these operators, though). The semantics of a pointer increment are different than the semantics of an integer increment; applying the ++ operator to a pointer increments that pointer variable by the size of the object at which the pointer refers. For example, if pi is a pointer that points at a 32-bit integer value somewhere in memory, then ++pi adds four to pi (rather than one); this cause pi to point at the next sequential memory location that can hold a 32-bit integer (without overlapping the current integer in memory). Similarly, the -- (decrement) operator subtracts the size of the object at which a pointer refers from the pointer's value. So --pi would subtract four from pi if pi points at a 32-bit integer. So the basic conversion of the ++ and -- operator to assembly language is as Table 3-6 describes.

Table 3-6: Converting C/C++ Increment/Decrement Operators to Assembly
C/C++
HLA1
int i;
++i;
i++;
inc( i );
int *pi;
++pi;
pi++;
add( @size(int32), i );
int i;
--i;
i--;
dec( i );
int *pi;
--pi;
pi--;
sub( @size( int32 ), i );
1In the pointer examples, substitute the appropriate type identifier when incrementing a pointer to some type other than int32.

The increment and decrement operators may appear before or after a variable. If a C/C++ statement consists of a single variable with one of these operators, then whether you use the pre-increment/decrement form (sticking the ++ or -- before the variable) or the post-increment/decrement form (placing the ++ or -- operator after the variable) is irrelevant. In either case the end result is that the program will increment or decrement the variable accordingly:

    c++;  // is equivalent to

    ++c;

// and

    --c;  // is equivalent to

    c--;

If the C/C++ increment and decrement operators are attached to a variable within a larger expression, then the issue of pre-increment/decrement versus post-increment/decrement makes a big difference in the final result. Consider the statements "a = ++c;" and "a = c++;". In both cases the program will add one to variable c (assuming c is an integer rather than a pointer to some object). However, these two statements are quite different with respect to the value they assign to variable a. The first example here first increments the value in c and then assigns the value in c to a (hence the term pre-increment since the program first increments c and then uses its value in the expression). The second statement here first grabs the value in c, assigns that to a, and then increments c (hence the term post-increment since this expression increments c after using its value). Here's some sample HLA code that implements these two statements:

    // a = ++c;

        inc( c );         // pre-increment the value in c.

        mov( c, eax );

        mov( eax, a  );

    // a = c++;

        mov( c, eax );

        mov( eax, a );

        inc( c );         // post-increment the value in c.

The C/C++ compiler effectively ignores the unary "+" operator. If you attach this operator to an operand, it does not affect that value of that operand in any way. It's presence in the language is mainly for notational purposes. It lets you specify positive numeric constants like +123.456 in the source file. Sometimes explicitly place the "+" in front of such a constant can make the program more readable. However, since this operator rarely appears in real-world C/C++ programs, you're unlikely to see it.

The unary "-" operator negates the expression/variable that immediately follows it. The important thing to note is that this operator negates the value of the operand immediately following the "-" for use in the expression containing the operator. In particular, if a simple variable immediately follows the unary "-" this operator doesn't negate that operator directly. Therefore, you cannot use the 80x86 neg instruction on the variable except for the very special case where you have a statement like the following:

i = -i;

Instead, you must move the value of the variable into a register, negate the value of that register, and then use that register's value within the expression. For example, consider the following:

//  j = -i;

    mov( i, eax );

    neg( eax );

    mov( eax, j );

The unary "!" operator is the logical not operator. If the sub-expression (i.e., variable) appearing immediately to the left of this operator is zero, the "!" operator returns one. If the value of that sub-expression is non-zero, this operator returns zero. To convert this to assembly language, what you would do is test the operand for zero and set the result to one if it is zero, zero if the operand is not zero. You can use the cmp (or test) instruction along with the 80x86 setne instruction to achieve this:

    // Convert !i to assembly, assume i is an int variable, leave result in AL(or EAX)

    cmp( i, 0 );

    setne( al );

   // movsx( al, eax );  // Do this if you need a 32-bit boolean result.

A very common construct you'll see in many C/C++ programs is a sub-expression like "!!i" (that is, apply the logical not operator twice to the same value. This double logical negation converts the value in i to zero if it was previously zero, to one if it was previously non-zero. Rather than execute the previous code fragment twice, you can easily achieve this effect as follows:

    // Convert !!i to assembly, assume i is an int variable, leave result in AL(or EAX)

    cmp( i, 0 );

    sete( al );

   // movsx( al, eax );  // Do this if you need a 32-bit boolean result.

The C/C++ unary "~" operator does a bitwise logical not on its operand (that is, it inverts all the bits of the operand). This is easily achieved using the 80x86 not instruction as follows:

// j = ~i

        mov( i, eax );

        not( eax );

        mov( i, j );

For the special case of "i = ~i;" you can use the 80x86 not instruction to negate i directly, i.e., "not( i );".

A simple C/C++ expression like "x = y * z;" is easily converted to assembly language using a code sequence like the following:

    // x = y * z;

    mov( y, eax );

    intmul( z, eax );

    mov( eax, x );

    // Note: if y is a constant, can do the following:

    // (because multiplication is commutative, this also works if z is a constant,

    //  just swap z and y in this code if that is the case):

    intmul( y, z, eax );

    mov( eax, x );

Technically, the intmul instruction expects signed integer operands so you would normally use it only with signed integer variables. However, if you're not checking for overflow (and C/C++ doesn't so you probably won't need to either), then a two's complement signed integer multiply produces exactly the same result as an unsigned multiply. See The Art of Assembly Language if you need to do a true unsigned multiply or an extended precision multiply. Also note that the intmul instruction only allows 16-bit and 32-bit operands. If you need to multiple two 8-bit operands, you can either zero extend them to 16 (or 32) bits or you can use the 80x86 imul or mul instructions (see The Art of Assembly Language for more details).

The C/C++ division and modulo operators ("/" and "%", respectively) almost translate into the same code sequence. This is because the 80x86 div and idiv instructions calculate both the quotient and remainder of a division at the same time.

Unlike integer multiplication, division of signed versus unsigned operands does not produce the same result. Therefore, when dividing values that could potentially be negative, you must use the idiv instruction. Only use the div instruction when dividing unsigned operands.

Another complication with the division operation is that the 80x86 does a 64/32-bit division (that is, it divides a 64-bit number by a 32-bit number). Since both C/C++ operands are 32-bits you will need to sign extend (for signed integer operands) or zero extend (for unsigned integer operands) the numerator to 64 bits. Also remember that the div and idiv instructions expect the numerator in the EDX:EAX register pair (or DX:AX for 32/16 divisions, or AH:AL for 16/8 divisions, see The Art of Assembly Language for more details). The last thing to note is that these instructions return the quotient in EAX (AX/AL) and they return the remainder in EDX (DX/AH). Here's the code to translate an expression of the form "x=y/z;" into 80x86 assembly code:

    // x = y / z;   -- assume all operands are unsigned.

    mov( y, eax );

    xor( edx, edx );  // zero extend EAX to 64 bits in EDX:EAX

    div( z );

    mov( eax, x );    // Quotient winds up in EAX

    // x = y % z;   -- assume all operands are unsigned.

    mov( y, eax );

    xor( edx, edx );  // zero extend EAX to 64 bits in EDX:EAX

    div( z );

    mov( edx, x );    // Remainder winds up in EDX

    // x = y / z;   -- assume all operands are signed.

    mov( y, eax );

    cdq();            // sign extend EAX to 64 bits in EDX:EAX

    idiv( z );

    mov( eax, x );    // Quotient winds up in EAX

    // x = y % z;   -- assume all operands are signed.

    mov( y, eax );

    cdq();            // sign extend EAX to 64 bits in EDX:EAX

    idiv( z );

    mov( edx, x );    // Remainder winds up in EDX

Converting C/C++ expressions involving the "+", "-", "&", "|", and "&" operators into assembly language is quite easy. A simple C/C++ expression of the form "a = b @ c;" (where `@' represents one of these operators) translates into the following assembly code:

//  a = b @ c;

    mov( b, eax );

    instr( c, eax );  //instr = add, sub, and, or, xor, as appropriate

    mov( eax, a );

The C/C++ programming language provides a shift left operator ("<<"). This dyadic operator returns the result of its left operand shifted to the left the number of bits specified by its right operand. An expression of the form "a=b<<c;" is easily converted to one of two different HLA instruction sequences (chosen by whether c is a constant or a variable expression) as follows:

    // a = b << c;   -- c is a constant value.

    mov( b, eax );

    shl( c, eax );

    mov( eax, a );

    // a = b << c;   -- c is a variable value in the range 0..31.

    mov( b, eax );

    mov( (type byte c), cl );  //assume H.O. bytes of c are all zero.

    shl( cl, eax );

    mov( eax, a );

C/C++ also provides a shift right operator, ">>". This translates to a sequence that is very similar to the conversion of the "<<" operator with one caveat: the 80x86 supports two different shift right instructions: shr (shift logical right) and sar (shift arithmetic right). The C/C++ language doesn't specify which shift you should use. Some compilers always use a logical shift right operation, some use a logical shift right for unsigned operands and they use an arithmetic shift right for signed operands. If you don't know what you're supposed to use when converting code, using a logical (unsigned) shift right is probably the best choice because this is what most programmers will expect. That being the case, the shift right operator (">>") appearing in an expression like "a=b>>c;" translates into 80x86 code thusly:

    // a = b >> c;   -- c is a constant value.

    mov( b, eax );

    shr( c, eax );

    mov( eax, a );

    // a = b >> c;   -- c is a variable value in the range 0..31.

    mov( b, eax );

    mov( (type byte c), cl );  //assume H.O. bytes of c are all zero.

    shr( cl, eax );

    mov( eax, a );

If you decide you need to use an arithmetic (signed) shift right operation, simply substitute sar for shr in this code.

The logical OR and logical AND operators ("||" and "&&") return the values zero or one based on the values of their two operands. The logical OR operator ("||") returns one if either or both operands are non-zero; it returns zero if both operands are zero. The logical AND operator ("&&") returns zero if either operand is zero, it returns one only if both operands are non-zero. There is, however, one additional issue to consider: these operators employ short-circuit boolean evaluation. When computing "X && Y" the logical AND operator will not evaluate Y if it turns out that X is false (there is no need because if X is false, the full expression is always false). Likewise, when computing "X || Y" the logical OR operator will not evaluate Y if it turns out that X is true (again, there will be no need to evaluate Y for if X is true the result is true regardless of Y's value). Probably for the majority of expressions it doesn't really matter whether the program evaluates the expression using short-circuit evaluation or complete boolean evaluation; the result is always the same. However, because C/C++ promises short-circuit boolean evaluation semantics, many programs are written to depend on these semantics and will fail if you recode the expression using complete boolean evaluation. Consider the following two examples that demonstrate two such situations:

    if( pi != NULL && *pi == 5 )

    {

        // do something if *pi == 5...

    }

        .

        .

        .

    if( --x == 0 || ++y < 10 )

    {

        // do something if x == 0 or y < 10

    }

The first if statement in this example only works properly in all cases when using short-circuit evaluation. The left-hand operand of the "&&" operator evaluates false if pi is NULL. In that case, the code will not evaluate the right operand and this is a good thing for if it did it would dereference a NULL pointer (which will raise an exception under Windows). In the second example above, the result is not as drastic were the system to use short-circuit evaluation rather than complete boolean evaluation, but the program would produce a different result in y when using complete boolean evaluation versus short-circuit boolean evaluation. The reason for this difference is that the right-hand side of the expression increments y, something that doesn't happen if the left operand evaluates true.

Handling short-circuit boolean evaluation almost always means using conditional jumps to skip around an expression. For example, given the expression "Z = X && Y" the way you would encode this using pure short-circuit evaluation is as follows:

    xor( eax, eax );   // Assume the result is false.

    cmp( eax, X );     // See if X is false.

    je isFalse;

    cmp( eax, Y );     // See if Y is false.

    je isFalse;

        inc( eax );     // Set EAX to 1 (true);

isFalse:

    mov( eax, Z );     // Save 0/1 in Z

Encoding the logical OR operator using short-circuit boolean evaluation isn't much more difficult. Here's an example of how you could do it:

    xor( eax, eax );   // Assume the result is false.

    cmp( eax, X );     // See if X is true.

    jne isTrue;

    cmp( eax, Y );     // See if Y is false.

    je isFalse;

isTrue:

        inc( eax );     // Set EAX to 1 (true);

isFalse:

    mov( eax, Z );     // Save 0/1 in Z

Although short-circuit evaluation semantics are crucial for the proper operation of certain algorithms, most of the time the logical AND and OR operands are simple variables or simple sub-expressions whose results are independent of one another and quickly computed. In such cases the cost of the conditional jumps may be more expensive than some simple straight-line code that computes the same result (this, of course, depends entirely on which processor you're using in the 80x86 family). The following code sequence demonstrates one (somewhat tricky) way to convert "Z = X && Y" to assembly code, assuming X and Y are both 32-bit integer variables:

    xor( eax, eax ); // Initialize EAX with zero

    cmp( X, 1 );     // Note: sets carry flag if X == 0, clears carry in all other cases.

    adc( 0, eax );   // EAX = 0 if X != 1, EAX = 1 if X = 0.

    cmp( Y, 1 );     // Sets carry flag if Y == 0, clears it otherwise.

    adc( 0, eax );   // Adds in one if Y = 0, adds in zero if Y != 0.

    setz( al );      // EAX = X && Y

    mov( eax, Z );

Note that you cannot use the 80x86 and instruction to merge X and Y together to test if they are both non-zero. For if X contained $55 (0x55) and Y contained $aa (0xaa) their bitwise AND (which the and instruction produces) is zero, even though both values are logically true and the result should be true. You may, however, use the 80x86 or instruction to compute the logical OR of two operands. The following code sequence demonstrates how to compute "Z = X || Y;" using the 80x86 or instruction:

    xor( eax, eax );  // Clear EAX's H.O. bytes

    mov( X, ebx );

    or( Y, ebx );

    setnz( al );      // Sets EAX to one if X || Y (in EBX) is non-zero.

The conditional expression in C/C++ is unusual insofar as it is the only ternary (three-operand) operator that C/C++ provides. An assignment involving the conditional expression might be

a = (x != y) ? trueVal : falseVal;

The program evaluates the expression immediately to the left of the "?" operator. If this expression evaluates true (non-zero) then the compiler returns the value of the sub-expression immediately to the right of the "?" operator as the conditional expression's result. If the boolean expression to the left of the "?" operator evaluates false (i.e., zero) then the conditional expression returns the result of the sub-expression to the right of the ":" in the expression. Note that the conditional operator does not evaluate the true expression (trueVal in this example) if the condition evaluates false. Likewise, the conditional operator does not evaluate the false expression (falseVal in this example) if the expression evaluates true. This is similar to short-circuit boolean evaluation in the "&&" and "||" operators. You encode the conditional expression in assembly as though it were an if/else statement, e.g.,

    mov( falseVal, edx );   // Assume expression evaluates false

    mov( x, eax );

    cmp( eax, y );

    jne TheyreNotEqual

    mov( trueVal, edx );    // Assumption was incorrect, set EDX to trueVal

TheyreNotEqual:

    mov( edx, a );          // Save trueVal or falseVal (as appropriate) in a.

C/C++ provides a set of assignment operators. The are the following:

=  +=  -=  &=  ^=  |=  *=  /=  %=  <<=  >>=

Generally, C/C++ programmers use these assignment operators as stand-alone C/C++ statements, but they may appear as subexpressions as well. If the C/C++ program uses these expressions as stand-alone statements (e.g., "x += y;") then the statement "x @= y;" is completely equivalent to "x = x @ y;" where `@' represents the operator above. Therefore, the conversion to assembly code is fairly trivial, you simply use the conversions we've been studying throughout this section. Table 3-7 lists the equivalent operations for each of the assignment operators.

Table 3-7: Converting Assignment Operators To Assembly Language
C/C++ Operator
C/C++ Example
Equivalent To This C/C++ Code
HLA Encoding
=
x = y;
x = y;
mov( y, eax );
mov( eax, x );
+=
a += b;
a = a + b;
mov( b, eax );
add( eax, a );
-=
a -= b;
a = a - b;
mov( b, eax );
sub( eax, a );
&=
a &= b;
a = a & b;
mov( b, eax );
and( eax, a );
|=
a |= b;
a = a | b;
mov( b, eax );
or( eax, a );
^=
a ^= b;
a = a ^ b;
mov( b, eax );
xor( eax, a );
<<=
a <<= b;
a = a << b;
mov( (type byte b), cl );
shl( cl, a );
>>=
a >>= b;
a = a >> b;
mov( (type byte b), cl );
shr( cl, a );
*=
a *= b;
a = a * b;
mov( a, eax );
intmul( b, eax );
mov( eax, a );
/=
a /= b;
a = a / b;
mov( a, eax );
cdq; // or xor( edx, edx );
div( b );
mov( eax, a ); // Store away quotient
%=
a %= b;
a = a % b;
mov( a, eax );
cdq; // or xor( edx, edx );
div( b );
mov( edx, a ); // Store away remainder

The comma operator in C/C++ evaluates two subexpressions and then throws the result of the first expression away (i.e., it computes the value of the first/left expression strictly for any side effects it produces). In general, just convert both sub-expressions to assembly using the rules in this section and then use the result of the second sub-expression in the greater expression, e.g.,

//  x = ( y=z, a+b );

    mov( z, eax );

    mov( eax, y );

    mov( a, eax );

    add( b, eax );

    mov( eax, x );

3.2.5.2: Operator Precedence

The precedence of an operator resolves the ambiguity that is present in an expression involving several different operands. For example, given the arithmetic expression "4+2*3" there are two possible values we could logically claim this expression produces: 18 or 10 (18 is achieve by adding four and two then multiplying their sum by three; 10 is achieved by multiplying two times three and adding their product together with four). Now you may be thoroughly convinced that 10 is the correct answer, but that's only because by convention most people agree that multiplication has a higher precedence than addition, so you must do the multiplication first in this expression (that is, you've followed an existing convention in order to resolve the ambiguity). C/C++ also has its own precedence rules for eliminating ambiguity. In Table 3-5 the precedence level appears in the left-most column. Operators with a lower precedence level have a higher precedence and, therefore, take precedence over other operators at a lower precedence. You'll notice that the multiplication operator in C/C++ ("*") has a higher precedence than addition ("+") so C/C++ will produce 10 for the expression "4+2*3" just as you've been taught to expect.

Of course, you can always eliminate the ambiguity by explicitly specifying parentheses in your expressions. Indeed, the whole purpose of precedence is to implicitly specify where the parentheses go. If you have two operators with different precedences (say `#' and `@') and three operands, and you have an expression of the form X#Y@Z then you must place parentheses around the operands connected by the operator with the higher precedence. In this example, if `@' has a higher precedence than `#' you'd wind up with X#(Y@Z). Conversely, if `#' has a higher precedence than `@' you'd wind up with (X#Y)@Z.

An important fact to realize when converting C/C++ code into assembly language is that precedence only controls the implicit placement of parentheses within an expression. That is, precedence controls which operands we associate with a given operator. Precedence does not necessarily control the order of evaluation of the operands. For example, consider the expression "5*4+2+3". Since multiplication has higher precedence than addition, the "5" and "4" operands attach themselves to the "*" operator (rather than "4" attaching itself to the "+" operator). That is, this expression is equivalent to "(5*4)+2+3". The operator precedence, contrary to popular opinion, does not control the order of the evaluation of this expression. We could, for example, compute the sub-expression "2+3" prior to computing "5*4". You still get the correct result when computing this particular addition first.

When converting a complex expression to assembly language, the first step is to explicitly add in the parentheses implied by operator precedence. The presence of these parentheses will help guide the conversion to assembly language (we'll cover the exact process a little later in this chapter).

3.2.5.3: Associativity

Precedence defines where the parentheses go when you have three or more operands separated by different operators at different precedence levels. Precedence does not deal with the situation where you have three or more operands separated by operators at the same precedence level. For example, consider the following expression:

5 - 4 - 3;

Does this compute "5 - (4 - 3);" or does it compute " (5 - 4) -3;"? Precedence doesn't answer the question for us because the operators are all the same. These two expressions definitely produce different results (the first expression produces 5-1=4 while the second produces 1-3=-2). Associativity is the mechanism by which we determine the placement of parentheses around adjacent operators that have the same precedence level.

Operators generally have one of three different associativities: left, right, and none. C/C++ doesn't have any non-associative operators, so we'll only consider left associative and right associative operators here. Table 3-5 lists the associativity of each of the C/C++ operators (left or right). If two left associative operators are adjacent to one another, then you place the left pair of operands and their operator inside parentheses. If two right associative operators appear adjacent to one another in an expression, then you place a pair of parentheses around the right-most pair of operands and their operator, e.g.,

        5 - 4 - 3     -becomes-    (5 - 4) - 3

        x = y = z     -becomes-    x = (y = z)

Like precedence, associativity only controls the implied placement of the parentheses within an expression. It does not necessarily suggest the order of evaluation. In particular, consider the following arithmetic expression:

5 + 4 + 3 + 2 + 1

Because addition is left associative, the implied parentheses are as follows:

(((5 + 4) + 3) + 2) + 1

However, a compiler is not forced to first compute 5+4, then 9 + 3, then 12 + 2, etc. Because addition is commutative, a compiler can rearrange this computation in any way it sees fit as long as it produces the same result as this second expression.

3.2.5.4: Side Effects and Sequence Points

A side effect is any modification to the global state of a program other than the immediate result a piece of code is producing. The primary purpose of an arithmetic expression is to produce the expression's result. Any other changes to the system's state in an expression is a side effect. The C/C++ language is especially guilty of allowing side effects in an arithmetic expression. For example, consider the following C/C++ code fragment:

    i = i + *pi++ + (j = 2) * --k

This expression exhibits four separate side effects: the decrement of k at the end of the expression, the assignment to j prior to using j's value, the increment of the pointer pi after dereferencing pi, and the assignment to i (generally, if this expression is converted to a stand-alone statement by placing a semicolon after the expression, we consider the assignment to i to be the purpose of the statement, not a side effect).

Another way to create side effects within an expression is via a function call. Consider the following C++ code fragment:

int k;

int m;

int n;

int hasSideEffect( int i, int& j )

{

    k = k + 1;

    hasSideEffect = i + j;

    j = i;

}

        .

        .

        .

    m = hasSideEffect( 5, n );

In this example, the call to the hasSideEffect function produces two different side effects: (1) the modification of the global variable k and the modification of the pass by reference parameter j (actual parameter is n in this code fragment). The real purpose of the function is to compute the function's return result; any modification of global values or reference parameters constitutes a side effect of that function, hence the invocation of such a function within an expression causes the expression to produce side effects. Note that although C does not provide "pass by reference" parameters as C++ does, you can still pass a pointer as a parameter and modify the dereferenced object, thus achieving the same effect.

The problem with side effects in an expression is that most C/C++ compilers do not guarantee the order of evaluation of the components that make up an expression. Many naive programmers (incorrectly!) assume that when they write an expression like the following:

i = f(x) + g(x);

the compiler will emit code that first calls function f and then calls function g. The C and C++ programming languages, however, do not specify this order of execution. That is, some compilers will indeed call f , then call g, and then add their return results together; some other compilers, however, may call g first, then f, and then compute the sum of the function return results. That is, the compiler could translate the expression above into either of the following simplified code sequences before actually generating native machine code:

// Conversion #1 for "i = f(x) + g(x);"

    temp1 = f(x);

    temp2 = g(x);

    i:= temp1 + temp2;

// Conversion #2 for "i = f(x) + g(x);"

    temp1 = g(x);

    temp2 = f(x);

    i = temp2 + temp1;

Note that issues like precedence, associativity, and commutativity have no bearing on whether the compiler evaluates one sub-component of an expression before another. For example, consider the following arithmetic expression and several possible intermediate forms for the expression:

    j = f(x) - g(x) * h(x);

// Conversion #1 for this expression:

    temp1 = f(x);

    temp2 = g(x);

    temp3 = h(x);

    temp4 = temp2 * temp3

    j = temp1 - temp4;

// Conversion #2 for this expression:

    temp2 = g(x);

    temp3 = h(x);

    temp1 = f(x);

    temp4 = temp2 * temp3

    j = temp1 - temp4;

// Conversion #3 for this expression:

    temp3 = h(x);

    temp1 = f(x);

    temp2 = g(x);

    temp4 = temp2 * temp3

    j = temp1 - temp4;

Many other combinations are possible.

The specification for the C/C++ programming languages explicitly leave the order of evaluation undefined. This may seem somewhat bizarre, but there is a good reason for this: sometimes a compiler can produce better machine code by rearranging the order it uses to evaluate certain sub-expressions within an expression. Any attempt on the part of the language designer to force a particular order of evaluation on a compiler's implementor may limit the range of optimizations possible. Therefore, very few languages explicitly state the order of evaluation for an arbitrary expression.

There are, of course, certain rules that most languages do enforce. Though the rules vary by language, there are some fairly obvious rules that most languages (and their implementation) always follow because intuition suggests the behavior. Probably the two most common rules that you can count on are the facts that all side effects within an expression occur prior to the completion of that statement's execution. For example, if the function f modifies the global variable x, then the following statements will always print the value of x after f modifies it:

    i = f(x);

    printf( "x= %d\n", x );

Another rule you can count on is that the assignment to a variable on the left hand side of an assignment statement does not get modified prior to the use of that same variable on the right hand side of the expression. I.e., the following will not write a temporary value into variable n until it uses the previous value of n within the expression:

n = f(x) + g(x) - n;

Because the order of the production of side effects within an expression is undefined in C/C++, the result of the following code is generally undefined:

int incN( void )

{

    incN = n;

    n := n + 1;

}

        .

        .

        .

    n = 2;

    printf( "%d\n", incN() + n*2 );

The compiler is free to first call the incN function (so n will contain three prior to executing the sub-expression "n*2") or the compiler may first compute "n*2" and then call the incN function. As a result, one compilation of this statement could produce the output "8" while a different compilation of this statement might produce the output "6". In both cases n would contain three after the execution of the writeln statement, but the order of computation of the expression in the writeln statement could vary.

Don't make the mistake of thinking you can run some experiments to determine the order of evaluation. At the very best, such experiments will tell you the order a particular compiler uses. A different compiler may very well compute sub-expressions in a different order. Indeed, the same compiler might also compute the components of a subexpression differently based on the context of that subexpression. This means that a compiler might compute the expression using one ordering at one point in the program and using a different ordering somewhere else in the same program. Therefore, it is very dangerous to "determine" the ordering your particular compiler uses and rely on that ordering. Even if the compiler is consistent in the ordering of the computation of side effects, what's to prevent the compiler vendor from changing this in a later version of the compiler?

As noted earlier, most languages do guarantee that the computation of side effects completes before certain points in your program's execution. For example, almost every language guarantees the completion of all side effects by the time the statement containing the expression completes execution. The point at which a compiler guarantees that the computation of a side effect is completed is called a sequence point. The end of a statement is an example of a sequence point.

In the C programming language, there are several important sequence points in addition to the semicolon at the end of a statement. C provides several important sequence points within expressions, as well. Beyond the end of the statement containing an expression, C provides the following sequence points:

    expression1, expression2                 (the C comma operator in an expression)

    expression1 && expression2               (the C logical AND operator)

    expression1 || expression2               (the C logical OR operator)

    expression1 ? expression2 : expression3  (the C conditional expression operator)

C12 guarantees that all side effects in expression1 are completed before the computation of expression2 or expression3 in these examples (note that for the conditional expression, C only evaluates one of expression2 or expression3, so only the side effects of one of these sub-expressions is ever done on a given execution of the conditional expression).

To understand how side effects and sequence points can affect the operation of your program in non-obvious ways, consider the following example in C:

    int array[6] = {0, 0, 0, 0, 0, 0};

    int i;

        .

        .

        .

    i = 0;

    array[i] = i++;

Note that C does not define a sequence point across the assignment operator. Therefore, the C language makes no guarantees about whether the expression "i" used as an index into array is evaluated before or after the program increments i on the right hand side of the assignment operator. Note that the fact that the "++" operator is a post increment operation only implies that "i++" returns the value of i prior to the increment; this does not guarantee that the compiler will use the pre-increment value of i anywhere else in the expression. The bottom line is that the last statement in this example could be semantically equivalent to either of the following statements:

    array[0] = i++;

-or-

    array[1] = i++;

The C language definition allows either form and, in particular, does not require the first form simply because the array index appears in the expression before the post-increment operator.

To control the semantics of the assignment to array in this example, you will have to ensure that no part of the expression depends upon the side-effects of some other part of the expression. That is, you cannot both use the value of i at one point in the expression and apply the post-increment operator to i in another part of the expression unless there is a sequence point between the two uses. Since no such sequence point exists between the two uses of i in this statement, the result is undefined by the C language standard (note that the standard actually says that the result is undefined; therefore, the compiler could legally substitute any value for i as the array index value, though most compilers will substitute the value of i before or after the increment occurs in this particular example).

Though this comment appears earlier in this section, it is worth being redundant to stress an important fact: operator precedence and associativity do not control when a computation takes place in an expression. Even though addition is left associative, the compiler may compute the value of the addition operator's right operand before it computes the value of the addition operator's left operand. Precedence and associativity control how the compiler arranges the computation to produce the final result. They do not control when the program computes the subcomponents of the expression. As long as the final computation produces the results one would expect on the basis of precedence and associativity, the compiler is free to compute the subcomponents in any order and at any time it pleases.

3.2.5.5: Translating C/C++ Expressions to Assembly Language

Armed with the information from the past several sections, it is now possible to intelligently describe how to convert complex arithmetic expressions into assembly language.

The conversion of C/C++ expressions into assembly language must take into consideration the issues of operator precedence, associativity, and sequence points. Fortunately, these rules only describe which operators you must apply to which operands and at which points you must complete the computation of side effects. They do not specify the order that you must use when computing the value of an expression (other than completing the computation of side effects before a given point). Therefore, you have a lot of latitude with respect to how you rearrange the computation during your conversion. Because the C/C++ programming language has some relaxed rules with regard to the order of computation in some expression, the result of a computation that relies on other side effects between a pair of sequence points is undefined. However, just because the language doesn't define the result, some programmers will go ahead and assume that the compiler computes results on a left to right basis within the statement. That is, if two subexpressions modify the value of some variable, the programmer will probably (though errantly) assume that the left-most side effect occurs first. So if you encounter such an undefined operation in a C/C++ code sequence that you're converting to assembly, the best suggestion is to compute the result using a left-to-right evaluation of the expression. Although this is no guarantee that you'll produce what the original programmer intended, chances are better than 50/50 that this will produce the result that programmer was expecting.

A complex expression that is easy to convert to assembly language is one that involves three terms and two operators, for example:

w = w - y - z;

Clearly the straight-forward assembly language conversion of this statement will require two sub instructions. However, even with an expression as simple as this one, the conversion is not trivial. There are actually two ways to convert this from the statement above into assembly language:

        mov( w, eax );

        sub( y, eax );

        sub( z, eax );

        mov( eax, w );

and

        mov( y, eax );

        sub( z, eax );

        sub( eax, w );

The second conversion, since it is shorter, looks better. However, it produces an incorrect result. Associativity is the problem. The second sequence above computes "W = W - (Y - Z);" which is not the same as "W = (W - Y) - Z;". How we place the parentheses around the subexpressions can affect the result. Note that if you are interested in a shorter form, you can use the following sequence:

        mov( y, eax );

        add( z, eax );

        sub( eax, w );

This computes "W=W-(Y+Z);". This is equivalent to "W = (W - Y) - Z;".

Precedence is another issue. Consider the C/C++ expression:

        X = W * Y + Z;

Once again there are two ways we can evaluate this expression:

        X = (W * Y) + Z;

or

        X = W * (Y + Z);

However, C/C++'s precedence rules dictate the use of the first of these statements.

When converting an expression of this form into assembly language, you must be sure to compute the subexpression with the highest precedence first. The following example demonstrates this technique:

// w = x + y * z;

        mov( x, ebx );

        mov( y, eax );      // Must compute y*z first since "*"

        intmul( z, eax );   //  has higher precedence than "+".

        add( ebx, eax );

        mov( eax, w );

The precedence and associativity rules determine the order of evaluation. Indirectly, these rules tell you where to place parentheses in an expression to determine the order of evaluation. Of course, you can always use parentheses to override the default precedence and associativity. However, the ultimate point is that your assembly code must complete certain operations before others to correctly compute the value of a given expression. The following examples demonstrate this principle:

// w = x - y - z

        mov( x, eax );   // All the same operator, so we need

        sub( y, eax );   //  to evaluate from left to right

        sub( z, eax );   //  because they all have the same

        mov( eax, w );   //  precedence and are left associative.

// w = x + y * z

        mov( y, eax );      // Must compute Y * Z first since

        intmul( z, eax );   // multiplication has a higher

        add( x, eax );      // precedence than addition.

        mov( eax, w );

// w = x / y - z

        mov( x, eax );      // Here we need to compute division

        cdq();              //  first since it has the highest

        idiv( y, edx:eax ); //  precedence.

        sub( z, eax );

        mov( eax, w );

// w = x * y * z

        mov( y, eax );      // Addition and multiplication are

        intmul( z, eax );   // commutative, therefore the order

        intmul( x, eax );   // of evaluation does not matter

        mov( eax, w );

There is one exception to the associativity rule. If an expression involves multiplication and division it is generally better to perform the multiplication first. For example, given an expression of the form:

        W = X/Y * Z        // Note: this is
 not
!

It is usually better to compute X*Z and then divide the result by Y rather than divide X by Y and multiply the quotient by Z. There are two reasons this approach is better. First, remember that the imul instruction always produces a 64 bit result (assuming 32 bit operands). By doing the multiplication first, you automatically sign extend the product into the EDX register so you do not have to sign extend EAX prior to the division. This saves the execution of the cdq instruction. A second reason for doing the multiplication first is to increase the accuracy of the computation. Remember, (integer) division often produces an inexact result. For example, if you compute 5/2 you will get the value two, not 2.5. Computing (5/2)*3 produces six. However, if you compute (5*3)/2 you get the value seven which is a little closer to the real quotient (7.5). Therefore, if you encounter an expression of the form:

        w = x/y*z;

You can usually convert it to the assembly code:

        mov( x, eax );

        imul( z, eax );						// Note the use of IMUL, not INTMUL!

        idiv( y, edx:eax );

        mov( eax, w );

Of course, if the algorithm you're encoding depends on the truncation effect of the division operation, you cannot use this trick to improve the algorithm. Moral of the story: always make sure you fully understand any expression you are converting to assembly language. Obviously if the semantics dictate that you must perform the division first, do so.

Consider the following C/C++ statement:

        w = x - y * x;

This is similar to a previous example except it uses subtraction rather than addition. Since subtraction is not commutative, you cannot compute y * z and then subtract x from this result. This tends to complicate the conversion a tiny amount. Rather than a straight forward multiply and addition sequence, you'll have to load x into a register, multiply y and z leaving their product in a different register, and then subtract this product from x, e.g.,

        mov( x, ebx );

        mov( y, eax );

        intmul( x, eax );

        sub( eax, ebx );

        mov( ebx, w );

This is a trivial example that demonstrates the need for temporary variables in an expression. This code uses the EBX register to temporarily hold a copy of x until it computes the product of y and z. As your expressions increase in complexity, the need for temporaries grows. Consider the following C/C++ statement:

        w = (a + b) * (y + z);

Following the normal rules of algebraic evaluation, you compute the subexpressions inside the parentheses (i.e., the two subexpressions with the highest precedence) first and set their values aside. When you've computed the values for both subexpressions you can compute their sum. One way to deal with complex expressions like this one is to reduce it to a sequence of simple expressions whose results wind up in temporary variables. For example, we can convert the single expression above into the following sequence:

        Temp1 = a + b;

        Temp2 = y + z;

        w = Temp1 * Temp2;

Since converting simple expressions to assembly language is quite easy, it's now a snap to compute the former, complex, expression in assembly. The code is

        mov( a, eax );

        add( b, eax );

        mov( eax, Temp1 );

        mov( y, eax );

        add( z, eax );

        mov( eax, Temp2 );

        mov( Temp1, eax );

        intmul( Temp2, eax );

        mov( eax, w );

Of course, this code is grossly inefficient and it requires that you declare a couple of temporary variables in your data segment. However, it is very easy to optimize this code by keeping temporary variables, as much as possible, in 80x86 registers. By using 80x86 registers to hold the temporary results this code becomes:

        mov( a, eax );

        add( b, eax );

        mov( y, ebx );

        add( z, ebx );

        intmul( ebx, eax );

        mov( eax, w );

Yet another example:

        x = (y+z) * (a-b) / 10;

This can be converted to a set of four simple expressions:

        Temp1 = (y+z)

        Temp2 = (a-b)

        Temp1 = Temp1 * Temp2

        X = Temp1 / 10

You can convert these four simple expressions into the assembly language statements:

        mov( y, eax );      // Compute eax = y+z

        add( z, eax );

        mov( a, ebx );      // Compute ebx = a-b

        sub( b, ebx );

        imul( ebx, eax );   // This also sign extends eax into edx.

        idiv( 10, edx:eax );

        mov( eax, x );

The most important thing to keep in mind is that you should attempt to keep temporary values, in registers. Remember, accessing an 80x86 register is much more efficient than accessing a memory location. Use memory locations to hold temporaries only if you've run out of registers to use.

Ultimately, converting a complex expression to assembly language is little different than solving the expression by hand. Instead of actually computing the result at each stage of the computation, you simply write the assembly code that computes the result. Since you were probably taught to compute only one operation at a time, this means that manual computation works on "simple expressions" that exist in a complex expression. Of course, converting those simple expressions to assembly is fairly trivial. Therefore, anyone who can solve a complex expression by hand can convert it to assembly language following the rules for simple expressions.

As noted earlier, this text will not consider the conversion of floating point expressions into 80x86 assembly language. Although the conversion is slightly different (because of the stack-oriented nature of the FPU register file), the conversion of floating point expressions into assembly language is so similar to the conversion of integer expressions that it isn't worth the space to discuss it here. For more insight into this type of expression conversion, please see The Art of Assembly Language.

3.2.6: Control Structures in C and Assembly Language

The C and C++ languages provide several high-level structured control statements. Among these, you will find the if/else statement, the while statement, the do/while statement, the for statement, the break/continue/return statements, and the goto statement. C/C++ also provides the function call, but we'll deal with that control structure later in this chapter. The C++ language provides exception handling facilities. However, as you're unlikely to encounter C++'s try/catch statements in Win32 API documentation, we won't bother discussing the conversion of those statements into assembly language in this book. If you have need to incorporate exception handling into your HLA programs, please check out the HLA try..exception..endtry statements in the HLA reference manual.

One advantage of a high level assembler like HLA is that it also provides high-level, structured, control statements. Although not as sophisticated as the similar statements you'll find in C/C++ (particularly with respect to the boolean expressions the C/C++ statements allow), it's fairly trivial to convert about 75-90% of the typical C/C++ control statements you'll encounter into assembly language (when using HLA).

This book will not cover the conversion of high level control structures into low-level assembly code (i.e., using conditional jumps and comparisons rather than the high-level control structures found in HLA). If you wish to use that conversion process and you're not comfortable with it, please see The Art of Assembly Language for more details.

For the most part, this book assumes that the reader is already an accomplished assembly language programmer. However, because many assembly language programmers might not have bothered to learn HLA's high level control structures, the following sections will describe the semantics of the HLA structured control statements in addition to describing how to convert C/C++ control structures into their equivalent assembly language statements. Since C/C++ does not provide as many control structures as C/C++, this section will not bother describing all of HLA's high level control structures - only those that have a C/C++ counterpart. For more details on HLA's high level control statements, please consult the HLA Reference Manual.

3.2.6.1: Boolean Expressions in HLA Statements

Several HLA statements require a boolean (true or false) expression to control their execution. Examples include the if, while, and repeat..until statements. The syntax for these boolean expressions represents the greatest limitation of the HLA high level control structures. In many cases you cannot convert the corresponding C/C++ statements directly into HLA code.

HLA boolean expressions always take the following forms13:

flag_specification

!flag_specification

register

!register

Boolean_variable

!Boolean_variable

mem_reg relop mem_reg_const

A flag_specification may be one of the following symbols:

  • @c carry: True if the carry is set (1), false if the carry is clear (0).
  • @nc no carry: True if the carry is clear (0), false if the carry is set (1).
  • @z zero: True if the zero flag is set, false if it is clear.
  • @nz not zero: True if the zero flag is clear, false if it is set.
  • @o overflow: True if the overflow flag is set, false if it is clear.
  • @no no overflow: True if the overflow flag is clear, false if it is set.
  • @s sign: True if the sign flag is set, false if it is clear.
  • @ns no sign: True if the sign flag is clear, false if it is set.

A register operand can be any of the 8-bit, 16-bit, or 32-bit general purpose registers. The expression evaluates false if the register contains a zero; it evaluates true if the register contains a non-zero value.

If you specify a boolean variable as the expression, the program tests it for zero (false) or non-zero (true). Since HLA uses the values zero and one to represent false and true, respectively, the test works in an intuitive fashion. Note that HLA requires such variables be of type boolean. HLA rejects other data types. If you want to test some other type against zero/not zero, then use the general boolean expression discussed next.

The most general form of an HLA boolean expression has two operands and a relational operator. Table 3-8 lists the legal combinations.

Table 3-8:
Left
Operand
Relational Operator
Right Operand

Memory Variable

or

Register
= or ==

Memory Variable,

Register,

or

Constant
<> or !=
<
<=
>
>=
Relational Operators in HLA

Note that both operands cannot be memory operands. In fact, if you think of the Right Operand as the source operand and the Left Operand as the destination operand, then the two operands must be the same that cmp instruction allows. This is the primary limitation to HLA boolean expressions and the biggest source of problems when converting C/C++ high level control statements into HLA code.

Like the cmp instruction, the two operands must also be the same size. That is, they must both be byte operands, they must both be word operands, or they must both be double word operands. If the right operand is a constant, it's value must be in the range that is compatible with the left operand.

There is one other issue: if the left operand is a register and the right operand is a positive constant or another register, HLA uses an unsigned comparison. You will have to use HLA's type coercion operator (e.g., "(type int32 eax)" ) if you wish to do a signed comparison.

Here are some examples of legal boolean expressions in HLA:

@c

Bool_var

al

ESI

EAX < EBX

EBX > 5

i32 < -2

i8 > 128

al < i8

HLA uses the "&&" operator to denote logical AND in a run-time boolean expression. This is a dyadic (two-operand) operator and the two operands must be legal run-time boolean expressions. This operator evaluates true if both operands evaluate to true. Example using an HLA if statement:

    if( eax > 0 && ch = `a' ) then

        mov( eax, ebx );

        mov( ` `, ch );

    endif;

The two mov statements appearing here execute only if EAX is greater than zero and CH is equal to the character `a'. If either of these conditions is false, then program execution skips over these mov instructions.

Note that the expressions on either side of the "&&" operator may be any legal boolean expression, these expressions don't have to be comparisons using the relational operators. For example, the following are all legal expressions:

    @z && al in 5..10

    al in `a'..'z' && ebx

    boolVar && !eax

HLA uses short circuit evaluation when compiling the "&&" operator. If the left-most operand evaluates false, then the code that HLA generates does not bother evaluating the second operand (since the whole expression must be false at that point). Therefore, in the last expression, the code will not check EAX against zero if boolVar contains false.

Note that an expression like "eax < 0 && ebx <> eax" is itself a legal boolean expression and, therefore, may appear as the left or right operand of the "&&" operator. Therefore, expressions like the following are perfectly legal:

    eax < 0  &&  ebx <> eax    &&    !ecx

The "&&" operator is left associative, so the code that HLA generates evaluates the expression above in a left-to-right fashion. If EAX is less than zero, the CPU will not test either of the remaining expressions. Likewise, if EAX is not less than zero but EBX is equal to EAX, this code will not evaluate the third expression since the whole expression is false regardless of ECX's value.

HLA uses the "||" operator to denote disjunction (logical OR) in a run-time boolean expression. Like the "&&" operator, this operator expects two legal run-time boolean expressions as operands. This operator evaluates true if either (or both) operands evaluate true. Like the "&&" operator, the disjunction operator uses short-circuit evaluation. If the left operand evaluates true, then the code that HLA generates doesn't bother to test the value of the second operand. Instead, the code will transfer to the location that handles the situation when the boolean expression evaluates true. Examples of legal expressions using the "||" operator:

    @z || al = 10

    al in `a'..'z' || ebx

    !boolVar || eax

As for the "&&" operator, the disjunction operator is left associative so multiple instances of the "||" operator may appear within the same expression. Should this be the case, the code that HLA generates will evaluate the expressions from left to right, e.g.,

    eax < 0  ||  ebx <> eax    ||   !ecx

The code above executes if either EAX is less than zero, EBX does not equal EAX, or ECX is zero. Note that if the first comparison is true, the code doesn't bother testing the other conditions. Likewise, if the first comparison is false and the second is true, the code doesn't bother checking to see if ECX is zero. The check for ECX equal to zero only occurs if the first two comparisons are false.

If both the conjunction and disjunction operators appear in the same expression then the "&&" operator takes precedence over the "||" operator. Consider the following expression:

    eax < 0 || ebx <> eax  && !ecx

The machine code HLA generates evaluates this as

    eax < 0 || (ebx <> eax  && !ecx)

If EAX is less than zero, then the code HLA generates does not bother to check the remainder of the expression, the entire expression evaluates true. However, if EAX is not less than zero, then both of the following conditions must evaluate true in order for the overall expression to evaluate true.

HLA allows you to use parentheses to surround sub-expressions involving "&&" and "||" if you need to adjust the precedence of the operators. Consider the following expression:

    (eax < 0 || ebx <> eax)  && !ecx

For this expression to evaluate true, ECX must contain zero and either EAX must be less than zero or EBX must not equal EAX. Contrast this to the result the expression produces without the parentheses.

HLA uses the "!" operator to denote logical negation. However, the "!" operator may only prefix a register or boolean variable; you may not use it as part of a larger expression (e.g., "!eax < 0"). To achieve logical negative of an existing boolean expression you must surround that expression with parentheses and prefix the parentheses with the "!" operator, e.g.,

    !( eax < 0 )

This expression evaluates true if EAX is not less than zero.

The logical not operator is primarily useful for surrounding complex expressions involving the conjunction and disjunction operators. While it is occasionally useful for short expressions like the one above, it's usually easier (and more readable) to simply state the logic directly rather than convolute it with the logical not operator.

3.2.6.2: Converting C/C++ Boolean Expressions to HLA Boolean Expressions

Although, superficially, C/C++ boolean expressions that appear within control structures look very similar to those appearing in HLA high-level structured control statements, there are some fundamental differences that will create some conversion problems. Fortunately, most boolean expressions appearing in C/C++ control structures are relatively simple and almost translate directly into an equivalent HLA expression. Nevertheless, a large percentage of expressions will take a bit of work to properly convert to a form usable by HLA.

Although HLA provides boolean expressions involving relation and logical (and/or/not) operators, don't get the impression that HLA supports generic boolean expressions as C/C++ does. For example, an expression like "(x+y) > 10 || a*b < c" is perfectly legal in C/C++, but HLA doesn't allow an expression like this. You might wonder why HLA allows some operators but not others. There is a good reason why HLA supports only a limited number of operators: HLA supports all the operations that don't require the use of any temporary values (i.e., registers). HLA does not allow any code in an expression that would require the use of a register to hold a temporary value; i.e., HLA will not modify any register values behind the assembly programmer's back. This severely limits what HLA can do since subexpressions like "(x+y)" have to be computed in a temporary register (at least, on the 80x86). The previous section presented most of the operators that are legal in an HLA boolean expression. Unfortunately, of course, C/C++ does allow fairly complex arithmetic/boolean expressions within a structured control statement. This section provides some guidelines you can use to convert complex C/C++ arithmetic/boolean expressions to HLA.

The first thing to note is that HLA only allows operands that are legal in a cmp instruction around one of the relational operators. Specifically, HLA only allows the operands in Table 3-9 around a relational operator.

Table 3-9: Legal Operands to a Relational Operator in an HLA Expression
Left Operand
Relational Operator
Right Operand
reg
<
<=
=
==
<>
!=
>
>=
reg
reg
mem
reg
const
mem
reg
mem
const

If you need to convert a boolean expression like "(x+y) > 10" from C/C++ into HLA, the most common approach is to compute the sub-expression "(x+y)" and leave the result in a register, then you can compare that register against the value 10, e.g.,

        mov( x, eax );

        add( y, eax );

        if( eax > 10 ) then

            .

            .

            .

        endif;

Unfortunately, the syntax of various high level control structures in HLA don't allow you to place the statements that compute the result before the control structure; we'll take a look at these problems in the sections that follow.

3.2.6.3: The IF Statement

The HLA IF statement uses the syntax shown in Table 3-2.

Figure 3-2: HLA IF Statement Syntax

The expressions appearing in an if statement must take one of the forms from the previous sections. If the boolean expression is true, the code after the then executes, otherwise control transfers to the next elseif or else clause in the statement.

Since the elseif and else clauses are optional, an if statement could take the form of a single if..then clause, followed by a sequence of statements, and a closing endif clause. The following is such a statement:

if( eax = 0 ) then

    stdout.put( "error: NULL value", nl );

endif;

If, during program execution, the expression evaluates true, then the code between the then and the endif executes. If the expression evaluates false, then the program skips over the code between the then and the endif.

Another common form of the if statement has a single else clause. The following is an example of an if statement with an optional else clause:

if( eax = 0 ) then

    stdout.put( "error: NULL pointer encountered", nl );

else

    stdout.put( "Pointer is valid", nl );

endif;

If the expression evaluates true, the code between the then and the else executes; otherwise the code between the else and the endif clauses executes.

You can create sophisticated decision-making logic by incorporating the elseif clause into an if statement. For example, if the CH register contains a character value, you can select from a menu of items using code like the following:

if( ch = `a' ) then

    stdout.put( "You selected the `a' menu item", nl );

elseif( ch = `b' ) then

    stdout.put( "You selected the `b' menu item", nl );

elseif( ch = `c' ) then

    stdout.put( "You selected the `c' menu item", nl );

else

    stdout.put( "Error: illegal menu item selection", nl );

endif;

Although this simple example doesn't demonstrate it, HLA does not require an else clause at the end of a sequence of elseif clauses. However, when making multi-way decisions, it's always a good idea to provide an else clause just in case an error arises. Even if you think it's impossible for the else clause to execute, just keep in mind that future modifications to the code could void this assertion, so it's a good idea to have error reporting statements in your code.

The C/C++ if statement is similar, but certainly not identical to, the HLA if statement. First of all, the C/C++ if statement is based on an older language design that allows only a single statement after an if or else. That is, C/C++ supports the following syntaxes for the if/else statement:

    if( boolean_expression)

        << single statement >>;

    if( boolean_expression )

        << single statement >>;

    else

        << single statement >>;

If you need to attach more than a single statement to a C/C++ if or else, you have to use a compound statement. A compound statement consists of a sequence of zero or more statements surrounded by braces. This means that there are six possible forms of the if statement you will find in a typical C/C++ program, as the following syntactical examples demonstrate:

1)

    if( boolean_expression)

        << single statement >>;

2)

    if( boolean_expression)

    {

        << zero or more statements >>

    }

3)

    if( boolean_expression )

        << single statement >>;

    else

        << single statement >>;

4)

    if( boolean_expression )

    {

        << zero or more statements >>

    }

    else

        << single statement >>;

5)

    if( boolean_expression )

        << single statement >>;

    else

    {

        << zero or more statements >>

    }

6)

    if( boolean_expression )

    {

        << zero or more statements >>

    }

    else

    {

        << zero or more statements >>

    }

To convert either of the first two forms to HLA is relatively easy. Simply convert the boolean expression to HLA form (including placing any necessary arithmetic computations before the if statement), convert the statement or statements attached to the if to their HLA equivalents, and then place an endif after the last statement attached to the if. Here are a couple of examples that demonstrate this conversion for the first two cases:

// if( a >= 0 )

//     ++a;

    if( a > 0 ) then

        inc( a );

    endif;

// if( (x*4) >= y && z < -5 )

// {

//    x = x - y;

//    ++z;

//  }

    mov( x, eax );

    shl( 2, eax );  // x*4

    if( eax >= y && z < -5 ) then

        mov( y, eax );

        sub( eax, x );

        inc( eax );

    endif;

Converting one of the other if/else forms from C/C++ to HLA is done in a similar fashion except, of course, you also have to include the else section in the HLA translation. Here's an example that demonstrates this:

// if( a < 256 )

// {

//    ++a;

//    --b;

// }

// else

// {

//    --a;

//    ++b;

// }

    if( a < 256 ) then

        inc( a );

        dec( b );

    else

        dec( a );

        inc( b );

    endif;

The C/C++ language does not directly support an elseif clause as HLA does, however, C/C++ programs often contain "else if" chains that you may convert to an HLA elseif clause. The following example demonstrates this conversion:

// if( x >= (y | z))

//    ++x;

//  else if( x >= 10 )

//    --x;

//  else

//  {

//     ++y;

//     --z;

//  }

    mov( y, eax );

    or( z, eax );

    if( x >= eax ) then

        inc( x );

    elseif( x >= 10 ) then

        dec( x );

    else

        inc( y );

        dec( z );

    endif;

Sometimes a C/C++ else-if chain can create some conversion problems. For example, suppose that the boolean expression in the "else if" of this example was "x >= (y & z)" rather than an expression that is trivially convertible to HLA. Unfortunately, you cannot place the computation of the temporary results immediately before the elseif in the HLA code (since that section of code executes when if clause evaluates true). You could place the computation before the if and leave the value in an untouched register, but this scheme has a couple of disadvantages - first, you always compute the result even when it's not necessary (e.g., when the if expression evaluates true), second, it consumes a register which is not good considering how few registers there are on the 80x86. A better solution is to use an HLA nested if rather than an elseif, e.g.,

// if( x >= (y | z))

//    ++x;

//  else if( x >= (y & z) )

//    --x;

//  else

//  {

//     ++y;

//     --z;

//  }

    mov( y, eax );

    or( z, eax );

    if( x >= eax ) then

        inc( x );

    else

        mov( y, eax );

        and( z, eax );

        if( x >= eax ) then

            dec( x );

        else

            inc( y );

            dec( z );

        endif;

    endif;

3.2.6.4: The SWITCH/CASE Statement

The HLA programming language doesn't directly provide a multi-way decision statement (commonly known as a switch or case statement). However, the HLA Standard Library provides a switch / case / default / endcase macro that provides this high level control statement in HLA. If you include the hll.hhf header file (which stdlib.hhf automatically includes for you), then you can use the switch statement exactly as though it were a part of the HLA language.

The HLA Standard Library switch statement has the following syntax:

Figure 3-3: Syntax for the Switch..case..default..endswitch Statement

Like most HLA high level language statements, there are several restrictions on the switch statement. First of all, the switch clause does not allow a general expression as the selection value. The switch clause will only allow a value in a 32-bit general purpose register. In general you should only use EAX, EBX, ECX, EDX, ESI, and EDI because EBP and ESP are reserved for special purposes.

The second restriction is that the HLA switch statement supports a maximum of 256 different case values. Few switch statements use anywhere near this number, so this shouldn't prove to be a problem. Note that each case in Figure 3-3 allows a constant list. This could be a single unsigned integer value or a comma separated list of values, e.g.,

        case( 10 )

-or-

        case( 5, 6, 8 )

Each value in the list of constants counts as one case constant towards the maximum of 256 possible constants. So the second case clause above contributes three constants towards the total maximum of 256 constants.

Another restriction on the HLA switch statement is that the difference between the largest and smallest values in the case list must be 1,024. Therefore, you cannot have cases (in the same switch statement) with values like 1, 10, 100, 1,000, and 10,000 since the difference between the smallest and largest values, 9999, exceeds 1,024.

The default section, if it appears in a switch statement, must be the last section in the switch statement. If no default section is present and the value in the 32-bit register does not match one of the case constants, then control transfers to the first statement following the endswitch clause.

Here is a typical example of a switch..endswitch statement:

    switch( eax )

        case( 1 )

            stdout.put( "Selection #1:" nl );

            << Code for case #1 >>

        case( 2, 3 )

            stdout.put( "Selections (2) and (3):" nl );

            << code for cases 2 & 3 >>

        case( 5,6,7,8,9 )

            stdout.put( "Selections (5)..(9)" nl );

            << code for cases 5..9 >

        default

            stdout.put( "Selection outside range 1..9" nl );

            << default case code >>

    endswitch;

The switch statement in a program lets your code choose one of several different code paths depending upon the value of the case selection variable. Among other things, the switch statement is ideal for processing user input that selects a menu item and executes different code depending on the user's selection.

The HLA switch statement actually supports the semantics of the Pascal case statement (as well as multi-way selection statements found in various other languages). The semantics of a C/C++ switch statement are slightly different. As it turns out, HLA's switch macro provides an option for selecting either Pascal or C/C++ semantics. The hll.hhf header file defines a special compile-time boolean variable, hll.cswitch, that controls which form of the switch statement HLA will use. If this compile-time variable contains false (the default), then HLA uses Pascal semantics for the switch statement. If this compile-time variable contains true, then HLA uses C/C++ semantics. You may set this compile-time variable to true or false with either of the following two statements:

    ?hll.cswitch := true;  // Enable C/C++ semantics for the switch statement.

    ?hll.cswitch := false; // Enable Pascal semantics for the switch statement.

The difference between C/C++ and Pascal semantics has to do with what will happen when the statements within some case block reach the end of that block (by hitting another case or the default clause). When using Pascal semantics, HLA automatically transfers control to the first statement following the endswitch clause upon hitting a new case. In the previous example, if EAX had contained one, then the switch statement would execute the code sequence:

            stdout.put( "Selection #1:" nl );

            << Code for case #1 >>

Immediately after the execution of this code, control transfers to the first statement following the endswitch (since the next statement following this fragment is the "case(2,3)" clause).

If you select C/C++ semantics by setting the hll.cswitch compile-time variable to true, then control does not automatically transfer to the bottom of the switch statement; instead, control falls into the first statement of the next case clause. In order to transfer control to the first statement following the endswitch at the end of a case section, you must explicitly place a break statement in the code, e.g.,

    ?hll.cswitch := true;  // Enable C/C++ semantics for the switch statement.

    switch( eax )

        case( 1 )

            stdout.put( "Selection #1:" nl );

            << Code for case #1 >>

            break;

        case( 2, 3 )

            stdout.put( "Selections (2) and (3):" nl );

            << code for cases 2 & 3 >>

            break;

        case( 5,6,7,8,9 )

            stdout.put( "Selections (5)..(9)" nl );

            << code for cases 5..9 >

            break;

        default

            stdout.put( "Selection outside range 1..9" nl );

            << default case code >>

    endswitch;

Note that you can alternately switch between C/C++ and Pascal semantics throughout your code by setting the hll.cswitch compile-time variable to true or false at various points throughout your code. However, as this makes the code harder to read, it's generally not a good idea to do this on a frequent basis. You should pick one form or the other and attempt to stick with it as much as possible. Pascal semantics are actually a little bit nicer (and safer) plus you get to continue using the break statement to break out of a loop containing a switch statement. On the other hand, some C/C++ switch statements need the ability to flow from one case to another, so if you're translating such a statement from C/C++ to HLA, the C/C++ switch statement format is easier to deal with. Of course, the purpose of this chapter is not to teach you how to convert a C/C++ Windows program to HLA, but rather to help you read and understand C/C++ documentation. In real life, if you have to convert a C/C++ switch statement to assembly language you're probably better off explicitly creating a jump table and using an indirect jump implementation of the switch statement (see The Art of Assembly Language for details).

3.2.6.5: The WHILE Loop

The HLA while statement uses the basic syntax shown in Figure 3-4.

Figure 3-4: HLA WHILE Statement Syntax

The while statement evaluates the boolean expression. If it is false, control immediately transfers to the first statement following the endwhile clause. If the value of the expression is true, then the CPU executes the body of the loop. After the loop body executes, control transfers back to the top of the loop where the while statement retests the loop control expression. This process repeats until the expression evaluates false.

The C/C++ statement uses a similar syntax and identical semantics. There are two principle differences between the HLA while loop and the C/C++ variant: (1) HLA uses "while(expr) do ... endwhile;" whereas C/C++ uses "while(expr) single_statement;", as with the C/C++ if statement, if you want to attach more than a single statement to the while you have to create a compound statement (using braces); (2) HLA's boolean expressions are limited compared to C/C++ boolean expressions (see the discussion in the section on converting boolean expressions from C/C++ to HLA and the section on the if statement for details).

One problem with converting C/C++ statements to HLA is the conversion of complex boolean expressions. Unlike an if statement, we cannot simply compute portions of a boolean expression prior to the actual test in the while statement, i.e., the following conversion doesn't work:

// while( (x+y) < z )

// {

//    printf( "x=%d\n", x );

//    ++x;

//    y = y + x;

// }

    mov( x, eax );   // Note: this won't work!

    add( y, eax );

    while( eax < z ) do

        stdout.put( "x=", x, nl );

        inc( x );

        mov( x, eax );

        add( eax, y );

    endwhile;

The problem with this conversion, of course, is that the computation of "x+y" needed in the boolean expression only occurs once, when the loop first executes, not on each iteration as is the case with the original C/C++ code. The easiest way to solve this problem is to use the HLA forever..endfor loop and a breakif statement:

// while( (x+y) < z )

// {

//    printf( "x=%d\n", x );

//    ++x;

//    y = y + x;

// }

    forever

        mov( x, eax );

        add( y, eax );

     breakif( eax < z );

        stdout.put( "x=", x, nl );

        inc( x );

        mov( x, eax );

        add( eax, y );

    endfor;

3.2.6.6: The DO..WHILE Loop

The C/C++ do..while loop is similar to the while loop except it tests for loop termination at the bottom of the loop rather than at the top of the loop (i.e., it executes the statements in the loop body at least once, regardless of the value of the boolean control expression the first time the program computes it). Like the while loop, the do..while loop repeats the execution of the loop body as long as the boolean expression evaluates true. HLA does not provide an exact equivalent of the do..while loop, but it does provide a repeat..until loop. The difference between these two loops is that a do..while loop repeats as long as (while) the expression evaluates true, the repeat..until loop repeats until the expression evaluates true (that is, it repeats the loop as long as the expression evaluates false).

The HLA repeat..until statement uses the syntax shown in Figure 3-5.

Figure 3-5: HLA repeat..until Statement Syntax

To convert a C/C++ do..while statement to an HLA repeat..until statement, you must adjust for the semantics of the loop termination condition. Most of the time, the conversion is immediately obvious; in those few cases where you've got a complex boolean expression whose negation is not instantly obvious, you can always use the HLA "!(...)" (not) operator to negate the result of the boolean expression, e.g.,

// do

// {

//    <<some code fragment>>

// }while( (x < 10) && (y > 5 ));

    repeat

        <<some code fragments converted to HLA>>

    until( !((x<10) && (y>5)) );

One advantage of the do..while loop over C/C++'s while loop is that statements appearing immediately before the while clause (and after the do clause) will generally execute on each iteration of the loop. Therefore, if you've got a complex boolean expression that tests for loop termination, you may place the computation of portions of that expression immediately before the HLA until clause, e.g.,

// do

// {

//    printf( "x=%d\n", x );

//    ++x;

//    y = y + x;

// }while( (x+y) < z )

    repeat

        stdout.put( "x=", x, nl );

        inc( x );

        mov( x, eax );

        add( eax, y );

        mov( x, eax );

        add( y, eax );

    until( !(eax < z) );

The only time this will not work is if there is a continue (or an HLA continueif) statement in the loop. The continue statement directly transfers control to the loop termination test in the until clause. Since continue statements in C/C++ appear so infrequently, the best solution is to replace the continue with a jmp instruction that transfers control the first statement that begins the execution of the termination test expression.

3.2.6.7: The C/C++ FOR Loop

The C/C++ statement for statement is a specialized form of the while loop. It should come as no surprise, then, that the conversion to HLA is very similar to that for the while loop conversion. The syntax for the C/C++ for loop is the following:

    for( expression1; expression2; expression3 )

        statement;

This C/C++ statement is complete equivalent to the following C/C++ sequence:

    expression1;

    while( expression2 )

    {

        statement;

        expression3;

    }

Although you can convert a C/C++ for statement to an HLA while loop, HLA provides a for statement that is syntactically similar to the C/C++ for statement. Therefore, it's generally easiest to convert such C/C++ statements into HLA for statements. The HLA for loop takes the following general form:

for( Initial_Stmt; Termination_Expression; Post_Body_Statement ) do

    << Loop Body >>

endfor;

The following gives a complete example:

for( mov( 0, i ); i < 10; add(1, i )) do

    stdout.put( "i=", i, nl );

endfor;

// The above, rewritten as a while loop, becomes:

mov( 0, i );

while( i < 10 ) do

    stdout.put( "i=", i, nl );

    add( 1, i );

endwhile;

There are a couple of important differences between the HLA for loop and the C/C++ for loop. First of all, of course, the boolean loop control expression that HLA supports has the usual restrictions. If you've got a complex boolean expression in a C/C++ loop, your best bet is to convert the for loop into a C/C++ while loop and then convert that while loop into an HLA forever..endfor loop as the section on the while loop describes.

The other difference between the C/C++ and HLA for loops is the fact that C/C++ supports arbitrary arithmetic expressions for the first and third operands whereas HLA supports a single HLA statement. 90% of the C/C++ for loops you'll encounter will simply assign a constant to a variable in the first expression and increment (or decrement) that variable in the third expression. Such for loops are very easy to convert to HLA as the following example demonstrates:

// for( i=0; i<10; ++i )

// {

//    printf( "i=%d\n", i );

// }

    for( mov( 0, i ); i<10; inc(i) ) do

        stdout.put( "i=", i , nl );

    endfor;

C/C++ allows a bizarre form of the for statement to create an infinite loop. The C/C++ convention for an infinite loop uses the following syntax:

    for(;;)

        statement;

HLA does not allow this same syntax for it's for loop. Instead, HLA provides an explicit statement for creating infinite loops: the forever..endfor statement. Figure 3-6 shows the syntax for the forever statement.

Figure 3-6: HLA forever..endfor Loop Syntax

Although for(;;) and forever..endfor, by themselves, create infinite loops, the truth is that most of the time a program that employs these statements also uses a break, breakif, or return statement in order to exit the loop somewhere in the middle of the loop. The next section discusses the break and breakif statements. A little bit later we'll look at C/C++'s return statement.

3.2.6.8: Break and Continue

C/C++ supports two specialized forms of the goto statement that immediately exits, or repeats the execution of, the loop containing these statements. The break statement exits the loop that contains the statement; the continue statement transfers control to the loop control expression (or simply to the top of the loop in the case of the infinite loop). As you've seen earlier, the break statement also ends a case sequence in the C/C++ switch statement.

HLA also provides the break and continue statements that have the same semantics within a loop. Therefore, you can trivially translate these two statements from C/C++ to HLA. HLA also provides breakif and continueif statements that will test a boolean expression and execute the break or continue only if the expression evaluates true. Although C/C++ doesn't provide a direct counterpart to these two HLA statements, you'll often see C/C++ statements like the following that you can immediately translate to an HLA breakif or continueif statement:

    if( C_expression ) break;

    if( C_expression ) continue;

3.2.6.9: The GOTO Statement

The C/C++ goto statement translates directly into an 80x86 jmp instruction. A C/C++ goto statement typically takes the following form:

        goto someLabel;

            .

            .

            .

    someLabel:  // The label may appear before the goto statement!

This usually translates to the following HLA code:

        jmp someLabel;

            .

            .

            .

    someLabel:

The only difference, besides substituting jmp for goto, is the fact that goto labels have their own namespace in C/C++. In HLA, however, statement labels share the same namespace as other local variables. Therefore, it is possible (though rare) that you'll get a "duplicate name" error if you use the same name in your HLA code that appears in the C/C++ program. If this happens, make a minor change to the statement label when translating the code to HLA.

3.3: Function Calls, Parameters, and the Win32 Interface

This section begins the second major portion of this chapter and, in fact, represents the most important material in this chapter from the perspective of an assembly language programmer: how C/C++ function calls translate into assembly language and how an HLA programmer would call a function written in C/C++. This information represents the major point of this chapter since all Win32 API calls are calls to C code. Furthermore, most Windows documentation that explains the Win32 API explains it in terms of C/C++ function calls, in order to understand how one makes calls to the Win32 API from assembly language, you must understand how C/C++ implements these function calls. Explaining that is the purpose of this section.

3.3.1: C Versus C++ Functions

There are some very important differences, both semantic and syntactical, between functions written in C and functions written in C++. The Win32 API uses the C calling and naming convention. Therefore, all the Win32 API documentation also uses the C calling and naming convention. Therefore, that's what we will concentrate on in this chapter.

C++ functions do offer several advantages over C functions. Function overloading is a good example of such a feature. However, function overloading (using the same function name for different functions and differentiating the actual functions by their parameter lists) requires the use of a facility known as name mangling in order to generate unique names that the linker can use. Unfortunately, there is no standard for name mangling among C++ compilers, so every one of them does it differently. Therefore, you rarely see assembly code (or other languages for that matter) interfacing with C++ functions.

In order to allow mixed-language programming with C++ (that is, the use of multiple programming languages on the same project), the C++ language defines a special "C" function syntax that allows you to tell the compiler to generate C linkage rather than C++. This is done with the C++ extern attribute:

extern "C"

{

    extern char* RetHW( void );

};

Please consult a C++ reference manual or your compiler's documentation for more details. Since the Win32 API doesn't use the C++ calling convention, we won't consider it any farther here.

Another useful C++ feature that this chapter will discuss, when appropriate, is pass by reference parameters (since HLA also supports this feature). However, the Win32 API doesn't use any C++ features, so when this chapter gets around to discussing pass by reference parameters, it will mainly be for your own edification.

3.3.2: The Intel 80x86 ABI (Application Binary Interface)

Several years ago, Intel designed what is known as the 80x86 Application Binary Interface, or ABI. The purpose of the ABI was to provide a standard that compiler designers to use to ensure interoperability between modules written in different languages. The ABI specifies what registers a function call should preserve (and which registers a function can modify without preserving), where functions return their results, alignment of data objects in structures, and several other conventions. Since Microsoft's C/C++ compilers (the ones used to compile Windows) adhere to these conventions, you'll want to be familiar with this ABI since the Win32 API uses it.

3.3.2.1: Register Preservation and Scratch Registers in Win32 Calls

The Intel 80x86 ABI specifies that functions must preserve the values of certain registers across a function call. If the function needs to modify the value of any of those registers, it must save the register's value and restore it before returning to the caller. The registers that must be preserved across calls are EBX, ESI, EDI, and EBP. This means two things to an assembly language programmer calling an Win32 function: first of all, Windows preserves the values of these registers across a Win32 API call, so you can place values in these registers, make an OS call, and be confident that they contain the same value upon return. The second implication has to do with callback functions. A callback function is a function you write whose address you pass to Windows. At various times Windows may choose to call that function directly. Such callback functions must obey the register preservation rules of the Intel 80x86 ABI. In particular, such callback functions must preserve the value of the EBX, ESI, EDI, and EBP registers.

On the flip side, the Intel 80x86 ABI specifies that a function may freely modify the values of the EAX, ECX and EDX registers without preserving them. This means that you can generally count on Win32 API functions disturbing the values in these registers; as you'll see in a moment, most Win32 API functions return a function result in the EAX register, so it's almost always wiped out. However, most Win32 API functions wipe out the values in ECX and EDX as well. If you need the values of any of these registers preserved across a Win32 API call, you must save their values yourself.

3.3.2.2: The Stack Pointer (ESP)

The ESP register is a special case. Function calls to the Win32 API generally do not preserve ESP because they remove any parameters from the stack that you push onto the stack prior to calling the API function. However, you can generally assume that ESP is pointing at an appropriate top of stack upon return from the function. In particular, any values you push onto the stack before pushing any API parameters (e.g., register values you want to preserve) will still be sitting on the stack when the function returns. Functions that follow the Intel 80x86 ABI do not arbitrarily mess with the value in the ESP register.

All Win32 API functions assume that the stack is aligned on a double-word boundary (that is, ESP contains a value that is an even multiple of four). If you call a Win32 API function and ESP is not aligned at a double-word address, the Win32 API function will fail. By default, HLA automatically emits code at the beginning of each procedure to ensure that ESP contains a value that is an even multiple of four bytes. However, many programmers choose to disable this code (to make their programs slightly more efficient). If you do this, always make sure that ESP contains a value whose L.O. two bits contain zeros (that is, an even multiple of four) before calling any Win32 API functions.

3.3.2.3: The Direction Flag

All Win32 functions assume that the direction flag is clear when you call them. The Win32 programming convention is to set the direction flag when you need it set and then immediately clear it when you are done using it in that state. Therefore, in all code where you have not explicitly set the direction flag yourself, you can assume that the direction flag is clear. You code should adhere to this policy as well (and always make sure the direction flag is clear when you make a Win32 API call). You can also assume that the direction flag is clear whenever Windows calls one of your callback routines.

3.3.2.4: Function Return Results

Table 3-10 lists the places that functions should return their result (depending on the size of the function return result). The Win32 API generally adheres to this convention. If a function returns more than eight bytes, Win32 API functions generally require that you pass a pointer (i.e., the address of) some block of memory where the function will store the final result.

Table 3-10: 80x86 ABI Function Return Result Locations
Size of Function Result in Bytes
Returned Here
1
al
2
ax
4
eax
8
edx:eax
other
See Compiler Documentation

3.3.2.5: Data Alignment and Padding

The Intel 80x86 ABI generally expects objects to appear at addresses in memory that are an even multiple of their natural size up to four bytes in length (i.e., byte objects may appear at any address, word objects always appear at even addresses, and larger objects are aligned on double-word addresses). This is true for static objects, automatic variables (local variables within a function), and fields within structures. Although this convention is easily circumvented by setting compiler options, the Win32 API pretty much adheres to this convention throughout.

If an object would normally start at an address that is not an even multiple of it's natural size14 (up to four bytes), then the Microsoft C compiler will align that object at the next highest address that is an even multiple of the object's native size. For data, the compiler usually fills (pads) the empty bytes with zeros, though you should never count on the values (or even presence) of padding bytes.

Parameters passed on the stack to a function are a special case. Parameters are always an even multiple of four bytes (this is done to ensure that the stack remains double-word aligned in memory). If you pass a parameter that is smaller than four bytes to some function, the Microsoft C compiler will pad it out to exactly four bytes. Likewise, if you pass a larger object that is not an even multiple of four bytes long, the compiler will pad the object with extra bytes so its length is an even multiple of four bytes long.

For information on padding within structures, please see the section on the struct data type earlier in this chapter.

3.3.3: The C, Pascal, and Stdcall Calling Conventions

There are many different function calling conventions in use today. Of these different calling conventions, three are of interest to us, the so-called C, Pascal, and Stdcall calling conventions. The C and Stdcall calling conventions are of interest because they're the ones that Win32 API calls use. The Pascal calling convention is of interest because that's the default calling convention that HLA uses.

The Pascal calling convention is probably the most efficient of the three and the easiest to understand. In the Pascal calling sequence, a compiler (or human programmer writing assembly code) pushes parameters on the stack as they are encountered in the parameter list when processing the parameters in a left-to-right fashion. Another nice feature of the Pascal calling sequence is that the procedure/function is responsible for removing the parameters from the stack upon return from the procedure; so the caller doesn't have to explicitly do this upon return. As an example, consider the following HLA procedure prototype and invocation:

    // Note: the "@pascal" attribute is optional, since HLA generally uses

    // the pascal calling convention by default.

    procedure proc( i:int32; j:int32; k:int32 ); @pascal; @external;

        .

        .

        .

    proc( 5, a, eax );

Whenever HLA encounters the high-level call to proc appearing in this example, it emits the following "pure" assembly code:

    pushd( 5 );

    push( a );    // Assumption: a is a 32-bit variable that is type compatible with int32

    push( eax );

    call proc;

Note that you have the choice of using HLA's high-level calling syntax or manually pushing the parameters and calling the procedure directly. HLA allows either form; the high-level calling syntax is generally easier to read and understand and it's less likely you'll make a mistake (that invariably hangs the program) when using the high level syntax. Some assembly programmers, however, prefer the low-level syntax since it doesn't hide what is going on.

The C calling convention does two things differently than the Pascal calling convention. First of all, C functions push their parameters in the opposite order than Pascal (e.g., from right to left). The second difference is that C functions do not automatically pop their parameters from the stack upon return. The advantage of the C calling convention is that it allows a variable number of parameters (e.g., for C's printf function). However, the price for this extra convenience is reduced efficiency (since the caller has to execute extra instructions to remove the parameters from the stack).

Although Windows is mostly written in C, most of the Win32 API functions do not use the C calling convention. In fact, only the API functions that support a variable number of parameters (e.g., wsprintf) use the C calling convention. If you need to make a call to one of these API functions (or you want to call some other function that uses the C calling convention), then you've got to ensure that you push the parameters on the stack in the reverse order of their declaration and you've got to remove them from the stack when the function returns. E.g.,

// int cProc( int i, int j, int k );

//    .

//    .

//    .

// cProc( a, 5, 2 );

    pushd( 2 );     // push last parameter first!

    pushd( 5 );

    push( a );      //assumes a is a dword variable.

    call cProc;

    add( 12, esp ); // Remove three dword parameters from stack upon return.

HLA supports the C calling convention using the @cdecl procedure attribute, e.g.,

procedure cProc( i:int32; j:int32; k:int32 ); @cdecl; @external;

HLA's high-level procedure call syntax will automatically push the actual parameters on the stack in the appropriate order (i.e., in reverse). However, you are still responsible for removing the parameter data from the stack upon returning from the procedure call:

    cProc( a, 5, 2 );  // Pushes 2, then 5, then a onto the stack

    add( 12, esp );    // Remove parameter data from the stack.

Don't forget that all procedure parameters are an even multiple of four bytes long. Therefore, when removing parameter data from the stack the value you add to ESP must reflect the fact that the Intel ABI rounds parameter sizes up to the next multiple of four bytes.

The last parameter passing mechanism of immediate interest to us is the Stdcall (standard call) parameter passing mechanism. The Stdcall scheme is a combination of the C and Pascal calling sequences. Like the C calling sequence, the Stdcall scheme pushes the parameters on the stack in the opposite order of their declaration. Like the Pascal calling sequence, the procedure automatically removes the parameters from the stack before returning. Therefore, the caller does not have to remove the parameter data from the stack (thus improving efficiency by a small amount). Most of the Win32 API functions use the Stdcall calling convention. In HLA, you can use the @stdcall procedure attribute to specify the Stdcall calling convention, e.g.,

procedure stdProc( i:int32; j:int32; k:int32 ); @stdcall; @exter-
nal;

HLA's high level procedure call syntax will automatically push the parameters on the stack in the proper (i.e., reverse) order:

stdProc( a, 5, 2 );

Of course, you can also manually call a Stdcall procedure yourself. Be sure to push the parameters in the reverse order!

    pushd( 2 );

    pushd( 5 );

    push( a );

    call stdProc;

Notice that this code does not remove any parameters from the stack. That is the function's job.

Some older HLA code (written before the @stdcall facility was added to the language) simulates the Stdcall calling convention by reversing the parameters in the procedure declaration (indeed, some of the HLA standard library code takes this one step farther and uses macros to swap the parameters prior to making calls to these procedures). Such techniques are obsolete and you shouldn't employ them; however, since there is some code lying around that does this, you should be aware of why it does this.

3.3.4: Win32 Parameter Types

Almost all Win32 parameters are exactly four bytes long. This is true even if the formal parameter is one byte (e.g., a char object), two bytes (a short int), or some other type that is smaller than four bytes. This is done to satisfy the Intel 80x86 ABI and to keep the stack pointer aligned on a four-byte boundary. Since all parameters are exactly four bytes long, a good question to ask is "how do you pass smaller objects, or objects whose size is not an even multiple of four bytes, to a Win32 API function?" This section will briefly discuss this issue.

Whenever you pass a byte parameter to some function, you must pad that byte out to four bytes by pushing an extra three bytes onto the stack. Note that the procedure or function you call cannot assume that those bytes contain valid data (e.g., the procedure/function cannot assume those three bytes all contain zeros). It is perfectly reasonable to push garbage bytes for the upper three bytes of the parameter. HLA will automatically generate code that pushes a byte-sized actual parameter onto the stack as a four-byte object. Most of the time, this code is relatively efficient. Sometimes, however, HLA may generate slightly less efficient code in the interest of safety. For example, if you pass the BH register as a byte-sized parameter (a reasonable thing to do), there is no way that HLA can push BH onto the stack as a double word with a single instruction. Therefore, HLA will emit code like the following:

    sub( 4, esp );    // make room for the parameter

    mov( bh, [esp] ); // Save BH in the L.O. byte of the object on top of stack.

Notice that the upper three bytes of this double-word on the stack will contain garbage. This example, in particular, demonstrates why you can't assume the upper three bytes of the double word pushed on the stack contain zeros. In this particular case, they contain whatever happened to be in those three bytes prior to the execution of these two instructions.

Passing the AL, BL, CL, or DL register is fairly efficient on the 80x86. The CPU provides a single byte instruction that will push each of these eight-bite values onto the screen (by passing the entire 32-bit register that contains these registers:

    push( eax );  // Passes al.

    push( ebx );  // Passes bl.

    push( ecx );  // Passes cl,

    push( edx );  // Passes dl

Passing byte-sized memory objects is a bit more problematic. Your average assembly language programmer would probably write code like the following:

    push( (type dword byteVar) );  // Pushes byteVar plus three following bytes

    call funcWithByteParam;

HLA, because it observes safety at the expense of efficiency, will not generate this code. The problem is that there is a tiny chance that this will cause the system to fail. This situation could occur if byteVar is located in memory within the last three bytes of a page of memory (4096 bytes) and the next page in memory is not readable. That would raise a memory access violation. Quite frankly, the likelihood of this ever occurring is so remote that your average programmer would ignore the possibility of it ever happening. However, compilers cannot be so cavalier. Even if the chance that this problem will occur is remote, a compiler must generate safe code (that will never break). Therefore, HLA actually generates code like the following:

    push( eax );            // Make room for parameter on stack.

    push( eax );            // Preserve EAX's value

    mov( byteVar, al );

    mov( al, [esp+4] );     // Save byteVar's value into parameter location

    pop( eax );             // Restore EAX's value.

    call funcWithByteParam; // Call the function.

As you can see, the code that HLA generates to pass a byte-sized object as a parameter can be pretty ugly (note that this is only true when passing variables).

Part of the problem with generating code for less-than-dword-sized parameters is that HLA promises to never mess with register values when passing parameters15. HLA provides a special procedure attribute, @use, that lets you tell HLA that it is okay to modify the value of a 32-bit register if doing so will allow HLA to generate better code. For example, suppose funcWithByteParam had the following external declaration:

    procedure funcWithByteParam( b:byte ); @use EAX; @external;

With this declaration, HLA can generate better code when calling the function since it can assume that it's okay to wipe out the value in EAX:

// funcWithByteParam( byteVar );

    mov( byteVar, eax );

    push( eax );

    call funcWithByteParam;

Because the Intel ABI specifies that EAX (and ECX/EDX) are scratch registers and any function following the Intel ABI is free to modify their values, and because the Win32 functions follow the Intel ABI, and because most Win32 API functions return a function return result in EAX (thereby guaranteeing that they wipe out EAX's value on any Win32 API call), you might wonder why you (or the HLA Standard Library) shouldn't just always specify "@use EAX;" on every Win32 function declaration. Well, there is a slight problem with doing this. Consider the following function declaration and invocation:

    procedure func( b:char; c:char; d:boolean ); @use eax; @external;

            .

            .

            .

    func( charVar, al, boolVar );

Here's code similar to what HLA would generate for this function call:

    mov( charVar, al );

    push( eax );

    push( eax );

    mov( boolVar, al );

    push( eax );

    call func;

Do you see the problem? Passing the first parameter (when using the @pascal calling convention) wipes out the value this code passes as the second parameter in this function invocation. Had we specified the @cdecl or @stdcall calling convention, then passing the third parameter would have done the dirty deed. For safety reasons, the HLA Standard Library that declares all the Win32 API functions does not attach the @use procedure attribute to each procedure declaration. Therefore, certain calls to Win32 API routines (specifically, those that pass memory variables that are less than four bytes long as parameters) will generate exceedingly mediocre code. If having smaller programs16 is one of your primary goals for writing Windows applications in assembly language, you may want to code calls containing such parameters manually.

If a parameter object is larger than four bytes, HLA will automatically round the size of that object up to the next multiple of four bytes in the parameter declaration. For example, real80 objects only require ten bytes to represent, but when you pass one as a parameter, HLA sets aside 12 bytes in the stack frame. When HLA generates the code to pass a real80 object to a procedure, it generates the same code it would use to pass two double word variables and a word variable; in other words, the code needed to pass the last two bytes could get ugly (for the same reasons we've just covered). However, since there aren't any Win32 API functions that expect a real80 parameter, this shouldn't be an issue.

Table lists the typical C/C++ data types, their HLA equivalents, and how much space they consume when you pass them as parameters to a Win32 API function.

Table 3-11: Space Consumed by Various C Types When Passed as Parameters
C Type
Corresponding HLA Types
Space Consumed on Stack
Padding
char
char, byte, int81
four bytes
three bytes
short
word, int16
four bytes
two bytes
int
dword, int32
four bytes
none
long
dword, int32
four bytes
none
long long
qword, int64
eight bytes
none
unsigned char
char, byte, uns8
four bytes
none
unsigned short
word, uns16
four bytes
none
unsigned
dword, uns32
four bytes
none
unsigned int
dword, uns32
four bytes
none
unsigned long
dword, uns32
four bytes
none
unsigned long long
qword, uns64
eight bytes
none
float
real32
four bytes
none
double
real64
eight bytes
none
long double
real64 (on some compilers)
eight bytes
none
real80 (on other compilers)
twelve bytes
two bytes
1Some compilers have an option that lets you specify the use of unsigned char as the default. In this case, the corresponding HLA type is uns8.

3.3.5: Pass by Value Versus Pass by Reference

The C programming language only supports pass by value parameters. To simulate pass by reference parameters, C requires that you explicitly take the address of the object you wish to pass and then pass this address through a pass by value parameter that is some pointer type. The following code demonstrates how this is done:

/* C code that passes some parameter by reference via pointers */

int someFunc( int *ptr )

{

    *ptr = 0;

}

        .

        .

        .

/* Invocation of this function, assume i is an int */

    someFunc( &i );

This function passes the address of i as the value of the ptr parameter. Within someFunc, the function dereferences this pointer and stores a zero at the address passed in through the pointer variable (since, in this example, we've passed in the address of i, this code stores a zero into the i variable).

HLA, like the C++ language, directly supports both pass by value and pass by reference parameters17. So when coding a prototype for some Win32 API function that has a pointer parameter, you've got the choice of specifying a pointer type as a value parameter or the pointer's base type as a reference parameter in the HLA declaration. The someFunc C function in the current example could be encoded with any of the following to HLA declarations:

    // A "typeless" variable declaration (since pointers are always 32-bit values)

    // that passes the parameter by value:

    procedure someFunc( ptr:dword );

    begin someFunc;

        mov( ptr, eax );

        mov( 0, (type dword [eax]) );

    end someFunc;

    // A typed version passing a pointer to an int32 object as a value parameter:

type

    pInt32 :pointer to int32;

        .

        .

        .

    procedure someFunc( ptr:pInt32 );

    begin someFunc;

        mov( ptr, eax );

        mov( 0, (type dword [eax]) );

    end someFunc;

    // A version using pass by reference parameters

    procedure someFunc( var ptr:int32 );

    begin someFunc;

        mov( ptr, eax );

        mov( 0, (type dword [eax]) );

    end someFunc;

Note that the function's body is exactly the same in all three cases. The function has to grab the address passed on the stack and store a zero at that memory address (just as the C code does). If you manually call someFunc (that is, if you use low-level assembly syntax rather than HLA's high-level procedure calling syntax), then the code you write to call any of these three versions is also identical. It is

// someFunc( i );

    lea( eax, i );   // Take the address of i, could use "pushd( &i );" if i is static.

    push( eax );     // This code assumes that it is okay to wipe out EAX's value.

    call someFunc;   // We're also assuming @pascal or @stdcall convention here.

The difference between these procedure declarations is only evident when you use HLA's high level procedure calling syntax. When the parameter is a double word or pointer value, the caller must explicitly write the code to calculate the address of the actual parameter and pass this computed address as the parameter's value, as the following example demonstrates:

// procedure someFunc( ptr:dword );

// -or-

// procedure someFunc( ptr:pInt32 );

//

// call someFunc, passing the address of "i":

    lea( eax, i );

    someFunc( eax );

When calling a procedure that has a pass by reference parameter, all you need do is pass the variable itself. HLA will automatically generate the code that takes the address of the variable:

// procedure someFunc( var ptr:int32 );

    someFunc( i );

If i is a static, storage, or readonly variable without any indexing applied to it, then HLA generates the following code for this statement:

    push( &i );

    call someFunc;

However, if the actual parameter (i in this case) is an indexed static object, or is a local variable, then HLA will have to generate code like the following:

    push( eax );

    push( eax );

    lea( eax, i );

    mov( eax, [esp+4] );

    pop( eax );

    call someFunc;

This happens because HLA promises not to mess with register values when passing parameters. Of course, you can improve the quality of the code that HLA generates by using the "@use" procedure attribute, remembering the caveats given earlier:

// procedure someFunc( var i:int32 ); @use EAX;

    someFunc( i );  // Assume i is a local (automatic) variable

// is equivalent to

    lea( eax, i );

    push( eax );

    call someFunc;

HLA's pass by reference parameter passing mechanism requires that you specify a memory address as an actual reference parameter. So what happens if you run into a situation when the address you want to pass is in a register and you've specified a pass by reference parameter? If you try to call the function with code like the following HLA will complain that you've not specified a valid memory address:

    someFunc( esi );

The trick is to give HLA what it wants: a memory address. This is easily achieved by specifying the following function call to someFunc:

    someFunc( [esi] );

This generates the following assembly code:

    push( esi );

    call someFunc;

HLA usually requires the type of the actual parameter (the parameter you pass in a procedure call) to exactly match the type of the formal parameter (the parameter appearing in the declaration of the procedure). You cannot pass the address of a char variable as a parameter when the original function calls for a boolean variable (even though both parameter types are one byte in length). There are a couple of exceptions worth noting. You may pass a byte variable as an actual parameter whenever the formal parameter is one byte in length. Similarly, HLA will allow an actual parameter whose type is word if the formal parameter's size is two bytes and HLA will allow an actual dword parameter whenever the formal parameter is four bytes. Also, if the formal parameter is a byte, word, or dword type, then HLA will allow you to pass an actual parameter that is one byte long, two bytes long, or four bytes long, respectively. HLA will also allow an anonymous memory object (e.g., "[eax]") as an actual parameter for any pass by reference parameter; such a parameter will simply pass the value of the specified register as the address for the reference parameter.

One feature that HLA supports as a convenience (especially for Win32 API function calls) is that if you pass a pointer variable as an actual pass by reference parameter, where the formal type of the reference parameter is the base type of the pointer, HLA will go ahead and pass the value of the pointer rather than returning an error (or passing the address of the pointer variable), e.g., the following demonstrates this:

type

    pi  :pointer to int32;

        .

        .

        .

    procedure hasRefParm( var i:int32 );

    begin hasRefParm;

        .

        .

        .

    end hasRefParm;

static

    myInt :int32;

    pInt  :pi;

        .

        .

        .

    hasRefParm( myInt );  // Computes and passes address of myInt.

        .

        .

        .

    hasRefParm( pInt );   // Passes the value of the pInt pointer variable

The choice of whether to pass a parameter as a pointer by value or as a variable by reference is mainly a matter of convenience. If you are usually passing an actual parameter that is a memory variable whose type matches the formal parameter's type, then pass by reference is probably the way to go. However, if you're doing pointer arithmetic or constantly passing the address of objects whose type doesn't exactly match the formal parameter's type (and you're sure you know what you're doing when you do this), then passing a pointer by value is probably going to be more convenient.

Many Win32 API functions accept the address of some buffer as a parameter. Often, the prototype for the function specifies the pointer type as "void *". This means that the caller is supplying the address of a block of memory and the abstract type attached to that block of memory is irrelevant to the compiler. HLA also provides a special form of the pass by reference parameter passing mechanism that suspends type checking on the actual parameters you pass to the procedure. Consider the following HLA procedure prototype:

procedure untypedVarParm( var parm:var ); @external;

Specifying "var" as the parameter's type tells HLA that this is an untyped pass by reference parameter. The caller can supply any memory address as the parameter and HLA will pass that address on to the function. Like normal pass by reference parameters, the actual parameter you supply to this function must be a memory location, you cannot supply a constant or a register as an operand (though you can specify "[reg32]" as a parameter and HLA will pass the value of the specified 32-bit general purpose register as the memory address). This special pass by reference form is especially useful when passing Win32 API functions the address of some buffer where it can place data that Windows returns to the caller. There is, however, one big "gotcha" associated with untyped pass by reference parameters: HLA always passes the address of the variable you pass the function. This is true even if the variable you pass as a parameter is a pointer variable. The following is syntactically acceptable to HLA, but probably doesn't do what the programmer expects:

    procedure hasUntypedParm( var i:var );

    begin hasUntypedParm;

        .

        .

        .

    end hasUntypedParm;

static

    myInt :int32;

    pInt  :pi;

        .

        .

        .

    hasUntypedParm( myInt );  // Computes and passes address of myInt.

        .

        .

        .

    hasUntypedParm( pInt );   // Computes and passes address of pInt

In particular, note that this code does not pass the value of the pointer variable in the second call. Instead, it takes the address of the pointer variable and passes that address on to the hasUntypedParm procedure. So take care when choosing to use untyped pass by reference parameters; their behavior is slightly different than regular pass by reference parameters as this example shows.

There is one important issue that HLA programmers often forget: HLA string variables are pointers! Most Win32 API functions that return data via a pass by reference parameter return character (string) data. It's tempting to be lazy and just declare all pass by reference parameters as untyped parameters. However, this can create havoc when calling certain Win32 API functions that return string data. Consider the following Win32 API procedure prototype:

static

    GetFullPathName: procedure

    (

           lpFileName    : string;

           nBufferLength : dword;

       var lpBuffer      : var;

       var lpFilePart    : var

    );

    @stdcall; @returns( "eax" ); @external( "__imp__GetFullPathNameA@16" );

This function stores a zero-terminated string into the block of memory pointed at by lpBuffer. It might be tempting to call this procedure as follows:

static

    s  :string;

    fp :pointer to char;

        .

        .

        .

    stralloc( 256 );

    mov( eax, s );

        .

        .

        .

    GetFullPathName( "myfile.data", 256, s, fp );

    mov( s, ebx );                                 // GetFullPathName returns the actual

    mov( eax, (type str.strRec [ebx]).length );    // string length in EAX.

The objective of this code is (obviously) to have the call to GetFullPathName place the full path name of the myfile.data file into the string variable s. Unfortunately, this code does not work as advertised. The problem is that the lpBuffer variable is an untyped reference parameter. As a result, the call to GetFullPathName takes the address of whatever variable you pass it, even if that variable is a pointer variable. Since strings are four-byte pointers (that contain the address of the actual character data), this example code doesn't do what the author probably intended. Rather than passing the address of the character string data buffer as you might expect, this code passes the address of the four-byte pointer variable s to GetFullPathName as the buffer address. On return, this function will have overwritten the pointer value (and probably the values of other variables appearing in memory immediately after s). Notice how the original example of this function call appearing earlier in the chapter handled this situation:

static

    fullName :string;

    namePtr  :pointer to char;

        .

        .

        .

    stralloc( 256 );       // Allocate sufficient storage to hold the string data.

    mov( eax, fullName );

        .

        .

        .

    mov( fullName, edx );  // Get the address of the data buffer into EDX!

    GetFullPathName

    (

        "myfile.exe",                          // File to get the full path for.

        (type str.strRec [edx]).MaxStrLen,     // Maximum string size

        [edx],                                 // Pointer to buffer

        namePtr                                // Address of base name gets stored here

    );

    mov( fullName, edx );                     // Note: Win32 calls don't preserve EDX

    mov( eax, (type str.strRec [edx]).length  // Set the actual string length

We'll return to this issue a little later in this chapter when we discuss the conversion of Win32 API function prototypes from C to HLA.

The C language always passes arrays by reference. Whenever the C language sees the name of a function without an index operator ( "[...]") attached to it, C substitutes the address of the first element of the array for that array. Similarly, if you specify some array as a formal parameter in a C function declaration, C assumes that you will actually be passing in a pointer to an element of that array type18.

Structures, on the other hand, C always passes by value (unless, of course, you explicitly take the address of a struct object using the address-of operator and pass that pointer to the struct as your parameter value). Win32 API functions always pass pointers to structures (that is, they expect you to pass structures by reference rather than by value), so when you create a prototype for a Win32 API function call that has a struct as a parameter, you'll always specify a pointer to the structure or pass it by reference in the HLA declaration.

3.4: Calling Win32 API Functions

The Windows operating system consists of several dynamic linked library (DLL) modules in memory. Therefore, when you call a Win32 API function, you're not actually calling that function directly. Indeed, unless you declare your function in a special way, there may be two levels of indirection involved before you get to the actual Win32 kernel code within the DLL. This section will give you a quick primer on Win32 DLLs and how to design your Win32 API function prototypes in HLA to make them slightly more efficient.

The phrase "dynamic linked library" means that linkage to a library module is done at run-time. That is, supplying the run-time address of the library function in question could, technically, be done after your program begins execution. The linking process normally involves patching the address fields of all the call instructions that reference a given function in some library code being linked. However, at run-time (that is, after Windows has loaded your program into memory and begun its execution), it's impractical to attempt to locate every call to some function so that you can modify the address field to point at the new location of that function in memory. The solution to this problem is to provide a single object that has to be changed in memory to provide the linkage, put that object in a known location, and then update that single object whenever dynamically linking in the the function. By having a "single point of contact" the OS can easily change the address of that contact object.

There are two ways to add such a "single point of contact" to a machine code program. The first way is to use a pointer that holds the address of the ultimate routine to call. The application code, when it wants to invoke the Win32 API function (or any other function in some DLL) would simply issue an indirect call through this pointer. The second way is to place a jmp instruction at a known location and modify the jmp instruction's address operand field to point at the first instruction of the desired function within the DLL. The indirect call mechanism is a little more efficient, but it requires encoding a special form of the call instruction whenever you call a function in the DLL (and many compilers will not generate this special form of the call instruction by default, if they provide the option to do it at all). The use of the jmp instruction mechanism is more compatible with existing software development tools, but this scheme requires the execution of an extra instruction in order to transfer control to the actual function in the DLL (specifically, you have to execute the jmp instruction after "calling" the function). Windows, as it turns out, combines both of these mechanisms when providing an interface to the Win32 API functions. The API interface consists of an indirect jmp instruction that transfers control to some location specified by a double-word pointer. The linking code can use any form of the call (or other control transfer) instruction to transfer control to the indirect jmp instruction. Then the indirect jmp transfers control to the actual function specified by the pointer variable. The operating system can dynamically change the target address of the function within the DLL by simply changing this pointer value in memory.

Of course, there is an efficiency drawback to this scheme. Not only must the code execute that extra jmp instruction, but an indirect jmp is typically slower than a direct jmp. So Windows' solution is the slowest of the three forms: you pay the price for the extra jmp instruction and the extra cost associated with the use of indirection. Fortunately, one of the advantages of assembly language is that you can easily circumvent this extra work.

Consider the following HLA procedure prototype to the Win32 ExitProcess function:

procedure ExitProcess( uExitCode:uns32 ); @stdcall; @external( "_ExitProcess@4" );

The _ExitProcess@4 label is actually the label of an indirect jmp instruction that will be linked in with any program that calls ExitProcess. In HLA form, the code at the address specified by the _ExitProcess@4 label looks something like the following (assuming labels like "_ExitProcess@4" were legal in HLA):

_ExitProcess@4: jmp( _imp__ExitProcess@4 );

The "__imp__ExitProcess@4" symbol is the name of a double word pointer variable that will contain the address of the actual ExitProcess function with the Win32 OS kernel, i.e.,

static

    _imp__ExitProcess@4 :dword;  // Assuming "@" was actually legal within an HLA ID.

Note that the library files (e.g., kernel32.lib) that you link your programs with contain definitions for both the symbols _ExitProcess@4 and _imp__ExitProcess@4. The "standard" symbols (e.g., _ExitProcess@4) refer to the indirect jmp instruction. The symbols with the "_imp_" prefix refer to the double word pointer variable that will ultimately hold the address of the actual kernel code. Therefore, you can circumvent the execution of the extra jmp instruction by calling the kernel function indirectly through this pointer yourself, e.g.,

    call( _imp__ExitProcess@4 );   // Assuming "@" was actually legal within an HLA ID.

The major problem with this approach is that it doesn't allow the use of the HLA high level function call syntax. You would be forced to manually push any parameter(s) on the stack yourself when using this scheme. In a moment, you'll soon see how to circumvent this problem. Another minor issue is that HLA doesn't allow the "@" symbol in an identifier (as all the previous code fragments have noted). This, too, is easily corrected.

HLA allows you to declare both external procedures and variables. We'll use that fact to allow external linkage to both the jmp instruction (that is effectively the Win32 API function's entry point) and the pointer to the variable. The following two declarations demonstrate how you can do this:

static

    _imp__ExitProcess :dword; @external( "_imp__ExitProcess@4" );

procedure ExitProcess( uExitCode:uns32 ); @stdcall; @external( "_ExitProcess@4" );

HLA also allows the declaration of procedure variables. A procedure variable is a four-byte pointer to a given procedure. HLA procedure variables are perfect for Win32 API declarations because they allow you to use HLA's high level syntax for procedure calls while making an indirect call through a pointer. Consider the following declaration:

static

    ExitProcess :procedure( uExitCode:uns32 );

                 @stdcall; @external( "_imp__ExitProcess@4" );

With this declaration, you can call ExitProcess as follows:

ExitProcess( 0 );

Rather than calling the code beginning with the indirect jmp instruction, this HLA high level procedure call does an indirect call through the _imp__ExitProcess@4 pointer. Since this is both convenient and efficient, this is the scheme this book will generally employ for all Win32 API function calls.

3.5: Win32 API Functions and Unicode Data

Before discussing how to create HLA procedure prototypes for all the Win32 API functions, a short digression is necessary in order to understand certain naming conventions in the Win32 API. For many years, there were actually two different Win32 OS families: the Windows 95/98/2000ME family and the Windows NT/2000/XP family. The branch starting with Windows 95 was based on Windows 3.1 and MS-DOS to a large extent. The OS branch that started with NT was written mostly from scratch without concern about legacy (DOS) compatibility. As such, the internal structure of these two operating system families was quite different. One area where the difference is remarkable is with respect to character data. The Windows 95 family uses standard eight-bit ANSI (ASCII) characters internally while the NT branch uses Unicode internally. Unicode, if you're unfamiliar with it, uses 16-bit character codes allowing the use of up to 65,536 different characters. The beauty of Unicode is that you can represent most character symbols in use by various languages with a single character code (unlike ASCII, which only supports 128 different character values and isn't even really adequate for English, much less English plus dozens of other languages). Since Microsoft was interested in producing an international operating system, Unicode seemed like the right way to go.

Unicode has some great advantages when it comes to write applications that work regardless of the natural language of the application's users. However, Unicode also has some serious disadvantages that prevent it from immediately taking over the world:

  • Few software tools directly support Unicode, so it is difficult to develop Unicode-enabled applications (though this is changing as time passes).
  • Unicode data requires twice as much storage as ANSI data. This has the effect of doubling the size of many databases and other applications that manipulate a considerable amount of character data.
  • Because Unicode characters are twice as long as ANSI characters, processing Unicode data typically takes twice as long as processing ANSI/ASCII characters (a serious defect to most assembly language programmers).
  • Many programs that manipulate character data use look-up tables and bit maps (character sets) to operate on that data. An ASCII-based look-up table requires 128 bytes, an ANSI look-up table typically requires 256 bytes, a Unicode-based look-up table would require 65,536 bytes (making it impractical to use a look-up table for all but the most specialized of cases when using Unicode). Even implementing a character set using a power set (i.e., a bit map) would require 8,192 bytes; still too large for most practical purposes.
  • There are nowhere near as many Unicode-based string library functions available as there are for ASCII/ANSI based strings. For example, the HLA Standard Library provides almost no Unicode-based string functions at all (actually, it provides none, but a few routines will work with Unicode-based strings).
  • Another problem with using Unicode is that HLA v1.x provides only basic support for Unicode characters19. At the time this was being written, HLA supported the declaration of wchar and wstring constants and variables as well as Unicode character and string literal constants (of the form u'A' and u"AAA"). You could also initialize wchar and wstring static objects as the following example demonstrates. However, HLA constant expression parser does not (as of this writing) support Unicode string operations nor does the HLA Standard Library provide much in the way of Unicode support. The following is an example of static initialization of Unicode data (see the HLA reference manual for more details):
        static

            wCharVar   :wchar := u'w';

            wStringVar :wstring := u"Unicode String";

For all these reasons, and many more, Microsoft realized (while designing Windows NT) that they couldn't expect everyone to switch completely over to Unicode when writing applications for Windows NT (or when using applications written for Windows NT). Therefore, Microsoft's engineers provided duomorphic20 interfaces to Windows NT that involve character data: one routine accepts and returns ANSI data, another routine accepts and returns Unicode data. Internally, of course, Windows NT doesn't really have two sets of routines. Instead, the ANSI routines simply convert incoming data from ANSI to Unicode and the outgoing data from Unicode to ANSI.

In Microsoft Visual C++ (and other high level languages) there is a little bit of macro trickery used to hide the fact that the application has to choose between the Unicode-enabled and the ANSI versions of the Win32 API function calls. By simply changing one macro definition in a properly-written C++ program, it's possible to switch from ANSI to Unicode or from Unicode to ANSI with no other changes to the program. While the same trick is theoretically possible in assembly language (at least, in HLA), the dearth of a good set of Unicode library functions reduces this to the status of an interesting, but not very useful, trick. Therefore, this book will concentrate on producing ANSI-compatible applications with a small discussion of how to do Unicode applications when doing so becomes more practical in assembly language.

Windows duomorphic interface only applies to those functions that accept or return character data. A good example of such a routine is the Win32 API DeleteFile function that has the following two interfaces:

    procedure DeleteFile( lpFileName :string ); @stdcall; @external( "_DeleteFileA@4" );

// -or-

    procedure DeleteFile( lpFileName :wstring ); @stdcall; @external( "_DeleteFileW@4" );

If you look closely at these two declarations, you'll notice that the only difference between the two is a single character appearing in the external name and the type of the parameter. One of the external names has an "A" (for ANSI) immediately before the "@" while the other has a "W" (for Wide) immediately before the "@" character in the name. Wide, in this context, means a two-byte character format; so the name with the embedded "W" is the Unicode version of the function's name.

The presence of the "A" or the "W" at the end of the function's name in the external declaration (i.e., just before the "@", whose purpose the next section covers) determines whether the function is the ANSI version or the Unicode version ("A"=ANSI, "W"=Unicode). There is only one catch: when reading C/C++ documentation about the Windows API, you'll generally see the function referred to as "DeleteFile" (or whatever), not "DeleteFileA" or "DeleteFileW". So how can you tell whether the function's external name requires the "A" or "W"? Well, if any of the parameters involves character or string data, it's a given that the function will have ANSI and Unicode counterparts. If you're still not sure, you can always run Microsoft's dumpbin.exe utility on one of the Win32 API interface libraries (e.g., kernel32.lib, gdi32.lib, user32.lib, etc.) to extract all the exported names:

dumpbin /exports kernel32.lib

This command lists all the Win32 API function names that the kernel32.lib library module exports. If you save the output of this command to a text file (by using I/O redirection) you can search for a particular function name with nearly any text editor. Once you find the filename, if there is an "A" or "W" at the end of the name, you know that you've got a duomorphic function that deals with ANSI or Unicode characters. If no such character appears, then the function only works with binary (non-character) data.

Please note that the official name for a Win32 API function does not include the "A" or "W" suffix. That is, the Win32 documentation refers only to names like DeleteFile, never to names like DeleteFileA or DeleteFileW. The assumption is that an application is only going to use one of the two different character types. Either all character data is ANSI, in which case the application will call those functions with the "A" suffix, or all character data is in Unicode form and the application will call those functions with the "W" suffix. Although it's easy enough to switch between the two in an assembly language program, it's probably a good idea to stick to one form or another in a given application (less maintenance issues that way). The examples in this book will all use the ANSI forms of these functions, since assembly language better supports eight-bit character codes.

This book will also adopt the Win32 convention of specifying the API function names without the "A" or "W" suffix. That is, we'll call functions like DeleteFile and GetFullPathName and not worry about whether it's ANSI or Unicode on each call. The choice will be handled in the declaration of the prototype for the particular Win32 API function. This makes it easy (well, easier) to change from one character format to another should the need arise in the future.

For the most part, this book will stick to the ANSI character set because HLA provides much better support for that character set. If you need to use Unicode in your programs, you'll need to adjust the Win32 API prototypes and HLA char/string declarations accordingly.

Note that the names that have the "A" and "W" suffixes are really external names only. C/C++ documentation doesn't mention these suffixes. Again, if you're unsure whether the suffix is necessary, run the dumpbin program to get the actual library name.

3.6: Win32 API Functions and the Parameter Byte Count

As you've seen in a few examples appearing in this chapter, the external Win32 API function names typically have an at-sign ("@") and a number appended to the end of the function's external name. This numeric value specifies the number of bytes passed as parameters to the functions. Since most parameters are exactly four bytes long, this number (divided by four) usually tells you how many parameters the API function requires (note that a few API calls have some eight-byte parameters, so this number isn't always an exact indication of the number of parameters, but it does hold true the vast majority of the time).

Note that the names that have the "@nn" suffix are really external names only. C/C++ documentation doesn't mention this suffix. Furthermore, since HLA doesn't allow you to embed at signs ("@") into identifiers, you cannot use these external names as HLA identifiers. Fortunately, HLA's @external directive allows you to specify any arbitrary string as the external symbol name.

This book will also adopt the Win32 convention of specifying the API function names without the "@nn" suffix. That is, we'll call functions like DeleteFile and GetFullPathName and not worry about tacking on the number of bytes of parameters to the name. The full name will be handled in the external prototype declaration for the particular Win32 API function. If you need to determine the exact constant for use in an external declaration, you can run the Microsoft dumpbin program on the appropriate .LIB file to determine the actual suffix.

3.7: Creating HLA Procedure Prototypes for Win32 API Functions

Although the HLA distribution includes header files that provide prototypes for most of the Win32 API functions (see the next section for details), there are still some very good reasons why you should be able to create your own HLA external declarations for a Win32 function. Here is a partial list of those reasons:

  • HLA provides most, but not all, of the Win32 API Prototypes (e.g., as Microsoft adds new API calls to Windows, HLA's header files may become out of date).
  • Not every HLA prototype has been thoroughly tested (there are over 1,500 Win32 API function calls one could make and some of those are quite esoteric). There very well could be a defect in the prototype of some function that you want to call.
  • The choice of data type for a give API function may not exactly match what you want to use (e.g., it could specify an uns32 type when you'd prefer the more general dword type).
  • You may disagree with the choice of passing a parameter by reference versus passing a pointer by value.
  • You may disagree with the choice of an untyped reference parameter versus a typed reference parameter.
  • You may disagree with the choice of an HLA string type versus a character buffer.

There are certainly some other reasons for abandoning HLA's external prototypes for various Win32 API functions. Whatever the reason, being able to create an HLA prototype for these functions based on documentation that provides a C prototype is a skill you will need. The following subsections condense the information appearing in the previous sections, toss out a few new ideas, and discuss the "hows and whys" of Win32 API prototyping in HLA.

3.7.1: C/C++ Naming Conventions Versus HLA Naming Conventions

Before jumping in and describing how to translate C/C++ prototypes into HLA format, a slight digression is necessary. Sometimes, you'll run into a minor problem when translating C/C++ code to HLA: identifiers in a C/C++ program don't always map to legal identifiers in an HLA program. Another area of contention has to do with the fact that Microsoft's programmers have created many user-defined types that the Windows system uses. More often than not, these type names are isomorphisms (that is, a different name for the same thing; for example, Microsoft defines dozens, if not hundreds, of synonyms for dword). However, if you understand Microsoft's naming conventions, then figuring out what HLA types to substitute for all these Microsoft names won't prove too difficult.

HLA and C/C++ use roughly the same syntax for identifiers: identifiers may begin with an alphabetic (uppercase or lowercase) character or an underscore, and zero or more alphanumeric or underscore characters may follow that initial character. Given that fact, you'd think that converting C/C++ identifiers to HLA would be fairly trivial (and most of the time, it is). There are, however, two issues that prevent the translation from being completely trivial: HLA reserved words and case sensitivity. We'll discuss these issues shortly.

Even when a C/C++ identifier maps directly to a legal HLA identifiers, questions about that identifier , its readability, applicability, etc., may arise. Unfortunately, C/C++ naming conventions that have been created over the years tend to be rather bad conventions (remember, C was create circa-1970, back in the early days of "software engineering" before people really studied what made one program more readable than another). Unfortunately, there is a lot of inertia behind these bad programming conventions. Someone who is not intimately familiar with those conventions may question why a book such as this one (which covers a different language than C/C++) would continue to propagate such bad programming style. The reason is practical: as this chapter continues to stress, there is a tremendous amount of documentation written about the Win32 API that is C-based. While there is an aesthetic benefit to renaming all the poorly-named identifiers that have become standards in C/C++ Windows source files, doing so almost eliminates the ability to refer to non-HLA based documentation on the Win32 API. That would be a much greater loss than having to deal with some poorly named identifiers. For that reason alone, this book attempts to use standard Windows identifiers (which tend to follow various C/C++ naming conventions) whenever referring to those objects represented by the original Windows identifiers. Changes to the Windows naming scheme are only made where absolutely necessary. However, this book will only use the Windows naming conventions for pre-existing, reknown, Windows (and C/C++) identifiers. This book will adopt the standard "HLA naming convention" (given a little later) for new identifiers.

One problem with C/C++ naming conventions is that they are inconsistent. This is because there isn't a single C/C++ naming convention, but several that have sprung up over the years. Some of them contain mutually exclusive elements, still it isn't unusual to see several of the conventions employed within the same source file. Since the main thrust of this chapter is to prepare you to read Win32 API documentation, the sections that follow will concentrate on those conventions and problems you'll find in typical Windows documentation.

3.7.1.1: Reserved Word Conflicts

The HLA language defines hundreds of reserved words (this is reasonable, since there are hundreds of machine instructions in the 80x86 instruction set, though there is no arguing against that fact that HLA has a large number of reserved words above and beyond the machine instructions). Since not all of HLA's reserved words are reserved words in C/C++, it stands to reason that there are some programs out there than inadvertently use HLA reserved words as identifiers in their source code. This fact is just as true for the Win32 API definitions appearing in Microsoft's C/C++ header files as it is for application programs. There will be some C/C++ identifiers in the Win32 C/C++ documentation that we will not be able to use simply because they are HLA reserved words. Fortunately, such occurrences are rare. This book will deal with such issues on a case-by-case basis, providing a similar name that is not an HLA reserved word when such a conflict arises.

3.7.1.2: Alphabetic Case Conflicts

Another point of conflict between HLA identifiers and C/C++ identifiers is the fact that C/C++ is a case sensitive language whereas HLA is a case neutral language. HLA treats upper and lower case characters as distinct, but will not allow you to create an identifier that is the same as a different identifier except for alphabetic case. C/C++, on the other hand, will gladly let you create two different identifiers whose only difference is the case of alphabetic characters within the symbols. Worse, some C/C++ programmers have convinced themselves that it's actually a good idea to take advantage of this "feature" in the language (hint: it's a terrible idea to do this, it make programs harder to read and understand). Regardless of your beliefs of the utility of this particular programming style, the fact remains that C/C++ allows this (and some programmers take advantage of it) while HLA does not. The question is "which identifier do we modify and how do we modify it?"

Most of the time there is a case neutrality violation in a C/C++ program (that is, two identifiers are the same except for alphabetic case), it's usually the situation where one of the identifiers is either a type definition or a constant definition (the other identifier is usually a function or variable name). This isn't true all the time, but it is true in the majority of the cases where this conflict occurs. When such a conflict occurs, this book will use the following convention (prioritized from first to last):

  • If one of the conflicting identifiers is a type name, we'll convert the name to all lowercase characters and append "_t" to the name (a common Unix convention).
  • If one of the conflicting identifiers is a constant (and the other is not a type), we'll convert the name to all lowercase and append "_c" to the name (an HLA convention, based on the Unix convention).
  • If neither of the above conditions hold, we'll give one of the identifiers a more descriptive name based on what the identifier represents/contains/specifies rather than on its classification (e.g., type of symbol).

A good convention to follow with respect to naming identifiers is the "telephone test." If you can read a line of source code over the telephone and have the listener understand what you're saying without explicitly spelling out an identifier, then that identifier is probably a decent identifier. However, if you have to spell out the identifier (especially when using phrases like "upper case" and "lower case" when spelling out the name), then you should consider using a better name. HLA, of course, prevents abusing and misusing alphabetic case in identifiers (being a case neutral language), so it doesn't even allow one to create identifiers that violate the telephone test (at least, from an alphabetic case perspective).

3.7.1.3: Common C/C++ Naming Conventions

If you search on the Internet for "C Naming Conventions" you'll find hundreds of pages extolling the benefits of that particular web page author's favorite C naming scheme. It seems like nearly every C programmer with an opinion and a web page is willing to tell the world how identifiers should be designed in C. The really funny thing is that almost every one of these pages that specifies some naming convention is mutually exclusive with every other such scheme. That is, if you follow the rules for naming C identifiers found at one web site, you're invariably break one or more rules at nearly every other site that provides a C naming convention. So much for convention; so much for standards.

Interestingly enough, the one convention that nearly everybody agrees upon is also, perhaps, the worst naming convention ever invented for programming language identifiers. This is the convention of using all uppercase characters for identifiers that represent constant values. The reason everyone agrees on this one convention is fairly obvious to someone who has been programming in the C programming language for some time: this is one of the few naming conventions proposed by Kernighan and Ritchie in their original descriptive text The C Programming Language. In Programming in C: A Tutorial by Brian W. Kernighan, Mr. Kernighan describes this choice thusly:

Good style typically makes the name in the #define upper case; this makes parameters more visible.

This quote probably offers some insight into why Kernighan and Ritchie proposed the use of all uppercase for constants in the first place. One thing to keep in mind about this naming convention (using all upper case for #define symbols) was that it was developed in the very early 1970s. At the time, many mainframes and programming languages (e.g., FORTRAN) only worked with uppercase alphabetic characters. Therefore, programmers were used to seeing all uppercase alphabetic characters in a source file and lowercase was actually unusual (despite the fact that C was developed on an interactive system that supported lower case). In fact, Kernighan and Ritchie really got it backwards - if they'd wanted the parameters to stand out, they should have made them all uppercase and made the #define name lower case. Another interesting thing to note from this quote was that the all uppercase convention was specifically created for macros, not manifest constants. The "good style" Brian Kernighan was referring to was an attempt to differentiate the macro name from the macro parameters. Manifest constants (that is, the typical constants you create with a #define definition) don't have parameters, so there is little need to differentiate the name from the macro's parameter list (unless, of course, Mr. Kernighan was treating the remainder of the line as the "parameters" to the #define statement).

Psychologists have long known (long before computer programming languages became popular) that uppercase text is much harder to read than lower case text. Indeed, to a large extent, the whole purpose of uppercase alphabetic text is to slow the reader down and make them take notice of something. All uppercase text makes material harder to read, pure and simple. Don't believe this? Try reading the following:

PSYCHOLOGISTS HAVE LONG KNOWN (LONG BEFORE COMPUTER PROGRAMING LANGUAGES BECAME POPULAR) THAT UPPERCASE TEXT IS MUCH HARDER TO READ THAN LOWER CASE TEXT. INDEED, TO A LARGE EXTENT, THE WHOLE PURPOSE OF UPPERCASE ALPHBETIC TEXT IS TO SLOW THE READER DOWN AND MAKE THEM TAKE NOTICE OF SOMETHING. ALL UPPERCASE TEXT MAKES MATERIAL HARDER TO READ, PURE AND SIMPLE. DON'T BELEVE THIS? TRY REREADING THIS PARAGRPH.

There are four intentional spelling mistakes in the previous paragraph. Did you spot them all the first time you read this paragraph? They would have been much more obvious had the text been all lowercase rather than all uppercase. Reading all upper case text is so difficult, that most readers (when faced with reading a lot of it) tend to "short-circuit" their reading and automatically fill in words once they've read enough characters to convince them they've recognized the word. That's one of the reasons it's so hard to immediately spot the spelling mistakes in the previous paragraph. Identifiers that cause a lack of attention to the details are obviously problematic in a programming language and they're not something you want to see in source code. A C proponent might argue that this isn't really much of a problem because you don't see as much uppercase text crammed together as you do in the paragraph above. However, some long identifiers can still be quite hard to read in all upper case; consider the following identifier taken from the C/C++ windows.inc header file set: CAL_SABBREVMONTHNAME1. Quick, what does it mean?

Sometimes it is useful to make the reader slow down when reading some section of text (be it natural language or a synthetic language like C/C++). However, it's hard to argue that every occurrence of a constant in a source file should cause the reader to slow down and expend extra mental effort just to read the name (thus, hopefully, determining the purpose of the identifier). The fact that it is a constant (or even a macro) is far more easily conveyed using some other convention (e.g., the "_c" convention that this book will adopt).

Now some might argue that making all macro identifiers in a program stand out is a good thing. After all, C's macro preprocessor is not very good and it's macro expander produces some unusual (non-function-like) semantics in certain cases. By forcing the programmer to type and read an all-uppercase identifier, the macro's designer is making them note that this is not just another function call and that it has special semantics. An argument like this is valid for macros (though a suffix like "_m" is probably a better way to do this than by using all uppercase characters in the identifier), but is completely meaningless for simple manifest constants that don't provide any macro parameter expansion. All in all, using all uppercase characters for identifiers in a program is a bad thing and you should avoid it if at all possible.

This text, of course, will continue to use all uppercase names for well-known constants defined in Microsoft's C/C++ header files. The reason is quite simple: they are documented in dozens and dozens of Windows programming books and despite the fact that such identifiers are more difficult to read, changing them in this text would prove to be a disaster because the information appearing herein would not be compatible with most of the other books on Windows programming in C/C++21. For arbitrary constant identifiers (i.e., those that are not predefined in the C/C++ Windows header files), this book will generally adopt the "_c" convention for constants.

One C/C++ naming convention that is specified by the original language definition is that identifiers that begin and end with an underscore are reserved for use by the compiler. Therefore, C/C++ programmers should not use such identifiers. This shouldn't prove to be too onerous for HLA programmers because HLA imposes the same restriction (identifiers beginning and ending with an underscore are reserved for use by the compiler and the HLA Standard Library).

Beyond these two conventions that have existed since the very first operational C compilers, there is very little standardization among the various "C Naming Conventions" documents you'll find on the Internet. Many suggestions that one document makes are style violations another document expressedly forbids. So much for standardized conventions! The problem with these myriad of non-standardized "standards" is that unless you include the style guide in the comments of a source file, the guidelines you're following are more likely to confuse someone else reading the source file who is used to operating under a different set of style guidelines.

Perhaps one of the most confusing set of style guidelines people come up with for C/C++ programs is what to do about alphabetic case. Wise programmers using alphabetic case differences for formatting only. They never attach meaning to the case of alphabetic characters within an identifier. All upper case characters for constants is fairly easy to remember (because it is so well ingrained in just about every C/C++ style guide ever written), but how easy is it to remember that "variables must be written in mixed case starting with a lower case character" and "Names representing types must be in mixed case beginning with an uppercase character"? There are some so-called style guidelines that list a dozen different ways to use alphabetic case in an identifier to denote one thing or another. Who can remember all that? What happens when someone comes along and doesn't intimately know the rules? Fortunately, you see little of this nonsense in Windows header files.

As noted earlier in this document, a common (though not ubiquitous) Unix/C programming convention is to append the suffix "_t" to type identifiers. This is actually an excellent convention (since it emphasizes the classification of an identifier rather than its type, scope, or value). The drawback to this scheme is that you rarely see it used consistently even within the same source file. An even bigger drawback is that you almost never see this naming convention in use in Windows code (Windows code has a nasty habit of using all uppercase to denote type names, as well as constant, macro, and enum identifiers, thus eliminating almost any mnemonic value the use of all uppercase might provide; about the only thing you can say about an all-uppercase symbol in a Windows program is that it's probably not a variable or a function name). Once again, this book will use standard Windows identifiers when referencing those IDs, but will typically use the Unix convention of the "_t" suffix when creating new type names.

Without question, the most common naming convention in use within C/C++ Windows applications is the use of Hungarian Notation. Hungarian notation uses special prefix symbols to denote the type of the identifier. Since Hungarian notation is so prevalent in Windows programs, it's worthwhile to spend some time covering it in detail...

3.7.1.4: Hungarian Notation

Hungarian notation is one of those "innovations" that has come out of Microsoft that lots of people love and lots of people hate. Originally developed by Charles Simonyi in a technical paper (searching on the Internet for "Hungarian Notation Microsoft Charles Simonyi" is bound to turn up a large number of copies of his paper (or links to it), Hungarian notation was adopted internally within Microsoft and the popularized outside Microsoft via the Windows include files and Charles Petzold's "Programming Windows" series of books (which push Hungarian notation). As a result of these events, plus the large number of programmers that "cut their teeth" at Microsoft and went on to work at companies elsewhere, Hungarian notation is very a popular naming convention and it's difficult to read a C/C++ Windows program without seeing lots of examples of this notation.

Hungarian notation is one of those conventions that everyone loves to hate. There are lots of good, technical, reasons for not using Hungarian notation. Even many proponents of Hungarian notation will admit that it has its problems. However, people don't use it simply because Microsoft pushes it. In spite of the problems with Hungarian notation, the information it provides is quite useful in large programs. Even if the convention wasn't that useful, we'd still need to explore it here because you have to understand it in order to read C/C++ code; still, because it is somewhat useful, this book will even adopt a subset of the Hungarian notation conventions on an "as useful" basis.

Hungarian notation is a naming convention that allows someone reading a program to quickly determine the type of a symbol (variable, function, constant, type, etc.) without having to look up the declaration for that symbol. Well, in theory that's the idea. To someone accustomed to Hungarian notation, the use of this convention can save some valuable time figuring out what someone else has done. The basic idea behind Hungarian notation is to add a concise prefix to every identifier that specifies the type of that identifier (actually, full Hungarian notation specifies more than that, but few programmers use the full form of Hungarian notation in their programs). In theory, Hungarian notation allows programmers to create their own type prefixes on an application by application basis. In practice, most people stick to the common-predefined type prefixes (tags) let it go at that.

An identifier that employs Hungarian notation usually takes the following generic form:

prefix tag qualifier baseIdentifier

Each of the components of the identifier are optional, subject of course, to the limitation that the identifier must contain something. Interestingly enough, the baseIdentifier component (the name you'd normally think of as the identifier) is itself optional. You'll often find Hungarian notation "identifiers" in Windows programs that consist only of a possible prefix, tag, and/or qualifier. This situation, in fact, is one of the common complaints about Hungarian notation - it encourages the use of meaningless identifiers in a source file. The baseIdentifier in Hungarian notation is the symbol you'd normally use if you weren't using Hungarian notation. For the sake of example, we'll use Variable in the examples that follow as our base identifier.

The tag component in the Hungarian notation is probably the most important item to consider. This item specifies the base type, or use, of the following symbol. Table 3-12 lists many of the basic tags that Windows programs commonly use; note that Hungarian notation does not limit a program to these particular types, the user is free to create their own tags.

Table 3-12: Basic Tag Values in Hungarian Notation
Tag
Description
f
Flag. This is a true/false boolean variable. Usually one byte in length. Zero represents false, anything else is true.
ch
Character. This is a one-byte character variable.
w
Word. Back in the days of 16-bit Windows systems (e.g., Windows 3.1), this tag meant a 16-bit word. However, as a perfect demonstration of one of the major problems with Hungarian notation, the use of this prefix became ambiguous when Win32 systems started appearing. Sometimes this tag means 16-bit short, sometimes it means a 32-bit value. This prefix doesn't provide much in the way of meaningful information in modern Windows systems.
b
Byte. Always a one-byte value.
l
Long. This is generally a long integer (32 bits).
dw
Double Word. Note that this is not necessarily the same thing as an "l" object. In theory, the usage of this term is as ambiguous as "w", though in 80x86 Windows source files this is almost always a 32-bit double word object.
u
Unsigned. Typically denotes an unsigned integer value (usually 32 bits). Sometimes you will see this symbol used as a prefix to one of the other integer types, e.g., uw is an unsigned word.
r
Real. Four-byte single precision real value.
d
Double. Eight-byte double precision real value.
bit
A single bit. Typically used with field names that are bit fields within some C struct.
bm
Bit map. A collection of bits (e.g., pixel values).
v
Void. An untyped object. Typically used only with the pointer prefix (see the discussion of prefixes). Untyped pointers are always 32 bit objects under Win32.
st
String. Object is a Pascal string with a length prefix.
sz
String, zero terminated. Object is a C/C++ zero terminated string object.

In Table 3-12 you see the basic type values commonly associated with symbols employing Hungarian notation. Table lists some modifier prefixes you may apply to these types (though there is no requirement that a tag appear after one of the prefixes, a lone prefix followed by the base identifier is perfectly legal, though not as readable as an identifier consisting of a prefix, tag, and base identifier).

Table 3-13: Common Prefix Values in Hungarian Notation
Prefix
Description
p
Pointer to some type.
lp
Long pointer to some type. Today, this is a synonym for "p". Back in the days of 16-bit Windows system, an "lp" object was 32 bits and a "p" object was 16 bits. Today, both pointer types are identical and are 32 bits long. Although you'll see this prefixed used quite a bit in existing code and header files, you shouldn't use this prefix in new code.
hp
Huge pointer to some type. Yet another carry-over from 16-bit Windows days. Today, this is synonymous with lp and p. You shouldn't use this prefix in new code.
rg
Lookup table. Think of an index into an array as a function parameter, the function's result (i.e., the table entry) is the range of that function, hence the designation "rg". This one is not common in many Windows programs.
i
An index (e.g., into an array). Also commonly employed for for loop control variables.
c
A count. cch, for example, might be the number of characters in some array of characters.
n
A count. Same as c but more commonly used to avoid ambiguity with ch.
d
The difference between two instances of some type. For example, dX might be the difference between to x-coordinate values.
h
A Handle. Handles are used through Windows to maintain resources. Many Win32 API functions require or return a handle value. Handles are 32-bit objects under Win32.
v
A global variable. Many programmers use `g' rather than `v' to avoid confusion with the `v' basic tag specification.
s
A static variable (local or global)
k
A const object.

Here are some examples of names you'll commonly see (e.g., from the Windows.hhf header file) that demonstrate the use of these identifiers:

    char *lpszString;    // Pointer to zero-terminated string of characters.

    int  *pchAnswer;     // Pointer to a single character holding an answer.

    HANDLE hFile;        // Handle of a file.

Note all of these prefixes and tags are equally popular in Windows programs. For example, you'll rarely see the "k" prefix specification in many Windows source files (instead, the programmer will probably use the common C/C++ uppercase convention to denote an identifier). Also, many of the prefix/tag combinations are ambiguous. Fortunately, few people (including the Windows header files) push Hungarian notation to the limit. Usually, you'll just see a small subset of the possibilities in use and there is little ambiguity.

Another component of Hungarian notation, though you'll rarely see this used in real life, is a qualifier. More often that not, qualifiers (if they appear at all) appear in place of the base identifier name. Table lists some of the common qualifiers used in Hungarian notation.

Table 3-14: Common Qualifiers in Hungarian Notation
Qualifier
Description
First
The first item in a set, list, or array that the program is working with (this does not necessarily indicate element zero of an array). E.g., iFirstElement.
Last
The last item in a set, list, or array that the program has worked upon (this does not necessarily indicate the last element of an array or list). E.g., pchLastMember. Note that Last index objects are always valid members of the set, list, or array. E.g., array[ iLastIndex ] is always a valid array element.
Min
Denotes the minimum index into a set, list, or array. Similar to First, but First specifies the first element you're dealing with rather than the first object actually present.
Max
Denotes an upper limit (plus one, usually) into an array or list.

There are many, many different variants of Hungarian notation. A quick perusal of the Internet will demonstrate that almost no one really agrees on what symbols should be tags, prefixes, or qualifiers, much less what the individual symbols in each of the classes actually mean. Fortunately, the Windows header files are fairly consistent with respect to their use of Hungarian notation (at least, where they use Hungarian notation), so there won't be much difficulty deciphering the names from the header files that we'll use in this book.

As for using Hungarian notation in new identifiers appearing in this book, that will only happen when it's really convenient to do so. In particular, you'll see the "h" (for Handle) and "p" (for pointer) prefixes used quite a bit. Once in a while, you may see some other bits and pieces of Hungarian notation in use (e.g., b, w, and dw for byte, word, and dword objects). Beyond that, this book will attempt to use descriptive names, or at least, commonly used names (e.g., i, j, and k for array indexes) rather than trying to explain the identifier with a synthetic prefix to the identifier.

3.8: The w.hhf Header File

Provided with the HLA distribution is the w.hhf include file that define most of the Win32 API functions, constants, types, variables, and other objects you'll ever want to reference from your assembly language code. All in all, you're talking well over 30,000 lines of source code! It is convenient to simply stick an HLA #include statement like the following into your program and automatically include all the Windows definitions:

#include( "w.hhf" )

The problem with doing this is that it effectively increases the size of your source file by 30,000 lines of code. Fortunately, recent modifications to HLA have boosted the compile speed of this file to about 25,000 lines/second, so it can process this entire include file in just a few seconds. Most people don't really care if an assembly takes two or three seconds, so including everything shouldn't be a problem (note that the inclusion of all this code does not affect the size of the executable nor does it affect the speed of the final program you're writing; it only affects the compile-time of the program). For those who are bothered by even a few seconds of compile time, there is a solution.

A large part of the problem with the HLA/Windows header files is that the vast majority of the time you'll never use more than about 10% of the information appearing in these header files. Windows defines a tremendous number of esoteric types, constants, and API functions that simply don't get used in the vast majority of Windows applications. If we could pick out the 10% of the definitions that you were actually going to use on your next set of projects, we could reduce the HLA compilation overhead to almost nothing, far more acceptable to those programmers that are annoyed by a few seconds of delay. To do this, you've got to extract the declarations you need and put them in a project-specific include file. The examples in this book won't bother with this (because compiling the w.hhf file is fast enough), but feel free to do this if HLA compile times bother you.

3.9: And Now, on to Assembly Language!

Okay, we've probably spent enough time discussing C/C++ in a book dedicated to writing Windows programs in assembly language. The aim of this chapter has been to present a firm foundation for those who need to learn additional Windows programming techniques by reading C/C++ based documentation. There is no way a single chapter (even one as long as this one) can completely cover all the details, but there should be enough information in this chapter to get you well on your way to the point of understanding how to interface with Windows in assembly language by reading C/C++ documentation on the subject. Now, however, it's time to turn our attention to writing actual Windows applications in assembly language.

1For our purposes, a scalar data type is a primitive or atomic data type; one that the language treats as a single unit, that isn't composed of smaller items (like, say, elements of an array or fields of a structure).
2Only recent versions of the C programming language support the "long double" floating point type.
3For those who are interested in using the align directive and other advanced record field alignment facilities, please consult the HLA reference manual.
4Technically, you should align a record object on a boundary that is an even multiple of the largest field's size, up to four bytes. Aligning record variables on a four-byte boundary, however, will also work.
5Some compilers may actually allocate a few extra bytes of padding at the end of the string to ensure that the literal string constant's length is an even multiple of four bytes. However, this is not universal among compilers so don't count on it.
6The only `gotcha' is that you cannot expect the string to remain in a consistent HLA format if your code that operates on C/C++ strings makes any modifications to the HLA string data.
7Many programmers (and languages) consider pointers to be a scalar type because the value is not composed of other types. This book treats pointers as a separate classification simply because it has to discuss both scalar and composite data types before being able to discuss pointers.
8Actually, this calling scheme works for HLA procedure variables, too.
9Dereferencing means to access the data pointed at by a pointer variable.
10A later section in this chapter will discuss C/C++ operator precedence and associativity. Please see that section for more details concerning operator precedence and associativity.
11The implication is only visual. This form is completely equivalent to the previous form since addition is still commutative.
12C++ compilers generally provide the same sequence points at C, although the original C++ standard did not define any sequence points.
13There are a few additional forms, some of which we'll cover a little later in this section..
14The natural size of an object is the size of the object if it's a scalar, the size of an element if it's an array, or the size of the largest field (up to four bytes) if it's a structure.
15Indeed, the only time HLA messes with any register value behind your back is when invoking a class method. However, HLA well-documents that fact that class method and procedure calls may wipe out the values in ESI and EDI.
16In the big picture, this extra code is not going to affect the running time of your code by a significant factor. Win32 API functions are sufficiently slow to begin with that the few extra clock cycles consumed by the "safe" code is insignificant.
17Actually, HLA supports several different parameter passing mechanisms. However, pass by value and pass by reference are the only ones that are of interest when calling Win32 API functions, so we'll discuss only those here. See the HLA reference manual for more details on the more advanced parameter passing mechanisms.
18As noted earlier, C does not differentiate pointer or array access syntax. Both are identical to C, for the most part. This is how C gets away with passing all arrays as a pointer to their first element.
19Least you use this as an argument against using HLA, note that HLA actually provides some Unicode support. Most assemblers provide no Unicode support whatsoever at all.
20Two-faced.
21Note, however, that there is a precedent for changing the Win32 API identifiers around when programming in a language other than C/C++. Borland's documentation for Delphi, for example, changes the spelling of many Windows identifiers to something more reasonable (note, however, that Pascal is a case insensitive language and some changes were necessary for that reason alone).
TOC PREV NEXT INDEX


Chapter 4: The RadASM IDE for HLA

4.1: Integrated Development Environments

An integrated development environment (IDE) traditionally incorporates a text editor, a compiler/assembler, a linker, a debugger, a project manager, and other development tools under the control of a single main application. Integrated doesn't necessarily mean that a single program provides all these functions. However, the IDE does automatically run each of these applications as needed. An application developer sees a single "user interface" to all these tools and doesn't have to learn different sets of commands for each of the components needed to build an application.

The central component of most IDEs is the editor and project manager. A project in a typical IDE is a related collection of files that contain information needed to build a complete application. This could, for example, include assembly language source files, header files, object files, libraries, resource files, and binary data files. The point of an IDE project is to collect and manage these files to make it easy to keep track of them.

Most IDEs manage the files specific to a given project by placing those files in a single subdirectory. Shared files (such as library and shared object code files) may appear elsewhere but the files that are only used for the project generally appear within the project directory. This makes manipulation of the project as a whole a bit easier.

RadASM, created by Ketil Olsen, is a relatively generic integrated development environment. Many IDEs only work with a single language or a single compiler. RadASM, though designed specifically for assembly language development, works with a fair number of different assemblers. The nice thing about this approach is that you may continue to use RadASM when you switch from one assembler to another. This spares you the effort of having to learn a completely new IDE should you want to switch from one assembler to another (e.g., switching between FASM, MASM, NASM, TASM, and HLA is relatively painless because RadASM supports all of these assemblers; however, were you to switch from SpASM to one of these assemblers you'd have to relearn the IDE because SpASM has it's own unique IDE). One drawback to generic IDEs is that they aren't as well integrated with the underlying toolset as an IDE designed around a specific toolset. RadASM, however, is extremely customizable, allowing you to easily set it up with different assemblers/compilers or even modify it according to your own personal tastes. By properly setting up RadASM to work with HLA, you can speed up the development of assembly language software under Windows. A fair percentage of this chapter deals with setting up and customizing the RadASM environment to suit your preferences and typical Windows assembly development.

4.2: Traditional (Command Line) Development in Assembly

RadASM isn't a integrated development environment in the sense that it integrates all of its functionality into the same program. Instead, RadASM is a shell program that invokes other applications in response to requests or commands within the IDE. For example, to compile an HLA program, RadASM does not include a copy of the HLA compiler within RadASM - it simply runs the HLA.EXE compiler in response to an assemble/compile command. Part of the work involved in customizing RadASM is to define the command line parameters to send to various tools like the HLA.EXE compiler. Therefore, in order to properly set up RadASM to operate HLA, you need to understand how to use HLA (and other tools) from a command line interface. Indeed, there are many times when it's much more convenient to use HLA from the command line rather than from an IDE. For these two reasons, it's important to first begin the discussion of using RadASM by describing how to use HLA without RadASM - straight from the command line.

In this book, we'll take the approach of always supporting both RadASM and command-line projects. This approach has the disadvantage of negating some of the benefits of using an IDE (i.e., automatic project maintenance) but it has the bigger advantage of being more flexible and not tying you down to using RadASM (should you prefer not to use RadASM for any reason, temporary or permanent). Fortunately, RadASM is flexible enough to support just about any command-line based development scheme you can throw at it, so supporting both schemes is not all that difficult.

This book assumes that you're already comfortable using the HLA compiler from the command line. This is not an unreasonable assumption because this book also assumes that you already know HLA and using HLA generally involves compiling files using command line tools. If you're not already comfortable with HLA command-line options, please take a moment to review these options in the HLA reference manual.

For very simple projects, those involving only a single code file (and, possibly, a few include files), building an HLA project into an executable is fairly trivial - simply specify the program's name on the HLA command line and let HLA take care of all the rest of the work for you, e.g.,

hla ProgramToCompile

Assuming ProgramToCompile.hla is a complete, compilable, HLA source file, this command will produce the ProgramToCompile.exe executable file. For many trivial projects (including many of those projects appearing early in this text), this is actually the most convenient way to produce an executable file from the HLA source file(s). However, as your projects grow more complex and wind up consisting of multiple source files that you compile separately and then link together, attempting to build the final executable by manually issuing a series of commands from the command line becomes rather tedious. Therefore, most programmers use the make subsystem to build their projects and they create make files to control the compilation of their projects. We'll use that same approach in this book.

4.3: HLA Project Organization

This book will adopt the (reasonable) convention of placing each HLA project in its own subdirectory. A given project directory will contain the following files and directories:

  • All source files specific to the project (this includes make files, .hla, .hhf, .rc, .rap [RadASM project] and other files created specifically for this project, but does not include any standard library header or generic library files that all projects use).
  • A "makefile" file that follows the template given in Chapter One.
  • A "Tmp" subdirectory where HLA can place temporary files it creates during compilation (normally these files wind up in the same directory as the HLA source files; placing them in the "Tmp" directory prevents clutter of the main project directory).
  • A "Bak" subdirectory where backup files can be kept.
  • A "Doc" directory where project-related documentation can be found.

Every project in this book will have a makefile associated with it. You can either build the project by typing "make" from a command prompt window or you can build the project by selecting an appropriate option from the RadASM "make" menu (which processes this very same makefile). The WPA (Windows Programming in Assembly) subdirectory that holds all the projects associated with this book also has a makefile that will automatically build all projects associated with this text (this is useful, for example, for easily verifying that modifications to the HLA compiler don't break any of the example programs in this book)1.

The RadASM IDE provides the ability to maintain projects itself. The examples in this book will not use the built-in RadASM "project make" facility for a couple of reasons: (1) the make program is more powerful, especially for larger projects involving many files; (2) this book has to create a makefile anyway (for command line processing of HLA projects) and maintaining (and keeping consistent) two different project management systems is problematic; and, finally, (3) the makefile scheme is a little more flexible.

The drawback to using makefiles to maintain the project is that you've got to manually create the makefile; RadASM won't do this for you automatically (as it does with its own projects). Fortunately, 90% of your makefile creations will simply be copying an existing makefile to your project's directory, editing the file, and changing the filenames from the previous project to the current project (indeed, this operation is so common that you'll find a generic makefile in the "sniplets" RadASM directory provide with the accompanying CD-ROM. You can easily create a copy of this generic makefile from RadASM's "Tools > Sniplets" menu, as you'll see soon enough).

4.4: Setting Up RadASM to Work With HLA

RadASM is a relatively generic integrated development environment for assembly language development. This single programs supports the HLA, MASM, TASM, NASM, and FASM assemblers. Each of these different assemblers feature different tool sets (executable programs), command line parameters, and ancillary tools. In order to control the execution of these different programs, the RadASM system uses ".INI" files to let you specifically configure RadASM for the assembler(s) you're using. HLA users will probably want to make modifications to two different ".INI" files that RadASM reads: radasm.ini and hla.ini. You'll find these two files in the subdirectory containing the radasm.exe executable file. Both files are plain ASCII text files that you can edit with any regular text editor (including the editor that is built into RadASM).

The RadASM package includes an ".RTF" (Word/Wordpad) documentation file that explains the basic format of these ".INI" files that RadASM uses. Readers interested in making major changes to these ".INI" files, or those attempting to adopt RadASM to a different assembler, will want to read that document. In this chapter, we'll explore the modifications to a basic set of ".INI" files that a typical HLA user might want to make. The assumption is that you're starting with the stock radasm.ini and hla.ini files that come with RadASM and you're wanting to customize them to support the development paradigm that this book proposes.

4.4.1: The RADASM.INI Initialization File

The radasm.ini file specifies all the generic parameters that RadASM uses. In particular, this ".INI" file specifies initial window settings, file histories, OS and language information, and menu entries for certain user modifiable menus. RadASM, itself, actually modifies most of the information in this ".ini" file. However, there are a few entries an HLA user will need to change and a couple of entries an HLA user may want to change. We'll discuss those sections here.

Note: there is a preconfigured radasm.ini file found in the WPA samples subdirectory. This initialization file is compatible with all the sample programs found in this book and is a good starting point should you decide to make your own customizations to RadASM.

The first item of interest in the radasm.ini file is the "[Assembler]" section. This section in the ".INI" file specifies which assemblers RadASM supports and which assembler is the default assembler it will use when creating new projects. By default, the "[Assembler]" section takes the following form:

[Assembler]

Assembler=masm,fasm,tasm,nasm,hla

The first assembler in this list is the default assembler RadASM will use when creating a new project. The standard radasm.ini file is set up to assume that MASM is the default assembler (the first assembler in the list is the default assembler). HLA users will probably want to tell RadASM to use HLA as the default assembler, this is easily achieved by changing the "Assembler=" statement to the following:

[Assembler]

Assembler=hla,masm,fasm,tasm,nasm

Changing the default assembler is the only "necessary" change that you'll need to make. However, there are a few additional changes you'll probably want that will make using RadASM a little nicer. Again, by default, RadASM assumes that you're developing MASM32 programs. Therefore, the help menu contains several entries that bring up help information for MASM32 users. While some of this information is, arguably, of interest to HLA users, a good part of the default help information doesn't apply at all to HLA. Fortunately, RadASM's radasm.ini file lets you specify the entries in RadASM's help menu and where to locate the help files for those menu entries. The "[MenuHelp]" and "[F1-Help]" sections specify where RadASM will look when the user requests help information (by selecting an item from the "Help" menu or by pressing the F1 key, respectively). The default radasm.ini file specifies these two sections as follows:

[MenuHelp]

1=&Win32 Api,0,H,$H\Win32.hlp

2=&X86 Op Codes,0,H,$H\x86eas.hlp

3=&Masm32,0,H,$H\Masm32.hlp

4=$Resource,0,H,$H\Rc.hlp

5=A&gner,0,H,$H\Agner.hlp

[F1-Help]

F1=$H\Win32.hlp

CF1=$H\x86eas.hlp

SH1=$H\Masm32.hlp

CSF1=$H\Rc.hlp

Each numbered line in the "[MenuHelp]" section corresponds to an entry in RadASM's "Help" menu. These entries must be have sequential numbers starting from one and these numbers specify the order of the item in the "Help" menu (the order in the radasm.ini file does not specify the order of the entries in the "Help" menu, you do not have to specify the "[MenuHelp]" entries in numeric order, RadASM will rearrange them according to the numbers you specify). Entry entry in the "[MenuHelp]" section takes the following form:

menu# = Menu Text, accelerator, H, helpfile

where "menu#" is a numeric value (these values must start from one and there can be no gaps in the set), "Menu Text" is the text that RadASM will display in the menu for that particular item, accelerator is a Windows' accelerator key value (generally, this is zero, meaning no accelerator value), "H" is required by RadASM to identify this as a "Help" entry, and "helpfile" is the path to the help file to display (or a program that will bring up a help file).

You may have noticed the ampersand character ("&") in the menu text. The ampersand precedes the character you can press on the keyboard to select a menu item when the menu is opened. For example, pressing "X" when the menu is open (with the "[HelpMenu]" items in this example) selects the "X86 Op Codes" menu entry.

You will note that the paths in the "[MenuHelp]" section all begin with "$H". This is a RadASM shorthand for "the path where RadASM can find all the help files." There is no requirement that you use this shortcut or even place all your help files in the same directory. You could just also specify the path to a particular help file using a fully qualified pathname like c:\hla\doc\Win32.hlp. However, it's often convenient to specify paths using the various shortcuts that RadASM provides. RadASM supplies the shortcuts found in Table 4-1.

Table 4-1: Path Shortcuts for Use in RadASM ".INI" Files
Shortcut
Meaning
$A=
Path to where RadASM is installed
$B=
Where RadASM finds binaries and executables (e.g., c:\hla)
$D=
Where RadASM finds "Addin" modules. Usually $A\AddIns.
$H=
Where RadASM finds "Help" files. Default is $A\Help, but you'll probably want to change this to $B\Doc.
$I=
Where RadASM finds include files. Default is $A\Include, but you'll probably want to change this to $B\include.
$L=
Where RadASM finds library files. Default is $A\Lib but you'll probably want to change this to $B\hlalib.
$R=
Path where RadASM is started (e.g., c:\RadASM).
$P=
Where RadASM finds projects. This is usually $R\Projects.
$S=
Where RadASM find snippets. This is usually $R\Snippets.
$T=
Where RadASM finds templates. This is usually $R\Templates
$M=
Where RadASM finds keyboard macros. This is usually $R\Macro

You can define several of these variables in the hla.ini file. See the next section for details.

As noted earlier, the default help entries are really intended for MASM32 users and do not particularly apply to HLA users. Therefore, it's a good idea to change the "[MenuHelp]" entries to reflect the location of some HLA-related help files. Here are the "[MenuHelp]" entries that might be more appropriate for an HLA installation (assuming, of course, you've placed all these help files in a common directory on your system):

[MenuHelp]

1=&Win32 Api,0,H,$H\Win32.hlp

2=&Resource,0,H,$H\Rc.hlp

3=A&gner,0,H,$H\Agner.hlp

4=&HLA Reference,0,H,$H\PDF\HLARef.pdf

5=HLA Standard &Library,0,H,$H\pdf\HLAStdlib.pdf

6=&Kernel32 API,0,H,$H\pdf\kernelref.pdf

7=&User32 API,0,H,$H\pdf\userRef.pdf

8=&GDI32 API,0,H,$H\pdf\GDIRef.pdf

Here's a suggestion for the F1, Ctrl-F1, Shift-F1, and Ctrl-Shift-F1 help items:

[F1-Help]

F1=$H\Win32.hlp

CF1=$H\PDF\HLARef.pdf

SF1=$H\pdf\HLAStdlib.pdf

CSF1=$H\Rc.hlp

These are probably the extent of the changes you'll want to make to the radasm.ini file for HLA use; there are, however, several other options you can change in this file, please see the radASMini.rtf file that accompanies the RadASM package for more details on the contents of this file.

4.4.2: The HLA.INI Initialization File

The hla.ini file is actually where most of the customization for HLA takes place inside RadASM. This file lets you customize RadASM's operation specifically for HLA2. The hla.ini file appearing in the WPA subdirectory (on the accompanying CD-ROM or in the Webster HLA/Examples download file) contains a set of default values that provide a good starting point for your own customizations.

Note: although hla.ini provides a good starting point for a system, you will probably need to make changes to this file in order for it to work on your specific system. Without these changes, RadASM may not work on your system.

Without question, the first section to look at in the hla.ini file is the section that begins with "[Paths]". This is where you tell RadASM the paths to various directories where it expects to find various files it needs (see Table 4-1 for the meaning of these various path values). A typical "[Paths]" section might look like the following:

[Paths]

$A=C:\Hla

$B=$A

$D=$R\AddIns

$H=$A\Doc

$I=$A\Include

$L=$A\hlalib

$P=$R\Hla\Projects

$S=$R\Hla\Sniplets

$T=$R\Hla\Templates

$M=$R\Hla\Macro

Note that the $A prefix specifies the path where RadASM can find the executables for HLA. In fact, RadASM does not run HLA directly (remember, we're going to have the make program run HLA for us), but the application path ($A) becomes a prefix directory we'll use for defining other directory prefixes. Be sure to check this path in your copy of the hla.ini file and verify that it points at your main HLA subdirectory (usually "C:\HLA" though this may be different if you've installed HLA elsewhere).

The $R prefix specifies the path to the subdirectory containing RadASM. RadASM automatically sets up this prefix, you don't have to explicitly set its value. The remaining subdirectory paths are based off either the $A prefix or the $R prefix.

The "[Project]" section of the hla.ini file is where the fun really begins. This section takes the following form in the default file provided in the WPA subdirectory:

[Project]

Type=Console App,Dialog App,Windows App,DLL

Files=hla,hhf,rc,def

Folders=Bak,Res,Tmp,Doc

MenuMake=Build,Build All,Compile RC,Check Syntax,Run

Group=1

GroupExpand=1

The line beginning with "Type=" specifies the type of projects RadASM supports for HLA. The default configuration supports console applications ("Console App"), dialog applications ("Dialog App"), Windows applications ("Windows App"), and dynamic linked library ("DLL"). The names are arbitrary, though other sections of the hla.ini file will use these names. Whenever you create a new project in HLA, it will create a list of "Project Type" names based on the list of names appearing after "Type=" in the "[Project]" section. Adding a string to this comma-separated list will add a new name to the project types that the RadASM user can select from (note, however, that to actually support these project types requires some extra work later on in the hla.ini file). Figure 4-1 shows what the New Project dialog box in RadASM displays in response to the entries on the "Type=..." line.

Figure 4-1: RadASM Project Types

The line beginning with "Files=" in the "[Project]" section specifies the suffixes for the files that RadASM will associate with this project. The "hla" and "hhf" entries, of course, are the standard file types that HLA uses. The ".rc" file type is for resource compiler files (we'll talk about the resource compiler in a later chapter). If you want to be able to create additional file types and include them in a RadASM project, you would add their suffix here.

The "Folders=..." statement tells RadASM what subdirectories it should allow the user to create when they start a new project. The make file system we're going to use will assume the presence of a "Tmp" directory, hence that option needs to be present in the list. the "bak", "res", and "doc" directories let the user create those subdirectories.

Figure 4-2 shows the dialog box that displays the information found on the "Files=" and "Folders=" lines. By checking the appropriate boxes in the File Creation group, the RadASM user can tell RadASM to create a file with the project's name and the appropriate suffix as part of the project. Similarly, by checking the appropriate boxes in the Folder Creation group, the RadASM user can tell RadASM to create the appropriate directories.

Figure 4-2: File and Folder Creation Dialog Box in RadASM

The "MenuMake=..." line specifies the IDE options that will be available for processing the files in this project. Unlike the other options, you cannot specify an arbitrary list of commands here. RadASM provides five items in the Make menu that you can modify, you don't have the option of adding additional items here (you can disable options if you want fewer, though). Originally, these five slots were intended for the following commands:

  • Compile RC (compile a resource file)
  • Assemble (assemble the currently open file)
  • Link (create an EXE from the object files)
  • Run (execute the EXE file, building it if necessary)
  • Res To Obj (convert a resource file to an object file)

Because these options aren't as applicable to HLA projects as they are to MASM projects (on which the original list was built), the default hla.ini file co-opts these make items for operations that make more sense for the way we're going to be building HLA applications in this book. You can actually turn these make items on or off on a project by project basis (for certain types of projects, certain make objects may not make sense). Figure 4-3 shows the dialog box that RadASM displays and presents this information. Note that in the version used here, RadASM only displays the correct labels for the check boxes in the Make Menu group. The labels on the text entry boxes should also be "Build", "Build All", "Compile RC", "Check Syntax", and "Run" (in that order), but these labels turn out to be hard-coded to the original MASM specifications. Fortunately, you won't normally use these text entry boxes (the default text appearing in them appears in the hla.ini file), so you can ignore the fact that they are mis-labelled here.

Figure 4-3: RadASM Make Menu Selection Dialog Box

For each of the project types you specify on the "Type=..." line in the "[Project]" section, you must add a section to the hla.ini file, using that project type's name, that tells RadASM how to deal with projects of that type. In the hla.ini file we're discussing here, the project types are "Console App", "Dialog App", "Windows App", and "DLL" so we will need to have sections names "[Console App]", "[Dialog App]", "[Windows App]", and "[DLL]". It also turns out that RadASM requires one additional section named "[MakeDefNoProject]" that RadASM uses to process files in the IDE that are not associated with a specific project.

When running RadASM, you can have exactly one project open (or no project at all open, just some arbitrary files) at a time. This project will be one of the types specified on the "Type=..." line in the "[Project]" section. Based on the open project, RadASM may execute a different set of commands for each of the items in the Make menu; the actual commands selected are specified in the project-specific sections of the hla.ini file. Here's what the "[MakeDefNoProject]" section looks like:

[MakeDefNoProject]

MenuMake=1,1,1,1,1

1=0,O,make build,

2=0,O,make buildall,

3=0,O,make compilerc,

4=0,O,make syntax,

5=0,O,make run,

11=0,O,make dbg_build,

12=0,O,make dbg_buildall,

13=0,O,make dbg_compilerc,

14=0,O,make dbg_syntax,

15=0,C,make dbg_run,

The "MenuMake=..." line specifies which items in the RadASM Make menu will be active when a project of this type is active in RadASM (or, in the case of MakeDefNoProject, when no project is loaded). This is a list of boolean values (true=1, false=0) that specify whether the menu items in the Make menu will be active or deactivated. Each of these values correspond to the items on the MenuMake line in the "[Project]" section (in our case, this corresponds to "Build", "Build All", "Compile RC", "Syntax Check", and "Run", in that order). A "1" activates the corresponding menu item, a zero deactivates it. For most HLA project types, we'll generally leave all of these options enabled. The exception is DLL; normally you don't "run" DLLs so we'll disable the run option when building DLL projects.

The remaining lines specify the actions RadASM will take whenever you select one of the items from the Make menu. To understand how these items work, let's first take a look at another section in the hla.ini file, the "[MenuMake]" section:

[MenuMake]

1=&Build,55,M,1

2=Build &All,31,M,2

3=&Compile RC,91,M,3

4=&Syntax,103,M,4

5=-,0,M,

6=&Run,67,M,5

Each item in the "[MenuMake]" section corresponds to a menu entry in the Make menu. The numbers specify the index to the menu entry (e.g., "1=" specifies the first menu item, "2=" specifies the second menu item, etc.). The first item after the "n=" prefix specifies the actual text that will appear in the Make menu. If this text is just the character "-" then RadASM displays a menu separator for that particular entry. As you can see, the default menu entries are "Build", "Build All", "Compile RC", "Syntax", and "Run".

The next item, following the menu item text, is the accelerator value. These are "magic" values that specify keystrokes that do the same job as selecting items from the menu. For example, 55 (in the "Build" item) corresponds to Shift+F5, 31 (in "Build All") corresponds to F5. We'll discuss accelerators in a later chapter. So just ignore (and copy verbatim) these files for right now.

The third item on each line is always the letter "M". This tells RadASM that this is a make menu item.

The fourth entry on each line is probably the most important. This is the command to execute when someone selects this particular menu item. This is either some text containing the command line to execute or a numeric index into the current project type. As you can see in this example, each of the commands use an index value (one through five in this example). These numbers correspond to the lines in each of the project sections. For example, if you select the "Build" option from the Make menu, RadASM notes that it is to execute command #1. It goes to the current project type section and locates the line that begins with "1=..." and executes that operation, e.g.,

1=0,O,make build,

In a similar vein, selecting "Build All" from the Make menu instructs RadASM to execute the command that begins with "2=..." in the current project type's section (i.e., "2=0,O,make buildall,"). And so on.

The lines in the project type section are divided into two groups, those that begin with 1, 2, 3, 4, or 5 and those that begin with 11, 12, 13, 14, or 15. The "[MenuMake]" command index selects one of the commands from these two groups based on whether RadASM is producing a "release build" or a "debug build". Release builds always execute the command specified by the "[MenuMake]" command index (i.e. 1-5). If you're building a debug version, then RadASM executes the commands in the range 11-15 in response to command indexes 1-5. We'll ignore debug builds for the time being (we'll discuss them in a later chapter on debugging). So for right now, we'll always assume that we're building a release image.

The fields of each of the indexed commands in the project type section have the following meanings:

index = delete_option, output_option, command, files

The delete_option item specifies which files to delete before doing the build. If this entry is zero, then RadASM will not delete any files before the build. Because we're having a make file do the actual build for us, and it can take care of cleaning up any files that need to be deleted first, we'll always put a zero here when using RadASM with HLA.

The output_option item is either "C", "O" (that's an "oh" not a "zero"), or zero. This specifies whether the output of the command will go to a Windows console window ("C"), the RadASM output window ("O", which is "oh"), or the output will simply be thrown away (zero). We'll usually want the output sent to RadASM's output window, so most of the time you'll see the letter "O" ("oh") here.

The command entry is the command line text that RadASM will pass on to windows whenever you execute this command. This can be any valid command prompt operation. For our purposes, we'll always use a make command with a single parameter to specify the type of make operation to perform. Here are the commands we're going to support in RadASM:

  • Build - "make build"
  • Build All - "make buildall"
  • Compile RC - "make compilerc"
  • Syntax - "make syntax"
  • Run - "make run"

Now it's up to the makefile to handle each of these various commands properly (using the standard makefile scheme we defined in the first chapter).

This may seem like a considerable amount of indirection -- why not just place the commands directly in the "[MenuMake]" section? However, this scheme is quite flexible and makes it easy to adjust the options on a project type by project type basis (in fact, it's even possible to set these options on a project by project basis).

With this discussion out of the way, it's time to look at the various project type sections. Without further ado, here they are:

[Console App]

Files=1,1,1,1,0,0

Folders=1,0,1,0

MenuMake=1,1,1,1,1,0,0,0

1=0,O,make build,

2=0,O,make buildall,

3=0,O,make compilerc,

4=0,O,make syntax,

5=0,C,make run,

11=0,O,make build,

12=0,O,make buildall,

13=0,O,make compilerc,

14=0,O,make syntax,

15=0,C,make run,

Console applications, by default, want to create an .HLA file and a .HHF file, a BAK folder and a TMP folder. All menu items are active for building and running console apps (that is, there are five ones after "MenuMake"). Finally, the commands ("1=..." "2=...", etc.) are all the standard build commands.

[Dialog App]

Files=1,1,1,0,0

Folders=1,1,1

MenuMake=1,1,1,1,1,0,0,0

1=0,O,make build,

2=0,O,make buildall,

3=0,O,make compilerc,

4=0,O,make syntax,

5=0,C,make run,

11=0,O,make build,

12=0,O,make buildall,

13=0,O,make compilerc,

14=0,O,make syntax,

15=0,C,make run,

By default, dialog applications will create HLA, HHF, RC, and DEF files and they will create a BAK and a TMP subdirectory. All five menu items will be active and dialog apps use the standard command set.

[Windows App]

Files=1,1,1,1,0

Folders=1,1,1,1

MenuMake=1,1,1,1,1,0,0,0

1=0,O,make build,

2=0,O,make buildall,

3=0,O,make compilerc,

4=0,O,make syntax,

5=0,C,make run,

11=0,O,make build,

12=0,O,make buildall,

13=0,O,make compilerc,

14=0,O,make syntax,

15=0,C,make run,

By default, window applications will create HLA, HHF, RC, and DEF files and they will create a BAK, RES, DOC, and a TMP subdirectory. All five menu items will be active and dialog apps use the standard command set.

The hla.ini file allows you to control several other features in RadASM. The options we've discussed in this chapter are the crucial ones you must set up, most of the remaining options are of an aesthetic or non-crucial nature, so we won't bother discussing them here. Please see the RadASM documentation (the RTF file mentioned earlier) for details on these other options.

Once you've made the appropriate changes to the hla.ini file (and, of course, you've made a backup of your original file, right?), then you can copy the file to the RadASM subdirectory and replace the existing hla.ini file with your new one. After doing this, RadASM should operate with the new options when you run RadASM..

4.4.3: Additional Support for RadASM on the CD-ROM

In addition to the hla.ini and radasm.ini files, there are several additional files of interest to RadASM users provided on the accompanying CD-ROM (or in the WPA\RadASM subdirectory of the "Examples" module download on the HLA download page at http://webster.cs.ucr.edu). This section will describe these additional files.

To install most of the auxiliary files appearing in the WPA\RadASM directory all you have to do is copy them to the corresponding folders in your RadASM folders. For example, you'd normally just copy the hla.ini and radasm.ini files directly from the WPA\RadASM folder to your RadASM folder (or whatever folder contains the RadASM installation). In the WPA\RadASM folder you'll find a help folder. This folder contains several additional help files (to which the new radasm.ini file refers), so you'll want to copy these files into your RadASM's help folder3. The WPA\RadASM folder contains several API files and two subdirectories (Sniplets and Templates). You should copy the API files to your RadASM\HLA folder (after verifying that the api files are newer), you should copy the files in the Templates directory to the RadASM\HLA\Templates directory, and you should copy the files from the Sniplets\Code directory into your RadASM\HLA\Sniplets\Code subdirectory.

The Templates folder contains project templates you might find useful (for example, there is a template for creating a generic Win32 assembly application that is a great tool for starting new projects). The Sniplets\Code folder contains a generic makefile and a generic win32 application whose code you can cut and paste into a file to create customized versions of these files.

Note that because RadASM is under continuous development, it's quite possible that a version of RadASM you're downloading contains newer versions of the help, template, or sniplet files. Therefore, it's a good idea to make backups of the old files first, rather than simply replacing them with the files from the WPA\RadASM subdirectory. That way, should it turn out that RadASM ships with a new file, you can easily recover without downloading the RadASM package again. For the most part, however, you'll find that the files appearing in the WPA\RadASM subdirectory are new files that aren't part of the standard RadASM distribution.

4.5: Running RadASM

Like most Windows applications, you can run RadASM by double-clicking on its icon or by double clicking on a "RadASM Project" file (".rap" suffix). Simply double-clicking on the RadASM icon brings up a window similar to the one appearing in Figure 4-4.

Figure 4-4: RadASM Opening Screen

The main portion of the RadASM window is broken down into three panes. The larger of the three panes is where text editing takes place. The upper right hand pane is the "project management" window. The pane in the lower right hand corner lists the properties of the currently opened project.

4.5.1: The RadASM Project Management Window

The project management window initially lists the project folders you've created; you can select an existing project by double-clicking on the project's folder in this window. For example, RadASM ships with two sample projects, Dialog (that creates a small dialog box application) and hw (that creates a small "Hello World" console application). Assuming you're running RadASM prior to creating any new projects beyond these two default projects, the Project pane will look something like Figure 4-5.

Figure 4-5: Default RadASM Project Pane

Double-clicking on the hw folder opens the folder containing that project. This changes the pane to look something like that appearing in Figure 4-6.

Figure 4-6: RadASM Project Pane With hw Folder Opened

By default, RadASM does not show all the files present in the folder you've opened. Instead, RadASM filters out files that don't have a certain file suffix. By default, RadASM only displays files with the following suffixes:

  • .asm
  • .inc
  • .rc
  • .txt
  • .doc
  • .rtf
  • .dlg
  • .mnu
  • .rap
  • .bmp
  • .ico
  • .cur
  • .hla
  • .hhf

This list is actually designed to generically handle all file types for every assembler that RadASM works with. HLA users might actually want to drop ".asm" and ".inc" from this list as files with these suffixes are temporary files that HLA produces (much like ".obj" files, which don't normally appear in this list). You can change the filter suffixes in one of two places. The first place is in the radasm.ini file. Search for the "[FileBrowser]" section and edit the line that begins with "Filter=...". You can delete or add suffixes to your heart's content on this line. The second way to change the default filters, arguably the easiest way, is within RadASM itself. From the application's menu, select "Option>File Browser" (that is, select the "File Browser" menu item from the "Option" menu). This brings up the dialog box appearing in Figure 4-7. The text edit box at the bottom of this dialog window (labelled "Filter:") lets you edit the suffixes that RadASM uses for filtering files in the Project window pane.

Figure 4-7: RadASM File Browser Options Dialog Box

By default, RadASM only displays those files whose file suffixes appears in the filter list. If, for some reason, you need to see all files that appear in a project subdirectory, you can turn the file filtering off. There is a toolbar button at the top of the Project window pane that lets you activate or deactivate file filtering. Clicking on this button toggles the display mode. So clicking on this button once will deactivate file filtering, to display all the files in the directory, clicking on this button a second time reactivates file filtering. Figure 4-8 shows the effects of clicking on this button.

Figure 4-8: File Filtering in RadASM's Project Pane

If you've descended into a subdirectory by double-clicking on it's folder icon and you decide to return to an upper level directory, you can move to that upper level directory by clicking on the "Up One Level" button in the RadASM Project pane (see Figure 4-9).

Figure 4-9: RadASM "Up One Level" Directory Navigation Button

The left and right arrow buttons allow you to quickly scan through several different directories in the system (see Figure 4-10). By default, RadASM displays a couple of interesting (HLA-related) subdirectories in the Project pane when you scan through the list using the left and right arrows in the Project pane. In general, however, you'll want to customize the directories RadASM visits when you press these two arrow buttons. You can add (or change) directory paths in the "[FileBrowser]" section of the radasm.ini file, though it's probably easier to select the "Option>File Browser" menu item to open up the File Browser Option dialog box and make your changes there (see Figure 4-7). The "Folders:" list in the File Browser Option dialog box lists all the directories that RadASM will rotate through when you press the left and right buttons in the Project window pane. You can add, delete, edit, and rearrange the items in this list.

Figure 4-10: Project Folder Selection Arrows

To edit an existing entry, click on that entry with the mouse and then edit the directory path appearing in the text edit box immediately below the "Folders:" list (see Figure 4-7). You may either type in the path directly, or browse for the path by pressing the "browse" button immediately to the right of the text entry box (see Figure 4-11).

Figure 4-11: The RadASM File Browser Option "Browse" Button

To delete an entry from the File Browser Option list, select that item with the mouse and then press the "Delete" button appearing in the File Browser Option Window. To add a new entry to the list, press the "Add" button and then type the path into the text edit box (or use the browse button to locate the subdirectory you want to add). Note: do not type the new entry in and then press "Add". This sequence will change the currently selected item and then add a new, blank, entry. The correct sequence is to first press the "Add" button, and then edit the blank entry that RadASM creates.

The remaining buttons in the Project window are only applicable to open projects. Note that opening a project folder is not the same thing as opening a RadASM project. To open a RadASM project you must either create a new project or open an existing ".rap" file. For example, you can open the "Hello World" project in the hw directory by double-clicking on the hw.rap file that appears in the project window. Opening the hw.rap file does two things to the RadASM windows: first, it displays the hw.hla source file in the editor window and, second, it switches the Project window pane from "File Browser mode" to "Project Browser mode." In project browser mode RadASM displays only the files you've explicitly added to the project. Any incidental or generated files will not appear here (unless you explicitly add them). For example, whereas the "File Browser" mode displays several ".inc" and ".asm" files (assuming you've not removed these suffixes from the file filter), the "Project Browser mode" only displays the hw.hla file because this is the only file that was originally added to the project. Another difference between the file browser and project browser modes is the fact that RadASM displays the files in "pseudo-directories" according to the file's type. For example, it displays the hw.hla file under the sub-heading "Assembly" (see Figure 4-12). The hw.rap project is a relatively simple project, only having a single assembly file. The Dialog.rap project (that appears in the "Dialog" project folder) is a slightly more complex application, having a couple of resource files in addition to an assembly file (see Figure 4-13). Note that you can "flatten" RadASM's view of these files by pressing the "Project Groups" button in the Project window pane (see Figure 4-14). Pressing this button a second time restores the project groups display.

Figure 4-12: Project Window "Project Browser Mode"

Figure 4-13: Dialog.rap Project Browser Display

Figure 4-14: Effect of Pressing the "Project Groups" Button

When you've got a project loaded, RadASM displays the project view by default. By pressing the "File Browser" and "Project Browser" buttons in the Project window pane, you can switch between these two views of your files (see Figure 4-15).

Figure 4-15: The Project Browser and File Browser Buttons

4.5.2: Creating a New Project in RadASM

While the two default projects that RadASM supplies are useful for demonstrating the RadASM Project window pane, you're probably far more interested in creating your own RadASM/HLA projects. Creating your own project is a relatively straight-forward process using RadASM's project creation wizard. To begin this process, select the "File>New Project" menu item. This opens the project wizard dialog box (see Figure 4-16).

Figure 4-16: RadASM Project Wizard Dialog Box

The "Assembler" pop-up menu list lets you select the assembler that you want to use for this project. Remember, RadASM supports a variety of different assemblers and the "rules" are different for each one. Because you're probably using HLA (if you're reading this book), you'll want to select the HLA assembler from this list. HLA should be the default assembler in this list assuming you've placed "hla" at the front of the list in the "[Assembler]" section of the radasm.ini file. If you're not using the radasm.ini file supplied on the CD-ROM accompanying this book (or in the WPA\RadASM subdirectory in the HLA Examples download package on Webster), then you should make sure that HLA appears first in this list in the radasm.ini file.

The "Project Type" group is a set of radio buttons that let you select the type of project you're creating. RadASM populates this list of radio buttons from the "[Project]" section of the hla.ini file. The "Type=..." statement in this section specifies the valid projects that RadASM will create. RadASM creates the radio button items in the order the project type names appear in the "Type=..." list; the first item in the list is the one that will have the default selection. If you're going to be developing Windows' GUI applications most of the time, you'll probably want to change this list so that "Windows App" appears first in the list. This will slightly streamline the use of the Project Wizard because you won't have to explicitly select "Windows App" every time you create a new Windows application.

The "Project Name:" text entry box is where you specify the name of the project you're creating. RadASM will create a folder by this name and any other default files it creates (within the project folder) will also have this name as their filename prefix. The text you enter at this point must be a valid Windows filename. Note that this should be a simple file name (directory name), not a path. You'll supply the path to this file/directory in a moment.

The "Project Description:" text entry box allows you to place a descriptive comment that describes the project. This is any arbitrary text you choose. It should be a brief (one-line) description of the project.

The "Projects Folder:" text entry box is where you select the path to the spot in the file system where RadASM will create the project folder. You can type the path in directly, or you can press the browse button to the right of this text entry box and use a Windows' dialog box to select the subdirectory that will hold the project's folder.

The "Template:" text entry box and browse button lets you select a template for your project. If you don't select a template, then RadASM will create an empty project for you (i.e., the main ".hla" file will be empty). If you select one of the templates (e.g., the ".tpl" files found in the RadASM\Hla\Templates directory) then RadASM will create a "skeletal" project based on the project template you've chosen. For example, the WPA\RadASM directory on the accompanying CD-ROM contains a win32app.tpl file that you can select. This automatically supplies a skeletal Win32 application from which you can build your Windows programs.

Once you've selected the assembler type, project type, entered the project name and description, and optionally selected the folder and a template, press the "Next>" button to move on to the next window of the Project Wizard dialog. This dialog box appears in Figure 4-17. In this dialog box you select the initial set of files and folders that RadASM will create in the project's folder for you. At the very least, you're going to want a ".hla" file and a "Tmp" subdirectory. It's probably a good idea to create a "BAK" subdirectory as well (RadASM will maintain backup files in that subdirectory, if it is present). More complex Windows applications will probably need a header file (".HHF") and a resource file (".RC") as well. If you're creating a dynamically linked library (DLL), you'll probably want a definition file (".DEF") as well. If you plan on writing documentation, you might want to create a DOC subdirectory - the choice is yours. Check the files and folders you want to create and press the "Next >" button in the dialog box.

Figure 4-17: Project Wizard Dialog Box #2

The last dialog box of the Project Wizard lets you specify the options present in the Make menu and the commands each of these options executes (see Figure 4-18). RadASM initializes each of the items appearing in this dialog box from the values appearing in the hla.ini file. For the most part, you shouldn't have to change any of these options. RadASM properly initializes each item based upon the project type you've chosen (Windows App, Console App, Dialog App, DLL, etc.). However, it is quite possible to customize how RadASM works on a project by project basis. This dialog box is where you do that customization. Because we'll be using the makefile scheme to build executables, most of the customization you'll do will actually take place in the makefile, not in the RadASM Project Wizard Dialog. Therefore, most of the time you'll just press the "Finish >" button to conclude the construction your new RadASM/HLA project. Figure 4-19 shows what the RadASM window looks like after create a sample "Windows App" application based on the win32app template found on the CD-ROM (this project was given the name "MyApp").

Figure 4-18: Project Wizard Dialog Box #3

Figure 4-19: Typical RadASM Window After Project Creation

4.5.3: Working With RadASM Projects

Of course, once you've created a RadASM project, you can open up that project and continue work on it at some later point. RadASM saves all the project information in a ".rap" (RadAsm Project) file. This ".rap" file keeps track of all the files associated with the project, project-specific options, and so on. These project files are actually text files, you can load them into a text editor (e.g., RadASM's editor) if you want to see their format. As a general rule, however, you should not modify this file directly. Instead, let RadASM take care of this file's maintenance.

There are several ways to open an existing RadASM project file - you can double-click on the file's icon within Windows and RadASM will begin running and automatically load that project. Another way to open a RadASM project is to select the "File>Open Project" menu item and open some ".rap" file via this open command. A third way to open a RadASM project is to use the File Browser to find a ".rap" file in one of your project directories and double-click on the project file's icon (the ".rap" file) that appears in the project browser. Any one of these schemes will open the project file you've specified.

RadASM only allows one open project at a time. If you have a currently open project and you open a second project, RadASM will first close the original project. You can also explicitly close a project, without concurrently opening another project, by selecting the "File>Close Project" menu item.

Once you've opened a RadASM project, RadASM's "Project" menu becomes a lot more interesting. When you create a project, RadASM gives you the option of adding certain "stock" files to the project (either empty files, or files with data if you select a template when creating the project). All of the files that RadASM creates bear the project's name (with differing suffixes). As a result, you can only create one ".hla" file (and likewise, only one ".hhf" file, only one ".rc" file, etc.). For smaller assembly projects, this is all you'll probably need. However, as you begin writing more complex applications, you'll probably want additional assembly source files (".hla" files), additional header files (".hhf"), and so on. RadASM's Project menu is where you'll handle these tasks (and many others). Figure 4-20 shows the entries that are present in the Project menu.

Figure 4-20: The RadASM Project Menu

To add new, empty, files to a RadASM project, you use the "Project > Add New" menu item. This opens up a new submenu that lets you select an assembly file (".hla" file), an include file (".hhf"), a resource compiler file (".rc"), a text file, and so on. Selecting one of these submenu items opens up an "Add New File" dialog box that lets you specify the filename for the file. Enter the filename and RadASM will create an empty text file with the name you've specified. Later on, you can edit this source file with RadASM and add whatever text is necessary to that file. Note that RadASM will automatically add that file to the appropriate group based on the file's type (i.e., it's suffix).

The "Project > Add Existing" sub-menu lets you add a pre-existing file to a project. This is a useful option for creating a RadASM project out of an existing HLA (non-RadASM) project or adding files from some other project (e.g., library routines) into the current project. Note that this option does not create a copy of the files you specify, it simply notes (in the ".rad" file) that the current project includes that file. To avoid problems, you should make a copy of the actual source file to the current project's folder before adding it to the project; then add the version you've just copied to your project. It's generally unwise to add the same source file to several different projects. If you change that source file in one project, the changes are automatically reflected in every other project that links this file in. Sometimes this is desirable, but most of the time programmers expect changes to a source file to be localized to the current project. That's why it's always best to make a copy of a source file when adding that file to a new project. In those cases where you do want the changes reflect to every application that includes the file, it's better to build a library module project and link the resulting ".lib" file with your project rather than recompile the source file in.

The "Project > Project options" menu item opens up a "Project Options" dialog box that lets you modify certain project options (see Figure 4-21). This dialog box lets you change certain options that were set up when you first created the project using the "File > New Project" Project Wizard dialogs. Most of the items in this dialog box should have been described earlier, but a few of the items do need a short explanation.

Figure 4-21: "Project > Project Options" Dialog Box

The Project Options dialog box provides two radio buttons that let you select whether RadASM will do a "debug build" or a "release build." As you may recall from earlier in this chapter, the choice of release versus debug build controls which set of commands RadASM executes when you select an item from the Make menu. We'll discuss debug builds in a later chapter. Until then, be sure that the "Release" radio button is selected.

Most of the other options in the Project menu we'll get around to discussing in later chapters as we discuss the Windows features to which they pertain.

4.5.4: Editing HLA Source Files Within RadASM

The RadASM text editor is quite similar to most Windows based text editors you've used in the past (i.e., RadASM generally adheres to the Microsoft Common User Access (CUA) conventions. So the cursor keys, the mouse, and various control-key combinations (e.g., ctrl-Z, ctrl-X, and ctrl-C) behave exactly as you would expect in a Windows application. Because this is an advanced programming book, this chapter will assume that you've used a CUA-compliant editor (e.g., Visual Studio) in the past and we'll not waste time discussing mundane things like how to select text, cutting and pasting, and other stuff like that. Instead, this section will concentrate on the novel features you'll find in the RadASM editor.

Of course, the first file navigation aid to consider is the Project Browser pane. We've already discussed this RadASM feature in earlier sections of this chapter, but it's worth repeating that the Project Browser pane lets you quickly switch between the files you're editing in a RadASM project. Just double-click on the icon of the file you want to edit and that file will appear in the RadASM editor window pane.

Immediately below the Project Browser pane is the "Properties" pane (if this pane is not present, you can bring it up by selecting "View > Properties" from the RadASM View menu). This pane contains two main components: a pull down-down menu item that lets you select the information that RadASM displays in the lower half of this window. If not already selected, you should select the ".code" item from this list. The ".code" item tells RadASM to list all the sections of code that it recognizes as procedures (or the main program) in an HLA source file (see Figure 4-22).

Figure 4-22: The HLA Properties Window Pane

One very useful RadASM feature is that you can quickly jump to the start of a procedure's body (at the begin statement) by simply double-clicking on that procedure's name in the Properties Window pane. In the example appearing in Figure 4-22 (this is the "Dialog" project supplied with RadASM for HLA), double-clicking on the "Dialog;" and "DialogProc;" lines in this list box automatically navigates to the start of the code for the selected procedure.

The pull-down menu in the Properties window lets you select the type of objects the assembler provides. For example, by selecting ".const" you can take a look at constant declarations in HLA. The "macro" selection lets you view the macro definitions that appear in the source file. As this chapter was first being written, the other property items weren't 100% functional; hopefully by the time you read this RadASM will have additional support for other types of HLA declarations.

Another neat feature that RadASM provides is an "outline" view of the source file. Looking back at Figure 4-22 you'll notice that "begin DialogProc;" statement has a rectangle with a minus sign in it just to the left of the source code line. Clicking on this box closes up all the code between the begin and the corresponding end in the source file. Figure 4-23 shows what the source file looks like when the Dialog and DialogProc procedures are collapsed in outline mode. The neat thing about outline mode is that it lets you view the "big picture" without out the mind-numbing details of the source code for each procedure in the program. In outline view, you can quickly skim through the source file looking for important code and "drill down" to a greater level of detail by opening up the code for a procedure you're interested in looking at. You can also rapidly collapse or expand all procedure levels by pressing the "expand" or "collapse" buttons appearing on the lower left hand corner of the text editor window (see Figure 4-23).

Figure 4-23: RadASM Outline View (with Collapsed Procedures)

Another useful feature RadASM provides is the ability to display line numbers with each line of source code. Pressing on the line number icon in the lower-left hand corner of the text editor window (the icon with the "123" in it) toggles the display of line numbers in the editor's window. See Figure 4-24 to see what the source file looks like with line numbers displayed. The line number display mode is quite useful when searching for a line containing a syntax error (as reported by HLA). Note that you can also navigate to a given line number by pressing ctrl-G and entering the line number (you can also select "Edit > Goto line" from the "Edit" menu).

Figure 4-24: Displaying Line Numbers in RadASM's Editor

Another useful navigation feature in RadASM is support for bookmarks. A bookmark is just a point in the source file that you can mark. You can create a bookmark by selecting a line of text (by clicking the mouse on the gray bar next to the line) and selecting "Edit > Toggle BookMark" or by pressing shift-F8. You can navigate between the bookmarks by pressing F8 or ctrl-F8 (these move to the next or previous bookmarks in the source file). RadASM (by default) provides several icons on it's toolbar to toggle bookmarks, navigate to the previous or next bookmark, or clear all the bookmarks. Which method (edit menu, function keys, or toolbar) is most convenient simply depends on where your hands and the mouse cursor currently sits.

The RadASM "Format" menu also provides some useful features for editing HLA programs. The "Format > Indent" and "Format > Outdent" items (also accessible by pressing F9 and ctrl-F9) move a selected block of text in or out four spaces (so you can indent text between an if and endif, for example). You can also convert tabs in a document to spaces (or vice versa) from the "Format > Convert > Spaces To Tab" and "Format > Convert > Tab To Spaces" menu selections.

You'll notice that RadASM provides syntax coloring in the editor window (that is, it sets the text color for various classes of reserved words and symbols to different colors, making them easy to identify with a quick glance in the editor window). The hla.ini file appearing on the CD-ROM accompanying this book contains a set of reasonable color definitions for HLA's different reserved word types. However, if you don't particularly agree with this color scheme, it's really easy to change the colors that RadASM uses for syntax highlighting. Just select the "Options > Colors & Keywords" menu item and select an item from the Syntax/Group list box (Figure 4-25 shows what this dialog box looks like with the Group #00 item selected). By double-clicking on an item within the Group list box, you can change the color for all the items in that particular group (e.g., see Figure 4-26). RadASM automatically updates the hla.ini file to remember your choice of colors the next time you run RadASM.

Figure 4-25: Option>Colors & Keywords Dialog Box with Group#00 Selected

Figure 4-26: Color Selection Dialog Box

You can also set the display fonts to something you're happier with if the default font (Courier New, typically) isn't to your liking. This is also achievable from the RadASM "Option" menu.

4.6: Creating and Compiling HLA Projects With RadASM

There are two ways to create a new RadASM/HLA project - from scratch or via a template. Creating a new project from scratch is great when you've got some HLA (or other) files that you want to incorporate into a new project; using a template is a great solution when you want to begin a brand-new project in RadASM. We'll explore both of these approaches here.

To create a new project, either from scratch or from a template, you select the "File > New Project" menu item. This brings up the Project Wizard dialog box (see Figure 4-16). By default, assuming you've set up your radasm.ini file properly, the HLA assembler should be selected. If not, select that assembler from the "Assembler" pull down menu (and, by all means, fix your radasm.ini file and make HLA the default assembler, as described earlier in this chapter).

Next, select the type of project you'll want to create. For the most part, we'll want to create "Windows Applications" so you'd normally select the "Windows App" radio button in the "Project Type" group box. However, because we've not discussed how to write a Windows application yet, you should select "Console App" for the time being.

The next step is to give this project a name by typing a valid file name into the "Project Name:" text edit box. This must be a valid Windows file name without a path prefix and without any suffix (RadASM will supply appropriate suffixes). In the examples that follow, this chapter will use the name "myconsoleProject" for the project name. If you supply a different name, RadASM will use that name in place of "myconsoleProject" but everything will work as you expect. For the sake of example, you should use "myconsoleProject" for this exercise and then repeat this process with a different base filename later, just to see how RadASM works.

After entering the project's name, type an English (or other natural language) description of the project into the "Project Description:" text edit box. RadASM will display this description in the Project pane when you open the project.

By default, RadASM assumes that you want to create a new project directory in the ....\RadASM\Hla\Projects subdirectory. If this is fine, you can ignore the "Projects Folder:" box. If you want to place the project folder somewhere else, then click on the "browse" button to the right of the "Projects Folder:" text entry box and select the folder where you want RadASM to put your project directory (you can also type the path in if you prefer). Note that you do not include the project's directory name in this path; RadASM will create the project folder in the subdirectory you specify.

The last entry in the (first) Project Wizard dialog box is the "Template:" entry. You'll specify a template name here if you want to create a new project based on an existing template. Because we're starting from scratch at this point, we'll go ahead and select a template. To do this, press the "Browse" button that appears just to the right of the "Template:" text entry box. This opens up the Template Selection Dialog box (see Figure 4-27). For the time being, select the "consApp.tpl" template (which is a template for a Windows console application). Note that the "consApp.tpl" template is only available if you've copied the template files from the WPA\RadASM\Templates subdirectory to your RadASM\Hla\Templates subdirectory. The "consApp.tpl" template file is not a standard part of the RadASM distribution. Once you've selected the "consApp.tpl" template, the Project Wizard dialog box should look something like that appearing in

Figure 4-27: Template Selection Dialog Box

Figure 4-28: Project Wizard Dialog Box - consApp.tpl Template

At this point, press the "Next" button to continue with the project creation. This brings up the second Project Wizard dialog box (see Figure 4-17). We only need an "HLA" file and "BAK" and "TMP" subdirectories, so make sure these checkboxes contain checks and all the other checkboxes are empty. Once you've done this, press the "Next" button to continue.

At this point you should be looking at the third Project Wizard dialog box (see Figure 4-18). Because we're not working with any ".rc" files in this application, you can uncheck the check box next to "Compile RC". All the other checkboxes should be checked and the text entry boxes should all be filled in except for "Asm Module:". At this point, press the "Finish" button to complete the construction of the project. Figure 4-29 shows you what the RadASM project pane looks like after you've completed the construction of this project.

Figure 4-29: RadASM Project Pane After Creating a Console Project From a Template

The first thing you've got to do is edit the makefile that the template has created and save the result as "makefile". Note (in Figure 4-29) that the template created a file called "myconsoleProject.mak". Because make really prefers you to name this file "makefile", plus the fact that the generic makefile needs some editing, our first step is to edit this file. So double-click on the myconsoleProject.mak file icon to bring this file up into the editor.

Throughout the makefile, you'll see `filenames' of the form "<<file>>". You need to replace these stubs with the name of your project ("myconsoleApp" in this particular case). So press ctrl-R to enter search and replace mode and enter "<<file>>" for the "Find What:" string and "myconsoleApp" for the "replace:" string. Hit the "Replace All" button to quickly replace each occurrence of "<<file>>" by "myconsoleApp" throughout the makefile.

The next step is to save the file. Select "File > Save" to save the changes over the top of myconsoleApp.mak. Note that we also need to save this as makefile in the current project directory in order for RadASM's make menu to work properly. Select the "File > Save File As" menu item and browse the myconsoleApp project directory (unless you've specified a different path, this will be in the .....\RadASM\Hla\Projects folder). Type "makefile." into the "File Name:" text edit box. Note the period at the end of "makefile."! This is important. Without it, RadASM will automatically tack a ".hla" suffix to the end of the filename, which is not what we want. Also, be sure to select "All Files" from the "Save As Type:" pull-down menu. Again, unless you select this, RadASM will add a suffix to the end of the filename, which is not something we want to happen.

To clean things up a little bit, let's remove myconsoleApp.mak from the project and add makefile to the project (to avoid confusion). To remove myconsoleApp.mak from the project, right-click on the myconsoleApp.mak icon in the Project window pane and select "Remove From Project" in the pop-up menu that appears. To add makefile to the project, select the "Project > Add Existing > File" menu item. This brings up the Add Existing File dialog box (seeFigure 4-30). Note that makefile doesn't appear in this list. This is because RadASM is applying file filtering here and is only displaying ".hla" and ".hhf" files. To see the make file, select "All Files" from the "Files of type:" pull down menu. Once you've done this (and assuming you've saved makefile into the project's directory), you should be able to select makefile and then press the "Open" button to add it to the project. RadASM adds the makefile to the Misc file group. Don't get the impression that it's unnecessary to add the makefile to your project. Through RadASM will find the makefile just fine when building the application (assuming the makefile is in your project directory), as your projects become more complex you'll need to edit the makefile whenever you add new files to the project. Therefore, it's wise to make the makefile a part of the RadASM project so that it's easy to edit.

Figure 4-30: Add Existing File Dialog Box

Once you've edited the makefile and added it to the project, you should be able to build the application by selecting the RadASM "Make > Build" menu item. This should produce output in the RadASM output window like that appearing in Figure 4-31. Of course, the template we've copied does produce a useful program (it's just an empty HLA program that simply returns to Windows). To make this program do something useful, click on the myconsoleApp.hla icon in the Project window pane and edit the file. Add the following two lines to this program:

    stdout.put( "A Console App" nl );

    stdin.readLn();

Save the file (by selecting the "File > Save" menu item or by pressing the little disk icon on the toolbar). Then build the application by selecting the "Make > Build All" menu item. Finally, run the application by selecting the "Make > Run" menu item. When the application runs, press the Enter key to close the window that opens up. Congratulations! You've just created, edited, and run your first application from within RadASM.

Figure 4-31: Building myconsoleApp

Creating a new project from scratch follows this same process, except you don't select a template file in the first dialog box of the Project Wizard. Rather than create HLA and make files that contain generic information, this process will create empty files (based on your selections in the second Project Wizard dialog box, see Figure 4-17). You'll edit these empty files to create the programs you want. If you want to add new files to the project, you can do so by selecting "Project > Add New > File" and specifying the filename. For example, creating a new project without a template does not create a makefile for you, so it's probably a good idea to create a makefile right away for this project. Rather than type in the entire makefile from scratch, you can take advantage of another neat RadASM feature - sniplets. A sniplet is simply a small text file containing a short sequence of code (or other data) that you often use. You can open up a sniplet and cut and paste it into a file in your current project. For example, if your makefile is currently open for editing, Select the "Tools > Sniplets" menu item. In the left-most pane of the window that opens up, you see a selection of files you can copy data from. Select the "console-makefile" item and this will display the contents of that sniplet file in the right-most pane of the sniplet window (see Figure 4-32). Press the "Select All" button and then the "clipboard" button to copy all the selected text to the clipboard and then press the "Close" button to close the Sniplets Browser window. Now select "Edit > Paste" to paste this text into your makefile (which, presumably, is the open file in the RadASM editor window). Of course, this is a generic console makefile, requiring the same editing as before, so make those changes and then save your makefile to disk.

Figure 4-32: The Sniplet Browser Window

4.7: Developing Small Projects with RadASM

RadASM, by itself, supports the ability to drive HLA. If you select the "Project > Project Options" menu item and edit the make commands (or edit these corresponding commands in the hla.ini file), you can tell RadASM to call HLA directly to assemble your files. The only problem with this approach is that RadASM will assemble all files rather than just the ones that require changing. For small HLA-based applications, recompiling all the source files on each build is no big deal. But as the project gets larger and more complex, the compile times will start to become annoying if you recompile the entire application on every build. As you've seen, by using a make file, you can recompile only those modules that need to be recompiled. This is one of the primary reasons for running make from RadASM rather than running HLA directly. Another advantage of having the make files is that it's easy to recompile from the command line (e.g., by typing "make build") and produce the same results you get inside RadASM.

However, if you're developing small Windows applications, you may find the use of make files a bit too complex for your tastes. In particular, RadASM can automatically maintain projects for you so that you don't have to edit a make file every time you want to add or remove a file from the project. This can solve some consistency problems you might run into if you're not careful about properly maintaining your make files. This book assumes the use of, and always provides, make files. However, for your own projects it's quite possible to edit the hla.ini file to run HLA directly rather than run make to run HLA. This book will not spend any more time describing how to do this (though this chapter does provide the basic information you'll need); please see the RadASM documentation for details on this technique.

4.8: Plus More!

This chapter barely begins to scratch the surface with respect to the features that RadASM supports. Some things (like Rapid Application Development features) will have to wait for a later chapter when we've covered some Windows programming. Other features are simply to numerous to describe here. Your best bet is to sit down with RadASM and play around with it. Note that some features are not applicable to HLA users, so don't be disappointed if some feature doesn't work for you. If you've got questions about features in RadASM or you're unsure how to do something, you should

1The WPA subdirectory is found in the "Examples" module of the HLA download on Webster.
2There are comparable initialization files for MASM, TASM, NASM, FASM, and other assemblers that RadASM supports.
3It might not be a bad idea to check the dates on any of the original files you're replacing to make sure that they're older than the files you're replacing them with.
TOC PREV NEXT INDEX


Chapter 5: The Event-Oriented Programming Paradigm

5.1: Who Dreamed Up This Nonsense?

A typical programmer begins their programming career in a beginning programming course or by learning programming on their own from some book that teaches them step-by-step how to write simple programs. In almost every case, the programming tutorial the student uses begins with a program not unlike the following:

#include <stdio.h>

int main( int argc, char **argv )

{

    printf( "Hello world\n" );

}

This program seems so quaint and simple, but keep in mind that there is considerable education and effort needed to get this trivial program running. A beginning programmer has to learn how to use the computer, an editor, various operating system commands, how to invoke the compiler (and possibly linker), and how to execute the resulting executable file. Though the (C language) program code may be quite trivial, the steps leading up to the point where the student can compile and run this simple program are not so trivial. It should come as no surprise then, that the programming projects that immediately follow the "hello world" program build upon the lessons before them.

One thing that quickly becomes taken for granted by a programming student's "programmer's view of the world" is that there is some concept of a main program that takes control of the CPU when the program first starts running and this main program drives the application. The main program calls the various functions and subroutines that make up the application and, most importantly, the main program (or functions/subroutines that the main program calls) makes requests for operating system services via calls to the OS, which return once the OS satisfies that service request. OS calls are always synchronous; that is, you call an OS function and when it completes, it returns control back to your program. In particular, the OS doesn't simply call some function within your program without you explicitly expecting this to happen. In fact, in the normal programming paradigm the OS never calls a function in your program at all - it simply returns to your program after you've called it.

In the Windows operating system, the concept where the user's application has control and calls the operating system when it needs some service done (like reading a value from the standard input device) is tossed out the window (pardon the pun). Instead, the OS takes control of the show. It is, effectively, the "main program" that tracks events throughout the system and calls functions within various applications as Windows accumulates events (like keypresses or mouse button presses) that it feels the application needs to service. This completely changes the way one writes a program from the application programmer's perspective. Before, the application was in control and knew when things were happening in the application (mainly by virtue of where the program was executing at any given time). In the Windows programming paradigm, however, the OS can arbitrarily call any function in the application (well, not really, but you'll see how this actually works in a little bit) without the application explicitly requesting that the OS call that function. This makes writing applications quite a bit more complex because any of a set of functions could be called at any one given time - the application has no way of predicting the order of invocation. Furthermore, convenient OS facilities like "read a line of text from the keyboard" or "read an integer value from the keyboard" simply don't exist. Instead, the OS calls some function in your code every time the user presses a key on the keyboard. Your code has to save up each keystroke and decide when it has read a full line of text from the keyboard (or when it has read a complete integer value, at which time the application must convert the string to an integer value and pass it on to whatever section of the application needed the integer value). Perhaps even more frustrating is the fact that a Windows GUI application cannot even output data whenever it wants to. Instead, the application needs to save up any output it wishes to display and wait for Windows to send it an event saying "Okay, now update the display screen." The days of slipping in a quick "printf" statement (or something comparable in whatever language you're using) are long gone. Even worse, most programmers learned to write software in an environment where the program is doing only one thing at a time; for example, when reading an integer from the user, the program doesn't have to worry about values magically appearing in other variables based on user input - no additional input may occur until the user inputs the current value. In a Windows GUI application, however, the user can actually enter a single digit for one numeric value, switch to a different text entry box and enter several digits for a different number, then switch back to the original input and continue entering data there. Not only does the program need to deal with the simultaneous entry of several different values, but it also has to handle partial inputs in a meaningful way (i.e., if the user ultimately enters the value 1234, the program has to be able to deal with the partial input values 1, 12, 123, as well as the file value, 1234). Since few programmers have had to deal with this type of activity when writing console (non-GUI) applications, this new programming paradigm requires some time before the programmer becomes comfortable with it.

This programming paradigm is known as the event-oriented programming paradrgm. It's called event-oriented because the operating system detects events like keypresses, mouse activity, timer timeouts, and other system events, and then passes control to a program that is expecting one or more of these events to occur. Once the application processes the event, the application transfers control back to the operating system which waits for the next event to occur.

The event-oriented programming paradigm presents a perspective that is backwards from the way most programmers first learned to write software. Although this programming scheme takes a little bit of effort to become accustomed to, it's not really that difficult to master. Although it may be frustrating at first, because it seems like you're having to learn how to program all over again, fret not, before too long you'll adjust to the "Windows way of doing things" and it will become second nature to you.

5.2: Message Passing

Windows isn't actually capable of calling an arbitrary function within your application. Although Windows does provide a specialized mechanism for calling certain callback functions within your code, most of the time Windows communicates between itself and your application by send your application messages. Message passing is just a fancy term for a procedure call. A message is really nothing more than a parameter list. The major difference (from your application's perspective) between a standard procedure call and a message being passed to your application is that the message often contains some value that tells the application what work it expects it to do. That is, rather than having Windows call any of several dozen different subroutines in your application, Windows simply calls a special procedure (known as the window procedure, or wndproc) and passes it the message (that is, a parameter list). Part of the message tells the window procedure what event has occurred that the window procedure must handle. The window procedure then transfers control (dispatches) to some code that handles that particular event.

If you've ever written a 16-bit DOS application in assembly language, you've done some message passing. The INT 21h instruction that "calls" DOS is equivalent to calling DOS' "window procedure". The values you pass in the 80x86 registers correspond to the message and, in particular, the value in the AH register selects the particular DOS function you wish to invoke. Although the perspective is different (Windows is calling you instead of you calling Windows), the basic idea behind message passing is exactly the same.

The messages that Windows explicitly sends to your applications are actually quite small: just a 16-byte payload. Four of those bytes contain the message identification (that is, an integer value that tells your window procedure what operation to perform), four bytes form a window handle that various functions you call will need, and two double-word data parameters provide message-specific data. Since Windows defines the type of messages it sends to your window procedure, this small message payload (that is, the data) was chosen because it's sufficient for the vast majority of messages and it's efficient to pass between Windows and your application (Windows memory address space is actually in a different address space on the 80x86 from your application and copying large amounts of data between address spaces can be expensive). For those few calls where Windows needs to pass more than eight bytes of data to your application, Windows will allocate a block of memory within your process' address space and pass a pointer to that block of data in one of the two four-byte double words that comprise the message's payload.

Figure 5-1 provides a block diagram of a typical Windows application. This diagram shows how the main program and the window procedure are disconnected. That is, the main program doesn't call the actual application code; instead, Windows handles that activity.

Figure 5-1: General Organization of a Windows Program

For those who are comfortable with client-server architectures, another way to view a Windows application is as a server for Windows' messages. That is, Windows is a client that needs to have certain work done. The application is a server that is capable of performing this work. The server (that is, the application) waits for messages to arrive from Windows telling it what services to perform (e.g., what system events the application should handle). When such a message comes along, the application handles it and then waits for the next message (service request) from the client (Windows). Of course, calling the Window/application relationship a client/server relationship is stretching the point somewhat, because from other perspectives the application is a client that often requests Windows' services. Nonetheless, from the perspective of an application's main program, the client/server relationship is a useful model.

One question you might have is "how does Windows know the address of your window procedure?" The short answer is "you tell it." The discussion in this chapter has given the impression that Windows applications don't have a main program. Strictly speaking, this is not true. Windows applications do have a main program that the operating system calls when you first run the application. In theory, this main program could execute just like the old-fashioned programs, taking control of the CPU and making (certain) calls to the OS and doing all its processing the old fashioned way. The only catch is that such an application wouldn't behave like a standard Windows GUI application. This is how you write console applications under Windows, but presumably you're reading this book to learn how to write Windows GUI apps rather than Windows console apps. So we'll not consider this possibility any farther.

The real purpose of a Windows main program is to initialize the application, register the window procedure with the Windows OS, and then execute an event loop that receives messages from Windows, checks to see if Windows has asked the application to terminate, and then calls a Windows OS function that dispatches the message to whomever it belongs (which is usually your window procedure). Take special note of the phrase "register the window procedure...". This is where you pass Windows the address of your window procedure so it knows how to pass messages to that code. As it turns out, the operation of the main program in a typical Windows application is so standardized that most of the time you will simply "cut and paste" the main program from your previous Windows application into your new application. Rarely will you need to change more than one or two lines in this main program.

When writing a Windows GUI application in HLA, you place the code for the main program of your application between the begin and end associated with the program in HLA (i.e., in the main program section of the HLA program). This may seem completely obvious to an HLA programmer, but to someone who has C/Windows programming experience, this is actually unusual. The main program for a Windows application is usually called winmain, at least, if you're writing the application in C/C++. However, the name "winmain" is actually a C/C++ programming convention; the operating system does not require this name at all. To avoid confusion, we'll continue to place our main program where HLA expects the main program when writing GUI applications.

5.3: Handles

Before discussing actual Windows code, the first thing we must discuss is a very important Windows data structure that you'll use everywhere: the handle. The Windows operating system uses handles to keep track of objects internal to Windows that are not present in the application's address space. Since there is often need to refer to such internal objects, Windows provides values known as handles to make such reference possible. A handle is simply a small integer value (held in a 32-bit dword variable) whose value has meaning only to Windows. Undoubtedly, the handle's value is an index into some internal Windows table that contains the data (or the address of the data) to which the handle actually refers. Windows returns handle values via API function calls, your application must save these values and use them whenever referring to the object that Windows allocated or create via the call.

The Windows C/C++ header files include all kinds of different names for handle object types. This book will simply declare all handles as dword variables rather than trying to differentiate them by type. The truth is, you don't do any operations on handles (other than to pass their values to Win32 API functions), so there is little need to go to the extreme of creating dozens (if not hundreds) of different types that are all just isomorphisms of the dword type. This book will adopt the Windows/Hungarian notation of prepending an "h" to handle object names (e.g., hWnd could be a window handle).

5.4: The Main Program

The main program of a GUI application changes very little from application to application. Indeed, most of the time you'll simply cut and paste the main program from your previous application and then edit one or two lines when creating a new Windows application. One problem with the main program in a Windows application is that it quickly becomes "out of sight, out of mind" and the knowledge of what is going on inside the main program quickly becomes forgotten. Therefore, it's worthwhile to spend some time carefully describing the typical Windows main program so you'll have a good idea of what you can (or should) change with each application that you write.

As noted earlier, one of the most important things the main program of a GUI application does is register a window procedure with the operating system. Actually, registering the window procedure is part of a larger operation: registering a window class with Windows. A window class is simply a data structure that maintains important information about a window associated with an application. One of the main tasks of the main program is to initialize this data structure and then call Windows to register the window class.

Although Microsoft uses the term "class" to describe this data structure, don't let this term confuse you. It really has little to do with C++ or HLA class types and objects. This is really just a fancy name for an instance (that is a variable) of a Windows' w.WNDCLASSEX struct/record. Keep in mind, Windows was originally designed before the days of C++ and before object-oriented programming in C++ became popular. So terms like "class" in Windows don't necessarily correspond to what we think of as a class today.

5.4.1: Filling in the w.WNDCLASSEX Structure and Registering the Window

Here's what the definition of w.WNDCLASSEX looks like in the HLA windows.hhf header file:

type

         WNDCLASSEX: record

            cbSize          : dword;

            style           : dword;

            lpfnWndProc     : WNDPROC;

            cbClsExtra      : dword;

            cbWndExtra      : dword;

            hInstance       : dword;

            hIcon           : dword;

            hCursor         : dword;

            hbrBackground   : dword;

            lpszMenuName    : string;

            lpszClassName   : string;

            hIconSm         : dword;

            align(4);

        endrecord;

Since the application's main program must fill in each of these fields, it's a good idea to take a little space to describe the purpose of each of the fields. The following paragraphs describe these fields.

cbSize is the size of the structure. The main program must initialize this with the size of a w.WNDCLASSEX structure. Windows uses the value of this field as a "sanity check" on the w.WNDCLASSEX structure (i.e., are you really passing a reasonable structure to the function that registers a window class?). Assuming you have a variable wc (window class) of type w.WNDCLASSEX, you can initialize the cbSize field using a single statement like the following:

mov( @size( w.WNDCLASSEX ), wc.cbSize );

The style field specifies the window's style and how Windows will display the window. This field is a collection of bits specifying several boolean values that control the window's appearance. You may combine these styles using the HLA constant expression bitwise OR operator ("|"). The following paragraphs describe the predefined bit values that are legal for this field:

w.CS_BYTEALIGNCLIENT This style tells Windows to align the window's client area (the part of the screen where the application can draw) on an even byte boundary in order to speed up redraw operations. Note that the use of this option affects where Windows can place the open window on the screen (i.e., dragging the window around may require the window to jump in discrete steps depending on the bit depth of the window). Note that individual pixels on modern video display cards tend to consume multiple bytes, so this option may not affect anything on a modern PC.

w.CS_BYTEALIGNWINDOW This style tells Windows to align the whole window on an even byte boundary in order to speed up redraw operations. Note that the use of this option affects where Windows can place the open window on the screen (i.e., dragging the window around may require the window to jump in discrete steps depending on the bit depth of the window). Note that individual pixels on modern video display cards tend to consume multiple bytes, so this option may not affect anything on a modern PC.

w.CS_CLASSDC Allocates a single device context (which this book will discuss in a later chapter) to be used by all windows in a class. Generally useful in multithreaded applications where multiple threads are writing to the same window.

w.CS_DBLCLKS Tells Windows to send double-click messages to the window procedure when the user double-clicks the mouse within a window belonging to the class. Generally, this option is specified for controls that respond to double-clicks (which are, themselves, windows); you wouldn't normally specify this option for the main window class of an application.

w.GLOBALCLASS This option is mainly for use by DLLs. We won't consider this option here.

w.CS_HREDRAW This class style tells Windows to force a redraw of the window if a movement or size adjustment occurs in the horizontal direction. Most window classes you create for your main window will specify this style option.

w.CS_NOCLOSE This style option disables the close command for this window on the system menu.

w.CS_OWNDC Allocates a unique device context for each window in the class. This option is the converse of w.CS_CLASSDC and you wouldn't normally specify both options.

w.CS_PARENTDC Specifies that child windows inherit their parent window's device context. More efficient in certain situations.

w.CS_SAVEBITS Tells Windows to save any portion of a window that is temporarily obscured by another window as a bitmap in Windows' system memory. This can speed up certain redraws, and the window procedure for that window won't have to process as many redraw operations, but it may take longer to display the window in the first place and it does consume extra memory to hold the bitmap. This option is generally useful for small windows and dialog boxes that don't appear on the screen for long periods of time but may be obscured for brief periods.

w.CS_VREDRAW This option tells Windows to redraw the window if a vertical movement or resize operation occurs. This is another option you'll usually specify for the main application's window that a GUI app creates.

Typically, an application will set the w.CS_HREDRAW and w.CS_VREDRAW style options. A few applications with special requirements might include one or two of the other styles as well. The following is a typical statement that you'll find in a GUI application that sets these two style options for the application's main window (again, assuming that wc is a variable of type w.WNDCLASSEX):

mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

The lpfnWndProc field of w.WNDCLASSEX holds the address of the window procedure for the application's main window. Initializing this field is how you tell Windows where it can find the window procedure that is going to process all the messages that Windows passes to your application. The window procedure must have the following generic prototype:

type

        WNDPROC:

            procedure

            (

                var lpPrevWndFunc   :var;

                    hWnd            :dword;

                    Msg             :dword;

                    _wParam         :dword;

                    _lParam         :dword

            );

                @stdcall;

                @returns( "eax" );

If you've got an HLA procedure named WndProc, you can initialize the wc.lpfnWndProc field using the following code:

mov( &WndProc, wc.lpfnWndProc );

(note that the type declaration above is in the w namespace, so there isn't a name conflict between the w.WNDPROC type and the local WndProc procedure in your program).

The cbClsExtra field specifies the number of bytes of storage to allocate immediately after the window class structure in memory. This provides room for application-specific information associated with the window class. Note that if you have more than one instance of this window class (that is, if you create multiple windows from this same class), they will all share this same storage. Windows will initialize this extra storage with zero bytes. Most applications don't need any extra storage associated with their main window class, so this parameter is usually zero. However, you must still explicitly initialize it with zero if you don't need the extra storage:

mov( 0, wc.cbClsExtra );

The cbWndExtra field specifies the number of bytes of extra storage Windows will allocate for each instance of the window that you create. As you see before too long, it's quite possible to create multiple instances of a single window class; this is unusual for the main window of an application, but it's very common for other "windows" in the system like pushbuttons, text edit boxes, and other controls. This extra storage could hold the data associated with that particular control (e.g., possibly the text associated with a text manipulation control). Windows will allocate this storage in memory immediately following the window instance and initializes the bytes to zeros. Few main application windows need this extra storage, so most Windows' main programs will initialize this field to zero in the window class object for the main window, e.g.,

mov( 0, wc.cbWndExtra );

The hInstance field is a handle that identifies the window instance for this application. Your program will have to get this value from Windows by making the w.GetModuleHandle API call (which returns the hInstance handle value in the EAX register). You can initialize the hInstance field using the following code:

    w.GetModuleHandle( NULL ); // NULL tells Windows to return this process' handle.

    mov( eax, wc.hInstance );  // Save handle away in wc structure so Windows knows

                               // which process owns this window.

The hIcon field is a handle to a Windows icon resource. The icon associated with this handle is what Windows will draw whenever you minimize the application on the screen. Windows also uses this code for other purposes throughout the system (e.g., showing a minimized icon on the task bar and in the upper left hand corner of the Window). Later, this book will discuss how to create your own custom icons. For the time being, however, we can simply request that Windows use a "stock icon" as the application's icon by calling the w.LoadIcon API function and passing a special value as the icon parameter:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

The second parameter to w.LoadIcon is usually a string containing the name of the icon resource to use. However, Windows also accepts certain small integer values (values that string pointers are never equal to) to specify certain "canned" or "stock" icons. Normally, you cannot pass such a constant where HLA is expecting a reference parameter, however, by prefixing the parameter with the HLA val keyword, you can tell HLA to pass the value of the constant as the address for the reference parameter. The value of w.IDI_APPLICATION is a Windows predefined constant that tells Microsoft Windows to use the stock application icon for this application. Note that if you pass NULL as the value of the second parameter (e.g., rather than w.IDI_APPLICATION), Windows will tell the application to draw the icon whenever the user minimizes the application. You could use this feature, for example, if you want a dynamic icon that changed according to certain data the application maintains.

The hCursor field of the w.WNDCLASSEX record holds a handle to a cursor resource that Windows will draw whenever the user moves the cursor over the top of the window. Like the hIcon field discussed previously, this handle must be a valid handle that Windows has given you. And just like the initialization of the hIcon field, we're going to call a Windows API function to get a stock cursor we can use for our application. Specifically, we're going to ask Windows to give us the handle of an arrow cursor that will draw an arrow cursor whenever the user moves the cursor over our window. Here's the code to do that:

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

The w.IDC_ARROW constant is a special Windows-defined value that we supply instead of a pointer to a cursor name to tell Windows to use the standard arrow cursor. Like the w.LoadIcon function, if you pass NULL (e.g., rather than w.IDC_ARROW) as the second parameter to w.LoadCursor, Windows will expect the application to draw the cursor whenever the mouse moves over the application's window.

The hbrBackground field specifies the "brush" that Windows will use to paint the background of a window. A Windows' brush is simply a color and pattern to draw. Generally, you'll specify one of the following color constants as this handle value (though you could create a custom brush and use that; this book will discuss the creation of brushes later on):

  • w.COLOR_ACTIVEBORDER
  • w.COLOR_ACTIVECAPTION
  • w.COLOR_APPWORKSPACE
  • w.COLOR_BACKGROUND
  • w.COLOR_BTNFACE
  • w.COLOR_BTNSHADOW
  • w.COLOR_BTNTEXT
  • w.COLOR_CAPTIONTEXT
  • w.COLOR_GRAYTEXT
  • w.COLOR_HIGHLIGHT
  • w.COLOR_HIGHLIGHTTEXT
  • w.COLOR_INACTIVEBORDER
  • w.COLOR_INACTIVECAPTION
  • w.COLOR_MENU
  • w.COLOR_MENUTEXT
  • w.COLOR_SCROLLBAR
  • w.COLOR_WINDOW
  • w.COLOR_WINDOWFRAME
  • w.COLOR_WINDOWTEXT

Actually, the value you must supply for the hbrBackground value is one of the above constants plus one. This is just a Windows idiosyncrasy you'll have to keep in mind. w.COLOR_WINDOW (a solid white background) is the typical window color you'll probably use. The following code demonstrates this assignment:

mov( w.COLOR_WINDOW+1, wc.hbrBackground );

The lpszMenuName field contains the address of a string specifying the resource name of the class' main menu, as the name appears in a resource file. This book will discuss menus and resource files a little later. In the meantime, if your window class doesn't have a main menu associated with it (or you want to assign the menu later), simply set this field to NULL:

mov( NULL, wc.lpszMenuName );

The lpszClassName field is a string that specifies the class name for this window. This is an important name that you'll use in a couple of other places. Generally, you'll specify the application's name as this string, e.g.,

readonly

    myAppClassName :string := "MyAppName";

        .

        .

        .

    mov( myAppClassName, eax );

    mov( eax, wc.lpszClassName );

The hIconSm is a handle to a small icon associated with the window class. This handle was used in Windows 95, but was ignored by Win NT (and later versions of Windows). The Windows documentation claims that you should initialize this field to NULL in NT and later OSes (and that Windows will set this field to NULL upon return). Most applications, however, seem to initialize this field with the same value they shove into the hIcon field; probably not a bad idea, even if Windows does set this field to NULL later.

Once you fill in all the fields of the w.WNDCLASSEX structure (i.e., wc), you register the window class with Windows by calling the w.RegisterClassEx API function, passing the window class object (wc) as the single parameter, e.g.,

w.RegisterClassEx( wc );

The following is all the code appearing throughout this section collected into a contiguous fragment so you can see the complete initialization of the wc variable and the registration of the window class:

readonly

    myAppClassName :string := "MyAppName";

        .

        .

        .

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( 0, wc.cbClsExtra );

    mov( 0, wc.cbWndExtra );

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );      // Save in a global variable for future use

    mov( eax, wc.hInstance );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( myAppClassName, eax );

    mov( eax, wc.lpszClassName );

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, w.IDC_ARROW );

    mov( eax, wc.hCursor );

    w.RegisterClass( wc );

5.4.2: "What is a `Window Class' Anyway?"

This chapter has made considerable use of the Windows' term window class with only a cryptic discussion of the fact that window classes are not the same thing as C++ or HLA classes. This section will explain the difference between classes in traditional object-oriented programming languages, window classes in Windows, and instances of window classes.

In a language like HLA, a class is a data type. As a general rule, there is no run-time memory associated with a class definition1. It's only when you allocate storage for an instance of that class, that is create an object variable, that there is storage associated with that class. A class, therefore, is a layout of how an object actually uses the memory allocated to it; that is, like a record or structure definition, a class simply defines how the program should treat blocks of memory cells at some offset from the object's base address.

Window classes, on the other hand, do have memory allocated for them. The wc variable of the previous section is a good example of a window class that has storage associated with it (indeed, the main purpose of that section was to describe how to initialize the memory storage associated with that window class). So from the very start, we see the major difference between classes in an object-oriented language and windows class: storage. Lest you wonder what Microsoft's engineers were thinking when they created this terminology, just keep in mind that Windows was designed long before object-oriented programming became popular (i.e., before the advent of C++, HLA, and many other popular OOP languages) and terminology like objects versus classes was not as well-known as it is today.

So, then, exactly what is a window class? Well, a window class is a template2 that describes a common structure in memory that programs will often duplicate when creating multiple copies of a window. The beautiful thing about a window class is that it lets you initialize the window class record just once and then make multiple copies of that window without having to initialize the data structure associated with each instance of that window class. Now, perhaps, it's a bit difficult to understand why you would want multiple copies of a window or why this is even important based upon the one example we've had in this book to this point. After all, how many times does an application need more than one copy of the application's window (and in the few cases where they do, who really cares about the extra work needed to initialize the window class record, since this is done so infrequently?). Well, if the application's main window were the only window an application would use, there would be little need for window classes. However, a typical Windows GUI application will use dozens, if not hundreds of different windows. This is because Microsoft Windows supports a hierarchical window structure with smaller (child) windows appearing within larger (parent) windows. Most user interface components (buttons, text edit boxes, lists, etc.) are examples of windows in and of themselves. Each of these windows has its own window class. Although an application may have but a single main window, that application may have many, many, different buttons. Each button appearing on the screen is a window in and of itself, having a window procedure and all the other information associated with a window class. However, all the buttons (at least, of the same type) within a given application share the same windows class. Therefore, to create a new button all you have to do is create a new window based on the button window class. There is no need to initialize a new window class structure for each button if that button shares the attributes common to other buttons the application uses.

Another nice thing about window classes is that Microsoft pre-initializes several common window classes (e.g., the common user interface objects like buttons, text edit boxes, and lists) so you don't even have to initialize the window class for such objects. If you want a new button in your application, you simply create a new window specifying the "button" window class. Since Windows has already registered the button's window class, you don't have to do this. Therein lies the whole purpose of the w.RegisterWindow API call: it tells Microsoft Windows about this new window class. Once you register a window class with Microsoft Windows, your application can create instances of that window via the w.CreateWindowEx API call (which the next section describes). Although your application will typically create only a single instance of the main application's window, it is quite likely you'll create other window classes that represent custom controls that appear within your application. Then your application can create multiple instances of those custom controls by simply calling the w.CreateWindowEx API for each instance of the control.

5.4.3: Creating and Displaying a Window

Registering a window with the w.RegisterWindowEx API call does not actually create a window your application can use, nor does it display the window on your video screen. All this API does is create a template for the window and let Microsoft Windows know about the template so future calls can create instances of that window. The API function that actually creates the window is called (obviously enough) w.CreateWindowEx.

The w.CreateWindowEx prototype (appearing in the user32.hhf header file) is the following:

CreateWindowEx: procedure

(

        dwExStyle       :dword;

        lpClassName     :string;

        lpWindowName    :string;

        dwStyle         :dword;

        x               :dword;

        y               :dword;

        nWidth          :dword;

        nHeight         :dword;

        hWndParent      :dword;

        hMenu           :dword;

        hInstance       :dword;

    var lpParam         :var

);

@stdcall;

@returns( "eax" );

@external( "__imp__CreateWindowExA@48" );

The dwExStyle parameter specifies an extended style value for this window (the extended style parameter is what differentiates the w.CreateWindowEx function from the older w.CreateWindow API call). This parameter is a bitmap containing up to 32 different style settings that are enabled or disabled by setting the appropriate bit. The windows.hhf header file defines a set of constants with names of the form w.WS_EX_* that correspond to the possible extended styles. There are a few too many of these, and most of them are a bit too complex, to present at this time. Please see the user32 reference manual (appearing on the CD-ROM accompanying this book) for more details on these extended style values). For the time being, you can initialize this field with zero or, if you prefer you can use the constant w.WS_EX_APPWINDOW which tells Windows to put an icon on the taskbar for a top-level instance of this window.

The lpClassName field specifies the name of the window class on which you're basing the window you're creating. Generally, this is the string you've supplied as the class name in the call to w.RegisterWindow. For certain pre-defined window classes that Windows defines, you can also supply an atom value here. An atom is a small 16-bit integer value that uniquely specifies an existing window class (e.g., like the cursor and icon values we saw in the last section). Windows differentiates atoms from strings by looking at the H.O. word of the lpClassName parameter value. If this H.O. word contains zero, then Windows assumes that it's an atom value, if the H.O. word is non-zero, then Windows assumes that this parameter contains the address of some string object (note that pointer values in Windows always have a H.O. word that is non-zero).

To pass an atom value rather than a string object as this first parameter, you should use the HLA val keyword as a prefix on the atom value, e.g.,

    w.CreateWindowEx

    (

                0,

        val		SomeAtomValue,         // Atom values need the "VAL" keyword prefix.

                "WindowName",

                w.WS_OVERLAPPEDWINDOW, // We'll explain the following momentarily...

                w.CW_USEDEFAULT,

                w.CW_USEDEFAULT,

                w.CW_USEDEFAULT,

                w.CW_USEDEFAULT,

                NULL,

                NULL,

                hInstance,

                NULL

    );

Technically, the lpClassName parameter points at a zero-terminated string. However, since HLA string objects are upwards compatible with zero-terminated strings, the w.CreateWindowEx prototype specifies an HLA string variable as this parameter. This turns out to be most convenient because most calls to w.CreateWindowEx will specify a literal string constant or an HLA string variable here. However, if you've got a zero-terminated string that you'd like to use, you don't need to first convert it to an HLA string, you can use code like the following to directly pass the address of that zero-terminated string to w.CreateWindowEx:

    lea( eax, SomeZeroTerminatedString );

    w.CreateWindowEx

    (

            0,

            (type string eax),        // Passes pointer to zstring found in EAX.

            "WindowName",

            w.WS_OVERLAPPEDWINDOW,    // We'll explain the following momentarily...

            w.CW_USEDEFAULT,

            w.CW_USEDEFAULT,

            w.CW_USEDEFAULT,

            w.CW_USEDEFAULT,

            NULL,

            NULL,

            hInstance,

            NULL

    );

Here are some constant values that Windows predefines that you may pass as atom values in place of a string for the lpClassName parameter (an in-depth explanation of these class types will appear later in this book):

w.BUTTON This is a small rectangular window that corresponds to a push button the user can click to turn it on or off.

w.COMBOBOX Specifies a control that consists of a list box and a text edit control combined into a single control. This control allows the user to select some text from a list or type the text from the keyboard.

w.EDIT This specifies an edit box which is a rectangular window into which the user may type some text.

w.LISTBOX This atom specifies a list of character strings. The user may select one of these strings by clicking on it.

w.MDICLIENT Designates an MDI (multiple document interface) client window. This tells Windows to send MDI messages to the window procedure associated with this window.

w.RichEdit Specifies a Rich Edit 1.0 control. This provides a rectangular window that supports text entry and formatting and may include embedded COM objects.

w.RICHEDIT_CLASS Specifies a Rich Edit 2.0 control

w.SCROLL_BAR Specifies a rectangular window used to hold a scroll bar control with direction arrows at both ends of the scroll bar.

w.STATIC Specifies a text field, box, or rectangle used to label, box, or separate other controls.

For the main application's window, you would not normally specify one of these atoms as a window class. Instead, you'd supply a string specifying a name for the application's window class. We'll return to the discussion of controls in a later chapter in this book.

The third w.CreateWindowEx parameter, lpWindowName, is a string that holds the window's name. This is caption that is associated with the window's title bar. Some applications will also identify an instance of a window on the screen by using this string. Typically, if you have multiple instances of a window class appearing on the screen at the same time, you will give each instance a unique window name so you can easily differentiate them. Generally, the class name and the window name are similar, but not exactly the same. A class name typically looks like a program identifier (i.e., no embedded spaces and the characters in the name would be those that are legal in a program source file). The window name, on the other hand, is usually formatted for human consumption.

The fourth parameter, dwStyle, specifies a set of window styles for the window. Like the dwExStyle parameter, this object is a bitmap containing a set of boolean values that specify the presence or absence of some window attribute. The following is a partial list of values you may logically OR together for form the dwStyle value. We'll explain the terminology and specifics later in this book. These are thrown out here just for completeness. We'll actually only use a single window style for our application's main window.

w.WS_BORDER Specifies a thin border around the window.

w.WS_CAPTION Creates a window that has a title bar (also sets the w.WS_BORDER attribute).

w.WS_CHILD Creates a child window. Mutually exclusive to the w.WS_POPUP attribute.

w.WS_CHILDWINDOW Same as w.WS_CHILD attribute.

w.WS_CLIPCHILDREN Excludes the area occupied by child windows when drawing occurs within the parent window. Use this style when creating a parent window.

w.WS_CLIPSIBLINGS Clips child windows relative to one another. Specify this when creating a child window when you have several child windows that could overlap one another.

w.WS_DISABLED Creates a window that is initially disabled.

w.WS_DLGFRAME Creates a window with a border designed for a dialog box.

w.WS_GROUP Specifies the first control of a group of controls (remember, controls are windows). The next control that has the w.WS_GROUP style ends the current group and begins the next group.

w.WS_HSCROLL Creates a window with a horizontal scroll bar.

w.WS_ICONIC Creates a window that is initially minimized.

w.WS_MAXIMIZE Creates a window that is initially maximized.

w.WS_MAXIMIZEBOX Creates a window that has a maximize button.

w.WS_MINIMIZE Creates a window that is initially minimized (same as w.WS_ICONIC).

w.WS_MINIMIZEBOX Creates a window that has a minimize button.

w.WS_OVERLAPPED Creates an overlapped window.

w.WS_OVERLAPPEDWINDOW This is a combination of several styles include w.WS_OVERLAPPED, w.WS_CAPTION, w.WS_SYSMENU, w.WS_SIZEBOX, w.WS_MINIMIZEBOX, and w.WS_MAXIMIZEBOX. This is the typical style an application's window will use.

w.WS_POPUP Creates a popup window. Mutually exclusive to the w.WS_CHILD window style.

w.WS_POPUPWINDOW Creates a pop-up window with the following styles: w.WS_BORDER, w.WS_POPUP, w.WS_SYSMENU. The w.WS_POPUPWINDOW and w.WS_CAPTION styles must both be active to make the system menu visible.

w.WS_SIZEBOX Creates a window that has a sizing border. This style is the same as the w.WS_THICKFRAME style.

w.WS_SYSMENU Creates a window that has a system menu box in its title bar. You must also specify the w.WS_CAPTION style when specifying this attribute.

w.WS_THICKFRAME Same as w.WS_SIZEBOX style.

w.WS_TILED Save as the w.WS_OVERLAPPED style.

w.WS_TILEDWINDOW Same as the w.WS_OVERLAPPEDWINDOW style.

w.WS_VISIBLE Creates a window that is initially visible.

w.WS_VSCROLL Creates a window that has a vertical scroll bar.

These styles are appropriate for generic windows. Certain window classes have their own specific set of window styles. In particular, the button window class, the combobox window class, the text edit window class, the list box window class, the scroll bar window class, the static window class, and the dialog window class have their own set of window style values you can supply for this parameter. We'll cover this specific window styles when we discuss those controls later in this book.

For generic windows, the w.WS_OVERLAPPEDWINDOW style is a good style to use. Depending on your needs, you may want to merge in the w.WS_HSCROLL and w.WS_VSCROLL styles as well. You can also specify the w.WS_VISIBLE style if you like, but we'll be making a call to make the window visible soon after calling w.CreateWindowEx, so merging in this style isn't necessary.

The next four parameters, x, y, nWidth and nHeight specify the position and size of the window on the display. If your window must be a certain size and it must appear at a certain location on the screen, then you may fill in this parameter with appropriate screen coordinate values. Another good use of these parameters is to automatically restore the application window's position and size from their values the last time the user ran the application (presumably, you've saved the values in a file or in the system registry before quitting if your application is going to do this). Most applications (particularly, those that allow the user to resize the window) don't really care about the initial size and position of the main application window. After all, if the user doesn't like what comes up, the user can move or resize the window to their liking. In such situations, a user can supply the generic constant w.CW_USEDEFAULT that tells Windows to place the window at an appropriate point on the screen. Windows will typically center such windows and have them consume approximate half the screen's size.

If you decide to supply explicit coordinates and dimensions for the application's window, be cognizant of the fact that Windows runs on a wide variety of machines with window sizes ranging from 640x480 (and, technically even smaller) to very large. When choosing a screen position and size for your window, be sure to consider the fact that someone may be running your application on a machine with a smaller screen than the one on your machine. This is why using w.CW_USEDEFAULT, if possible, is a good idea. Windows can automatically adjust the window dimensions as appropriate for the machine on which the application is running.

The hWndParent parameter supplies the handle of a parent window whenever you're creating a child window. Buttons, text edit boxes, and other controls are good examples of child windows. An application's main window, however, isn't a child window. So you'll normally supply NULL for this parameter when creating the main window for an application.

The hMenu parameter provides the handle for a menu to be used with a window or a child window identifier for the child window style. We'll come back to the discussion of menus in a later chapter. For now, you can place a NULL in this field to tell windows that your application's window doesn't have a menu.

The hInstance parameter is where you pass the module (application) handle. You obtain this value via the w.GetModuleHandle API call. Note that the window class variable (wc in the previous section) also requires this handle, when the application's main program initialized the class variable it also saved the application's handle into a global variable hInstance for use by w.CreateWindowEx API calls. Because future calls will need this value as well, having it available in a global variable is a good idea (of course, it's also present in the wc.hInstance field, but it's still convenient to keep it in a global variable).

The last w.CreateWindowEx parameter is used to specify the address of a w.CREATESTRUCT object for MDI windows. If you're not creating an MDI window (and most applications don't), you can specify NULL for this field.

The w.CreateWindowEx API function returns a handle to the window it creates in the EAX register. You will use this handle whenever referencing the window. Therefore, it's a good idea to save away this variable into a global variable immediately upon return from w.CreateWindowEx (you'll want to use a global variable because lots of different procedures and functions through out the application will need to reference this variable's value).

The w.CreateWindowEx API function creates an actual instance of some window class and initializes it appropriately. It does not, however, actually put the window on the screen. That takes another couple of calls and some extra work. To tell windows to show your window (i.e., make it visible), you use the w.ShowWindow API call thusly3:

w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

The first parameter to this function is the window handle that w.CreatWindowEx returns. The second parameter specifies how Windows should display the window, the w.SW_SHOWNORMAL is the appropriate value to use when displaying the window for the first time.

Despite its name, w.ShowWindow doesn't actually make your window visible on the display. It simply sets the "show state" for this particular window. Although Windows will draw the frame of your window for you, it is your responsibility to actually fill in the "client area" of the window. That is done by having Windows send your application a message telling it to paint itself. Although you currently have control of the CPU, one thing you cannot arbitrarily do is draw to the screen without Windows telling you to do so (this is especially important because your window isn't even on the screen at this point). In order to draw your window, you've got to tell Windows to send your window procedure a message and then your window procedure can do the job of actually filling in the screen information. You can do this with a w.UpdateWindow call as follows:

w.UpdateWindow( hwnd );

Again, remember, w.UpdateWindow does not actually draw the window. It simply tells Windows to send your application a message that will cause it to draw the window (inside the window procedure). The actual drawing does not take place in your application's main program.

Once you've told Windows to update your window so it can be drawn for the first time, all that's left for your main program to do is to process Windows' messages. The next section describes that activity. At this point, you've created your window and told Windows to display it. Once you begin processing Windows' messages, you'll actually display the window (since one of the first messages that will come along is the message telling your application to draw its window).

5.4.4: The Message Processing Loop

After you initialize, register, and create your application's main window and tell Windows to display the window, the last major piece of work your application's main program must do is begin processing Windows messages. The message processing loop is actually a small piece of code, so short that we'll just reproduce the whole thing in one chunk:

    forever

        w.GetMessage( msg, NULL, 0, 0 );  // Get a message from Windows

        breakif( EAX = 0 );               // When GetMessage returns zero, time to quit

        w.TranslateMessage( msg );        // Converts keyboard codes to ASCII

        w.DispatchMessage( msg );         // Calls the appropriate window procedure

    endfor;

    mov( msg.wParam, eax );              // Get this program's exit code

    w.ExitProcess( eax );                // Quit the application

This code repeatedly calls w.GetMessage until w.GetMessage returns false (zero) in the EAX register. This is a signal from Windows that the user has decided to terminate our amazing program. If w.GetMessage returns true, then the message loop calls w.TranslateMessage (which mainly processes keystrokes) and then it calls w.DispatchMessage (which passes the messages on to the window procedure, if appropriate).

Thew.GetMessage function transfers control from your program to Windows so Windows can process keystrokes, mouse movements, and other events. When such an event occurs (and is directed at your program), Windows returns from w.GetMessage after having filled in the msg variable with the appropriate message information. The filter parameters should contain zero (so w.GetMessage will return all messages from the queue). The second parameter normally contains NULL which means that the program will process all messages sent to any window in the program. If you put a window handle here, then w.GetMessage will only return those messages directed at the specified window.

On return, the msg parameter contains the message information returned by Windows. Normally, you can ignore the contents of this message variable, all you really need to do is pass the message on to the w.TranslateMessage and w.DispatchMessage functions. However, just in case you're interested, here's the definition of the w.MSG type in the windows.hhf header file:

type

    MSG: record

        hwnd    : dword;

        message : dword;

        wParam  : dword;

        lParam  : dword;

        time    : dword;

        pt      : POINT;

    endrecord;

The w.TranslateMessage API function takes messages containing keyboard virtual scan codes and computes the ASCII/ANSI code associated with that keystroke. By placing this function call in the main message passing loop, Microsoft effectively provides a "hook" allowing you to replace this translation operation with a function of your own choosing. The w.TranslateMessage takes scan codes of the form shift down, shift up, `A' key down, `A' key up, control key down, and control key up and decides whether a virtual key code like the code for the `A' key should be converted to the character `a', `A', control-A, Alt-A, etc. Normally, you'll want this default translation to take place, so you'll leave in the call to w.TranslateMessage. However, by breaking out the call in this fashion, Windows allows you to replace w.TranslateMessage entirely, or inject some code to handle a specific keystroke sequence that you want to handle specially within your application.

The w.DispatchMessage API function takes the translated message and calls the appropriate window procedures, passing along the (translated) message. Upon return from w.DispatchMessage, every application window that has reason to deal with that message will have done so.

At first blush, it might seem weird that Microsoft would even make you write the message processing loop as part of your main program. After all, the loop simply makes three calls to Win32 API functions; surely the OS could bury this code inside the operating system and spare the application's main program the (admittedly small) expense of dealing with this operation. However, the main reason for requiring this code in the application program is explicitly to provide the application with the ability to hook into the message processing loop both before and after the call to w.DispatchMessage.

5.4.5: The Complete Main Program

Here's the source code for a complete Windows' main program, collected into one spot:

program main;

#include( "wpa.hhf" )        // Abridged version of windows.hhf/w.hhf

storage

    hInstance  :dword;        // Application's module handle

    hwnd       :dword;        // Main application window handle

    msg        :w.MSG;        // Message data passed in from Windows

    wc         :w.WNDCLASSEX; // Windows class for main app window

readonly

    myAppClassName :string := "MyAppName";

    << Other declarations and procedures would go here... >>

begin main;

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( 0, wc.cbClsExtra );

    mov( 0, wc.cbWndExtra );

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );      // Save in a global variable for future use

    mov( eax, wc.hInstance );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( myAppClassName, eax );

    mov( eax, wc.lpszClassName );

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc,hIconSm );

    w.LoadCursor( NULL, w.IDC_ARROW );

    mov( eax, wc.hCursor );

    w.RegisterClass( wc );

    w.CreateWindowEx

    (

        0,                     // No specific extended styles

        myAppClassName,        // This application's class name.

        "My First App",        // Window caption

        w.WS_OVERLAPPEDWINDOW, // Draw a normal app window.

        w.CW_USEDEFAULT,       // Let Windows choose the initial

        w.CW_USEDEFAULT,       //  size and position for this window.

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,                  // This is the parent window.

        NULL,                  // This window has no default menu.

        hInstance,             // Application's handle.

        NULL                   // We're not a child window.

    );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    forever

        w.GetMessage( msg, NULL, 0, 0 );  // Get a message from Windows

        breakif( EAX = 0 );               // When GetMessage returns zero, time to quit

        w.TranslateMessage( msg );        // Converts keyboard codes to ASCII

        w.DispatchMessage( msg );         // Calls the appropriate window procedure

    endfor;

    mov( msg.wParam, eax );              // Get this program's exit code

    w.ExitProcess( eax );                // Quit the application

end main;

5.5: The Window Procedure

Since the application's main program doesn't call any other functions within the application, someone reading the source code to a Windows application for the first time may very well wonder how the rest of the code in the application executes. As this chapter notes in several places, Windows automatically calls the window procedure whose address appears in the lpfnWndProc field of the window class variable when it needs to send the application a message. Part of the message package that Windows passes to the window procedure is a value that specifies the message type. The window procedure interprets this value to determine what activity to perform in response to the message. The window procedure (or subroutines called by the window procedure) is where all the activity takes place in a typical windows application.

The prototype for a window procedure takes the following form:

    procedure WndProc( hwnd: dword; uMsg:dword; wParam:dword; lParam:dword );

        @stdcall;

        @nodisplay;

        @nostackalign;

The traditional name for this procedure is WndProc and that's the name you'll see most programs use. However, you may use any name you like here. All that Windows cares about is that you initialize the lpfnWndProc field of the window class variable with the address of this procedure prior to registering the window. So if you named this procedure MyWindowProcedure it would work fine as long as you initialized the window class variable (say, wc) with its address as follows:

mov( &MyWindowProcedure, wc.lpfnWndProc );

The hwnd parameter is a handle to the window at which this message is explicitly directed. All of the windows instantiated from the same window class share the same window procedure. This allows a single window procedure to process messages for several different windows. Of course, typically there is only a single instance of the main application's window class (that is, the main application's window) so your main window procedure typically handles messages for only one window. However, if you create multiple instances of some window class (e.g., you're creating a component like a button), you can explicitly test to see if the message is directed at a specific instance of that window class by comparing the hwnd parameter against the handle value that w.CreateWindowEx returns. In this chapter, we'll assume that there is only one instance of the main application's window, so we'll just ignore the hwnd parameter.

The uMsg parameter is an unsigned integer value that specifies the type of the message Windows is sending the window procedure. There are, literally, hundreds of different messages that Windows can send an application. You can find their values in the windows header files by searching for the constant definitions that begin with "WM_" (the WM, obviously, stands for "Windows Message"). There are far too many to present the entire list here, but the following constant declarations provide examples of some common Windows messages that could be sent to your application's window procedure:

const

    WM_CREATE := $1;

    WM_DESTROY := $2;

    WM_MOVE := $3;

    WM_SIZE := $5;

    WM_ACTIVATE := $6;

    WM_PAINT := $0F;

    WM_CLOSE := $10;

    WM_CUT := $300;

    WM_COPY := $301;

    WM_PASTE := $302;

    WM_CLEAR := $303;

    WM_UNDO := $304;

The important thing to notice is that commonly used message values aren't necessarily contiguous (indeed, they can be widely spaced apart) and there are a lot of them. This pretty much precludes using a switch/case statement (or an assembly equivalent - a jump table) because the corresponding jump table would be huge. Since few window procedures process more than a few dozen messages, many application's window procedures just use a if..else if chain to compare uMsg against the set of messages the window procedure handles; therefore, a typical window procedure often looks somewhat like the following:

    procedure WndProc( hwnd: dword; uMsg:dword; wParam:dword; lParam:dword );

        @stdcall;

        @nodisplay;

        @nostackalign;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    if( eax = w.WM_DESTROY ) then

        w.PostQuitMessage( 0 );   // Do this to quit the application

    elseif( eax = w.WM_PAINT ) then

        << At this point, do whatever needs to be done to draw the window >>

    else

        // If an unhandled message comes along,

        // let the default window handler process the

        // message.  Whatever (non-zero) value this function

        // returns is the return result passed on to the

        // event loop.

        w.DefWindowProc( hwnd, uMsg, wParam, lParam );

    endif;

end WndProc;

There are two problems with this approach. The major problem with this approach is that you wind up processing all your application's messages in a single procedure. Although the body of each if statement could, in theory, call a separate function to handle that specific message, in practice what really happens is the program winds up putting the code for a lot of the messages directly into the window procedure. This makes the window procedure really long and more difficult to read and maintain. A better solution would be to call a separate procedure for each message type.

The second problem with this organization for the window procedure is that it is effectively doing a linear search using the uMsg value as the search key. If the window procedure processes a lot of messages, this linear search can have a small impact on the performance of the application. However, since most window procedures don't process more than a couple dozen messages and the code to handle each of these messages is usually complex (often involving several Win32 API calls, which are slow), the concern about using a linear search is not too great. However, if you are processing many, many, different types of messages, you may want to consider using a binary search or hash table search to speed things up a bit. We'll not worry about the problem of using a linear search in this book; however, the cost of getting to the window procedure and the cost associated with processing the message is usually so great that it swamps any savings you obtain by using a better search algorithm. However, those looking to speed up their applications in certain circumstances may want to consider a better search algorithm and see if it produces better results. Of course, another alternative is to go ahead and use a jump table (large though it might be) which can transfer control to an appropriate handler in a fixed amount of time.

There are a couple of solutions to the first problem (organizing the code so that it is easier to read and maintain). The most obvious solution, as noted earlier, is to call a procedure within each if..then body. A possibly better solution, however, is to use a table of message values and procedure addresses and search through the table until the code matches a message value; then the window procedure can call the corresponding procedure for that message. This scheme has a couple of big advantages over the if..then..elseif chain. First of all, it allows you to write a generic window procedure that doesn't change as you change the set of messages it has to handle. Second, adding new messages to the system is very easy. Here's the data structures we'll use to implement this:

type

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue:   dword;

            MessageHndlr:   MsgProc_t;

        endrecord;

MsgProc_t is the generic prototype for the message handler procedures we're going to write. The parameters to this function almost mirror the parameters Windows passes to the window procedure; the uMsg parameter is missing because, presumably, each different message value invokes a different procedure so the procedure should trivially know the message value. MsgProcPtr_t is a record containing two entries: a message number (MessageValue) and a pointer to the message handler procedure (MessageHndlr) to call if the current message number matches the first field of this record. The window procedure will loop through an array of these records comparing the message number passed in by Windows (in uMsg) against the MessageValue field. If a match is made, then the window procedure calls the function specified by the MessageHndlr field. Here's what a typical table (named Dispatch) of these values looks like in HLA:

readonly

    Dispatch:   MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

Each entry in the table consists of a record constant (e.g., MsgProcPtr_t:[w.WM_DESTROY,&QuitApplication]) containing a message number constant and the address of the procedure to call when the current message number matches that constant. The end of the list contains zeros (NULL) in both entries (e.g., MsgProcPtr_t:[0,NULL]).

To handle a new message in this system, all you have to do is write the message handling procedure and stick a new entry into the table. No changes are necessary in the window procedure. This makes maintenance of the window procedure very easy. The window procedure itself is fairly straight-forward, here's an example of a window procedure that processes the entries in the Dispatch table:

// The window procedure.

//

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

    procedure WndProc( hwnd: dword; uMsg:dword; wParam:dword; lParam:dword );

        @stdcall;

        @nodisplay;

        @nostackalign;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = NULL ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            break;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above. This code manually

            // pushes the parameters and calls the handler procedure (note

            // that the message handler procedure uses the HLA/Pascal calling

            // sequence, so we must push the actual parameters in the same

            // order as the formal parameters were declared).

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );  // Move on to next table entry.

    endfor;

end WndProc;

This code uses EDX to step through the table of MsgProcPtr_t records. This procedure begins by initializing EDX to point at the first element of the Dispatch array. This code also loads the uMsg parameter into EAX where the procedure can easily compare it against the MessageValue field pointed at by EDX. A zero routine address marks the end of the Dispatch list, so this code first moves the value of that field into ECX and checks for zero. When the code reaches the end of the Dispatch list without finding a matching message number, it calls the Windows API w.DefWindowProc function that handles default message handling (that is, it handles any messages that the window procedure doesn't explicitly handle).

If the window procedure dispatch loop matches the value in EAX with one of the Dispatch table's message values, then this code calls the associated procedure. Since the address is already in ECX (from the comparison against NULL for the end of the list), this code manually pushes the parameters for the message handling procedure onto the stack (in the order of their declaration, since the message handling functions using the HLA/Pascal calling convention) and then calls the handler procedure via the address in ECX.

This routine chose EAX, ECX, and EDX because the Intel ABI (and Windows) allows you to trash these registers within a procedure call. The Intel ABI also specifies that functions should return 32-bit results in the EAX register, which is another reason for using EAX - it's going to get trashed by the return result anyway. Note that the message handler procedures must also follow these rules. That is, they are free to disturb the values in EAX, ECX, and EDX, but they must preserve any other registers that they modify. Also note that upon entry into the message handling procedures, EAX contains the message number. So if having this value is important to you (for example, if you use the same message handler procedure for two separate messages), then just reference the value in EAX.

Once we have the Dispatch table and the WndProc procedure, all that's left to do is write the individual message handling procedures and we'll have a complete Windows application. The question that remains is: "What applications shall we write?" Well, historically, most programming books (including almost every Windows programming book) has started off with the venerable "Hello World" program. So it makes perfect sense to continue that fine tradition here.

5.6: Hello World

To create a complete Windows GUI application based on the code we've written thus far, we've only got to add two procedures: QuitApplication and Paint. A minimal Windows GUI application (like HelloWorld) will have to handle at least two messages: w.WM_DESTROY (which tells the application to destroy the window created by the main program and terminate execution) and w.WM_PAINT (which tells the application to draw its main window).

The QuitApplication is a fairly standard procedure; almost every Windows GUI app you write with HLA will use the same code. Here is a sample implementation:

// QuitApplication:

//

//  This procedure handles the "wm.Destroy" message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

@nodisplay;

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

The w.PostQuitMessage API function does just what its name implies - it sends ("posts") a message to the main message loop that tells the message loop to terminate the program. On the next iteration of the message loop in the main program, the w.GetMessage function will return zero in EAX which tells the application to terminate (look back at the main program example for details). The parameter you pass to w.PostQuitMessage winds up in the msg.wParam object in the main program, this is the program's return code. By convention, main programs return a zero when they successfully terminate. If you wanted to return an error code, you'd pass that error code as the parameter to w.PostQuitMessage.

One embellishment you could make to the QuitApplication procedure is to add any application-specific code needed to clean up the execution state before the program terminates. This could include flushing and closing files, releasing system resources, freeing memory, etc. Another possibility is that you could open up a dialog box and ask the user if they really want to quit the program.

The other procedure you'll need to supply to have a complete, functional, HelloWorld program is the Paint procedure. The Paint procedure in our Win32 application is responsible for drawing window data on the screen. Explaining exactly what goes into the Paint procedure is actually the subject of much of the rest of this book and it would be foolish to try and explain everything that Paint must do in the few words available in this section. So rather than try and anticipate questions with a lot of premature explanation, here's the Paint procedure without too much ado:

// Paint:

//

//  This procedure handles the "wm.Paint" message.

//  For this simple "Hello World" application, this

//  procedure simply displays "Hello World" centered in the

//  application's window.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @nodisplay;

var

    hdc:    dword;              // Handle to video display device context

    ps:     w.PAINTSTRUCT;      // Used while painting text.

    rect:   w.RECT;             // Used to invalidate client rectangle.

begin Paint;

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    w.BeginPaint( hwnd, ps );

    mov( eax, hdc );

    w.GetClientRect( hwnd, rect );

    w.DrawText

    (

        hdc,

        "Hello World!",

        -1,

        rect,

        w.DT_SINGLELINE | w.DT_CENTER | w.DT_VCENTER

    );

    w.EndPaint( hwnd, ps );

end Paint;

The w.BeginPaint and w.EndPaint procedure calls must bracket all the drawing that takes place in the Paint procedure. These procedures set up a device context (hdc) that Windows uses to determine where the output should wind up (typically, the video display, but it could wind up somewhere else like on a printer). We'll have a lot more to say about these functions in the very next chapter, for now just realize that they're a requirement in order to draw on the window.

The w.GetClientRect API function simply returns the x- and y-coordinates of the outline of the client area of the window. The client area of a window is that portion of the window where the application can draw (the client area, for example, does not include the scroll bars, title bar, and border). This function returns the outline of the client area in a w.RECT object (the rect parameter, in this case). The Paint function retrieves this information so it can print a string centered within the client area.

The w.DrawText function is what does the real work as far as the nature of this program is concerned: this is the call that actually displays "Hello World!" within the window. The w.DrawText function uses the following prototype:

    DrawText: procedure

    (

            hDC         :dword;

            lpString    :string;

            nCount      :dword;

        var lpRect      :RECT;

            uFormat     :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__DrawTextA@20" );

The hDC parameter is a handle to the device context where w.DrawText is to put the text. In the call to w.DrawText appearing earlier, the Paint procedure passes in the hdc value returned by w.BeginPaint. The lpString parameter is a pointer to a zero-terminated sequence of ASCII characters (e.g., an HLA string object). The nCount parameter specifies the number of characters to print from the string; if you pass -1 (as this call does) then w.DrawText will display all the characters up to the zero-terminating byte. The lpRect parameter specifies a pair of (X,Y) coordinates that form a rectangle in the client area; w.DrawText will draw the text within this rectangular area based on the value of the uFormat parameter. The w.DT_SINGLELINE, w.DT_CENTER, and w.VCENTER parameters tell w.DrawText to place a single line of text in the window, centered vertically and horizontally within the rectangle supplied as the lpRect parameter.

After the call to w.DrawText, the Paint procedure calls the w.EndPaint API function. This completes the drawing sequence and it is at this point that Windows actually renders the text on the display device. Note that all drawing must take place between the w.BeginPaint and w.EndPaint calls. Additional calls to functions like w.DrawText are not legal once you call w.EndPaint. There are many additional functions you can use to draw information in the client area of the window; we'll start taking a look at some of these functions in the next chapter.

Here's the complete HelloWorld application:

// HelloWorld.hla:

//

// The Windows "Hello World" Program.

program HelloWorld;

#include( "wpa.hhf" )       // Standard windows stuff.

static

    hInstance:  dword;          // "Instance Handle" supplied by Windows.

    wc:     w.WNDCLASSEX;       // Our "window class" data.

    msg:    w.MSG;              // Windows messages go here.

    hwnd:   dword;              // Handle to our window.

readonly

    ClassName:  string := "HWWinClass";             // Window Class Name

    AppCaption: string := "Hello World Program";    // Caption for Window

// The following data type and DATA declaration

// defines the message handlers for this program.

type

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue:   dword;

            MessageHndlr:   MsgProc_t;

        endrecord;

// The dispatch table:

//

//  This table is where you add new messages and message handlers

//  to the program.  Each entry in the table must be a tMsgProcPtr

//  record containing two entries: the message value (a constant,

//  typically one of the wm.***** constants found in windows.hhf)

//  and a pointer to a "tMsgProc" procedure that will handle the

//  message.

readonly

    Dispatch:   MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the "wm.Destroy" message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

@nodisplay;

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the "wm.Paint" message.

//  For this simple "Hello World" application, this

//  procedure simply displays "Hello World" centered in the

//  application's window.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @nodisplay;

var

    hdc:    dword;              // Handle to video display device context

    ps:     w.PAINTSTRUCT;      // Used while painting text.

    rect:   w.RECT;             // Used to invalidate client rectangle.

begin Paint;

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    w.BeginPaint( hwnd, ps );

    mov( eax, hdc );

    w.GetClientRect( hwnd, rect );

    w.DrawText

    (

        hdc,

        "Hello World!",

        -1,

        rect,

        w.DT_SINGLELINE | w.DT_CENTER | w.DT_VCENTER

    );

    w.EndPaint( hwnd, ps );

end Paint;

/**************************************************************************/

/*                   End of Application Specific Code                     */

/**************************************************************************/

// The window procedure.  Since this gets called directly from

// windows we need to explicitly reverse the parameters (compared

// to the standard STDCALL declaration) in order to make HLA's

// Pascal calling convention compatible with Windows.

//

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword  );

    @stdcall;

    @nodisplay;

    @noalignstack;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin HelloWorld;

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    mov( eax, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end HelloWorld;

5.7: Compiling and Running HelloWorld From the Command Line

The hla.exe command-line program automatically runs several different programs during the compilation of an HLA source file. It runs the HLA compiler, proper (hlaparse.exe), it runs the ml.exe (MASM, the Microsoft Macro Assembler) program to assemble the .asm file that HLA produces4, it optionally runs the rc.exe (resource compiler) program if you specify any .rc files on the HLA command line, and it runs the link.exe program to link all the object files together to produce an executable. The hla.exe program is so flexible, it is all you will need to use for small projects5. However, there is one issue that you must consider when compiling GUI Windows applications with HLA: by default, HLA generates console applications, not Windows applications. Since we're compiling actual Windows applications, we need to tell HLA about this.

Telling HLA to compile Windows applications rather than console applications is very easy. All you've got to do is include the "-w" command line option as follows6:

hla -w helloWorld.hla

This command line option passes some information to the link.exe program so that it generates appropriate object code for a Windows app versus a console app. That's all there is to it! However, don't forget to include this option or your application may misbehave.

To run the helloWorld.exe application, you can either type "helloWorld" at the command line prompt or you can double-click on the helloWorld.exe application's icon. This should bring up a window in the middle of your display screen heralding the phrase "Hello World!" You can quit the program by clicking on the application's close box in the upper right hand corner of the window.

Although it is a relatively trivial matter to compile the "Hello World" program directly from the command line, this book will always provide a makefile that you can use to completely compile any full program example. That way, you can always use the same command to compile trivial as well as complex Windows applications. The accompanying CD-ROM contains all the source code for each major project appearing in this book; with each project appearing in its own subdirectory and each subdirectory containing a makefile that will build the executable for that project. Following the plan from Chapters one and three, the makefile for the "Hello World" application provide several options that interface with RadASM (see the next section) and provide the ability to do several different types of compiles from the command line. Here's a makefile for the "Hello World" application:

build: HelloWorld.exe

buildall: clean HelloWorld.exe

compilerc:

        echo No Resource Files to Process!

syntax:

        hla -s HelloWorld.hla

run: HelloWorld.exe

        HelloWorld

clean:

        delete /F /Q tmp

        delete *.exe

        delete *.obj

        delete *.link

        delete *.inc

        delete *.asm

        delete *.map

HelloWorld.obj: HelloWorld.hla wpa.hhf

        hla -p:tmp -w -c HelloWorld

HelloWorld.exe: HelloWorld.hla wpa.hhf

        hla -p:tmp -w HelloWorld

By default (that is, if you just type "make" at the command line) this makefile will build the executable for the HelloWorld.exe program, if it is currently out of date. You may also specify command line options like "buildall" or "clean" to do other operations. See Chapters one and three for more details on these options.

Whenever you consider the text-based version of the HLA "Hello World" program, this GUI version seems somewhat ridiculous. After all, the text-based version only requires the following HLA code:

program helloWorldText;

#include( "stdlib.hhf" )

begin helloWorldText;

    stdout.put( "Hello World!" nl );

end helloWorldText;

So why must the GUI version be so much larger? Well, for starters, the GUI version does a whole lot more than the text version. The text version prints "Hello World!" and that's about it. The GUI version, on the other hand, opens up a window that you can move around on the screen, resize, open up a system menu, minimize, maximize, and close. Today, people have been using Windows and Macintosh applications for so long that they take the effort needed to write such "trivial" code for granted. Rest assured, doing what this simple GUI "Hello World" application does would be a tremendous amount of work when running under an operating system like Microsoft's old DOS system where all the graphics manipulation was totally up to the application programmer. What the GUI "Hello World" application accomplishes in fewer than 300 lines of code would take thousands of lines of code under an OS like DOS.

5.8: Compiling and Running HelloWorld from RadASM

The HelloWorld directory on the accompanying CD-ROM contains the RadASM ".rap" (RadAsm Project) file and the makefile that RadASM can use to build this file. Just load HelloWorld.rap into RadASM and select "Build" or "Run" from the "Make" menu.

5.9: Goodbye World!

Well, we've just about beat the HelloWorld program into the ground. But that's good. Because you'll discover in the very next chapter than most Windows programs we write will not be written from scratch. Instead, we'll take some other program (usually HelloWorld) and tweak it according to our needs. So if you just skimmed through this material and said "uh-huh" and "oh-yeah" but you didn't really follow everything here, go back and read it again (and again, and again, and...). This chapter is truly the basis of everything that follows.

1One could argue that virtual method table and static class data is associated with the class, not an individual instance of a class, but unless you have at least one instance (object) of a class, there is no need for the static data or virtual method table in memory.
2The use of the term template, in this context, is generic. This has nothing to do with C++ templates.
3Despite its name, you actually use the w.ShowWindow API function to show or hide a window. See the API documentation for more details.
4HLA can produce code for other assemblers like TASM, FASM, and Gas. In this book, however, we'll assume the use of MASM.
5For larger projects, you will probably want to consider using a "make" program like Microsoft's NMAKE.EXE in order to speed up the development process and ease maintenance of your code. This text will generally avoid the use of makefiles so that there is one less thing you have to be concerned about.
6This book assumes that you've properly installed HLA and you've been able to compile small console-mode applications like a text-based "Hello World" program. See the HLA documentation for more details on setting up HLA if you haven't done this already.
TOC PREV NEXT INDEX


Chapter 6: Text in a GUI World

6.1: Text Display Under Windows

An ancient Chinese proverb tells us that "a picture is worth a thousand words." While this may certainly be true in many instances, pictures alone cannot convey information as efficiently as text. Imagine, for example, trying to write this book using only images, no text. So while a picture may be worth a thousand words, sometimes two or three words does the job a whole lot better than those thousand words. Anyone who has used a Windows application suffering from "icon overload" can appreciate the fact that text is still important in a GUI world.

Of course, text under Windows is a far cry from "console" or "green-screen" applications of yesterday. Writing console applications (even under Windows) is a fairly trivial exercise when you view the console display as a "glass teletype" to which you send a stream of characters. Fancier console applications treat the display as a two-dimensional object, allowing the software to position the cursor and text on the screen. However, such applications generally deal with fixed size (fixed pitch) character cells, so keeping track of the information on the display is still fairly trivial. Windows, of course, supports text of different fonts and sizes and you can finely position text on the display. This vastly complicates the display of text in the system. Another big difference between typical console applications and a Windows application is that the console device itself remembers the text written to the display (at least, for as long as such display is necessary). A Windows application, however, must remember all the text it's written to the display and be ready to redraw that text whenever Windows sends the application a redraw message. As such, a simple "text application" under Windows can be quite a bit more complex than an equivalent text-based console application.

As you saw in the previous chapter, writing text to a Windows' GUI display is not quite as trivial as calling HLA's stdout.put procedure. Console output is fundamentally different than text output in a GUI application. In console applications, the output characters are all the same size, they all use the same type style (typeface), and they typically employ a monospaced font (that is, each character is exactly the same width) that makes it easy to create columns of text by controlling the number of characters you write after the beginning of the line. The Windows' console API automatically handles several control characters such as backspaces, tabs, carriage returns, and linefeeds that make it easy to manipulate strings of text on the display. All the assumptions one can make about how the system displays text in a console application are lost when displaying text in a GUI application. There are three fundamental differences between console applications and GUI applications:

  • Console applications use a fixed font whereas GUI applications allow multiple fonts in a window display.
  • Console applications can assume that character output always occurs in fixed "character cells" on the display; this simplifies actions like aligning columns of text and predicting the output position of text on the screen. Furthermore, console applications always write lines of text to fixed line positions on the display (whereas GUI apps can write a line of text at any pixel position on the display).
  • Console applications can use special control characters in the output stream, like tab, carriage return, and line feed characters, to control the position of text on the display. GUI applications have to explicitly position this text on the display.

Because of the simplifying nature of text output in a console app, writing GUI apps is going to involve more complexity than writing console apps. The purpose of this chapter is to explain that additional complexity.

6.2: Painting

Although this chapter specifically deals with text output in a GUI application, you must remember that on the display screen there really is no such thing as text; everything is a graphic image. The text that appears on the screen is really nothing more than a graphic image. Therefore, drawing text is really nothing more than a special case of drawing some graphic image on a Windows display. Therefore, before we can begin discussing text output under Windows, we need to first discuss some generic information about drawing objects under Windows.

Windows that an application displays are usually divided into two different areas or regions: a client region and a non-client region. The client region consists of the area in the window that the application is responsible for maintaining. The non-client region is that portion of the window that Microsoft Windows maintains, including the borders, scroll bars, and title bar (see Figure 6-1). Although it is possible for an application to affect the non-client region under some special circumstances, for the most part, all drawing by the application in response to a w.WM_PAINT message occurs in the client region.

Figure 6-1: Client Versus Non-Client Areas in a Window

As the previous chapter explains, an application does not arbitrarily draw information in the application's window (client area or otherwise). Instead, Windows sends a message (w.WM_PAINT) to the application's window procedure and the window procedure takes the responsibility of (re)drawing the information in the window. One immediate question that should come up is "when does Windows notify the application that it needs to redraw the client area?" As you saw in the last chapter, one way to force Windows to send this message is by calling the w.UpdateWindow API function. When you called w.UpdateWindow and pass it the handle of a given window, Microsoft Windows will invalidate that window and post a message telling the Window to redraw itself.

Calling the w.UpdateWindow function isn't the only way that the contents of some window could become invalid. An application's window could become invalid because the user drags (or opens) some other window over the top of the window and then closes that other window. Because the area covered by the second window in the original application's window now contains the image drawn for that second window, the client area of the original window is invalid - it does not contain a valid image for the original window. When this happens, Microsoft Windows will invalidate the original window and post a w.WM_PAINT message to that window so it will redraw itself and repair the damage caused by overlaying that second window over the first.

Generally, an application will only call the w.UpdateWindow API function to force a redraw of the window from the application's main program. Normally to achieve this, you'd call the w.InvalidateRect API function:

type

    InvalidateRect: procedure

    (

                hWnd :dword;

        var		lpRect :RECT;

                bErase :boolean

    );

    @stdcall;

    @returns( "eax" );

    @external( "__imp__InvalidateRect@12" );

The first parameter is the handle of the window you want redrawn (e.g., the application's main window if you want everything redrawn); if you specify NULL here, Windows will redraw all windows. The second parameter is a pointer to a w.RECT data structure that specifies the rectangular region of the window you want redrawn. If you specify NULL as this parameter, Windows will tell the application to repaint the entire window; we'll discuss the use of this parameter later (as a means to improve window redrawing performance), for now you can safely specify NULL as the value for this parameter. The third parameter is a boolean value (true or false) that specifies whether Windows will first erase the background before you actually redraw the window. Again, this is an optimization that can save time redrawing the window by skipping the erase operation. Until you understand how to use this parameter, it's probably safest to pass true as this parameter's value.

When a window becomes invalid, Microsoft Windows sends the w.WM_PAINT message to the window procedure which, presumably, invokes the code or function that redraws the window. As you saw in the previous chapter, the drawing code is sandwiched between calls to the w.BeginPaint and w.EndPaint API calls. We're actually going to create a set of macros to handle the calls to w.BeginPaint and w.EndPaint a little later in this chapter; however, to understand how to write those macros you'll need to understand how these functions operate. But first, we've got to discuss another Windows' object: the device context.

6.2.1: Device Contexts

A device context is a Windows abstraction of an output device that is capable of rendering information sent to a window. Device contexts, or simply DCs, provide a machine-independent view of various display and printer devices allowing an application to write data to a single "device" yet have that information appear properly on a multitude of different real-world devices. For the purposes of this chapter (and several that follow), the
DC will generally represent a video display device; however, keep in mind that much of what this chapter discusses applies to printers and certain other output devices as well.

The actual device context data structure is internal to Windows; an application cannot directly manipulate a DC. Instead, Windows returns a handle to a device context and the application references the DC via this handle. Any modifications an application wants to make to the DC is done via API functions. We'll take a look at some of these functions that manipulate device context attributes in the very next section.

So the first question to ask is "how does an application obtain a device context handle for a given window?" Well, you've already seen one way: by calling the w.BeginPaint API function. For example, the following code is taken from the Paint procedure of the HelloWorld program from the previous chapter:

    w.BeginPaint( hWnd, ps ); // Returns device context handle in EAX

    mov( eax, hDC );

Calls to Win32 API functions that actually draw data on the display will require the device context handle that w.BeginPaint returns.

Whenever a window procedure receives a w.WM_PAINT message, the window procedure (or some subservient procedure it calls, like the Paint procedure in the HelloWorld program) typically calls w.BeginPaint to get the device context and do other DC initialization prior to actually drawing information to the device context. That device context is valid until the corresponding call to w.EndPaint.

Between the w.BeginPaint and w.EndPaint calls, the window procedure (and any subservient procedures like Paint) calls various functions that update the window's client region, like the w.DrawText function from the HelloWorld program of the previous chapter. An important thing to realize is that these individual calls don't immediately update the display (or whatever physical device is associated with the device context). Instead, Windows stores up the drawing requests (as part of the w.PAINTSTRUCT parameter, ps in the current example, that you pass to the w.BeginPaint API function). When you call the w.EndPaint function, Windows will begin updating the physical display device.

One issue with the calls to w.BeginPaint and w.EndPaint is that Windows assigns a very low priority w.WM_PAINT messages. This was done to help prevent a massive number of window redraws in response to every little change the application wants to make to the display. By making the w.WM_PAINT message low priority, Windows tends to save up (and coalesce) a sequence of window updates so the window procedure receives only a single w.WM_PAINT message in response to several window updates that need to be done. This reduces the number of w.WM_PAINT messages sent to the application and, therefore, reduces the number of times that the application redraws its window (thus speeding up the whole process). The only problem with this approach is that sometimes the system is processing a large number of higher-priority messages and the window doesn't get updated for some time. You've probably seen a case where you've closed a window and the system doesn't redraw the windows underneath for several seconds. When this happens, Windows (and various Windows applications) are busy processing other higher-priority messages and the w.WM_PAINT message has to wait.

On occasion, you'll need to immediately update some window immediately, without waiting for Windows to pass a w.WM_PAINT message to your window procedure and without waiting for the application to handle all the other higher-priority messages. You can achieve this by calling the w.GetDC and w.ReleaseDC API functions. These two functions have the following prototypes:

type

    GetDC: procedure

    (

        hWnd :dword

    );

    @stdcall;

    @returns( "eax" );

    @external( "__imp__GetDC@4" );

    ReleaseDC: procedure

    (

        hWnd :dword;

        hDC :dword

    );

    @stdcall;

    @returns( "eax" );

    @external( "__imp__ReleaseDC@8" );

You use these two functions just like w.BeginPaint and w.EndPaint (respectively) with just a couple differences. First of all, you don't pass these two parameters a w.PAINTSTRUCT parameter. This is because all calls to the drawing routines between these two functions immediately update the window (rather than storing those drawing operations up in a w.PAINTSTRUCT object). The second major difference is that you can call w.GetDC and w.ReleaseDC anywhere you have a valid window handle, not just in the section of code that handles the w.WM_PAINT message.

There is another variant of w.GetDC, w.GetWindowDC that returns a device context for the entire window, including the non-client areas. An application can use this API function to draw into the non-client areas. Generally, Microsoft recommends against drawing in the non-client area, but if you have a special requirement (like drawing a special image within the title bar), then this function is available. You use w.GetWindowDC just like w.GetDC. Like the call to w.GetDC, you must call the w.ReleaseDC function when you are through drawing to the window. Also like w.GetDC, you can call w.GetWindowDC anywhere, not just when handling a w.WM_PAINT message. Here's the function prototype for the w.GetWindowDC call:

type

    GetWindowDC: procedure

    (

        hWnd :dword

    );

    @stdcall;

    @returns( "eax" );

    @external( "__imp__GetWindowDC@4" );

6.2.2: Device Context Attributes

Although a DC attempts to abstract away the differences between various physical devices, there is no escaping the fact that behind the abstraction is a real, physical, device. And different real-world devices have some real-world differences that an application cannot simply ignore. For example, some devices (like display adapters) are capable of displaying color information whereas some devices (e.g., most laser printers) are only capable of black & white output. Likewise, some display devices are capable of displaying millions of different colors, while some displays are only capable of placing thousands or hundreds of different colors on the screen simultaneously. Although Windows will attempt to convert data intended for a generic device to each of these specific devices, the actual display (or printout) that the user sees will probably be much better if the application limits itself to the capabilities of the actual physical device. For this reason, Windows provides a set of API calls that let an application determine the capabilities of the underlying physical device.

Some of the attributes associated with a device context exist for convenience, not because of the physical capabilities of the underlying device. For example, the device context maintains default information such as what color to use when drawing text, what background color to use, the current font, the current line drawing style, how to actually draw an image onto the display, and lots of other information. Carrying this information around in the device context saves the programmer from having to pass this information as parameter data to each and every drawing function the application calls. For example, when drawing text one rarely changes the font with each string written to the display. It would be rather painful to have to specify the font information on each call to a function like w.DrawText. Instead, there are various calls you can make to Windows that will set the current device context attributes for things like fonts, colors, and so on, that will take effect on the next API call that draws information to the device context. We'll take a look at some of these functions a little later in this chapter.

6.2.3: Painting Text in the Client Area

Once you obtain a device context (DC) with a call to w.BeginPaint, w.GetDC, or w.GetWindowDC, you may begin drawing in the window. The purpose of this chapter and the next chapter is to introduce you to the various functions you can call to display data in a window. This chapter covers textual output using functions like w.DrawText and w.TextOut, the next chapter discusses those functions you'll use to draw graphic images in a window.

A little later in this section we'll take a look at the actual functions you can call to place textual data in some window of an application. For now, the important thing to note is that Windows only provides functions that display strings of text. There are no formatting routines like C++ stream I/O (cut), C's formatted print (printf), or HLA's stdout.put that automatically convert various data types to string form upon output. Instead, you must manually convert whatever data you want to display into string form and then write that string to the display. Fortunately, HLA provides a string formatting function whose syntax is nearly identical to stdout.put (str.put) that lets you easily convert binary data to string form. So to print data in some internal (e.g., integer or floating point) format, you can first call the str.put procedure to convert the data to a string and then call a Win32 API function like w.DrawText or w.TextOut to display the text in your application's window.

The str.put procedure (macro, actually) takes the following form:

str.put( stringVariable, <<list of items to convert to string form>> );

If you're familiar with the HLA stdout.put procedure, then you'll be right at home with the str.put procedure. Except for the presence of a string variable at the beginning of the str.put parameter list, you use the str.put procedure the same way you use stdout.put; the major difference between these two functions is that str.put "writes" its output to the string variable you specify rather than writing it to the standard output device. Here's an example of a typical str.put call:

str.put( OutputString, "Value of i:", i:4, " Value of r:", r:10:2 );

The str.put procedure converts the items following the first parameter into a string form and then stores the resulting string into the variable you specify as the first parameter in the call to str.put. You must ensure that you've preallocated enough storage for the string to hold whatever data you write to the output string variable. If you're outputting lines of text to the window (one line at a time), then 256 characters is probably a reasonable amount of storage (more than generous, in fact) to allocate for the output string. If you don't have to worry about issues of reentrancy in multi-threaded applications, you can statically allocate a string to hold the output of str.put thusly:

static

    OutputString: str.strvar( 256 );

The str.init macro declares OutputString as a string variable and initializes it with a pointer to a string buffer capable of holding up to 256 characters. The problem, of course, with using static allocation is that if two threads wind up executing some code that manipulates OutputString simultaneously, then your program will produce incorrect output. Although most of the applications you'll write won't be multi-threaded, it's still a good idea to get in the habit of allocating the storage dynamically to avoid problems if you decide to change some existing code to make it multi-threaded in the future.

Of course, the standard way to dynamically allocate storage for a string in HLA is via the stralloc function. The only problem with stralloc is that it ultimately winds up calling Windows' memory management API. While there is nothing fundamentally wrong with doing this, keep in mind two facts: calling Win32 API functions are somewhat expensive (and the memory management calls can be expensive) and we're briefly allocating storage for this string. Once we've converted our output to string form and displayed it in the window, we don't really need the string data anymore. True, we can use the same string variable for several output operations, but the bottom line is that when our Paint procedure (or whomever is handling the w.WM_PAINT message) returns, that string data is no longer needed and we'll have to deallocate the storage (i.e., another expensive call to the Win32 API via the strfree invocation). A more efficient solution is to allocate the string object as local storage within the activation record of the procedure that contains the call to str.put. HLA provides two functions to help you do this efficiently: str.init and tstralloc.

The str.init function takes an existing (preallocated) buffer and initializes it for use as a string variable. This function requires two parameters: a buffer and the size (in bytes) of that buffer. This function returns a pointer to the string object it creates within that buffer area and initializes that string to the empty string. You would normally store the return result of this function (in EAX) into an HLA string variable. Here's an example of the use of this function:

procedure demoStrInit;

var

    sVar:			string;

    buffer:			char[272];

begin demoStrInit;

    str.init( buffer, @size( buffer ));

    mov( eax, sVar );

        .

        .

        .

end demoStrInit;

An important thing to note about str.init is that the pointer it returns does not contain the address of the first character buffer area whose address you pass as the first parameter. Instead, str.init may skip some bytes at the beginning of the buffer space in order to double-word align the string data, then it sets aside eight bytes for the HLA string's maximum length and current length values (str.init also initializes these fields). The str.init function returns the address of the first byte beyond these two double-word fields (as per the definition of an HLA string). Once you initialize a buffer for use as a string object via str.init, you should only manipulate the data in that buffer using HLA string functions via the string pointer that str.init returns (e.g, the sVar variable in this example). One issue of which you should be aware when using the str.init function is that the buffer you pass it must contain the maximum number of characters you want to allow in the string plus sixteen. That is why buffer in the current example contains 272 characters rather than 256. The str.init function uses these extra characters to hold the maximum and current length values, the zero terminating byte, and any padding characters needed to align the string data on a double-word boundary.

One thing nice about the str.init function is that you don't have to worry about deallocating the storage for the string object if both the string variable (where you store the pointer) and the buffer containing the string data are automatic (VAR) variables. The procedure call and return will automatically allocate and deallocate storage for these variables. This scheme is very convenient to use.

Another way to allocate and initialize a string variable is via the tstralloc function. This function allocates storage on the stack for a string by dropping the stack point down the necessary number of bytes and initializing the maximum and current length fields of the data it allocates. This function returns a pointer to the string data it creates that you can store into an HLA string variable. Here's an example of the call to the tstralloc function:

procedure demoTStrAlloc;

var

    s: string;

begin demoTStrAlloc;

    tstralloc( 256 );

    mov( eax, s );

        .

        .

        .

end demoTStrAlloc;

Note that the call to tstralloc drops the stack down by as many as 12 bytes beyond the number you specify. In general, you won't know exactly how many bytes by which tstralloc will drop the stack. Therefore, it's not a good idea to call this function if you've got stuff sitting on the stack that you'll need to manipulate prior to returning from the function. Generally, most people call tstralloc immediately upon entry into a procedure (after the procedure has built the activation record), before pushing any other data onto the stack. By doing so, the procedure's exit code will automatically deallocate the string storage when it destroys the procedure's activation record. If you've got some data sitting on the stack prior to calling tstralloc, you'll probably want to save the value in the ESP register prior to calling tstralloc so you can manually deallocate the string storage later (by loading ESP with this value you've saved).

As you saw in the last chapter, the w.DrawText function is capable of rendering textual data in a window. Here's the HLA prototype for this API function:

static

    DrawText: procedure

    (

            hDC :dword;

            lpString :string;

            nCount :dword;

        var lpRect :RECT;

            uFormat :dword

    );

    @stdcall;

    @returns( "eax" );

    @external( "__imp__DrawTextA@20" );

The hDC parameter is a device context handle (that you obtain from the w.BeginPaint, or equivalent, function). The lpString parameter is a pointer to a zero terminated sequence of characters (e.g., an HLA string) that w.DrawText is to display. The nCount parameter specifies the length of the string (in characters). If nCount contains -1, then w.DrawText will determine render all characters up to a zero terminating byte. The lpRect parameter is a pointer to a w.RECT structure. A w.RECT record contains four int32 fields: top, left, bottom, and right. These fields specify the x- and y- coordinates of the top/left hand corner of a rectangular object and the bottom/right hand corner of that object. The w.DrawText function will render the text within this rectangle. The coordinates are relative to the client area of the window associated with the device context (see Figure 6-1 concerning the client area). Although Windows supports several different coordinate systems, the default system is what you'll probably expect with coordinate (0,0) appearing in the upper left hand corner of the window with x-coordinate values increasing as you move left and y-coordinate values increasing as you move down (see Figure 6-2).

Figure 6-2: Windows' Default Coordinate System

The w.DrawText function will format the string within the rectangle the lpRect parameter specifies, including wrapping the text if necessary. The uFormat parameter is a uns32 value that specifies the type of formatting that w.DrawText is to apply to the text when rendering it. There are a few too many options to list here (see the User32 API Reference on the accompanying CD for more details), but a few of the more common formatting options should give you the flavor of what is possible with w.DrawText:

  • w.DT_CENTER: centers the text within the rectangle
  • w.DT_EXPANDTABS: expands tab characters within the string
  • w.DT_LEFT: left-aligns the string in the rectangle
  • w.DT_RIGHT: right-aligns the string in the rectangle
  • w.DT_SINGLELINE: only allows a single line of text in the rectangle, turns off the processing of carriage returns and line feeds
  • w.DT_VCENTER: vertically centers the text in the rectangle
  • w.DT_WORDBREAK: turns on word break - lines are broken on word boundaries when you use this option

These w.DrawText uFormat values are bit values that you may combine with the HLA "|" bitwise-OR operator (as long as the combination makes sense). For example, "w.DT_SINGLELINE | w.DT_EXPANDTABS" is a perfectly legitimate value to supply for the uFormat parameter.

The w.DrawText function is very powerful, providing some very powerful formatting options. This formatting capability, however, comes at a cost. Not only is the function a tad bit slow (when it has to do all that formatting), but it is also somewhat inconvenient to use; particularly due to the fact that you must supply a bounding rectangle (which is a pain to set up prior to the call). Without question, the most popular text output routine is the w.TextOut function. Here's its prototype:

static

    TextOut:procedure

    (

            hDC			:dword;

            nXStart			:int32;

            nYStart			:int32;

            lpString			:string;

            cbString			:dword

    );

    @stdcall;

    @returns( "eax" );

    @external( "__imp__TextOutA@20" );

As usual, the hDC is the device context handle that a function like w.BeginPaint returns to specify where w.TextOut draws the text. The nXStart and nYStart values specify where w.TextOut will begin placing the text in the window (these coordinates are relative to the client area specified by the hDC parameter). The exact nature of these coordinate value depends upon the text-alignment mode maintained by the device context. Generally, these coordinates specify the upper-left-most point of the string when drawn to the window. However, you may change this behavior by calling the w.SetTextAlign function and changing this attribute within the device context. The lpString parameter is a pointer to a sequence of characters (e.g., an HLA string). Note that you do not have to zero-terminate this string, as you must supply the length of the string (in characters) in the cbString parameter. Here's a simple call to the w.TextOut API function that display's "Hello World" near the upper left-hand corner of some window:

w.TextOut( hDC, 10, 10, "Hello World", 11 );

The w.TextOut function does not process any control characters. Instead, it draws them on the screen using various graphic images for each of these character codes. In particular, w.TextOut does not recognize tab characters. Windows does provide a variant of w.TextOut, w.TabbedTextOut, that will expand tab characters. Here's the prototype for this function:

static

    TabbedTextOut:

        procedure

        (

                    hDC :dword;

                    X :dword;

                    Y :dword;

                    lpString :string;

                    nCount :dword;

                    nTabPositions :dword;

            var		lpnTabStopPositions :dword;

                    nTabOrigin :dword

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__TabbedTextOutA@32" );

The hDC, X, Y, lpString, and nCount parameters have the same meaning as the w.TextOut parameters (nCount is the number of characters in lpString). The nTabPositions parameter specifies the number of tab positions appearing in the lpnTabStopPositions (array) parameter. The lpnTabStopPositions parameter is a pointer to the first element of an array of 32-bit unsigned integer values. This array should contain at least nTabPositions elements. The array contains the number of pixels to skip to (relative to the value of the last parameter) for each tab stop that w.TabbedTextOut encounters in the string. For example, if nTabPositions contains 4 and the lpnTabStopPositions array contains 10, 30, 40, and 60, then the tab positions will be at pixels nTabOrigin+10, nTabOrigin+30, nTabOrigin+40, and nTabOrigin+60. As this discussion suggests, the last parameter, nTabOrigin, specifies the pixel position from the start of the string where the tab positions begin.

If the nTabPosition parameter is zero or lpnTabStopPositions pointer is NULL, then w.TabbedTextOut will create a default set of tab stops appearing at an average of eight character positions apart for the current default font. If nTabPosition is one, then w.TabbedTextOut will generate a sequence of tab stops repeating every n pixels, where n is the value held by the first (or only) element of lpnTabStopPositions. See the following listing for an example of the use of the w.TabbedTextOut function. Figure 6-3 shows the output from this application.

// TabbedText.hla:

//

// Demonstrates the use of the w.TabbedTextOut function.

program TabbedText;

#include( "stdlib.hhf" )

#include( "w.hhf" )     // Standard windows stuff.

#include( "wpa.hhf" )   // "Windows Programming in Assembly" specific stuff.

?@nodisplay := true;    // Disable extra code generation in each procedure.

?@nostackalign := true; // Stacks are always aligned, no need for extra code.

const

    tab :text := "#$9"; // Tab character

static

    hInstance:  dword;          // "Instance Handle" supplied by Windows.

    wc:     w.WNDCLASSEX;       // Our "window class" data.

    msg:    w.MSG;              // Windows messages go here.

    hwnd:   dword;              // Handle to our window.

readonly

    ClassName:  string := "TabbedTextWinClass";     // Window Class Name

    AppCaption: string := "TabbedTextOut Demo";     // Caption for Window

// The following data type and DATA declaration

// defines the message handlers for this program.

type

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue:   dword;

            MessageHndlr:   MsgProc_t;

        endrecord;

// The dispatch table:

//

//  This table is where you add new messages and message handlers

//  to the program.  Each entry in the table must be a tMsgProcPtr

//  record containing two entries: the message value (a constant,

//  typically one of the wm.***** constants found in windows.hhf)

//  and a pointer to a "tMsgProc" procedure that will handle the

//  message.

readonly

    Dispatch:   MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the "wm.Destroy" message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

@nodisplay;

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the "wm.Paint" message.

//  This procedure displays several lines of text with

//  tab characters embedded in them.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @nodisplay;

const

    LinesOfText := 8;

    NumTabs     := 4;

var

    hdc:    dword;              // Handle to video display device context

    ps:     w.PAINTSTRUCT;      // Used while painting text.

    rect:   w.RECT;             // Used to invalidate client rectangle.

readonly

    TabStops        :dword[ NumTabs ] := [50, 100, 200, 250];

    TextToDisplay   :string[ LinesOfText ] :=

        [

            "Line 1:" tab "Col 1" tab "Col 2" tab "Col 3",

            "Line 2:" tab "1234"  tab "abcd"  tab "++",

            "Line 3:" tab "0"     tab "efgh"  tab "=",

            "Line 4:" tab "55"    tab "ijkl"  tab ".",

            "Line 5:" tab "1.34"  tab "mnop"  tab ",",

            "Line 6:" tab "-23"   tab "qrs"   tab "[]",

            "Line 7:" tab "+32"   tab "tuv"   tab "()",

            "Line 8:" tab "54321" tab "wxyz"  tab "{}"

        ];

begin Paint;

    push( ebx );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    w.BeginPaint( hwnd, ps );

    mov( eax, hdc );

    for( mov( 0, ebx ); ebx < LinesOfText; inc( ebx )) do

        intmul( 20, ebx, ecx );

        add( 10, ecx );

        w.TabbedTextOut

        (

            hdc,

            10,

            ecx,

            TextToDisplay[ ebx*4 ],

            str.length( TextToDisplay[ ebx*4 ] ),

            NumTabs,

            TabStops,

            0

        );

    endfor;

    w.EndPaint( hwnd, ps );

    pop( ebx );

end Paint;

/**************************************************************************/

/*                   End of Application Specific Code                     */

/**************************************************************************/

// The window procedure.  Since this gets called directly from

// windows we need to explicitly reverse the parameters (compared

// to the standard STDCALL declaration) in order to make HLA's

// Pascal calling convention compatible with Windows.

//

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword  );

    @stdcall;

    @nodisplay;

    @noalignstack;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin TabbedText;

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    mov( eax, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end TabbedText;

Figure 6-3: TabbedText.HLA Output

The Microsoft Windows API also provides an extended version of the w.TextOut function, appropriately named w.ExtTextOut. This function uses a couple of additional parameters to specify some additional formatting options. Here's the prototype for the w.ExtTextOut function:

static

    ExtTextOut: procedure

    (

                hdc				:dword;

                x				:dword;

                y				:dword;

                fuOptions				:dword;

        var		lprc				:RECT;

                lpString				:string;

                cbCount				:dword;

        var		lpDx				:var

    );

    @stdcall;

    @returns( "eax" );

    @external( "__imp__ExtTextOutA@32" );

The hdc, x, y, lpString, and cbCount parameters are compatible to the parameters you supply to the w.TextOut function. The fuOptions parameter specifies how to use the application-defined lprc rectangle. The possible options include clipping the text to the rectangle, specifying an opaque background using the rectangle, and certain non-western alphabet options. Please consult the GDI32 documentation on the accompanying CD-ROM for more details concerning the fuOptions and lprc parameters. The lpDx array is option. If you pass NULL as this parameter's value, then the w.ExtTextOut function will use the default character spacing for each of the characters that it draws to the window. If this pointer is non-NULL, then it must point to an array of uns32 values containing the same number of elements as there are characters in the string. These array elements specify the spacing between the characters in the string. This feature allows you to condense or expand the characters' spacing when the w.ExtTextOut function draws those characters to the window. This feature is quite useful for word processors and desktop publishing systems that need to take detailed control over the output of characters on the output medium. This book will tend to favor the w.TextOut function over the w.ExtTextOut function, because the former is easier to use. For more details concerning the w.ExtTextOut function, please see the CD-ROM accompanying this book.

As noted in the previous section, the device context maintains certain default values so that you do not have to specify these values on each and every call to various GDI functions. One important attribute for text output is the text align attribute. This attribute specifies how functions like w.TextOut and w.ExtTextOut interpret the coordinate values you pass as parameters. The text align attributes (kept as a bitmap) are the following:

  • w.TA_BASELINE - The reference point will be on the base line of the text
  • w.TA_BOTTOM - The reference point will be on the bottom line of the bounding rectangle surrounding the text
  • w.TA_TOP - The reference point will be on the top line of the bounding rectangle surrounding the text
  • w.TA_CENTER - The reference point will be aligned with the horizontal center of the bounding rectangle surrounding the text
  • w.TA_LEFT - The reference point will be on the left edge of the bounding rectangle surrounding the text
  • w.TA_RIGHT - The reference point will be on the right edge of the bounding rectangle surrounding the text
  • w.TA_NOUPDATECP - The current position is not updated after outputting text to the window
  • w.TA_RTLREADING - Used only for right-to-left reading text (e.g., middle eastern text)
  • w.TA_UPDATECP - The output routines update the current position after outputting the text.

These values are all bits in a bit mask and you may combine various options that make sense (e.g., w.TA_BOTTOM and w.TA_TOP are mutually exclusive). The default values are "w.TA_TOP | w.TA_LEFT | w.TA_NOUPDATECP". This means that unless you change the text align attribute value, the x- and y- coordinates you specify in the w.TextOut and w.ExtTextOut calls supply the top/left coordinate of the bounding rectangle where output is to commence (see Figure 6-4). For right-aligned text, you'd normally use the combination w.TA_RIGHT | w.TA_TOP | w.TA_NOUPDATECP. Other options are certainly possible, applications that allow the user to align text with other objects would normally make use of these other text alignment options.

Figure 6-4: Default Reference Point for Textual Output

The w.TA_NOUPDATECP and w.TA_UPDATECP options are fairly interesting. If the w.TA_NOUPDATECP option is set then the application must explicitly state the (x,y) position where Windows begins drawing the text on the display. However, if the w.TA_UPDATECP flag is set, then Windows ignores the X- and Y- coordinate values you supply (except on the first call) and Windows automatically updates the cursor position based on the width of the text you write to the display (and any special control characters such as carriage returns and line feeds).

You can retrieve and set the text alignment attribute via the w.GetTextAlign and w.SetTextAlign API functions. These functions have the following prototypes:

static

    GetTextAlign:

        procedure

        (

            hdc :dword

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetTextAlign@4" );

    SetTextAlign:

        procedure

        (

            hdc :dword;

            fMode :dword

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetTextAlign@8" );

You pass the w.GetTextAlign function a device context handle (that you obtain from w.BeginPaint) and it returns the attribute bit map value in the EAX register. You pass the w.SetTextAlign function the device context handle and the new bitmap value (in the fMode parameter). This fMode parameter can be any combination of the w.TA_XXXX constants listed earlier; you may combine these constants with the HLA bitwise-OR operator ("|"), e.g.,

    // The following statement restores the default text align values:

    w.SetTextAlign( hDC, w.TA_TOP | w.TA_LEFT | w.TA_NOUPDATECP );

Another set of important text attributes are the text color and background mode attributes. Windows lets you specify a foreground color (that is used to draw the actual characters), a background color (the background is a rectangular area surrounding the text) and the opacity of the background. You can manipulate these attributes using the following Win32 API functions:

static

    GetBkColor:

        procedure

        (

            hdc :dword

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetBkColor@4" );

    GetTextColor:

        procedure

        (

            hdc :dword

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetTextColor@4" );

    SetBkColor:

        procedure

        (

            hdc :dword;

            crColor :dword

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetBkColor@8" );

    SetTextColor:

        procedure

        (

            hdc :dword;

            crColor :COLORREF

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetTextColor@8" );

    SetBkMode:

        procedure

        (

            hdc :dword;

            iBkMode :dword

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetBkMode@8" );

    GetBkMode:

        procedure

        (

            hdc :dword

        );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetBkMode@4" );

As is typical for most GDI (Windows' Graphic Device Interface) functions, these functions all take a parameter that is the handle for the current device context (hdc); as such, you must only call these functions between a w.BeginPaint and w.EndPaint sequence (or w.GetDC/w.GetWindowDC and w.ReleaseDC sequence). The w.GetXXXX functions return a color or mode value in EAX. The w.SetXXXX functions all take a second dword parameter that specifies the new color or mode attribute.

The `color' functions (w.SetTextColor, w.SetBkColor, w.GetTextColor, and w.GetBkColor) work with 24-bit "RGB" (red-green-blue) values. An RGB value consists of three eight-bit values specifying shades for red, green, and blue that Windows will use to approximate your desired color on the output device. The blue value appears in the low-order eight bits, the green value appears in bits 8-15 of the 24-bit value, and the red value appears in bits 16-23. The w.GetXXXX functions return an RGB value in the EAX register. The w.SetXXXX functions pass the 24-bit RGB value as a double word parameter (the high-order eight bits of the double word should contain zero). The wpa.hhf header file contains a macro (RGB) that accepts three eight-bit constants and merges them together to produce a 24-bit RGB value. You can use this macro in the w.SetTextColor and w.SetBkColor function calls thusly:

    w.SetTextColor( hdc, RGB( redValue, greenValue, blueValue ));

    w.SetBkColor( hdc, RGB( $FF, 0, 0 ));

It is important to remember that RGB is a macro that only accepts constants; you cannot supply variables, registers, or other non-constant values to this macro. It's easy enough to write a function to which you could pass three arbitrary eight-bit values, but merging red-green-blue values into a 24-bit is sufficiently trivial that it's best to simply do this operation in-line, e.g.,

    mov( blue, ah );

    mov( 0, al );

    bswap( eax );

    mov( red, al );

    mov( green, ah ); // RGB value is now in EAX.

The w.SetTextColor API function sets the color that Windows uses when drawing text to the device context. Note that not all devices support a 24-bit color space, Windows will approximate the color (using dithering or other techniques) if the device does not support the actual color you specify.

The w.SetBkColor function sets the background that Windows draws when rendering text to the device. Windows draws this background color to a rectangle immediately surrounding the output text. Obviously, there should be a fair amount of contrast between the background color you select and the foreground (text) color or the text will be difficult to read.

Windows always draws a solid color (or a dithering approximation), never a pattern as the background color for text. By default, Windows always draws the background color (rectangle) prior to rendering the text on the device. However, you can tell Windows to draw only the text without the background color by setting the background mode to "transparent". This is achieved via the call to w.SetBkMode and passing a device context handle and either the constant w.TRANSPARENT (to stop drawing the background rectangle) or w.OPAQUE (to start drawing the background rectangle behind the text).

The following listing demonstrates the use of the w.SetTextColor, w.SetBkColor, and w.SetBkMode functions to draw text using various shades of gray. This also demonstrates the w.OPAQUE and w.TRANSPARENT drawing modes by overlaying some text on the display. The output of this application appears in Figure 6-5.

// TextAttr.hla:

//

// Displays text with various colors and attributes.

program TextATtr;

#include( "w.hhf" )     // Standard windows stuff.

#include( "wpa.hhf" )   // "Windows Programming in Assembly" specific stuff.

?@nodisplay := true;    // Disable extra code generation in each procedure.

?@nostackalign := true; // Stacks are always aligned, no need for extra code.

static

    hInstance:  dword;          // "Instance Handle" supplied by Windows.

    wc:     w.WNDCLASSEX;       // Our "window class" data.

    msg:    w.MSG;              // Windows messages go here.

    hwnd:   dword;              // Handle to our window.

readonly

    ClassName:  string := "TextAttrWinClass";       // Window Class Name

    AppCaption: string := "Text Attributes";        // Caption for Window

// The following data type and DATA declaration

// defines the message handlers for this program.

type

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue:   dword;

            MessageHndlr:   MsgProc_t;

        endrecord;

// The dispatch table:

//

//  This table is where you add new messages and message handlers

//  to the program.  Each entry in the table must be a tMsgProcPtr

//  record containing two entries: the message value (a constant,

//  typically one of the wm.***** constants found in windows.hhf)

//  and a pointer to a "tMsgProc" procedure that will handle the

//  message.

readonly

    Dispatch:   MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the "wm.Destroy" message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

@nodisplay;

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the "wm.Paint" message.

//  This procedure displays several lines of text with

//  different colors.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @nodisplay;

const

    LinesOfText := 8;

    TextToDisplay :string := "Text in shades of gray";

    OverlaidText  :string := "Overlaid Text";

    fgShades :w.RGBTRIPLE[ LinesOfText ] :=

        [

            w.RGBTRIPLE:[ 0, 0, 0],

            w.RGBTRIPLE:[ $10, $10, $10 ],

            w.RGBTRIPLE:[ $20, $20, $20 ],

            w.RGBTRIPLE:[ $30, $30, $30 ],

            w.RGBTRIPLE:[ $40, $40, $40 ],

            w.RGBTRIPLE:[ $50, $50, $50 ],

            w.RGBTRIPLE:[ $60, $60, $60 ],

            w.RGBTRIPLE:[ $70, $70, $70 ]

        ];

    bgShades : w.RGBTRIPLE[ LinesOfText ] :=

        [

            w.RGBTRIPLE:[ $F0, $F0, $F0 ],

            w.RGBTRIPLE:[ $E0, $E0, $E0 ],

            w.RGBTRIPLE:[ $D0, $D0, $D0 ],

            w.RGBTRIPLE:[ $C0, $C0, $C0 ],

            w.RGBTRIPLE:[ $B0, $B0, $B0 ],

            w.RGBTRIPLE:[ $A0, $A0, $A0 ],

            w.RGBTRIPLE:[ $90, $90, $90 ],

            w.RGBTRIPLE:[ $80, $80, $80 ]

        ];

var

    hdc:    dword;              // Handle to video display device context

    ps:     w.PAINTSTRUCT;      // Used while painting text.

    rect:   w.RECT;             // Used to invalidate client rectangle.

begin Paint;

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    w.BeginPaint( hwnd, ps );

    mov( eax, hdc );

    w.SetBkMode( hdc, w.OPAQUE );

    #for( i := 0 to LinesOfText-1 )

        w.SetTextColor

        (

            hdc,

            RGB

            (

                fgShades[i].rgbtRed,

                fgShades[i].rgbtGreen,

                fgShades[i].rgbtBlue

            )

        );

        w.SetBkColor

        (

            hdc,

            RGB

            (

                bgShades[i].rgbtRed,

                bgShades[i].rgbtGreen,

                bgShades[i].rgbtBlue

            )

        );

        w.TextOut( hdc, 10, i*20+10, TextToDisplay, @length( TextToDisplay ));

    #endfor

    w.SetBkMode( hdc, w.TRANSPARENT );

    #for( i := 0 to LinesOfText-1 )

        w.SetTextColor

        (

            hdc,

            RGB

            (

                fgShades[i].rgbtRed,

                fgShades[i].rgbtGreen,

                fgShades[i].rgbtBlue

            )

        );

        w.TextOut( hdc, 100, i*20+20, TextToDisplay, @length( TextToDisplay ));

    #endfor

    w.EndPaint( hwnd, ps );

end Paint;

/**************************************************************************/

/*                   End of Application Specific Code                     */

/**************************************************************************/

// The window procedure.  Since this gets called directly from

// windows we need to explicitly reverse the parameters (compared

// to the standard STDCALL declaration) in order to make HLA's

// Pascal calling convention compatible with Windows.

//

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword  );

    @stdcall;

    @nodisplay;

    @noalignstack;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin TextATtr;

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    mov( eax, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end TextATtr;

Figure 6-5: Output From TextAttr Application

6.2.4: BeginPaint, EndPaint, GetDC, GetWindowDC, and ReleaseDC Macros

One minor issue concerns the use of GDI and Windows' User Interface API functions like w.TextOut - they are only legal between calls to functions like w.BeginPaint and w.EndPaint that obtain and release a device context. Furthermore, if an application calls a function like w.BeginPaint, it must ensure that it calls the corresponding function to release the DC (e.g., w.EndPaint). Unfortunately, Windows depends upon programmer discipline to enforce these requirements. Fortunately, this situation is easy to correct in HLA using HLA's context-free macro facilities.

An HLA context-free macro invocation always consists of at least two keywords: an initial macro invocation and a corresponding terminator invocation. Between these two invocations, HLA maintains a macro context that allows these two macros (as well as other keyword macros) to communicate information. The initial invocation and the terminator invocation bracket a sequence of statements, much like the HLA begin/end while/endwhile, repeat/until, and for/endfor reserved word pairs. An HLA context-free macro declaration takes the following form:

    #macro macroName(optionalParameters) :<<optional local symbol definitions>>;

        <<Text to expand on macroName invocation>>

        // #keyword is optional, and you may have multiple keyword macros:

        #keyword keywordName(optionalParameters) :<<optional local symbol definitions>>;

            <<Text to expand on keywordName invocation>>

        // Must have exactly one #terminator declaration if this is a context-free

        // macro:

        #terminator terminatorName(optionalParameters) :<<optional local symbols>>;

            <<Text to expand on terminatorName invocation>>

    #endmacro

For more information about the syntax of an HLA context-free macro declaration, please see the HLA reference manual (it appears on the CD-ROM accompanying this book).

Whenever you invoke a context-free macro, you must also invoke the corresponding terminator associated with that context-free macro. If you fail to do this, HLA will report an error. For example, we can create a simple BeginPaint and EndPaint context-free macro using the following HLA declaration:

    #macro BeginPaint( _hwnd, _ps );

        w.BeginPaint( _hwnd, _ps );

    #terminator EndPaint( _hwnd, _ps );

        w.EndPaint( _hwnd, _ps );

    #endmacro

Now you may invoke BeginPaint and EndPaint as follows:

    BeginPaint( hWnd, ps );

        mov( eax, hdc );

            .

            .

            .

        << code that uses the device context >>

            .

            .

            .

    EndPaint( hWnd, ps );

Although this macro saves you a small amount of typing (not having to type the "w." in front of the w.BeginPaint and w.EndPaint calls), saving some typing is not the purpose of this macro. Helping you produce correct code is the real purpose behind this macro invocation. As it turns out, HLA will not allow you to invoke the EndPaint macro without an earlier invocation of the BeginPaint macro. Likewise, if you fail to provide a call to the EndPaint macro, HLA will complain that it's missing (much like HLA complains about a missing endif or endwhile). Therefore, it's a little safer to use the BeginPaint/EndPaint macros rather than directly calling the w.BeginPaint and w.EndPaint API functions. This safety is the primary reason for using these macros.

Now saving typing isn't a bad thing, mind you. In fact, with a few minor changes to our macros, we can save a bit of typing. Consider the following macro declaration:

    #macro BeginPaint( _hwnd, _ps, _hdc );

        w.BeginPaint( _hwnd, _ps );

        mov( eax, _hdc );

    #terminator EndPaint;

        w.EndPaint( _hwnd, _ps );

    #endmacro;

With this macro declaration, you can invoke BeginPaint and EndPaint thusly:

    BeginPaint( hWnd, ps, hDC );

            .

            .

            .

        << Code that uses the device context (hDC) >>

            .

            .

            .

    EndPaint;

Note the convenience that this macro provides - it automatically supplies the parameters for the w.EndPaint call (because they're the same parameters you pass to BeginPaint, this context-free macro passes those same parameters on through to the w.EndPaint call). This is useful because you avoid the mistake of supplying an incorrect parameter to w.EndPaint (i.e., you don't have to worry about keeping the parameters passed to w.BeginPaint and w.EndPaint consistent).

HLA's context-free macros also allow you to create #keyword macros. These are macros that you may only invoke between the corresponding initial invocation and the #terminator macro invocation. Note that HLA will generate an error if you attempt to invoke one of these macros outside the initial/terminator macro invocation sequence. This works out perfectly for the GDI/User Interface API calls that reference a device context handle because you may only call these functions between a pair of calls that allocate and release a device context (like w.BeginPaint and w.EndPaint). By defining #keyword macros for all these functions, we can ensure that you may only call them between the BeginPaint and EndPaint macro invocations. A side benefit to defining these API calls this way is that we can drop a parameter (hDC) to each of these functions and have the macro automatically supply this parameter for us. Consider the following sequence that defines macros for the w.TextOut and w.TabbedTextOut API calls:

    #macro BeginPaint( _hwnd, _ps, _hdc );

        w.BeginPaint( _hwnd, _ps );

        mov( eax, _hdc );

      #keyword TextOut( _x, _y, _str, _len);

        w.TextOut( _hdc, _x, _y, _str, _len );

      #keyword TabbedTextOut( _x, _y, _str, _len, _tabCnt, _tabs, _offset );

        w.TabbedTextOut( _hdc, _x, _y, _str, _len, _tabCnt, _tabs, _offset );

      #terminator EndPaint;

        w.EndPaint( _hwnd, _ps );

    #endmacro;

The following is the Paint procedure from the TabbedTextDemo program (appearing earlier in this chapter) that demonstrates the use of the BeginPaint, TabbedTextOut, and EndPaint macro invocations:

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @nodisplay;

const

    LinesOfText := 8;

    NumTabs     := 4;

var

    hdc:    dword;              // Handle to video display device context

    ps:     w.PAINTSTRUCT;      // Used while painting text.

    rect:   w.RECT;             // Used to invalidate client rectangle.

readonly

    TabStops        :dword[ NumTabs ] := [50, 100, 200, 250];

    TextToDisplay   :string[ LinesOfText ] :=

        [

            "Line 1:" tab "Col 1" tab "Col 2" tab "Col 3",

            "Line 2:" tab "1234"  tab "abcd"  tab "++",

            "Line 3:" tab "0"     tab "efgh"  tab "=",

            "Line 4:" tab "55"    tab "ijkl"  tab ".",

            "Line 5:" tab "1.34"  tab "mnop"  tab ",",

            "Line 6:" tab "-23"   tab "qrs"   tab "[]",

            "Line 7:" tab "+32"   tab "tuv"   tab "()",

            "Line 8:" tab "54321" tab "wxyz"  tab "{}"

        ];

begin Paint;

    push( ebx );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., TabbedTextOut) must

    // appear within a BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        for( mov( 0, ebx ); ebx < LinesOfText; inc( ebx )) do

            intmul( 20, ebx, ecx );

            add( 10, ecx );

            TabbedTextOut

            (

                10,

                ecx,

                TextToDisplay[ ebx*4 ],

                str.length( TextToDisplay[ ebx*4 ] ),

                NumTabs,

                TabStops,

                0

            );

        endfor;

    EndPaint;

    pop( ebx );

end Paint;

Note the use of the HLA convention of intending the source code between an initial macro and a terminator macro, much like you would indent text between an if/endif or while/endwhile pair.

On the CD-ROM accompanying this book, you'll find a wpa.hhf header file containing the declarations specific to this book. The BeginPaint/EndPaint macro, along with a large percentage of the API calls you'd normally make between the calls to w.BeginPaint and w.EndPaint, appear in that header file. By simply including that header file in your Windows assembly applications, you may use these macros. Also note that this header file supplies context-free macro declarations for GetDC/ReleaseDC and GetWindowsDC/ReleaseDC.

One thing to keep in mind about the BeginPaint/EndPaint macro sequence: you must statically nest the API function calls between a BeginPaint..EndPaint sequence. Though this is, by far, the most common way you'll call all these API functions (that is, the call to w.BeginPaint appears first, followed in the source code by various API calls that require the device context, and then, finally, the call to w.EndPaint), Windows doesn't require this organization at all. Windows only requires that your call w.BeginPaint at some point in time before you make any of the other API calls and that you call w.EndPaint at some point in time after you make all those other API calls. There is no requirement that this sequence of statement occur in some linear order in your source file, e.g., the following is perfectly legal (though not very reasonable):

        jmp doBegin;

doEnd:

        w.EndPaint( hWnd, ps );

        jmp EndOfProc

APICalls:

        w.TextOut( hdc, x, y, stringToPrint, strlen );

        jmp doEnd;

doBegin:

        w.BeginPaint( hWnd, ps );

        mov( eax, hdc );

        jmp APICalls;

EndOfProc:

Another thing you can do with the standard Windows API calls is bury the calls to w.BeginPaint, w.EndPaint, and any of your API calls requiring the device context inside different procedures. It is only the sequence of the calls that matters to Windows. HLA, on the other hand, requires that the invocations of the initial macro, the keyword macros and the terminator macros occur in a linear fashion within the same function. Fortunately, this isn't much of a restriction because almost all the time this is exactly how you will call these functions.

Because of their safety and convenience, this book will use these context-free macros through the remaining example programs in this book. You should consider using them in your applications for these same reasons. Of course, if you need to write one of these rare applications that needs to call these API functions in a different order (within your source file), there is nothing stopping you from calling the original Windows functions to handle this task.

The wpa.hhf header file defines #keyword macros for all the GDI/User Interface API functions that this book uses (plus a few others). It may not define all the functions you might want to call between BeginPaint and EndPaint. However, extending these macros is a trivial process; don't feel afraid to add your own #keyword entries to the macros in the wpa.hhf header file.

6.3: Device Capabilities

Although Windows attempts to abstract away differences between physical devices so that you may treat all devices the same, the fact is that your applications will have to be aware of many of the underlying characteristics of a given device in order to properly manipulate that device. Examples of important information your applications will need include the size of the device (both the number of displayable pixels and the drawing resolution), the number of colors the device supports, and the aspect ratio the device supports. Windows provides a function named w.GetDeviceCaps that returns important device context information for a given device. Here's the prototype for the w.GetDeviceCaps function:

static

        GetDeviceCaps: procedure

        (

                hdc     :dword;

                nIndex  :dword

        );

                @stdcall;

                @returns( "eax" );

                @external( "__imp__GetDeviceCaps@8" );

The hdc parameter is a handle that specifies a device context (and, therefore, indirectly the device); like all other functions that expect a device context handle, you should only call this function between the BeginPaint and EndPaint invocations (or some other device context pair, such as GetDC/ReleaseDC). Note that the BeginPaint/EndPaint, GetDC/ReleaseDC, and GetWindowDC/ReleaseDC macros in the wpa.hhf header file include a #keyword macro for GetDeviceCaps so you can invoke this macro directly between these pairs of wpa.hhf macro invocations.

The hIndex parameter is a special Windows value that tells the w.GetDeviceCaps function. Table 6-1 lists some of the possible constants that you supply and the type of information this function returns in the EAX register (many of these values we will discuss in the next chapter, so if an entry in the table doesn't make sense to you, ignore it for now).

Table 6-1: Common GetDeviceCaps nIndex Values
Index
Value Returned in EAX
w.HORZSIZE
The horizontal size, in millimeters, of the display (or other device).
w.VERTSIZE
The vertical size, in millimeters, of the display (or other device).
w.HORZRES
The display's width, in pixels.
w.VERTRES
The display's height, in pixels.
w.LOGPIXELSX
The resolution, in pixels per inch, along the displays' X-axis.
w.LOGPIXELSY
The resolution, in pixels per inch, along the displays' Y-axis.
w.BITSPIXEL
The number of bits per pixel (specifying color information).
w.PLANES
The number of color planes.
w.NUMBRUSHES
The number of device-specific brushes available.
w.NUMPENS
The number of device-specific pens available.
w.NUMFONTS
The number of device specific fonts available.
w.NUMCOLORS
Number of entries in the device's color table.
w.ASPECTX
Relative width of a device pixel used for drawing lines.
w.ASPECTY
Relative height of a device pixel used for drawing lines.
w.ASPECTXY
Diagonal width of the device pixel used for line drawing (45 degrees).
w.CLIPCAPS
This is a flag value that indicates whether the device can clip images to a rectangle. The w.GetDeviceCaps function returns one if the device supports clipping, zero otherwise.
w.SIZEPALETTE
Number of entries in the system palette (valid only if the display device uses a palette).
w.NUMRESERVED
Number of system reserved entries in the system palette (valid only if the display device uses a palette).
w.COLORRES
Actual color resolution of the device, in bits per pixel. For device drivers that use palettes.
w.PHYSICALWIDTH
For printing devices, the physical width of the output device in whatever units that device uses.
w.PHYSICALHEIGHT
For printing devices, the physical height of the output device.
w.PHYSICALOFFSETX
For printing devices, the horizontal margin.
w.PHYSICALOFFSETY
For printing devices, the vertical margin.
w.SCALINGFACTORX
For printing devices, the scaling factor along the X-axis.
w.SCALINGFACTORY
For printing devices, the scaling factor along the Y-axis.
w.VREFRESH
For display devices only: the current vertical refresh rate of the device, in Hz.
w.DESKTOPHORZRES
Width, in pixels, of the display device. May be larger than w.HORZRES if the display supports virtual windows or more than one display.
w.DESKTOPVERTRES
Height, in pixels, of the display device. May be larger than w.VERTRES if the display supports virtual windows or more than one display.
w.BITALIGNMENT
Preferred horizontal drawing alignment. Specifies the drawing alignment, in pixels, for best drawing performance. May be zero if the hardware is accelerated or the alignment doesn't matter.

The w.HORZRES and w.VERTRES return values are particularly useful for sizing your windows when your application first starts. You can use these values to determine the size of the display so you can make sure that your window fits entirely within the physical screen dimensions (there are few things more annoying to a user than an application that opens up with the windows' borders outside the physical screen area so they cannot easily resize the windows).

The w.LOGPIXELSX, w.HORZSIZE, w.LOGPIXELSY, and w.VERTSIZE constants let you request the physical size of the display using real-world units (millimeters and inches; it is interesting that Windows mixes measurement systems here). By querying these values, you can design your software so that information appears on the display at approximately the same size it will print on paper. The return values that Windows supplies via w.GetDeviceCaps for these constants are very important for applications that want to display information in a WYSIWYG (what-you-see-is-what-you-get) format.

The w.BITSPIXEL w.PLANES, w.NUMCOLORS, w.SIZEPALETTE, w.NUMRESERVED, and w.COLORRES index values let you query Windows about the number of colors the device can display. Note that not all of these return values make sense for all devices. Some display devices, for example, support a limited number of colors (e.g., 256) and use a pixel's value (e.g., eight-bits) as an index into a look-up table known as a palette. The system extracts a 24-bit color value from this lookup table/palette. Other display devices may provide a full 24 bits of color information for each pixel and, therefore, don't use a palette. Obviously, the w.GetDeviceCaps queries that return palette information don't return meaningful information on systems whose display card doesn't have a palette.

The w.NUMBRUSHES, w.NUMPENS, and w.NUMFONTS queries request information about the number of GDI objects that the device context currently supports. We'll return to the use of w.NUMBRUSHES and w.NUMPENS in the next chapter, we'll take a look at the use of w.NUMFONTS in the next section.

The w.ASPECTX and w.ASPECTY queries returns a couple of very important values for your program. These values, taken together, specify the aspect ratio of the display. On some displays, the width of a pixel is not equivalent to the height of a pixel (that is, they do not have a 1:1 aspect ratio). If you draw an object on such a display expecting the pixels to have a width that is equivalent to the height, your images will appear squashed or stretched along one of the axes. For example, were you to draw a circle on such a display, it would appear as an oval on the actual display device. By using the w.GetDeviceCaps return values for the w.ASPECTX and w.ASPECTY queries, you can determine by how much you need to "squash" or "stretch" the image to undo the distortion the display creates.

The w.GetDeviceCaps call returns several other values beyond those mentioned here, we've just touched on some of the major return values in this section. Because these return values are static (that is, they don't change while the system is operational), you might wonder why Windows doesn't simply return all of these values with a single call (i.e., storing the return values in a record whose address you pass to the w.GetDeviceCaps call). The answer is fairly simple: the "single-value query" method is very easy to expand without breaking existing code. Nevertheless, it's a bit of a shame to call w.GetDeviceCaps over and over again, always returning the same values for a given query index value, while your program is running. So although Windows doesn't provide a single API call that reads all of these values, it's not a major problem for you to write your own function that reads the device capabilities your program needs into a single record so you can access those values within your application without making (expensive) calls to Windows. The following program demonstrates just such a function (this application reads a subset of the w.GetDeviceCaps values into a record and then displays those values in a Window when the application receives a w.WM_PAINT message, see Figure 6-6 for the output).

// GDCaps.hla:

//

// Displays the device capabilities for the video display device.

// Inspired by "DEVCAPS1.C" by Charles Petzold

program GDCaps;

#include( "w.hhf" )       // Standard windows stuff.

#include( "wpa.hhf" )     // "Windows Programming in Assembly" specific stuff.

#include( "memory.hhf" )  // tstralloc is in here

#include( "strings.hhf" ) // str.put is in here

?@nodisplay := true;      // Disable extra code generation in each procedure.

?@nostackalign := true;   // Stacks are always aligned, no need for extra code.

// dct_t and dcTemplate_c are used to maintain the device context data

// structures throughout this program.  Each entry in the dcTemplate_c

// array specifies one of the values we'll obtain from Windows via

// a GetDeviceCap call.  The fieldName entry should contain the name

// of the device capability field to get from Windows. This name must

// exactly match the corresponding w.GetDeviceCap index value with the

// exception of alphabetic case (it doesn't need to be all upper case)

// and you don't need the leading "w."  The desc entry should contain

// a short description of the field.

//

// This is the only place you'll need to modify this code to retrieve

// and display any of the GetDeviceCap values.

type

    dct_t   :record

        fieldName   :string;

        desc        :string;

    endrecord;

const

    dcTemplate_c :dct_t[] :=

    [

        dct_t:[ "HorzSize",     "Width in millimeters" ],

        dct_t:[ "VertSize",     "Height in millimeters" ],

        dct_t:[ "HorzRes",      "Width in pixels" ],

        dct_t:[ "VertRes",      "Height in pixels" ],

        dct_t:[ "BitsPixel",    "Color Bits/pixel" ],

        dct_t:[ "Planes",       "Color planes" ],

        dct_t:[ "NumBrushes",   "Device brushes" ],

        dct_t:[ "NumPens",      "Device pens" ],

        dct_t:[ "NumFonts",     "Device fonts" ],

        dct_t:[ "NumColors",    "Device colors" ],

        dct_t:[ "AspectX",      "X Aspect value" ],

        dct_t:[ "AspectY",      "Y Aspect value" ],

        dct_t:[ "AspectXY",     "Diag Aspect value" ],

        dct_t:[ "LogPixelsX",   "Display pixels (horz)" ],

        dct_t:[ "LogPixelsY",   "Display pixels (vert)" ],

        dct_t:[ "SizePalette",  "Size of palette" ],

        dct_t:[ "NumReserved",  "Reserved palette entries" ],

        dct_t:[ "ColorRes",     "Color resolution" ]

    ];

    DCfields_c := @elements( dcTemplate_c );

// The deviceCapRecord_t and deviceCapabilities_t types are record objects

// that hold the values we're interested in obtaining from the

// w.GetDevCaps API function. The #for loop automatically constructs all

// the fields of the deviceCapRecord_t record from the dcTemplate_c constant

// above.

type

    deviceCapRecord_t :record

        #for( i in dcTemplate_c )

            @text( i.fieldName ) :int32;

        #endfor

    endrecord;

    deviceCapabilities_t :union

        fields      :deviceCapRecord_t;

        elements    :int32[ DCfields_c ];

    endunion;

static

    hInstance:  dword;          // "Instance Handle" supplied by Windows.

    wc:     w.WNDCLASSEX;       // Our "window class" data.

    msg:    w.MSG;              // Windows messages go here.

    hwnd:   dword;              // Handle to our window.

    // Record that holds the device capabilities that this

    // program uses:

    appDevCaps: deviceCapabilities_t;

readonly

    ClassName:  string := "GDCapsWinClass";         // Window Class Name

    AppCaption: string := "Get Device Capabilities";// Caption for Window

// The following data type and DATA declaration

// defines the message handlers for this program.

type

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue:   dword;

            MessageHndlr:   MsgProc_t;

        endrecord;

// The dispatch table:

//

//  This table is where you add new messages and message handlers

//  to the program.  Each entry in the table must be a tMsgProcPtr

//  record containing two entries: the message value (a constant,

//  typically one of the wm.***** constants found in windows.hhf)

//  and a pointer to a "tMsgProc" procedure that will handle the

//  message.

readonly

    Dispatch:   MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

// Create-

//

//  This function reads an application-specific set of device capabilities

// from Windows using the w.GetDevCaps API function.  This function stores

// the device capabilities into the global appDevCaps record.

procedure Create;

var

    hdc :dword;

begin Create;

    GetDC( hwnd, hdc );

        // Generate a sequence of calls to GetDeviceCaps that

        // take the form:

        //

        //  GetDeviceCaps( w.FIELDNAMEINUPPERCASE );

        //  mov( eax, appDevCaps.FieldName );

        //

        // Where the field names come from the deviceCapabilities_t type.

        #for( field in dcTemplate_c )

            GetDeviceCaps( @text( "w." + @uppercase( field.fieldName, 0 )) );

            mov( eax, @text( "appDevCaps.fields." + field.fieldName ));

        #endfor

    ReleaseDC;

end Create;

// QuitApplication:

//

//  This procedure handles the "wm.Destroy" message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the "wm.Paint" message.

//  This procedure displays several lines of text with

//  different colors.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @nodisplay;

var

    hdc:    dword;              // Handle to video display device context

    ps:     w.PAINTSTRUCT;      // Used while painting text.

    rect:   w.RECT;             // Used to invalidate client rectangle.

    outStr: string;

type

    dclbl_t:    record

        theName     :string;

        desc        :string;

    endrecord;

static

    DClabels    :dct_t[ DCfields_c ] := dcTemplate_c;

begin Paint;

    push( ebx );

    push( edi );

    // Allocate temporary storage for a string object

    // (automatically goes away when we return):

    tstralloc( 256 );

    mov( eax, outStr );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        for( mov( 0, ebx ); ebx < DCfields_c; inc( ebx )) do

            // Sneaky trick: Although the global appDevCaps is really

            // a structure, this code treats it as an array of

            // dwords (because that's what it turns out to be).

            str.put

            (

                outStr,

                DClabels.desc[ ebx*8 ],

                " (",

                DClabels.fieldName[ ebx*8 ],

                "): ",

                appDevCaps.elements[ ebx*4 ]

            );

            intmul( 20, ebx, edx ); // Compute y-coordinate for output.

            TextOut( 10, edx, outStr, str.length( outStr ) );

        endfor;

    EndPaint;

    pop( edi );

    pop( ebx );

end Paint;

// The window procedure.  Since this gets called directly from

// windows we need to explicitly reverse the parameters (compared

// to the standard STDCALL declaration) in order to make HLA's

// Pascal calling convention compatible with Windows.

//

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword  );

    @stdcall;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin GDCaps;

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    mov( eax, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end GDCaps;

Figure 6-6: GDCaps Output

This program introduces a new Windows message and some novel HLA concepts, so it's worthwhile to spend a small amount of time discussing certain parts of this source code.

Device capability information tends to be static. That is, the capabilities of a device are usually fixed by the manufacturer of that device and these capabilities generally don't change while your application is running. For example, it is rare for the display resolution to change during the execution of your program (possible, but rare; the user, for example, could run the "display settings" control panel while your application is executing and resize the display). For many applications, it's probably safe to assume that the device capabilities remain static as long as the program doesn't crash or lose data if this assumption turns out to be false (i.e., if the worst effect is that the application's display looks a little funny, you can probably get away with obtaining this information just once rather than querying Windows for the values every time you want to use them). The GDcaps.hla program uses this approach - it does not read the device capabilities every time it receives a w.WM_DRAW message. Instead, it reads the information into an array that is local to the program and then displays the data from this array every time it repaints the screen. This spares the application from having to make a couple dozen calls to the Win32 w.GetDeviceCaps function every time a paint message comes along.

One problem when reading the data only once is "when do you read this data?" You might be tempted to make the w.GetDeviceCaps call from the application's main program, before it starts processing messages. This approach, however, won't work. The w.GetDeviceCaps API function requires a device context value, which is only available after you've created a window. One way to handle this situation is to create a static boolean flag that you've initialized with false and then initialize the in-memory data structure on the first call to the Paint procedure; by testing this flag (and setting it to true once you've initialized the capability data structure), you can ensure that you only make the calls to w.GetDeviceCap once - on the very first call to the Paint procedure. There are a couple of problems with this approach. First, it clutters up your Paint procedure with code that really has nothing to do with painting the window. Second, although testing a boolean flag isn't particularly inefficient, it is somewhat unaesthetic (especially in an assembly language program) to have to repeat this test over and over again on each call to the Paint procedure even though the initialization of the code takes place exactly once. Yet another problem with this static variable approach is that there are actually some times when you'll want to reinitialize the device capabilities data structure; for example, if the application destroys the window and then creates a new one, you get a new device context and it's possible that the capabilities have changed. To solve these problems (and more), the GDcaps.hla application taps into a different message, w.WM_CREATE, in order to initialize the in-memory data structure. Windows will send a w.WM_CREATE message to an application's window procedure immediately after it creates a new window. By placing the code to initialize the capabilities data structure in a procedure that responds to this message, you can ensure that you initialize the device capabilities array whenever Windows creates a new window for your application.

The Create procedure in the GDCaps application uses some novel code that is worth exploring. This code makes multiple calls to the w.GetDeviceCaps API function with index values for each of the capabilities this particular program wants to display. After each call to w.GetDeviceCaps, the Create procedure stores the result result in one of the fields of the appDevCaps data structure. The appDevCaps variable's type is deviceCapabilities_t which is a union of a record and an array type. The record component lets you access each of the device capabilities by (field) name, the array field lets you access all of the fields using a numeric index. Typically, the initialization code will access this object as an array and the rest of the application will access the fields of the record. The record structure is type deviceCapRecord_t and it has the following declaration:

type

    deviceCapRecord_t :record

        HorzSize					:int32;

        VertSize					:int32;

        HorzRes					:int32;

        BitsPixel					:int32;

        Planes					:int32;

        NumBrushes					:int32;

        NumPens					:int32;

        NumFonts					:int32;

        NumColors					:int32;

        AspectX					:int32;

        AspectY					:int32;

        AspectXY					:int32;

        LogPixelsX					:int32;

        LogPixelsY					:int32;

        SizePalette					:int32;

        NumReserved					:int32;

        ColorRes					:int32;

    endrecord;

However, if you look at the source code, you'll discover that the actual record type declaration takes the following form:

type

    deviceCapRecord_t :record

        #for( i in dcTemplate_c )

            @text( i.fieldName ) :int32;

        #endfor

    endrecord;

This strange looking code constructs all the fields for this record by iterating through the dcTemplate_c constant and extracting the field names from that array of records. The #for compile-time loop repeats once for each element of the dcTemplate_c (constant) array. Each element of this array is a record whose fieldName field contains the name of one of the fields in the deviceCapRecord_t type. On each iteration of the #for loop, HLA sets the loop control variable (i) to the value of each successive array element. Therefore, i.fieldName is a character string constant containing the field name for a given iteration of the loop. The "@text( i.fieldName )" construct tells HLA to substitute the text of the string in place of that string constant, hence HLA replaces each instance of the @text( i.fieldName ) compile-time function with a field name. That's how this funny looking code expands into the former record declaration.

Now you're probably wondering why this program uses such obtuse code; why not simply type all the field names directly into the record so the code is a little clearer? Well, this was done in order to make the program easier to maintain (believe it or not). As it turns out, the GDCaps application references these fields throughout the source code. This means that if you want to modify the code to add or remove fields from this record, you'd actually have to make several changes at various points throughout the source code to correctly make the change. Code is much easier to maintain if you only have to make a change (like adding or removing values to print) in one spot in the source code. The GDCaps program takes advantage of some of HLA's more sophisticated features to allow the program to collect all the field information into a single data structure: the dcTemplate_c array constant. Here's the declaration for this constant and its underlying type:

type

    dct_t   :record

        fieldName   :string;

        desc        :string;

    endrecord;

const

    dcTemplate_c :dct_t[] :=

    [

        dct_t:[ "HorzSize",     "Width in millimeters" ],

        dct_t:[ "VertSize",     "Height in millimeters" ],

        dct_t:[ "HorzRes",      "Width in pixels" ],

        dct_t:[ "VertRes",      "Height in pixels" ],

        dct_t:[ "BitsPixel",    "Color Bits/pixel" ],

        dct_t:[ "Planes",       "Color planes" ],

        dct_t:[ "NumBrushes",   "Device brushes" ],

        dct_t:[ "NumPens",      "Device pens" ],

        dct_t:[ "NumFonts",     "Device fonts" ],

        dct_t:[ "NumColors",    "Device colors" ],

        dct_t:[ "AspectX",      "X Aspect value" ],

        dct_t:[ "AspectY",      "Y Aspect value" ],

        dct_t:[ "AspectXY",     "Diag Aspect value" ],

        dct_t:[ "LogPixelsX",   "Display pixels (horz)" ],

        dct_t:[ "LogPixelsY",   "Display pixels (vert)" ],

        dct_t:[ "SizePalette",  "Size of palette" ],

        dct_t:[ "NumReserved",  "Reserved palette entries" ],

        dct_t:[ "ColorRes",     "Color resolution" ]

    ];

If you don't want GDCaps to display a particular item above, simply remove the entry from this array. If you want GDCaps to display a device capability that is not present in this list, simply add the field name and description to the dcTemplate_c array. That's all there is to it. The program recomputes all other changes automatically when you recompile the code.

The dcTemplate_c array is an array of records with each element containing two fields: a field name/GetDevCaps index constant and a description of the field/index constant. The field name you supply must be a string containing the name of the GetDevCaps index constant with two exceptions: you don't have to use all upper case characters (HLA will automatically convert the code to upper case later, so you can use more readable names like "HorzSize" rather than "HORZSIZE") and you don't need the "w." prefix (HLA will also supply this later). Note that HLA will use whatever field names you supply in the fieldNames field as the field names for the record it constructs using the #for loop that you saw earlier. To access these values in the application, you'd normally use identifiers like appDevCaps.fields.HorzSize and appDevCaps.fields.NumColors. The second field of each record is a short description of the value that GetDevCaps will return. The GDCaps program displays this string (along with the field name) in the output window.

Because we can only call w.BeginPaint and w.EndPaint within a w.WM_PAINT message handler, the Create procedure (that handles w.WM_CREATE messages) will have to call w.GetDC/w.ReleaseDC or w.GetWindowDC/w.ReleaseDC. Of course, by including the wpa.hhf header file, the GDcaps application gets to use the GetDC, ReleaseDC, and GetDeviceCap macros to simplify calling these API functions.

Normally, the code to read all these values from Windows and store them into the appDevCaps data structure would look something like this:

    GetDC( hwnd, hdc );

        GetDeviceCaps( w.HORZSIZE );

        mov( eax, appDevCaps.fields.HorzSize );

        GetDeviceCaps( w.VERTSIZE );

        mov( eax, appDevCaps.fields.VertSize );

            .

            .

            .

        GetDeviceCaps( w.COLORRES );

        mov( eax, appDevCaps.fields.ColorRes );

    ReleaseDC;

One problem with the "straight-line" code approach is that it's a bit difficult to maintain. If you add or remove fields in the dcTemplate_c array, you'll have to manually modify the Create procedure to handle those modifications. To avoid this, the GDcaps application uses HLA's compile-time #for loop to automatically generate all the code necessary to initialize the appDevCaps structure. Here's the code that achieves this:

        #for( field in dcTemplate_c )

            GetDeviceCaps( @text( "w." + @uppercase( field.fieldName, 0 )) );

            mov( eax, @text( "appDevCaps.fields." + field.fieldName ));

        #endfor

Because this code is somewhat sophisticated, it's worthwhile to dissect this code in order to fully understand what's going on.

The first thing to note is that #for is a compile-time loop, not a run-time loop. This means that the HLA compiler repeats the emission of the text between the #for and the corresponding #endfor the number of times specified by the #for expression. This particular loop repeats once for each field in the dcTemplate_t constant array. Therefore, this code emits n copies of the GetDeviceCaps call and the mov instruction, where n is the number of elements in the dcTemplate_t array.

The #for loop repeats once for each element of this array, and on each iteration it assigns the current element to the field compile-time variable. Therefore, on the first iteration of this loop, the field variable will contain the record constant dct_t:["HorzSize", "Width in millimeters" ], on the second iteration it will contain dct_t:["VertSize", "Height in millimeters" ], on the third iteration it will contain dct_t:["HorzRes", "Width in pixels" ], etc. It is important to realize that this array of records and this field variable do not exist in your program's object code. They are internal variables that the compiler maintains only while compiling your program. Unless you explicitly tell it to do so, HLA will not place these strings in your object code. Within the body of the loop, the field compile-time variable behaves like an HLA const or val object.

Within the body of the #for loop, the code constructs Windows compatible w.GetDeviceCaps index names and appDevCaps field names using compile-time string manipulation. The compile-time function "@uppercase( field.fieldName, 0)" returns a string that is a copy of field.fieldName, except that it converts all lowercase alphabetic characters in the string to upper case. For example, on the first iteration of the #for loop (when field contains "HorzSize"), this function call returns the string "HORZSIZE". The "w." + @uppercase( field.fieldName, 0 ) expression concatenates the uppercase version of the field name to a "w." string, producing Windows compatible constant names like "w.HORZSIZE" and "w.COLORRES". The @text function translates this string data into text data so that HLA will treat the string as part of your source file (rather than as a string literal constant). Therefore, the iterations of the #for loop produces the equivalent of the following statements:

        GetDeviceCaps( w.HORZSIZE );

        GetDeviceCaps( w.VERTSIZE );

        GetDeviceCaps( w.HORZRES );

            .

            .

            .

        GetDeviceCaps( w.COLORRES );

This behavior, by the way, is why the field names in the first string you supply for each dcTemplate_c element must match the GetDeviceCaps constants except for alphabetic case. The #for loop uses those field names to generate the index constant names that it feeds to the GetDeviceCaps API function.

The second instruction in the #for loop copies the data returned by GetDeviceCaps (in EAX) into the appropriate field of the appDevCaps.field data structure. The construction of the field name is almost identical to that used for the GetDeviceCaps call except, of course, the code doesn't do an upper case conversion.

The #for..#endfor loop winds up producing code that looks like the following:

        GetDeviceCaps( w.HORZSIZE );

        mov( eax, appDevCaps.fields.HorzSize );

        GetDeviceCaps( w.VERTSIZE );

        mov( eax, appDevCaps.fields.VertSize );

        GetDeviceCaps( w.HORZRES );

        mov( eax, appDevCaps.fields.HorzRes );

            .

            .

            .

        GetDeviceCaps( w.COLORRES );

        mov( eax, appDevCaps.fields.ColorRes );

The nice thing about using the #for..#endfor loop, rather than explicitly coding these statements, is that all you have to do to change the device capabilities you retrieve is to modify the dcTemplate_c data type. The next time you compile the source file, the #for loop will automatically generate exactly those statements needed to retrieve the new set of fields for this record type. No other modifications to the code are necessary.

Of course, you could achieve this same result by writing a run-time loop. To do that, you'd need an array of index values (at run time) whose elements you pass to the GetDeviceCaps function on each iteration of the loop. The elements of that run-time array need to appear in the same order as the fields in the dcTemplate_c structure (note that you could write some HLA compile-time code to automatically build and initialize this array for you). If you're interested in this approach (which generates a little bit less code), feel free to implement it in your own applications. Of course, the number of fields you'll typically grab from GetDevCaps is usually so small that expanding the code in-line is not usually a problem.

The Paint procedure is responsible for actually displaying the device capabilities information. The procedure is relatively straight-forward - it fetches a list of labels and descriptions from one array and the list of capability values from the array/record read in the Create procedure, turns this information into a sequence of strings and displays these strings in the application's window.

The field names and descriptions are really nothing more than a run-time instantiation of the dcTemplate_c array. The Paint procedure creates this run-time array using the following declaration:

static

    DClabels    :dct_t[ DCfields_c ] := dcTemplate_c;

This generates a run-time array, DClabels, that contains the same information as the compile-time array constant dcTemplate_c. Remember that at run-time, HLA string objects are four-byte pointers. Therefore, DClabels is really an array of two four-byte pointer values (with one pointer containing the address of a string holding the field name and the second pointer containing the address of a string with the field's description). HLA stores the actual character data in a different section of memory, not directly within the DClabels array.

To print the data to the window, the Paint procedure converts the numeric information in the corresponding appDevCaps array element to a string and then concatenates the two strings in the DClabels array with this numeric string and calls w.TextOut to display the whole string. This sounds like a bit of work, but the HLA Standard Library comes to the rescue with the str.put function that lets you do all this work with a single function call:

            str.put

            (

                outStr,                       // Store the result here

                DClabels.desc[ ebx*8 ],       // Starts with description string

                " (",                         // Concatenates "(" followed by

                DClabels.fieldName[ ebx*8 ],  // the field name string and ")"

                "): ",                        // Then it concatenates the string

                appDevCaps.elements[ ebx*4 ]  // conversion of the GetDevCaps value.

            );

Because each element of the DClabels array is a pair of 32-bit pointers, this code uses the "*8" scaled indexed addressing mode to access elements of that array. The appDevCaps array is just an array of int32 values, so this code uses the "*4" scaled indexed addressing mode to access those array elements.

To actually output the text data to the Window, the Paint procedure uses the following two statements:

            intmul( 20, ebx, edx ); // Compute y-coordinate for output.

            TextOut( 10, edx, outStr, str.length( outStr ) );

The intmul instruction computes the separation between lines (20 pixels) and the TextOut call displays the string that the str.put call created. A run-time for loop around these statements repeats these statements for each of the fields in the DClabels and appDevCaps arrays.

Carefully writing this code to make it "maintainable" may seem like an exorbitant effort for what amounts to a demonstration program. However, this was done on purpose. The Create procedure and the related data structures are perfectly general and you can cut and paste this code into other applications. Because most applications won't need exactly the set of GetDevCaps values that GDCaps uses, spending a little bit of extra effort to make this code easy to modify will help you if you decide to lift portions of this code and place them in a different application.

6.4: Typefaces and Fonts

The first major difference between a console application and a GUI application is that GUI apps allow multiple fonts. In this section, we'll explore how to use multiple fonts in a GUI application.

Perhaps the first place to start this discussion is with the definition of a font. This side trip is necessary because many people confuse a font with a typeface. A font is a collection of attributes associated with printed or displayed text. The attributes that define a font include the typeface, the size, and other style attributes such as italic, bold, underlined, and so on.

A typeface is a family of type styles that specify the basic shape of the characters in a particular typeface. Examples of typefaces include Times Roman, Helvetica, Arial, and Courier. Note that Times Roman (contrary to popular belief) is not a font; there are many different fonts that use the Times Roman typeface (indeed, in theory there are an infinite number of fonts that employ the Times Roman typeface, because there are, in theory, an infinite number of sizes one could use with this typeface).

Another attribute of a font is the size of that font. Font sizes are usually specified in points. A point is approximately 1/72 inches. Therefore, a font with a 72 point size consists of characters that are approximately one inch tall. An important thing to realize is that 72 point characters are not all approximately one inch tall. The size of a font specifies a set of boundaries between which typical characters in that font actually fall. A 72-point lower-case `a', for example, is smaller than a 72-point uppercase `A' even though they both have the same "size". To understand how graphic artists measure font sizes, consider Figure 6-7. This figure specifies four terms that graphic artists typically use when discussing the size of a font: leading, baseline, descent, and ascent.

Figure 6-7: Font Sizes

Leading (pronounced "led-ing" as in the metal lead, not "lead-ing" as in "leading the pack") is the amount of spacing that separates two lines of text. This name comes from the days when graphic artists and printers used lead type in the printing process. "Leading" referred to thin lead strips inserted between rows of text to spread that text out. Windows actually defines two types of leading: internal leading and external leading (see Figure 6-8). External leading is the amount of space between the upper-most visible portion of any character in the font and the bottom-most visible portion of any character in the font on the previous line of text. Internal leading is the amount of space between the top-most component of any standard character in the font and the top of any accents appearing on characters in the font. Note that the external leading does not count towards the size of a font.

Figure 6-8: Internal Versus External Leading

The actual size of a font is measured as the distance from the bottom of the descent region to the top of the ascent region. The position of these two components appears in Figure 6-9 and Figure 6-10. Figure shows how the region that defines the font's size.

Figure 6-9: The Ascent of a Font

Figure 6-10: The Descent of a Font

Figure 6-11: The Size of a Font

There are two important things to note from these figures. First, as already stated, the size of a font does not specify the size of any individual character within the font. Instead, it (generally) provides a maximum range between the bottom-most portions of certain characters in the font and the top-most portions of certain characters within the font. It is very unusual for a single character's height to span this full range. Many characters (e.g., various lower-case characters) are just a little larger than half of the font's specified size. The second thing to note is that the size of a font says absolutely nothing about the width of the characters within that font. True, as the font size increases, the width of the characters tends to increase as well, but the actual width is generally dependent upon the typeface or other font attributes (e.g., certain fonts have a condensed attribute that tells a graphic artist that the individual characters are narrower than a standard font of the same size).

The exact width of characters within a font is typically a function of the typeface. However, we can classify fonts into two categories based upon the widths of the characters within the font: proportional fonts and non-proportional (or monospaced) fonts. In a proportional font, the widths of the characters vary according to the particular character and the whim of the font designer. Certain characters, like "W" require more horizontal space than other characters (e.g., "i") in order to look "proper" within a line of text. As a result, you cannot determine the amount of horizontal space a string of characters will consume by simply counting the number of characters in the string. Instead, you'll have to sum up the widths of each of the individual characters in the string in order to determine the width of the whole string. This makes it difficult to predict the number of characters you can display within a given horizontal distance without actually having the characters to work with.

Some typefaces (e.g., Courier) are non-proportional, or monospaced. This means that all the characters in the typeface have the same width. The font designer achieves this by condensing wide characters (like `W') and elongating narrow characters (like `i'). Computing the width of a string of characters rendered with monospaced fonts is easily achieved by counting the number of characters in the string and then multiply this length by the width of a single character.

Windows actually supports three distinct types of fonts: raster fonts, scalable (plotter) fonts, and True Type fonts. Each of these fonts have their own advantages and disadvantages that we'll explore in the following paragraphs. For the most part, this book will assume the use of True Type fonts, but much of the information this chapter discusses is independent of the font technology, so you may use whatever font system you like in your applications.

Raster fonts are bit-mapped images that have two primary attributes: Windows can quickly display raster fonts and raster fonts can be hand-tuned to look very good on a given display device. The disadvantage of the raster font technology is that the output only looks good if the system provides a font in the size that you request. Although Windows can scale a given raster font to almost any size, the result is rarely pretty. If your application needs to use a raster font, be sure to use a font size provided by the system to keep the result as legible as possible. Another problem with raster fonts is that you generally need different sets of fonts for different output devices. For example, you'll need one set of raster fonts for display output and one set for printer output (because of the inherent differences in display resolution, you'll need different bitmaps for the two devices).

"Scalable" fonts are an older font technology that uses line vectors to draw the outlines of the characters in the font. This font technology mainly exists to support certain output devices like pen plotters. Because such output devices are rare in modern computer systems (most plotting that occurs these days takes place on an ink-jet printer rather than a pen plotter), you'll probably not see many "scalable" fonts in the system. The advantage of scalable fonts over raster plots is that they define their individual characters mathematically via a sequence of points. The pen plotter simply draws lines between the points (in a "connect-the-dots" fashion). Mathematically, it is very easy to scale such fonts to a larger or smaller size by simply multiplying the coordinates of these points by a fixed value (using a vector multiplication). Therefore, it is easy to produce characters of any reasonable size without a huge quality loss as you get when attempting to scale raster fonts. Because you'll rarely see "scalable" fonts in use in modern Windows systems, we'll ignore this font technology in this book.

Although "scalable" fonts solve the problem of adjusting the size of the fonts, the scalable fonts that came with the original versions of Windows were, shall we say, a little less than aesthetic. Fortunately, Apple and Microsoft worked together to develop a high-quality scalable font technology known as True Type. Like the Postscript font technology that preceded it, the True Type technology uses a mathematical definition of the outlines for each character in the font. True Type technology uses a simpler, quadratic, mathematical definition (versus Postscript's bezier outlines) that is more efficient to compute; therefore, Windows can render True Type fonts faster than Postscript fonts. The end result is that Windows can efficiently scale a True Type font to any practical size and produce good-looking results. A raster font, displayed at its native size, can look better than a True Type font scaled to that same size (because raster fonts can be hand-optimized for a given size to look as good as possible); but in general, True Type fonts look better than raster fonts because they look good regardless of the size.

Another advantage of True Type fonts is that they can take advantage of technologies like anti-aliasing to improve their legibility on a video display. Anti-aliasing is the process of using various shades of the font's primary color to "soften" the edges around a particular character to eliminate the appearance of jagged edges between pixels in the character. In theory, it's possible to define anti-aliased raster fonts, but you rarely see such fonts in practice.

Yet another advantage of True Type fonts is the fact that you only have to keep one font definition around in the system. Unlike raster fonts, where you have to store each font (i.e., size) as a separate file, True Type fonts only require a single font outline and the system builds the explicit instances of each font (size) you request from that single mathematical definition. To avoid having to construct each character from its mathematical definition when drawing characters, Windows will convert each character in a given font to its bitmapped representation when you first use each character, and then it will cache those bitmaps (i.e., raster images) away for future reference. As long as you leave that font selected into the device context, Windows uses the bitmapped image it has cached away to rapidly display the characters in the font. Therefore, you only pay a "rasterizing" penalty on the first use of a given character (or on the first use after a character was removed from the font cache because the cache was full). Generally, if you follow the (graphic arts) rule of having no more than four fonts on a given page, Windows should be able to cache up all the characters you are using without any problems.

Regardless of what font technology you use, whenever you want to display characters in some font on the display, you have to tell Windows to create a font for you from a system font object. If the system font object is a raster font and you're selecting that font's native size, Windows doesn't have to do much with the font data (other than possibly load the font data from disk). If you're using a True Type font, or selecting a non-native raster font size, then Windows will need to create a bitmap of the font it can write to the display prior to displaying any characters from that font. This is the process of font creation and you accomplish it using the Windows API function w.CreateFontIndirect. This function has the following prototype:

static

        CreateFontIndirect: procedure

        (

                var     lplf:LOGFONT

        );

                @stdcall;

                @returns( "eax" );

                @external( "__imp__CreateFontIndirectA@4" );

This function call returns a handle to a font. You must save this handle so you can destroy the font when you are done using it.

The lplf parameter in the w.CreateFontIndirect call is a pointer to a logical font structure (w.LOGFONT). This structure takes the following form:

type

    LOGFONT			:record

        lfHeight						:int32;

        lfWidth						:int32;

        lfEscapement						:int32;

        lfOrientation						:int32;

        lfWeight						:int32;

        lfItalic						:byte;

        lfUnderline						:byte;

        lfStrikeOut						:byte;

        lfCharSet						:byte;

        lfOutPrecision						:byte;

        lfClipPrecision						:byte;

        lfQuality						:byte;

        lfPitchAndFamily						:byte;

        lfFaceName						:char[LF_FACESIZE];

    endrecord;

The lfHeight field is the height of the font using "device units". If you pass a zero in this field, then Windows returns the default size for the font. Most of the time, you'll want to specify the exact font size to use, so you'll need to convert sizes in a common measurement system (e.g., points) into device units. To compute the lfHeight value in points, you'd use the following formula:

lfHeight = -( pointSize * appDevCaps.LogPixelsY) / 72

(This assumes, of course, that you've read the value of the w.LOGPIXELSY device capability into appDevCaps.LogPixelsY as was done in the previous section.) For example, if you want to compute the lfHeight value for a 12-point type, you'd use code like the following:

    mov( pointSize, eax );

    imul( appDevCaps.LogPixelsY, eax ); // pointSize * appDevCaps.LogPixelsY

    idiv( 72, edx:eax );                //   / 72

    neg( eax );                         // Negate the result.

    mov( eax, lfVar.lfHeight );         // Store away in the lfHeight field.

The appDevCaps.LogPixelsY value specifies the number of logical pixels per inch for the given device context. Multiplying this by the desired point size and dividing by 72 points/inch produces the necessary font size in device units.

The w.LOGFONT lfWidth field specifies the average character width. If this field contains zero, then Windows will compute the "best" average width for the font's characters based on the lfHeight value. Generally, you'll want to supply a zero for this value. However you'd prefer a condensed or elongated font, you can supply a non-zero value here. The computation you'd use for this value is the same as for lfHeight.

The lfEscapement and lfOrientation values specify an angle from the baseline (in tenths of degrees) or x-axis that Windows will use to rotate the characters when drawing them. Note that this does not tell Windows to draw your lines of text at the specified angle. Instead, it tells Windows the angle to use when drawing each character on the display (rotating the text within each character cell position).

The lfWeight field specifies the boldness of the font using a value between zero and 1000. Here is a list of constants you may use to specify the weight of a character:

  • w.FW_DONTCARE (0)
  • w.FW_THIN (100)
  • w.FW_EXTRALIGHT (200)
  • w.FW_ULTRALIGHT (200)
  • w.FW_LIGHT (300)
  • w.FW_NORMAL (400)
  • w.FW_MEDIUM (500)
  • w.FW_SEMIBOLD (600)
  • w.FW_DEMIBOLD (600)
  • w.FW_BOLD (700)
  • w.FW_EXTRABOLD (800)
  • w.FW_ULTRABOLD (800)
  • w.FW_HEAVY (900)
  • w.FW_BLACK (900)

You'd normally select a bold font rather than using this field to specify the weight (assuming a bold font is available). If a bold font is not available, then this field provides an acceptable alternative to using a bold font (note that bold fonts are not simply "fatter" versions of each character in the typeface; the strokes are actually different for bold versus regular characters, increasing the weight of a character is only an approximation of a bold font).

The lfUnderline, lfStrikeout, and lfItalic fields are boolean variables (true/false or 1/0) that specify whether the font will have underlined characters, strikeout characters (a line through the middle of each character), or italic characters. Generally, you would not use the lfItalic flag - you'd choose an actual italic font instead. However, if an italic font is not available for a given typeface, you can approximate an italic font by setting the lfItalic flag.

The lfCharSet field specifies the character set to use. For the purposes of this text, you should always initialize this field with w.ANSI_CHARSET or w.OEM_CHARSET. For details on internationalization and other character set values that are appropriate for this field, please consult Microsoft's documentation.

The lfOutPrecision field tells Windows how closely it must match the requested font if the actual font you specify is not in the system or if there are two or more fonts that have the same name you've requested. Figure 6-2 lists some common values you'd supply for this field.

Table 6-2: lfOutPrecision Values (in w.LOGFONT Records)
Value
Description
w.OUT_DEFAULT_PRECIS
Specifies the default font mapper behavior
w.OUTLINE_PRECIS
This tells Windows to choose from True Type and other outline-based fonts (Win NT, 2K, and later only).
w.OUT_RASTER_PRECIS
If the font subsystem can choose between multiple fonts, this value tells Windows to choose a raster-based font.
w.OUT_TT_ONLY_PRECIS
This value tells the font subsystem to use only True Type fonts. If there are no True Type fonts, then the system uses the default behavior. If there is at least one True Type font, and the system does not contain the requested font, then the system will substitute a True Type font for the missing font (even if they are completely different).
w.OUT_TT_PRECIS
Tells the font subsystem to choose a True Type font over some other technology if there are multiple fonts with the same name.

The lfClipPrecision field defines how to clip characters that are partially outside the clipping region. A clipping region is that area of the display outside the area which an application is allowed to draw. You should initialize this field with the w.CLIP_DEFAULT_PRECIS value.

The lfQuality field specifies the output quality of the characters. Windows supports three constant values for this field: w.DEFAULT_QUALITY, w.DRAFT_QUALITY, and w.PROOF_QUALITY. For display output, you should probably choose w.DEFAULT_QUALITY.

The lfPitchAndFamily field specifies certain attributes of the font. The low-order two bits should contain one of w.DEFAULT_PITCH, w.FIXED_PITCH, or w.VARIABLE_PITCH. This specifies whether Windows will use proportional or monospaced fonts (or whether the decision is up to Windows, should you choose w.DEFAULT_PITCH). You may logically OR one of these constants with one of the following values that further specifies the font family:

  • w.FF_DECORATIVE - use a decorative font (like Old English)
  • w.FF_DONTCARE - use an arbitrary font (Windows chooses)
  • w.FF_MODERN - generally specified for monospaced fonts
  • w.FF_ROMAN - generally specified for proportional, serifed, fonts
  • w.FF_SCRIPT - specifies a font that uses a cursive (handwritten) style
  • w.FF_SWISS - specifies a proportional, sans-serifed font

The lfFaceName field is a zero-terminated character string that specifies the font name. The font name must not exceed 31 characters (plus a zero terminating byte). If this field contains an empty string, then Windows picks a system font based on the other font attributes appearing in the w.LOGFONT structure (e.g., the lfPitchAndFamily field). If this field is not an empty string, then the font choice takes precedence over the other attributes appearing in the w.LOGFONT record (e.g., if you specify "times roman" as the font, you'll get a variable pitch roman font, regardless of what value you specify in lfPtichAndFamily). If the name doesn't exactly match an existing font in the system, Windows may substitute some other font.

Once you load up a w.LOGFONT object with an appropriate set of field values, you can call w.CreateFontIndirect to have Windows construct a font that matches the characteristics you've supplied as closely as possible. An important fact to realize is that Windows may not give you exactly the font you've requested. For example, if you ask for a "Dom Casual" but the system doesn't have this typeface available, Windows will substitute some other font. If you've requested that Windows create a font that is a bit-mapped font, Windows may substitute a font in the same typeface family but of a different size. So never assume that Windows has given you exactly the font you've asked for. As you'll see in a little bit, you can query Windows to find out the characteristics of the font that Windows is actually using.

Once you create a font with w.CreateFontIndirect, Windows does not automatically start using that font. Instead, the w.CreateFontIndirect function returns a handle to the font that you can save and rapidly select into the device context as needed. This allows you to create several fonts early on and then rapidly switch between them by simply supplying their handles to the w.SelectObject function. The w.SelectObject API function lets you attach some GDI object to a device context. The prototype for this function is the following:

static

    SelectObject: procedure

    (

        hdc                 :dword;

        hgdiobj             :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SelectObject@8" );

The hdc parameter is a handle to the device context into which you want to activate the font. The hgdiob parameter is the handle of the object you want to insert (in this particular case, the font handle that w.CreateFontIndirect returns).

Because w.SelectObject requires a device context, you may only call it within a w.BeginPaint/w.EndPaint, w.GetDC/w.ReleaseDC, or w.GetWindowDC/w.ReleaseDC sequence. Of course, the wpa.hhf header file contains #keyword macro definitions for SelectObject so you can call it between a BeginPaint/EndPaint sequence thusly:

    BeginPaint( hwnd, ps, hdc );

            .

            .

            .

        SelectObject( hFontHandle );

            .

            .

            .

    EndPaint;

(note that the SelectObject macro only requires a single parameter because the BeginPaint macro automatically supplies the device context parameter for you.)

The w.SelectObject function (and, therefore, the SelectObject macro) returns the handle of the previous font in the EAX register. You should save this handle and then restore the original font (via another SelectObject invocation) when you are done using the font, e.g.,

    BeginPaint( hwnd, ps, hdc );

            .

            .

            .

        SelectObject( hFontHandle );

        mov( eax, oldFontHandle );

            .

            .

            .

        SelectObject( oldFontHandle );

    EndPaint;

If you don't know the characteristics of the font currently selected into the device context, you can call the w.GetTextMetrics API/GetTextMetrics macro in order to retrieve this information from Windows. The w.GetTextMetrics call as the following prototype:

static

    GetTextMetrics: procedure

    (

            hdc             :dword;

        var lptm            :TEXTMETRIC

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetTextMetricsA@8" );

As with most GDI function calls that require a device context, you'd call this function between w.BeginPaint/w.EndPaint, etc. The wpa.hhf header file supplies a GetTextMetrics macro (sans the first parameter that BeginPaint automatically fills in for you) that you can call within the BeginPaint..EndPaint sequence. The lptm parameter is the address of a w.TEXTMETRIC object, which takes the following form:

type

        TEXTMETRIC: record

                        tmHeight							:dword;

                        tmAscent							:dword;

                        tmDescent							:dword;

                        tmInternalLeading							:dword;

                        tmExternalLeading							:dword;

                        tmAveCharWidth							:dword;

                        tmMaxCharWidth							:dword;

                        tmWeight							:dword;

                        tmOverhang							:dword;

                        tmDigitizedAspectX							:dword;

                        tmDigitizedAspectY							:dword;

                        tmFirstChar							:byte;

                        tmLastChar							:byte;

                        tmDefaultChar							:byte;

                        tmBreakChar							:byte;

                        tmItalic							:byte;

                        tmUnderlined							:byte;

                        tmStruckOut							:byte;

                        tmPitchAndFamily							:byte;

                        tmCharSet							:byte;

        endrecord;

As you can see, many of these fields correspond to the values you pass in the w.LOGFONT structure when you create the font in the first place. After creating a font and selecting it into the device context, you can call GetTextMetrics to populate this data structure to verify that you've got a font with the values you expect.

Another important reason for calling GetTextMetrics is to obtain the height of the font you're currently using. You can use this height information to determine how far apart to space lines when drawing text to a window. In order to determine the nominal spacing between lines of text for a given font, simply add the values of the tmHeight and tmExternalLeading fields together. This sum provides the value you should add to the y-coordinate of the current line of text to determine the y-coordinate of the next line on the display. The example code in this book up to this point have always used a fixed distance of 20 pixels or so. While this is sufficient for the system font (and the examples you've seen), using a fixed distance like this is very poor practice; were the user to select in a larger system font, the lines of text could overlap if you use fixed height values.

When you are done with a font you have to explicitly destroy it. Fonts you select into a device context are persistent - that is, they hang around (taking up system resources) once your program terminates unless you explicitly tell Windows to delete those fonts. Failure to delete a font when you're through with it can lead to a resource leak. Internally, Windows only supports a limited number of resources for a given device context. If you fail to delete a resource you've selected into the context, and you lose track of the associated resource (i.e., font) handle, there is no way to recover that resource short of re-booting Windows. Therefore, you should take special care to delete all fonts when your done using them in your application via the w.DeleteObject API function. Here's the HLA prototype for the Windows version of this API function:

static

    DeleteObject: procedure

    (

        hObject :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__DeleteObject@4" );

The single parameter is the handle of the object you wish to delete. For a font, this would be the font handle that w.CreateFontIndirect returns. You will note that the w.DeleteObject function does not require a device context parameter value. Therefore, you may call this function anywhere, not just between a BeginPaint..EndPaint (or comparable) sequence. For this same reason, there is no DeleteObject macro that is part of the BeginPaint context-free macro declaration.

One problem with the w.CreateFontIndirect function is that it requires that you know the name of the font you want to create (or you have to be willing to live with a generic font that the system chooses for you). Although all modern (desktop) Windows platforms supply 13 basic True Type fonts, it's perfectly reasonable for the user to have installed additional fonts on the system. There is no reason your applications should limit users to the original set of fonts provided with Windows if they've installed additional fonts. The only question is: "how do you determine those font names so you can supply the name in the w.LOGFONT record that you pass to w.CreateFontIndirect?" Well, in modern Windows systems this is actually pretty easy; you bring up a font selection control window (provided by Windows) and you let the Windows code handle all the dirty work for you. We'll talk about this option in the chapter on controls later in this book. The other solution is to enumerate the fonts yourself and then pick a font from the list you've created.

Windows provides a function, w.EnumFontFamilies, that will iterate through all of the available fonts in the system and provide you with the opportunity to obtain each font name. The w.EnumFontFamilies function has the following prototype:

type

    FONTENUMPROC:

        procedure

        (

            var		lpelf			:ENUMLOGFONT;

            var		lpntm			:NEXTEXTMETRIC;

                    FontType			:dword;

                    lParam2			:dword

        );

static

    EnumFontFamilies:

        procedure

        (

                    hdc							:dword;

                    lpszFamily							:string;

                    lpEnumFontFamProc							:FONTENUMPROC;

            var		lParam							:var

        );

            @stdcall;

            @returns( "eax" );

            @external( "__imp__EnumFontFamiliesA@16" );

The w.EnumFontFamilies function requires these parameters:

  • hdc - The handle for the device context whose fonts you wish to enumerate. Note that w.EnumFontFamilies always iterates over the fonts for a specific device context.
  • lpszFamily - the address of a zero-terminated (e.g., HLA) string. This string must contain the name of the font family you wish to enumerate, or NULL if you want the function to enumerate all font families in the system.
  • lpEnumFontFamProc - the address of a "call back" function. Windows will call this function once for each font it enumerates. Your application must supply this function and Windows will pass information about the font to this callback function. Note that the call function's declaration must exactly match the w.FONTENUMPROC definition.
  • lParam - this is a 32-bit application-specific piece of data that the font numeration code passes on to the callback routine (in the lParam2 parameter). Generally, you will pass some application-specific data to the callback routine (such as the address where the callback routine can store the information that Windows passes to it) in this parameter.

For each font or font family present in the system, Windows will call the font enumeration callback routine whose address you pass as the lpEnumFontFamProc parameter to w.EnumFontFamilies. Windows will pass this callback procedure pointers to w.LOGFONT and w.TEXTMETRIC data structures that describe the current font. The fonts.hla sample program (in the listing that follows) demonstrates the use of the w.EnumFontFamilies function to create a list of all the available fonts in the system, as well as display an example of each font.

The fonts.hla program captures the w.WM_CREATE message for the main window to determine when the window is first created and the program can enumerate all the fonts in the system. The message handle for the create message begins by enumerating all the font families in the system. Then, for each of the font families, it enumerates each font in that family. Because the program doesn't know, beforehand, how many fonts are present in the system, this application uses a list data structure that grows dynamically with each font the program enumerates. If you're concerned about "linked list algorithms" and "node insertion" or "node traversal" algorithms, you're in for a pleasant surprise: HLA provides a generic list class that makes the creation and manipulation of lists almost trivial. The fonts.hla program takes advantage of this feature of the HLA standard library to reduce the amount of effort needed to create a dynamically sizeable list.

The fonts application actually needs to maintain a two-dimensional list structure. The main list is a list of font families (that is, each node in the list represents a single font family). Each font family also has a list of fonts that are members of that family (see Figure 6-12 )

Figure 6-12: Font Family List and Font List Structures

Here's the data structure for the fFamily_t class that maintains the list of font families in the system:

type

    fFamily_t:

        class inherits( node );

            var

                familyName  :string;            // Font family name.

                fonts       :pointer to list;   // List of fonts in family.

            override procedure create;

            override method destroy;

        endclass;

The familyName field points at a string that holds the font family's name. The fonts field points at a list of font_t nodes. These nodes have the following structure:

type

    font_t:

        class inherits( node );

            var

                tm          :w.TEXTMETRIC;

                lf          :w.LOGFONT;

                fontName    :string;

            override procedure create;

            override method destroy;

        endclass;

The create procedure and destroy method are the conventional class constructors and destructors that allocate storage for objects (create) and deallocate storage when the application is through with them. See the HLA documentation or The Art of Assembly Language Programming for more details on class constructors and destructors.

The fonts.hla Create procedure, that handles the w.WM_CREATE message, takes the following form:

// create:

//

//  The procedure responds to the "w.WM_CREATE" message. It enumerates

// the available font families.

procedure Create( hwnd: dword; wParam:dword; lParam:dword ); @returns( "eax" );

var

    hdc :dword;

begin Create;

    push( esi );

    push( edi );

    GetDC( hwnd, hdc );

        // Enumerate the families:

        w.EnumFontFamilies( hdc, NULL, &FontFamilyCallback, NULL );

        // Enumerate the fonts appearing in each family:

        foreach fontFamList.itemInList() do

            w.EnumFontFamilies

            (

                hdc,

                (type fFamily_t [esi]).familyName,

                &EnumSingleFamily,

                [esi]

            );

        endfor;

    ReleaseDC;

    pop( edi );

    pop( esi );

    mov( 0, eax );  // Return success.

end Create;

An interesting thing to note in this procedure is that it uses GetDC and ReleaseDC to obtain and release a device context (needed by w.EnumFontFamilies). Because this procedure is not handling a w.WM_PAINT message, we cannot use BeginPaint and EndPaint. This is a good example of when you need to use GetDC and ReleaseDC.

The first call to w.EnumFontFamilies in this code is responsible for building the fontFamList object that is the list of font families. The fact that the second parameter is NULL (it normally points at a string containing a font family name) tells the w.EnumFontFamilies function to enumerate only the families, not the individual fonts within a family. The w.EnumFontFamilies function will call the FontFamilyCallback function (whose address is passed as the third parameter) once for each font family in the system. It is FontFamilyCallback's responsibility to actually build the list of font families. Here's the code that builds this list:

// Font callback function that enumerates the font families.

//

//  On each call to this procedure we need to create a new

// node of type fFamily_t, initialize that object with the

// appropriate font information, and append the node to the

// end of the "fontFamList" list.

procedure FontFamilyCallback

(

    var lplf        :w.LOGFONT;

    var lpntm       :w.TEXTMETRIC;

        nFontType   :dword;

        lparam      :dword

);

    @stdcall;

    @returns( "eax" );

var

    curFam  :pointer to fFamily_t;

begin FontFamilyCallback;

    push( esi );

    push( edi );

    // Create a new fFamily_t node to hold this guy:

    fFamily_t.create();

    mov( esi, curFam );

    // Append node to the end of the font families list:

    fontFamList.append( curFam );

    // Initialize the font family object we just created:

    mov( curFam, esi );

    // Initialize the string containing the font family name:

    mov( lplf, eax );

    str.a_cpyz( (type w.LOGFONT [eax]).lfFaceName );

    mov( eax, (type fFamily_t [esi]).familyName );

    // Create a new list to hold the fonts in this family (initially,

    // this list is empty).

    list.create();

    mov( curFam, edi );

    mov( esi, (type fFamily_t [edi]).fonts );

    // Return success

    mov( 1, eax );

    pop( edi );

    pop( esi );

end FontFamilyCallback;

The first thing this function does is create a new fFamily_t object (by calling the create procedure for this class) and then appends this new object to the end of the fontFamList list. After adding this node to the font family list, the create procedure makes a copy of the font family's name and stores this into the object's familyName field (the str.a_cpyz standard library function converts a zero-terminated string to an HLA string and returns a pointer to that string in EAX, just in case you're wondering). Finally, this constructor creates an empty list to hold the individual fonts (that the application adds later).

Because the w.EnumFontFamilies function calls the FontFamilyCallback function once for each font family in the system, the FontFamilyCallback function winds up create a complete list (fontFamList) with all the font families present (because on each call, this function appends a font family object to the end of the fontFamList). When the w.EnumFontFamilies function returns to the Create procedure, therefore, the fontFamList contains a list of font family names as well as a set of empty lists ready to hold the individual font information. To fill in these empty font lists, the Create procedure simply needs to iterate over the fontFamList and call w.EnumFontFamilies for each of the individual font families. The foreach loop in the Create procedure execute's the HLA standard library itemInList iterator that steps through each node in the fontFamList list1. This foreach loop calls the w.EnumFontFamilies function for each node in the font families list. This call to w.EnumFontFamilies, however, passes the current font family name, so it only enumerates those fonts belonging to that specific family. There are two other differences between this call and the earlier call to w.EnumFontFamilies: this call passes the address of the EnumSingleFamily procedure and it also passes the address of the current font family list node in the lparam parameter (which Windows passes along to EnumSingleFamily for each font). The EnumSingleFamily needs the address of the parent font family node so it can append a font_t object to the end of the fonts list present in each font family node. Here's the code for the EnumSingleFamily procedure:

// Font callback function that enumerates a single font.

// On entry, lparam points at a fFamily_t element whose

// fonts list we append the information to.

procedure EnumSingleFamily

(

    var lplf        :w.LOGFONT;

    var lpntm       :w.TEXTMETRIC;

        nFontType   :dword;

        lparam      :dword

);

    @stdcall;

    @returns( "eax" );

var

    curFont :pointer to font_t;

begin EnumSingleFamily;

    push( esi );

    push( edi );

    // Create a new font_t object to hold this font's information:

    font_t.create();

    mov( esi, curFont );

    // Append the new font to the end of the family list:

    mov( lparam, esi );

    mov( (type fFamily_t [esi]).fonts, esi );

    (type list [esi]).append_last( curFont );

    // Initialize the string containing the font family name:

    mov( curFont, esi );

    mov( lplf, eax );

    str.a_cpyz( (type w.LOGFONT [eax]).lfFaceName );

    mov( eax, (type font_t [esi]).fontName );

    // Copy the parameter information passed to us into the

    // new font_t object:

    lea( edi, (type font_t [esi]).tm );

    mov( lpntm, esi );

    mov( @size( w.TEXTMETRIC), ecx );

    rep.movsb();

    mov( curFont, esi );

    lea( edi, (type font_t [esi]).lf );

    mov( lplf, esi );

    mov( @size( w.LOGFONT ), ecx );

    rep.movsb();

    mov( 1, eax ); // Return success

    pop( edi );

    pop( esi );

end EnumSingleFamily;

Like the fontFamilyCallback procedure, this procedure begins by create a new object (font_t in this case). The EnumSingleFamily appends this node to the end of the fonts list that is a member of some font family node (whose address Windows passes into this procedure in the lparam parameter). After creating the new node and appending it to the end of a fonts list, this procedure initializes the fields of the new object. First, this code creates an HLA string with the font's name (just as the fontFamilyCallback function did). This procedure also copies the w.TEXTMETRIC and w.LOGFONT data passed in as parameters into the font_t object (this particular application doesn't actually use most of this information, but this example code copies everything in case you want to cut and paste this code into another application).

When the foreach loop in the Create procedure finishes execution, the Create procedure has managed to build the entire two-dimensional font list data structure. If you've ever created a complex data structure like this before, you can probably appreciate all the work that the HLA lists class and the Windows' w.EnumFontFamilies is doing for you. While this code isn't exactly trivial, the amount of code you'd have to write to do all this list management on your own is tremendous. The presence of the HLA Standard Library saves considerable effort in this particular application.

Once the Create procedure constructs the font lists, the only thing left to do is to display the font information. As you'd probably expect by now, the Paint procedure handles this task. Just to make things interesting (as well as to demonstrate how to select new fonts into the device context), the paint procedure draws the font family name to the window using the system font (which is readable) and then displays some sample text for each of the fonts using each font to display that information. Here's the Paint procedure and the code that pulls this off:

// Paint:

//

//  This procedure handles the "w.WM_PAINT" message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @returns( "eax" );

var

    hdc         :dword;             // Handle to video display device context

    yCoordinate :dword;             // Y-Coordinate for text output.

    newFont     :dword;             // Handle for new fonts we create.

    oldFont     :dword;             // Saves system font while we use new font.

    ps          :w.PAINTSTRUCT;     // Used while painting text.

    outputMsg   :string;            // Holds output text.

    defaultHt   :dword;             // Default font height.

    tm          :w.TEXTMETRIC;      // Default font metrics

begin Paint;

    tstralloc( 256 );       // Allocate string storage on the stack

    mov( eax, outputMsg );  //  for our output string.

    push( edi );

    push( ebx );

    BeginPaint( hwnd, ps, hdc );

        // Get the height of the default font so we can output font family

        // names using the default font (and properly skip past them as

        // we output them):

        w.GetTextMetrics( hdc, tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        mov( eax, defaultHt );

        // Initialize the y-coordinate before we draw the font samples:

        mov( -10, yCoordinate );

        // Okay, output a sample of each font:

        push( esi );

        foreach fontFamList.itemInList() do

            // Add in a little extra space for each new font family:

            add( 10, yCoordinate );

            // Write a title line in the system font (because some fonts

            // are unreadable, we want to display the family name using

            // the system font).

            //

            // Begin by computing the number of fonts so we can display

            // that information along with the family title:

            push( esi );

            mov( (type fFamily_t [esi]).fonts, ebx );

            (type list [ebx]).numNodes();

            pop( esi );

            if( eax == 1 ) then

                // Only one font in family, so write "1 font":

                str.put

                (

                    outputMsg,

                    "Font Family: ",

                    (type fFamily_t [esi]).familyName,

                    " (1 font)"

                );

            else

                // Two or more fonts in family, so write "n fonts":

                str.put

                (

                    outputMsg,

                    "Font Family: ",

                    (type fFamily_t [esi]).familyName,

                    " (",

                    (type uns32 eax),

                    " fonts)"

                );

            endif;

            w.TextOut

            (

                hdc,

                10,

                yCoordinate,

                outputMsg,

                str.length(outputMsg)

            );

            // Skip down vertically the equivalent of one line in the current

            // font's size:

            mov( defaultHt, eax );

            add( eax, yCoordinate );

            // For each of the fonts in the current font family,

            // output a sample of that particular font:

            mov( (type fFamily_t [esi]).fonts, ebx );

            foreach (type list [ebx]).itemInList() do

                // Create a new font based on the current font

                // we're processing on this loop iteration:

                w.CreateFontIndirect( (type font_t [esi]).lf );

                mov( eax, newFont );

                // Select the new font into the device context:

                w.SelectObject( hdc, eax );

                mov( eax, oldFont );

                // Compute the font size in points.  This is computed

                // as:

                //

                //  ( <font height> * 72 ) / <font's Y pixels/inch>

                w.GetDeviceCaps( hdc, w.LOGPIXELSY ); // Y pixels/inch

                mov( eax, ecx );

                mov( (type font_t [esi]).lf.lfHeight, eax ); // Font Height

                imul( 72, eax );

                div( ecx, edx:eax );

                // Output the font info:

                str.put

                (

                    outputMsg,

                    (type font_t [esi]).fontName,

                    " (Size in points: ",

                    (type uns32 eax),

                    ')'

                );

                w.TextOut

                (

                    hdc,

                    20,

                    yCoordinate,

                    outputMsg,

                    str.length( outputMsg )

                );

                // Adjust the y-coordinate to skip over the

                // characters we just emitted:

                mov( (type font_t [esi]).tm.tmHeight, eax );

                add( (type font_t [esi]).tm.tmExternalLeading, eax );

                add( eax, yCoordinate );

                // Free the font resource and restore the original font:

                w.SelectObject( hdc, oldFont );

                w.DeleteObject( newFont );

            endfor;

        endfor;

        pop( esi );

    EndPaint;

    pop( ebx );

    pop( edi );

    mov( 0, eax );  // Return success

end Paint;

The Paint procedure operates using two nested foreach loops. The outermost foreach loop iterates over each node in the font families list, the inner-most foreach loop iterates over each node in the fonts list attached to each of the nodes in the font families list. The action, therefore, is to choose a font family, iterator over each font in that family, move on to the next family, iterate over each font in that new family, and repeat for each font family in the fontFamList object.

For each font family, the Paint procedure draws a line of text (in the system font) specifying the font family's name. Paint tracks the output position (y-coordinate) using the yCoordinate local variable. For each line of text that this procedure outputs, it adds the height of the font (plus external leading) to the yCoordinate variable so that the next output line will occur below the current output line.

Once the Paint procedure outputs the family name (and the number of fonts available in that family), it executes the nested foreach loop that displays a sample of each font in the family (see Figure 6-13 for typical output; note that your display may differ depending upon the fonts you've installed in your system). The foreach loop begins by creating a new font using the current fonts node's fontName string by calling w.CreateFontIndirect. Next, the code selects this font into the device context via a w.SelectObject call. Finally, just before writing the font sample to the window, this code sequence computes the size of the font (in points) by multiplying the height of the font (in pixels) by the number of points/inch (72) and then divides this product by the number of pixels/inch in the device context. After all this work, foreach loop displays the name of that font (in that font) along with the size of the font.

Probably the first thing you'll notice about font.hla's output is that it chops off the font listing at the bottom of the window. Fear not, we'll take a look at the solution to this problem (scroll bars) in the very next section.

Figure 6-13: Fonts Output

Here's the complete source code to the fonts.hla application:

// Fonts.hla:

//

// Displays all the fonts available in the system.

program Fonts;

#include( "w.hhf" )         // Standard windows stuff.

#include( "wpa.hhf" )       // "Windows Programming in Assembly" specific stuff.

#include( "strings.hhf" )   // String functions.

#include( "memory.hhf" )    // tstralloc is in here

#include( "lists.hhf" )     // List abstract data type appears here

?@nodisplay := true;        // Disable extra code generation in each procedure.

?@nostackalign := true;     // Stacks are always aligned, no need for extra code.

type

    // font_t objects are nodes in a list of fonts belonging to a single

    // family.  Such lists appearing in a font family object (class fFamily_t).

    font_t:

        class inherits( node );

            var

                tm          :w.TEXTMETRIC;

                lf          :w.LOGFONT;

                fontName    :string;

            override procedure create;

            override method destroy;

        endclass;

    // fFamily_t objects are nodes in a list of font families. Each node in

    // this list represents a single font family in the system.  Also note

    // that these objects contain a list of fonts that belong to that

    // particular family.

    fFamily_t:

        class inherits( node );

            var

                familyName  :string;            // Font family name.

                fonts       :pointer to list;   // List of fonts in family.

            override procedure create;

            override method destroy;

        endclass;

static

    hInstance   :dword;         // "Instance Handle" supplied by Windows.

    wc          :w.WNDCLASSEX;  // Our "window class" data.

    msg         :w.MSG;         // Windows messages go here.

    hwnd        :dword;         // Handle to our window.

    fontFamList :pointer to list;   // List of font families.

readonly

    ClassName:  string := "FontsWinClass";      // Window Class Name

    AppCaption: string := "Available Fonts";    // Caption for Window

// The following data type and DATA declaration

// defines the message handlers for this program.

type

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue:   dword;

            MessageHndlr:   MsgProc_t;

        endrecord;

// The dispatch table:

//

//  This table is where you add new messages and message handlers

//  to the program.  Each entry in the table must be a tMsgProcPtr

//  record containing two entries: the message value (a constant,

//  typically one of the wm.***** constants found in windows.hhf)

//  and a pointer to a "tMsgProc" procedure that will handle the

//  message.

readonly

    Dispatch:   MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// Methods and procedures for the font_t class.

// Remember, ESI contains the "THIS" pointer upon entry to these procedures

// and methods.

//

//

// create-  Constructor for a font_t node in a font list.

//          Note: returns pointer to object in ESI.  Allocates

//          new storage for a node object if ESI contains NULL upon entry.

procedure font_t.create;

begin create;

    push( eax );

    if( esi == NULL ) then

        // If this is a bare constructor call (font_t.create) then

        // allocate storage for a new node:

        malloc( @size( font_t ) );

        mov( eax, esi );

    endif;

    mov( NULL, this.fontName );

    push( ecx );

    push( edi );

    // Zero out the tm and lf data structures:

    lea( edi, this.tm );

    mov( @size( w.TEXTMETRIC ), ecx );

    xor( eax, eax );

    rep.stosb;

    lea( edi, this.lf );

    mov( @size( w.TEXTMETRIC ), ecx );

    rep.stosb;

    pop( ecx );

    pop( edi );

    pop( eax );

end create;

// font_t.destroy-

//

//  Destructor for a font_t node object.

//  Because this program never frees any item in the list, there

//  really is no purpose for this function; it is required by the

//  node class, hence its presence here.  If this application wanted

//  to free the items in the font lists, it would clear the storage

//  allocated to the fontName field (if non-NULL and on the heap)

//  and it would free the storage associated with the node itself.

//  The following code demonstrates this, even though this program

//  never actually calls this method.

method font_t.destroy;

begin destroy;

    // Free the string name if it was allocated on the heap:

    if( this.fontName <> NULL ) then

        if( strIsInHeap( this.fontName )) then

            strfree( this.fontName );

        endif;

    endif;

    // Free the object if it was allocated on the heap:

    if( isInHeap( esi /* this */ )) then

        free( esi );

    endif;

end destroy;

// Methods and procedures for the fFamily_t class.

// Remember, ESI contains the "THIS" pointer upon entry to these procedures

// and methods.

//

//

// create-  Constructor for a fFamily_t node in a font family list.

//          Note: returns pointer to object in ESI.  Allocates

//          new storage for a node object if ESI contains NULL upon entry.

procedure fFamily_t.create;

begin create;

    push( eax );

    if( esi == NULL ) then

        // If this is a bare constructor call (fFamily_t.create) then

        // allocate storage for a new node:

        malloc( @size( fFamily_t ) );

        mov( eax, esi );

    endif;

    // Initialize the family name to NULL (it will be filled in

    // by whomever is enumerating the family lists):

    mov( NULL, this.familyName );

    // Create a new list to hold the font information for this family:

    push( esi );

    font_t.create();

    mov( esi, eax );

    pop( esi );

    mov( eax, this.fonts );

    pop( eax );

end create;

// fFamily_t.destroy-

//

//  Destructor for a fFamily_t node object.

//  Because this program never frees any item in the list, there

//  really is no purpose for this function; it is required by the

//  node class, hence its presence here.  If this application wanted

//  to free the items in the font lists, it would clear the storage

//  allocated to the familyName field (if non-NULL and on the heap)

//  and it would free the storage associated with the node itself.

//  The following code demonstrates this, even though this program

//  never actually calls this method.

method fFamily_t.destroy;

begin destroy;

    // Free the string name if it was allocated on the heap:

    if( this.familyName <> NULL ) then

        if( strIsInHeap( this.familyName )) then

            strfree( this.familyName );

        endif;

    endif;

    // Free up the font list:

    push( esi );

    mov( this.fonts, esi );

    (type list [esi]).destroy();

    pop( esi );

    // Free the object if it was allocated on the heap:

    if( isInHeap( esi /* this */ )) then

        free( esi );

    endif;

end destroy;

// Font callback function that enumerates the font families.

//

//  On each call to this procedure we need to create a new

// node of type fFamily_t, initialize that object with the

// appropriate font information, and append the node to the

// end of the "fontFamList" list.

procedure FontFamilyCallback

(

    var lplf        :w.LOGFONT;

    var lpntm       :w.TEXTMETRIC;

        nFontType   :dword;

        lparam      :dword

);

    @stdcall;

    @returns( "eax" );

var

    curFam  :pointer to fFamily_t;

begin FontFamilyCallback;

    push( esi );

    push( edi );

    // Create a new fFamily_t node to hold this guy:

    fFamily_t.create();

    mov( esi, curFam );

    // Append node to the end of the font families list:

    fontFamList.append( curFam );

    // Initialize the font family object we just created:

    mov( curFam, esi );

    // Initialize the string containing the font family name:

    mov( lplf, eax );

    str.a_cpyz( (type w.LOGFONT [eax]).lfFaceName );

    mov( eax, (type fFamily_t [esi]).familyName );

    // Create a new list to hold the fonts in this family (initially,

    // this list is empty).

    list.create();

    mov( curFam, edi );

    mov( esi, (type fFamily_t [edi]).fonts );

    // Return success

    mov( 1, eax );

    pop( edi );

    pop( esi );

end FontFamilyCallback;

// Font callback function that enumerates a single font.

// On entry, lparam points at a fFamily_t element whose

// fonts list we append the information to.

procedure EnumSingleFamily

(

    var lplf        :w.LOGFONT;

    var lpntm       :w.TEXTMETRIC;

        nFontType   :dword;

        lparam      :dword

);

    @stdcall;

    @returns( "eax" );

var

    curFont :pointer to font_t;

begin EnumSingleFamily;

    push( esi );

    push( edi );

    // Create a new font_t object to hold this font's information:

    font_t.create();

    mov( esi, curFont );

    // Append the new font to the end of the family list:

    mov( lparam, esi );

    mov( (type fFamily_t [esi]).fonts, esi );

    (type list [esi]).append_last( curFont );

    // Initialize the string containing the font family name:

    mov( curFont, esi );

    mov( lplf, eax );

    str.a_cpyz( (type w.LOGFONT [eax]).lfFaceName );

    mov( eax, (type font_t [esi]).fontName );

    // Copy the parameter information passed to us into the

    // new font_t object:

    lea( edi, (type font_t [esi]).tm );

    mov( lpntm, esi );

    mov( @size( w.TEXTMETRIC), ecx );

    rep.movsb();

    mov( curFont, esi );

    lea( edi, (type font_t [esi]).lf );

    mov( lplf, esi );

    mov( @size( w.LOGFONT ), ecx );

    rep.movsb();

    mov( 1, eax ); // Return success

    pop( edi );

    pop( esi );

end EnumSingleFamily;

/**************************************************************************

**

** Message Handling Procedures:

*/

// QuitApplication:

//

//  This procedure handles the "wm.Destroy" message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// create:

//

//  The procedure responds to the "w.WM_CREATE" message. It enumerates

// the available font families.

procedure Create( hwnd: dword; wParam:dword; lParam:dword ); @returns( "eax" );

var

    hdc :dword;

begin Create;

    push( esi );

    push( edi );

    GetDC( hwnd, hdc );

        // Enumerate the families:

        w.EnumFontFamilies( hdc, NULL, &FontFamilyCallback, NULL );

        // Enumerate the fonts appearing in each family:

        foreach fontFamList.itemInList() do

            w.EnumFontFamilies

            (

                hdc,

                (type fFamily_t [esi]).familyName,

                &EnumSingleFamily,

                [esi]

            );

        endfor;

    ReleaseDC;

    pop( edi );

    pop( esi );

    mov( 0, eax );  // Return success.

end Create;

// Paint:

//

//  This procedure handles the "w.WM_PAINT" message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @returns( "eax" );

var

    hdc         :dword;             // Handle to video display device context

    yCoordinate :dword;             // Y-Coordinate for text output.

    newFont     :dword;             // Handle for new fonts we create.

    oldFont     :dword;             // Saves system font while we use new font.

    ps          :w.PAINTSTRUCT;     // Used while painting text.

    outputMsg   :string;            // Holds output text.

    defaultHt   :dword;             // Default font height.

    tm          :w.TEXTMETRIC;      // Default font metrics

begin Paint;

    tstralloc( 256 );       // Allocate string storage on the stack

    mov( eax, outputMsg );  //  for our output string.

    push( edi );

    push( ebx );

    BeginPaint( hwnd, ps, hdc );

        // Get the height of the default font so we can output font family

        // names using the default font (and properly skip past them as

        // we output them):

        w.GetTextMetrics( hdc, tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        mov( eax, defaultHt );

        // Initialize the y-coordinate before we draw the font samples:

        mov( -10, yCoordinate );

        // Okay, output a sample of each font:

        push( esi );

        foreach fontFamList.itemInList() do

            // Add in a little extra space for each new font family:

            add( 10, yCoordinate );

            // Write a title line in the system font (because some fonts

            // are unreadable, we want to display the family name using

            // the system font).

            //

            // Begin by computing the number of fonts so we can display

            // that information along with the family title:

            push( esi );

            mov( (type fFamily_t [esi]).fonts, ebx );

            (type list [ebx]).numNodes();

            pop( esi );

            if( eax == 1 ) then

                // Only one font in family, so write "1 font":

                str.put

                (

                    outputMsg,

                    "Font Family: ",

                    (type fFamily_t [esi]).familyName,

                    " (1 font)"

                );

            else

                // Two or more fonts in family, so write "n fonts":

                str.put

                (

                    outputMsg,

                    "Font Family: ",

                    (type fFamily_t [esi]).familyName,

                    " (",

                    (type uns32 eax),

                    " fonts)"

                );

            endif;

            w.TextOut

            (

                hdc,

                10,

                yCoordinate,

                outputMsg,

                str.length(outputMsg)

            );

            // Skip down vertically the equivalent of one line in the current

            // font's size:

            mov( defaultHt, eax );

            add( eax, yCoordinate );

            // For each of the fonts in the current font family,

            // output a sample of that particular font:

            mov( (type fFamily_t [esi]).fonts, ebx );

            foreach (type list [ebx]).itemInList() do

                // Create a new font based on the current font

                // we're processing on this loop iteration:

                w.CreateFontIndirect( (type font_t [esi]).lf );

                mov( eax, newFont );

                // Select the new font into the device context:

                w.SelectObject( hdc, eax );

                mov( eax, oldFont );

                // Compute the font size in points.  This is computed

                // as:

                //

                //  ( <font height> * 72 ) / <font's Y pixels/inch>

                w.GetDeviceCaps( hdc, w.LOGPIXELSY ); // Y pixels/inch

                mov( eax, ecx );

                mov( (type font_t [esi]).lf.lfHeight, eax ); // Font Height

                imul( 72, eax );

                div( ecx, edx:eax );

                // Output the font info:

                str.put

                (

                    outputMsg,

                    (type font_t [esi]).fontName,

                    " (Size in points: ",

                    (type uns32 eax),

                    ')'

                );

                w.TextOut

                (

                    hdc,

                    20,

                    yCoordinate,

                    outputMsg,

                    str.length( outputMsg )

                );

                // Adjust the y-coordinate to skip over the

                // characters we just emitted:

                mov( (type font_t [esi]).tm.tmHeight, eax );

                add( (type font_t [esi]).tm.tmExternalLeading, eax );

                add( eax, yCoordinate );

                // Free the font resource and restore the original font:

                w.SelectObject( hdc, oldFont );

                w.DeleteObject( newFont );

            endfor;

        endfor;

        pop( esi );

    EndPaint;

    pop( ebx );

    pop( edi );

    mov( 0, eax );  // Return success

end Paint;

/**************************************************************************/

/*                   End of Application Specific Code                     */

/**************************************************************************/

// The window procedure.  Since this gets called directly from

// windows we need to explicitly reverse the parameters (compared

// to the standard STDCALL declaration) in order to make HLA's

// Pascal calling convention compatible with Windows.

//

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword  );

    @stdcall;

    @nodisplay;

    @noalignstack;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin Fonts;

    // Create the font family list here:

    push( esi );

    list.create();

    mov( esi, fontFamList );

    pop( esi );

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    mov( eax, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end Fonts;

6.5: Scroll Bars

One problem that is immediately obvious with the fonts program from the previous section is that there is too much information to display in the window at one time. As a result, Windows truncates much of the information when drawing the text to the window. Scroll bars provide the solution for this problem. Scroll bars allow the user to use a window as a "window" into a larger viewing area (hence the name, window) by selecting the relative coordinate of the upper left-hand corner of the window within the larger viewing area. Applications that need to display more than one window full of information will generally employ a vertical scroll bar (to allow the user to move the display in the window up or down) and a horizontal scroll bar (to allow the user to move the display in the window from side to side). With a properly written application, a user may view any part of the document via the scroll bars (see Figure 6-14).

Figure 6-14: Scroll Bar Actions

From the user's perspective, the scroll bars move the document around within the window. From an application's perspective, however, what is really going on is that the scroll bars are repositioning the window over the document. If we think of coordinate (0,0) as being the upper-left hand pixel in the document, then adjust the view using the scroll bars simply defines the coordinate in the document that corresponds to the upper-left hand corner of the window within the document. This is why clicking on the "up arrow" on the scroll bar actually cause the contents of the window to scroll down. What's really happening is that the user is moving the "starting coordinate" of the window up on line in the document. Because the top of the window is now displaying one line earlier in the document, the contents of the window shifts down one line. A similar explanation applies to scrolling data left or right in the window via the scroll bars.

Adding scroll bars to your application's windows is very easy. All you've got to do is supply the w.WS_VSCROLL and w.WS_HSCROLL constants as part of the window style when calling w.CreateWindowEx, e.g.,

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW | w.WS_VSCROLL |  w.WS_HSCROLL,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

With these constants as part of the window style parameter in the call to w.CreateWindowEx, Windows automatically draws the scroll bars (if necessary) and starts sending appropriate messages to your window when the user clicks on a scroll bar or drags the thumb around. Note that although Windows will automatically handle all mouse activities on the scroll bar, Windows does not automatically process keystroke equivalents (e.g., PgUp and PgDn). You will have to handle such keystrokes yourself; we'll discuss how to do that in the chapter on Event-Driven Input/Output a little later in this book.

Scroll bars in a window have three important numeric attributes: a minimum range value, a maximum range value, and a current position value. The minimum and maximum range values are two numeric integers that specify the minimum and maximum values that Windows will use as the "thumb" (scroll box) position within the scroll bar. The current position value is some value, within this range, that represents the current value of the scroll bar's "thumb" (scroll box). At any time you may query these values using the w.GetScrollRange and w.GetScrollPos API functions:

type

    GetScrollPos: procedure

    (

        hWnd            :dword;

        nBar            :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetScrollPos@8" );

    GetScrollRange: procedure

    (

            hWnd        :dword;

            nBar        :dword;

        var lpMinPos    :dword;

        var lpMaxPos    :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetScrollRange@16" );

The hWnd parameter to these two functions is either the handle of a window containing a standard horizontal or vertical scroll bar, on the handle of a special scroll bar window you've created (we'll discuss how to create your own scroll bar windows in the chapter on Controls, Dialogs, Menus, and Windows later in this book; for now, we'll just supply the handle of a window that has the w.WS_VSCROLL or w.WS_HSCROLL window styles).

The nBar parameter in these two functions specifies which scroll bar values to retrieve. This parameter must be one of the following three constants:

  • w.SB_CTL - use this constant if you're reading the value of a custom scroll bar control. When you supply this constant, the hWnd parameter must be the handle of the scroll bar control whose value(s) you wish to retrieve.
  • w.SB_HORZ - use this constant if you want to retrieve the position or range values for the horizontal scroll bar in a standard window with the w.WS_HSCROLL or w.WS_VSCROLL styles.
  • w.SB_VERT - use this constant if you want to retrieve the position or range values for the vertical scroll bar in a standard window with the w.WS_HSCROLL or w.WS_VSCROLL styles.

The w.GetScrollPos function returns the current thumb position in the EAX register. The w.GetScrollRange returns the minimum and maximum positions in the lpMinPos and lpMaxPos parameters you pass by reference to the function.

Windows' scroll bars have a default range of 0..100 (i.e., the scroll position indicates a percentage of the document). The w.SetScrollRange API function lets you change the scroll bar range to a value that may be more appropriate for your application. Here's the prototype for this function:

type

    SetScrollRange: procedure

    (

        hWnd        :dword;

        nBar        :dword;

        nMinPos     :dword;

        nMaxPos     :dword;

        bRedraw     :boolean

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetScrollRange@20" );

The hWnd and nBar parameters have the same meaning as for w.GetScrollRange. The nMinPos and nMaxPos parameters specify the new minimum and maximum values for the scroll bar's range. Note that nMinPos must be less than or equal to nMaxPos. If the two values are equal, Windows will remove the scroll bar from the window. These should be unsigned values in the range 0..65535. Technically, Windows allows any unsigned 32-bit value here, but as you'll see in a little bit, it is a bit more efficient to limit the scroll bar positions to 16-bit values. Fortunately, 16-bit resolution is usually sufficient (i.e., you can still scroll a document with 65,536 lines one line at a time when using this resolution). The bRedraw parameter (true or false) determines whether Windows will redraw the scroll bar after you call w.SetScrollRange. Normally, you'd probably want to set this parameter to true unless you're about to call some other function (e.g., w.SetScrollPos) that will also redraw the scroll bar; setting bRedraw to false prevents Windows from redrawing the scroll bar twice (which slows down your application and make cause the scroll bar region to flash momentarily).

You may also set the current position of the scroll thumb using the w.SetScrollPos API function:

type

    SetScrollPos: procedure

    (

        hWnd        :dword;

        nBar        :dword;

        nPos        :dword;

        bRedraw     :boolean

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetScrollPos@16" );

The hWnd and nBar parameters have the same meaning as for w.GetScrollPos. The nPos parameter specifies the new thumb position in the scroll bar; this must be a value between the minimum and maximum scroll range values for the scroll bar. The bRedraw parameter has the same meaning as the w.SetScrollRange parameter of the same name: it controls whether Windows will redraw the scroll bar during this function call. Generally, you'll want to redraw the scroll bar at least once at the end of a sequence of modifications to the scroll bar parameters (i.e., range and position). So the last call to w.SetScrollRange or w.SetScrollPos for a given scroll bar should pass true as the bRedraw parameter value.

Although Windows automates much of the work associated with scroll bars, it does not do everything for you. Windows will handle mouse activities on the scroll bar and report such actions to your program. Windows redraws the scroll bar and positions the thumb as the user drags it around. Windows will also send the window procedure (for the window containing the scroll bar) a sequence of messages indicating certain activities within the scroll bar. However, it is your application's responsibility to respond to these messages and actually redraw the window with the new view suggested by the scroll bar values. It is also your application's responsibility to initialize the scroll bar range and update the scroll bar position (as appropriate).

Whenever the user clicks on the scroll bar or drags the thumb, Windows will send a w.WM_HSCROLL or w.WM_VSCROLL message to the window procedure of the window containing the affected scroll bar. The L.O. word of the wParam parameter in the window procedure contains a value that specifies the activity taking place on the scroll bar. This word typically contains one of the values found in Table 6-3.

Table 6-3: wParam Values for a w.WM_HSCROLL or w.WM_VSCROLL Message
Value
Description
w.SB_ENDSCROLL
Indicates that the user has released the mouse button (you can usually ignore this message).
w.SB_LEFT
These two values are actually the same. They indicate scrolling up or to the left (depending on whether the scroll bar is a horizontal or vertical scroll bar, i.e., whether you've received a w.WM_HSCROLL or w.WM_VSCROLL message.)
w.SB_UP
w.SB_RIGHT
These two values are actually the same. They indicate scrolling down or to the right (depending on whether the message was a w.WM_VSCROLL or w.VM_HSCROLL.)
w.SB_DOWN
w.SB_LINEUP
These two values are the same. They indicate scrolling up or left by one unit. Windows passes this value when the user clicks on the up arrow (in a vertical scroll bar) or a left arrow (in a horizontal scroll bar). Your application should scroll the text down one line if this is a w.WM_VSCROLL message, it should scroll the document one "position" to the right if this is a w.VM_HSCROLL message. Yes, you are scrolling the document in the opposite direction of the scroll message. Remember, the application's perspective of what is happening is opposite of the user's perspective.
w.SB_LINELEFT
w.SB_LINEDOWN
These two values are the same. They indicate scrolling down or right by one unit. Windows passes this value when the user clicks on the up arrow (in a vertical scroll bar) or a left arrow (in a horizontal scroll bar). Your application should scroll the text down one line if this is a w.WM_VSCROLL message, it should scroll the document one "position" to the right if this is a w.VM_HSCROLL message. Yes, you are scrolling the document in the opposite direction of the scroll message. Remember, the application's perspective of what is happening is opposite of the user's perspective.
w.SB_LINERGHT
w.SB_PAGEUP
These two values are the same. They indicate scrolling up or left by one screen. Windows passes this value when the user clicks between the thumb and the up-arrow (in a vertical scroll bar) or between the thumb and the left arrow (in a horizontal scroll bar). Your application should scroll the text down screen (or thereabouts) if this is a w.WM_VSCROLL message, it should scroll the document one screen to the right if this is a w.VM_HSCROLL message. Yes, you are scrolling the document in the opposite direction of the scroll message. Remember, the application's perspective of what is happening is opposite of the user's perspective.
w.SB_PAGELEFT
w.SB_PAGEDOWN
These two values are the same. They indicate scrolling down or right by one screen. Windows passes this value when the user clicks between the thumb and the down-arrow (in a vertical scroll bar) or between the thumb and the right arrow (in a horizontal scroll bar). Your application should scroll the text up screen (or thereabouts) if this is a w.WM_VSCROLL message, it should scroll the document one screen to the left if this is a w.VM_HSCROLL message. Yes, you are scrolling the document in the opposite direction of the scroll message. Remember, the application's perspective of what is happening is opposite of the user's perspective.

w.SB_PAGERIGHT
w.SB_THUMBPOSITION
This value indicates that the user has dragged the thumb (scroll box) and has released the mouse button (i.e., this is the end of the drag operation). The H.O. word of wParam indicates the position of the scroll box at the end of the drag operation.
w.SB_THUMBTRACK
This message indicates that the user is currently dragging the thumb in the scroll bar. Windows will send a stream of these messages to the application while the user is dragging the thumb around. The H.O. word of the wParam parameter specifies the current thumb position.

Generally, your applications will need to process the w.SB_LINEUP, w.SB_LINEDOWN, w.SB_LINELEFT, w.SB_LINERIGHT, w.SB_PAGEUP, w.SB_PAGEDOWN, w.SB_PAGELEFT, w.SB_PAGERIGHT, and w.SB_THUMBPOSITION. You have to decide what an appropriate distance is for a "line" and a "screen" with respect to these messages. Obviously, if you're creating a text-based application (like a text editor) the concept of line and screen are fairly obvious. However, if you're writing a graphical application, the concept of a line or screen can be somewhat fuzzy. Fortunately, Windows lets you decide how much screen real estate to scroll in response to these messages.

Optionally, you applications may also want to process the w.SB_THUMBTRACK messages as well as w.SB_THUMBPOSITION messages. The decision of whether to support or ignore w.SB_THUMBTRACK messages really depends upon the speed of your application. If you can rapidly redraw the entire screen, then supporting w.SB_THUMBTRACK messages provides an extra convenience for your end user. For example, most text editors and word processors support this message so that the user can quickly scan text as it scrolls by while they are dragging the scroll bar thumb around. This is a very handy feature to provide if the application can keep up with the user's drag speed. However, if your application cannot instantly (or very close to instantly) redraw the entire screen, then supporting the w.SB_THUMBTRACK operation can be an exercise in frustration for your users. There are few programs more frustrating to use than those that process these operational requests but cannot do so instantly. For example, if the user of a drawing program has created a particularly complex drawing that requires several seconds to redraw a single screen full of data, they will become very annoyed if that application processes w.SB_THUMBTRACK scrolling messages; when they attempt to drag the scroll bar thumb around, the application will take a few seconds to redraw the screen image, then scroll up a slight amount and take another few seconds to redraw the screen, scroll up a slight amount and take another few seconds... In an extreme case, the application could wind up taking minutes to scroll just a few pages through the document. Such applications should simply ignore the w.SB_THUMBTRACK scrolling messages and process only w.SB_THUMBPOSITION requests. Because Windows only sends a single w.SB_THUMBPOSITION message when the user drags the thumb around, the user will only have to sit through one redraw of the window.

You should note that Windows returns a 16-bit thumb position in the H.O. word of wParam in response to a w.SB_THUMBPOSITION or w.SB_THUMBTRACK message. If you need to obtain a 32-bit position, you can call the Windows API functions w.GetScrollPos or w.GetScrollInfo2 to obtain complete information about the current scroll thumb position. To avoid having to make such a call, you should try to limit the range of your scroll bar values to 0..65535 (16-bits).

Armed with this information, it's now possible to correct the problem with the fonts program from the previous section. This book, however, will leave it up to you to make the appropriate modifications to that program. In the interests of presenting as many ideas as possible, we'll write a short program, inspired by a comparable program in Petzold's "Programming Windows..." book, that displays system metric values. This program, sysmet.hla, displays some useful information about your particular computer system, with one line per value displayed. Unless you've got a really big video display, you're probably not going to see all of this information on the screen at one time. Even if you've got a sufficiently large display, you might not want the window to consume so much screen real estate while you're running the sysmets application. Therefore, this program employs both vertical and horizontal scroll bars to allow you to make the window as small as is reasonably possible and still be able to view all the information it has to display.

The w.GetSystemMetrics API function returns one of approximately 70 different system values. You pass w.GetSystemMetrics a value that selects a particular system metric and the function returns this system value (this is very similar to the w.GetDeviceCaps function we looked at earlier in this chapter). These index values generally have names that begin with w.SM_.... in the windows.hhf header file (the "SM" obviously stands for system metric). Here's the prototype for the w.GetSystemMetrics API function:

GetSystemMetrics: procedure

    (

        nIndex          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetSystemMetrics@4" );

We're not going to go into the details concerning what all the system metric values mean in this chapter. Fortunately, most of the values are fairly obvious from their corresponding constant (nIndex value) name. A few examples should give you the basic flavor:

  • w.SM_CXBORDER - width, in pixels, of a window border.
  • w.SM_CYBORDER - height, in pixels, of a window border.
  • w.SM_CMOUSEBUTTONS - number of buttons on the mouse.
  • w.SM_CXFULLSCREEN - width, in pixels, of the client area for a full-screen window on the primary display.
  • w.SM_CYFULLSCREEN - height, in pixels, of the client area for a full-screen window on the primary display.
  • w.SM_CXICON - the width, in pixels, of an icon.
  • w.SM_CYICON - the height, in pixels, of an icon.
  • w.SM_CXSCREEN - the width, in pixels, of the primary display.
  • w.SM_CYSCREEN - the height, in pixels, of the primary display.
  • etc. See the Microsoft documentation for all the possible constants and their meaning.

Rather than attempt to make individual calls to w.GetSystemMetrics for each value to output, the sysmets program executes a loop that retrieves the index value to submit to w.GetSystemMetrics from an array of records. Each element of that array has the following type:

MetricRec_t:

        record

            MetConst    :uns32;

            MetStr      :string;

            MetDesc     :string;

        endrecord;

The MetConst field holds a constant value like w.SM_CMOUSEBUTTONS that the program will pass on to w.GetSystemMetrics; this constant specifies the system metric to retrieve. The MetStr field is a string specifying the name of the constant (so we can display this constant name in the window). The MetDesc field is a brief English description of this particular system metric. The sysmet program statically initializes this array with the following values:

readonly

    MetricData: MetricRec_t[] :=

    [

        MetricRec_t:[ w.SM_CXSCREEN, "w.SM_CXSCREEN", "Screen width" ],

        MetricRec_t:[ w.SM_CYSCREEN, "w.SM_CYSCREEN", "Screen height" ],

        MetricRec_t:[ w.SM_CXVSCROLL, "w.SM_CXVSCROLL", "Vert scroll arrow width" ],

        MetricRec_t:[ w.SM_CYVSCROLL, "w.SM_CYVSCROLL", "Vert scroll arrow ht" ],

        MetricRec_t:[ w.SM_CXHSCROLL, "w.SM_CXHSCROLL", "Horz scroll arrow width" ],

        MetricRec_t:[ w.SM_CYHSCROLL, "w.SM_CYHSCROLL", "Horz scroll arrow ht" ],

        MetricRec_t:[ w.SM_CYCAPTION, "w.SM_CYCAPTION", "Caption bar ht" ],

        MetricRec_t:[ w.SM_CXBORDER, "w.SM_CXBORDER", "Window border width" ],

        MetricRec_t:[ w.SM_CYBORDER, "w.SM_CYBORDER", "Window border height" ],

        MetricRec_t:[ w.SM_CXDLGFRAME, "w.SM_CXDLGFRAME", "Dialog frame width" ],

        MetricRec_t:[ w.SM_CYDLGFRAME, "w.SM_CYDLGFRAME", "Dialog frame height" ],

        MetricRec_t:[ w.SM_CXHTHUMB, "w.SM_CXHTHUMB", "Horz scroll thumb width" ],

        MetricRec_t:[ w.SM_CYVTHUMB, "w.SM_CYVTHUMB", "Vert scroll thumb width" ],

        MetricRec_t:[ w.SM_CXICON, "w.SM_CXICON", "Icon width" ],

        MetricRec_t:[ w.SM_CYICON, "w.SM_CYICON", "Icon height" ],

        MetricRec_t:[ w.SM_CXCURSOR, "w.SM_CXCURSOR", "Cursor width" ],

        MetricRec_t:[ w.SM_CYCURSOR, "w.SM_CYCURSOR", "Cursor height" ],

        MetricRec_t:[ w.SM_CYMENU,  "w.SM_CYMENU", "Menu bar height" ],

        MetricRec_t:[ w.SM_CXFULLSCREEN, "w.SM_CXFULLSCREEN", "Largest client width" ],

        MetricRec_t:[ w.SM_CYFULLSCREEN, "w.SM_CYFULLSCREEN", "Largets client ht" ],

        MetricRec_t:[ w.SM_DEBUG, "w.SM_CDEBUG", "Debug version flag" ],

        MetricRec_t:[ w.SM_SWAPBUTTON, "w.SM_CSWAPBUTTON", "Mouse buttons swapped" ],

        MetricRec_t:[ w.SM_CXMIN, "w.SM_CXMIN", "Minimum window width" ],

        MetricRec_t:[ w.SM_CYMIN, "w.SM_CYMIN", "Minimum window height" ],

        MetricRec_t:[ w.SM_CXSIZE, "w.SM_CXSIZE", "Minimize/maximize icon width" ],

        MetricRec_t:[ w.SM_CYSIZE, "w.SM_CYSIZE", "Minimize/maximize icon height" ],

        MetricRec_t:[ w.SM_CXFRAME, "w.SM_CXFRAME", "Window frame width" ],

        MetricRec_t:[ w.SM_CYFRAME, "w.SM_CYFRAME", "Window frame height" ],

        MetricRec_t:[ w.SM_CXMINTRACK,  "w.SM_CXMINTRACK", "Minimum tracking width" ],

        MetricRec_t:[ w.SM_CXMAXTRACK,  "w.SM_CXMAXTRACK", "Maximum tracking width" ],

        MetricRec_t:[ w.SM_CYMINTRACK,  "w.SM_CYMINTRACK", "Minimum tracking ht" ],

        MetricRec_t:[ w.SM_CYMAXTRACK,  "w.SM_CYMAXTRACK", "Maximum tracking ht" ],

        MetricRec_t:[ w.SM_CXDOUBLECLK, "w.SM_CXDOUBLECLK", "Dbl-click X tolerance" ],

        MetricRec_t:[ w.SM_CYDOUBLECLK, "w.SM_CYDOUBLECLK", "Dbl-click Y tolerance" ],

        MetricRec_t:[ w.SM_CXICONSPACING, "w.SM_CXICONSPACING", "Horz icon spacing" ],

        MetricRec_t:[ w.SM_CYICONSPACING, "w.SM_CYICONSPACING", "Vert icon spacing" ],

        MetricRec_t:[ w.SM_CMOUSEBUTTONS, "w.SM_CMOUSEBUTTONS", " # of mouse btns" ]

    ];

const

    NumMetrics := @elements( MetricData );

With this data structure in place, a simple for loop that executes NumMetrics times can sequence through each element of this array, pass the first value to w.GetSystemMetrics, and print the second two fields of each element along with the value that w.GetSystemMetrics returns.

In addition to the usual w.WM_DESTROY, w.WM_PAINT, and w.WM_CREATE messages we've see in past programs, the sysmet application will need to process the w.WM_HSCROLL, w.WM_VSCROLL, and w.WM_SIZE (window resize) messages. Therefore, the Dispatch table will take the following form in sysmet:

readonly

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            MsgProcPtr_t:[ w.WM_HSCROLL, &HScroll           ],

            MsgProcPtr_t:[ w.WM_VSCROLL, &VScroll           ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

Naturally, we'll have to supply the corresponding HScroll, VScroll, and Size procedures as well as the window handling procedures we've written in past applications. We'll return to the discussion of these procedures (as well as the other message handling procedures) momentarily.

In order to handle the actual scrolling, our application is going to need to know how many lines it can display in the window at one time and how many characters it can display on a single line in the current window (on the average, because their widths vary). These calculations depend upon the size (height) of the font we're using, the average character width, and the current size of the window. To facilitate these calculations, the program will store the average character height, average character width, and the average capital character width (which is wider than the average character width) in a set of global variables. Because this program only uses the system font (the default font when the program first begins execution) and never changes the font, the program only needs to calculate these values once. It calculates them in the Create procedure when Windows first creates the application's main window. These global variables are the following:

static

    AverageCapsWidth    :dword;

    AverageCharWidth    :dword;

    AverageCharHeight   :dword;

    MaxWidth            :int32 := 0;

The Create procedure (w.WM_CREATE message handling procedure) is responsible for initializing the values of these variables. The Create procedure calls w.GetTextMetrics to get the font's height and average character width. It computes the average capital character width as 1.5 times the average character width if using a proportional font, it simply copies the average character width to the average caps width if using a monospaced font. MaxWidth holds the maximum width of a line of text. The computation of this value is based on the assumption that the maximum MetStr field is 25 characters long and consists of all capital letters while the MetDesc field (and the corresponding value) is a maximum of 40 characters long (mixed case and digits). Hence, each line requires a maximum of AverageCharWidth*40 + AverageCapsWidth*25 pixels. Here's the complete Create procedure that computes these values:

// Create-

//

//  This procedure responds to the w.WM_CREATE message.

//  Windows sends this message once when it creates the

//  main window for the application.  We will use this

//  procedure to do any one-time initialization that

//  must take place in a message handler.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc:    dword;              // Handle to video display device context

    tm:     w.TEXTMETRIC;

begin Create;

    GetDC( hwnd, hdc );

        // Initialization:

        //

        //  Get the text metric information so we can compute

        //  the average character heights and widths.

        GetTextMetrics( tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        mov( eax, AverageCharHeight );

        mov( tm.tmAveCharWidth, eax );

        mov( eax, AverageCharWidth );

        // If bit #0 of tm.tmPitchAndFamily is set, then

        // we've got a proportional font.  In that case

        // set the average capital width value to 1.5 times

        // the average character width.  If bit #0 is clear,

        // then we've got a fixed-pitch font and the average

        // capital letter width is equal to the average

        // character width.

        mov( eax, ebx );

        shl( 1, tm.tmPitchAndFamily );

        if( @c ) then

            shl( 1, ebx );              // 2*AverageCharWidth

        endif;

        add( ebx, eax );                // Computes 2 or 3 times eax.

        shr( 1, eax );                  // Computes 1 or 1.5 times eax.

        mov( eax, AverageCapsWidth );

    ReleaseDC;

    intmul( 40, AverageCharWidth, eax );

    intmul( 25, AverageCapsWidth, ecx );

    add( ecx, eax );

    mov( eax, MaxWidth );

end Create;

Whenever windows first creates a window, or whenever the user resizes the window, Windows will send a w.WM_SIZE message to the window procedure. Programs we've written in the past have simply ignored this message. However, once you add scroll bars to your window you need to intercept this message so you can recompute the scroll range values and scroll bar thumb positions. This function computes four important values and saves two other important values. It saves the size of the client window passed to the procedure in H.O. and L.O. words of the lParam parameter in the ClientSizeX and ClientSizeY variables. It then computes the values for the current VscrollPos, VscrollMax, HscrollPos, and HscrollMax variables. These variables have the following declarations in the global data area:

static

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

    VscrollPos          :int32 := 0;    // Tracks where we are in the document

    VscrollMax          :int32 := 0;    // Max display position (vertical).

    HscrollPos          :int32 := 0;    // Current Horz position.

    HscrollMax          :int32 := 0;    // Max Horz position.

VscrollMax specifies the maximum number of lines we can scroll through the window (see Figure 6-15). This is the number of lines in the document minus the number of lines we can actually display in the window (because we don't want to allow the user to scroll beyond the bottom of the document). In the actual computation in the Size procedure, we'll add two to this value to allow for some padding between the top of the client window and the first line as well as a blank line after the last line in the document.

Figure 6-15: VscrollMax Value

VscrollPos specifies the line number into the document that corresponds to the top line currently displayed in the window. Whenever the user resizes the window, we have to make sure that this value does not exceed the new VscrollMax position (that is, if the user makes the window larger and we're already displaying the text at the end of the document, we'll reduce VscrollPos to display more information towards the beginning of the document rather than more information towards the end of the document). The only time that sysmet will display a blank area beyond the end of the document is when the user opens up a window that is larger than the amount of data to display.

HscrollPos and HscrollMax are the corresponding values to VscrollMax and VscrollPos for the horizontal direction (see Figure 6-16). HscrollPos specifies how many "characters" into the text we've scrolled off the left hand side of the window; HscrollMax specifies the maximum character position we're allowed to scroll (horizontally) to without blank data appearing on the right hand side of the window.

Figure 6-16: HScrollMax and HscrollPos Values

Here's the complete code for the Size procedure:

// Size-

//

//  This procedure handles the w.WM_SIZE message

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    // VscrollMax = max( 0, NumMetrics+2 - ClientSizeY/AverageCharHeight )

    cdq();

    idiv( AverageCharHeight );

    mov( NumMetrics+2, ecx );

    sub( eax, ecx );

    if( @s ) then

        xor( ecx, ecx );

    endif;

    mov( ecx, VscrollMax );

    // VscrollPos = min( VscrollPos, VscrollMax )

    if( ecx > VscrollPos ) then

        mov( VscrollPos, ecx );

    endif;

    mov( ecx, VscrollPos );

    w.SetScrollRange( hwnd, w.SB_VERT, 0, VscrollMax, false );

    w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true );

    // HscrollMax =

    //  max( 0, 2 + (MaxWidth - ClientSizeX) / AverageCharWidth);

    mov( MaxWidth, eax );

    sub( ClientSizeX, eax );

    cdq();

    idiv( AverageCharWidth );

    add( 2, eax );

    if( @s ) then

        xor( eax, eax );

    endif;

    mov( eax, HscrollMax );

    // HscrollPos = min( HscrollMax, HscrollPos )

    if( eax > HscrollPos ) then

        mov( HscrollPos, eax );

    endif;

    mov( eax, HscrollPos );

    w.SetScrollRange( hwnd, w.SB_HORZ, 0, HscrollMax, false );

    w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true );

    xor( eax, eax ); // return success.

end Size;

When Windows sends the application a w.WM_HSCROLL message, the L.O. word of the wParam parameter holds the type of scroll activity the user has requested. The HScroll procedure must interpret this value to determine how to adjust the horizontal position (HscrollPos). If the user presses on the left or right arrows on the horizontal scroll bar, then Windows will pass w.SB_LINELEFT or w.SB_LINERIGHT in the L.O. word of wParam and the HScroll procedure will scroll the window one (average) character width to the left or right, assuming such an operation would not take you outside the range 0..Hscrollmax.

If the user clicks on the scroll bar between the thumb and one of the arrows, then Windows will pass along the constant w.SB_PAGELEFT or w.SB_PAGERIGHT in the L.O. word of the wParam parameter. The sysmet application defines a "page left" or "page right" operation as a "scroll eight average character positions in the appropriate direction." The choice of eight character positions was completely arbitrary. For text-based applications like sysmet, it doesn't make sense to scroll horizontally a whole screen at a time; the application achieves better continuity by scrolling only a few characters at a time. Typically, you'd probably want to scroll some percentage of the window's width rather than a fixed amount (like eight character positions). Probably somewhere on the order of 25% to 50% of the window's width would be a decent amount to scroll. Such a modification to the sysmet program is rather trivial; feel free to do it as an experiment with this program.

The sysmet program ignores w.SB_THUMBTRACK messages and processes w.SB_THUMBPOSITION messages. For this particular application there is no reason we couldn't process w.SB_THUMBTRACK messages as well (and, in fact, the VScroll procedure does process those messages). The sysmet application only processes w.SB_THUMBPOSITION messages on the horizontal scroll bar and w.SB_THUMBTRACK messages on the vertical scroll bar so you can compare the "feel" of these two mechanisms.

The HScroll procedure adjusts the value of the global variable HscrollPos based upon the type of scrolling activity the user specifies. It also checks to make sure that the scroll position remains in the range 0..HscrollMax. Once these calculations are out of the way, the HScroll procedure calls the w.ScrollWindow API function. This function has the following prototype:

static

    ScrollWindow: procedure

    (

            hWnd        :dword;

            XAmount     :dword;

            YAmount     :dword;

        var lpRect      :RECT;

        var lpClipRect  :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ScrollWindow@20" );

The hWnd parameter is the handle of the window whose client area you wish to scroll (which, of course, will be our application's main window). The XAmount and YAmount parameters specify how many pixels to scroll in the X and Y directions. Negative values scroll to the left or up, positive values scroll to the right or down. The lpRect parameter is the address of a w.RECT object that specifies the rectangle in the window to be scrolled. If this parameter contains NULL, then Windows scrolls the entire client area of the window. The lpClipRect specifies which pixels are to be repainted. If this parameter contains NULL, then Windows repaints all the pixels.

Once HScroll scrolls the window in the appropriate horizontal direction, it sets the new scroll position so that Windows will update the thumb position on the scroll bar. This is done with a call to w.SetScrollPos (discussed earlier).

Note that you do not repaint the window within the HScroll procedure. Remember, all window updates should take place only within the Paint procedure of the sysmet application. The call to w.ScrollWindow informs Windows that the client area of our application window is now invalid and should be repainted. This means that at some point in the future, Windows will be sending a w.WM_PAINT message to sysmet's window procedure so it can redraw the window.

Here's the complete code for the HScroll procedure:

// HScroll-

//

//  Handles w.WM_HSCROLL messages.

//  On entry, L.O. word of wParam contains the scroll bar activity.

procedure HScroll( hwnd: dword; wParam:dword; lParam:dword );

begin HScroll;

    // Convert 16-bit value in wParam to 32 bits so we can use the

    // switch macro:

    movzx( (type word wParam), eax );

    switch( eax )

        case( w.SB_LINEUP )

            mov( -1, eax );

        case( w.SB_LINEDOWN )

            mov( 1, eax );

        case( w.SB_PAGEUP )

            mov( -8, eax );

        case( w.SB_PAGEDOWN )

            mov( 8, eax );

        case( w.SB_THUMBPOSITION )

            movzx( (type word wParam[2]), eax );

            sub( HscrollPos, eax );

        default

            xor( eax, eax );

    endswitch;

    // eax =

    //  max( -HscrollPos, min( eax, HscrollMax - HscrollPos ))

    mov( HscrollPos, edx );

    neg( edx );

    mov( HscrollMax, ecx );

    add( edx, ecx );

    if( eax > (type int32 ecx) ) then

        mov( ecx, eax );

    endif;

    if( eax < (type int32 edx )) then

        mov( edx, eax );

    endif;

    if( eax <> 0 ) then

        add( eax, HscrollPos );

        imul( AverageCharWidth, eax );

        neg( eax );

        w.ScrollWindow( hwnd, eax, 0, NULL, NULL );

        w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true );

    endif;

    xor( eax, eax ); // return success

end HScroll;

The VScroll procedure is very similar in operation to the HScroll procedure; therefore, we don't need quite as detailed a discussion of its operation. However, there are a couple of important differences that we do need to discuss. As noted earlier, the VScroll procedure processes w.WM_VSCROLL/w.SB_THUMBTRACK messages rather than the w.WM_VSCROLL/w.SB_THUMBPOSITION messages that HScroll handles. There is one very big impact that this has on the execution of VScroll - VScroll cannot depend upon Windows issuing a w.WM_PAINT message in a timely manner. Windows' w.WM_PAINT messages are very low priority and Windows holds them back in the message queue while your application processes other messages (e.g., the stream of w.WM_VSCROLL/w.SB_THUMBTRACK messages that are screaming through the system). This may create a time lag between the movement of the scroll bar thumb and the corresponding update on the display, which is unacceptable. To overcome this problem, the VScroll procedure calls the w.UpdateWindow procedure. w.UpdateWindow tells Windows to immediately send a w.WM_PAINT message through the message queue (and make it high priority, so that it will be the next message that Windows sends to your window procedure). Therefore, when w.UpdateWindows returns, Windows will have already updated the display. This makes the behavior of the w.SB_THUMBTRACK request seem very fluid and efficient.

Here's the complete VScroll procedure:

// VScroll-

//

//  Handles the w.WM_VSCROLL messages from Windows.

//  The L.O. word of wParam contains the action/command to be taken.

//  The H.O. word of wParam contains a distance for the w.SB_THUMBTRACK

//  message.

procedure VScroll( hwnd: dword; wParam:dword; lParam:dword );

begin VScroll;

    movzx( (type word wParam), eax );

    switch( eax )

        case( w.SB_TOP )

            mov( VscrollPos, eax );

            neg( eax );

        case( w.SB_BOTTOM )

            mov( VscrollMax, eax );

            sub( VscrollPos, eax );

        case( w.SB_LINEUP )

            mov( -1, eax );

        case( w.SB_LINEDOWN )

            mov( 1, eax );

        case( w.SB_PAGEUP )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            neg( eax );

            if( (type int32 eax) > -1 ) then

                mov( -1, eax );

            endif;

        case( w.SB_PAGEDOWN )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            if( (type int32 eax) < 1 ) then

                mov( 1, eax );

            endif;

        case( w.SB_THUMBTRACK )

            movzx( (type word wParam[2]), eax );

            sub( VscrollPos, eax );

        default

            xor( eax, eax );

    endswitch;

    // eax = max( -VscrollPos, min( eax, VscrollMax - VscrollPos ))

    mov( VscrollPos, edx );

    neg( edx );

    mov( VscrollMax, ecx );

    add( edx, ecx );

    if( eax > (type int32 ecx) ) then

        mov( ecx, eax );

    endif;

    if( eax < (type int32 edx)) then

        mov( edx, eax );

    endif;

    if( eax <> 0 ) then

        add( eax, VscrollPos );

        intmul( AverageCharHeight, eax );

        neg( eax );

        w.ScrollWindow( hwnd, 0, eax, NULL, NULL );

        w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true );

        w.UpdateWindow( hwnd );

    endif;

    xor( eax, eax ); // return success.

end VScroll;

The last procedure of interest is the Paint procedure, that actually draws the system metric information to the display. Technically, we could just set up the Windows paint structure so that the clipping rectangle (the area that Windows allows us to draw into) only includes the new region we want to draw, and then draw the entire document. Windows will clip (not draw) all data outside the clipping region, so this is a cheap way to do scrolling - just adjust the clipping region and then draw the entire document. Unfortunately, this scheme is too inefficient to even consider for most applications. Suppose you've got a word processor application and the user has typed 100 pages into the word processor. Redrawing 100 pages every time the user scrolls one line (or worse yet, drags the thumb with w.SB_THUMBTRACK processing going on) would be incredibly slow. Therefore, the Paint procedure needs to be a little smarter about what it attempts to draw to the window.

The sysmet Paint procedure uses the VscrollPos variable to calculate the starting line to redraw in the window. In general, painting text is sufficiently fast on modern machines that this is all that would normally be necessary when painting the screen - just paint starting at VscrollPos for the number of lines of text that will fit in the window. When scrolling the entire window, you're going to wind up repainting the entire window anyway. However, there are many times when you don't actually need to repaint the entire window. For example, when a portion of sysmet's window is covered by some other window and the user closes that other window, Windows will only request that you redraw that portion of the client area that was originally covered by the closed window (that is, the invalid region). Although drawing text is relatively efficient, the sysmet application only redraws those lines of text that fall into the invalid region of the window. This will improve response time by a fair amount if painting the window is a complex operation. Once again, sysmet's painting isn't very complex but sysmet demonstrates this mechanism so you can see how to employ it in other applications.

As you may recall, the w.BeginPaint (i.e., the BeginPaint macro in wpa.hhf) has a parameter of type w.PAINTSTRUCT that windows initializes when you call w.BeginPaint. This object contains a field, rcPaint, of type w.RECT, that specifies the invalid region you must repaint. The sysmet ps.rcPaint.top and ps.rcPaint.bottom variables specify the starting vertical position in the client area and the ending vertical position in the client area that the Paint procedure must redraw. The Paint procedure divides these two values by the average character height to determine how many lines it can skip drawing at the top and bottom of the window. By combining the value of VscrollPos and ps.rcPaint.top the Paint procedure calculates firstMet - the index into the MetricData array where output is to begin. The Paint procedure uses a similar calculation based on NumMetrics, ps.rcPaint.bottom, and VscrollPos to determine the last line it will draw in the window from the MetricData array.

Here is the complete sysmet program, including the Paint procedure we've just discussed:

// Sysmet.hla-

//

//  System metrics display program.

program systemMetrics;

#include( "conv.hhf" )

#include( "strings.hhf" )

#include( "memory.hhf" )

#include( "hll.hhf" )

#include( "w.hhf" )

#include( "wpa.hhf" )

?NoDisplay := true;

?NoStackAlign := true;

type

    // Data type for the system metrics data array:

    MetricRec_t:

        record

            MetConst    :uns32;

            MetStr      :string;

            MetDesc     :string;

        endrecord;

    // Message and dispatch table related definitions:

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue    :dword;

            MessageHndlr    :MsgProc_t;

        endrecord;

static

    hInstance           :dword;         // "Instance Handle" Windows supplies.

    wc                  :w.WNDCLASSEX;  // Our "window class" data.

    msg                 :w.MSG;         // Windows messages go here.

    hwnd                :dword;         // Handle to our window.

    AverageCapsWidth    :dword;         // Font metric values.

    AverageCharWidth    :dword;

    AverageCharHeight   :dword;

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

    MaxWidth            :int32 := 0;    // Maximum output width

    VscrollPos          :int32 := 0;    // Tracks where we are in the document

    VscrollMax          :int32 := 0;    // Max display position (vertical).

    HscrollPos          :int32 := 0;    // Current Horz position.

    HscrollMax          :int32 := 0;    // Max Horz position.

readonly

    ClassName   :string := "SMWinClass";                // Window Class Name

    AppCaption  :string := "System Metrics Program";    // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            MsgProcPtr_t:[ w.WM_HSCROLL, &HScroll           ],

            MsgProcPtr_t:[ w.WM_VSCROLL, &VScroll           ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

readonly

    MetricData: MetricRec_t[] :=

    [

        MetricRec_t:[ w.SM_CXSCREEN, "w.SM_CXSCREEN", "Screen width" ],

        MetricRec_t:[ w.SM_CYSCREEN, "w.SM_CYSCREEN", "Screen height" ],

        MetricRec_t:[ w.SM_CXVSCROLL, "w.SM_CXVSCROLL", "Vert scroll arrow width" ],

        MetricRec_t:[ w.SM_CYVSCROLL, "w.SM_CYVSCROLL", "Vert scroll arrow ht" ],

        MetricRec_t:[ w.SM_CXHSCROLL, "w.SM_CXHSCROLL", "Horz scroll arrow width" ],

        MetricRec_t:[ w.SM_CYHSCROLL, "w.SM_CYHSCROLL", "Horz scroll arrow ht" ],

        MetricRec_t:[ w.SM_CYCAPTION, "w.SM_CYCAPTION", "Caption bar ht" ],

        MetricRec_t:[ w.SM_CXBORDER, "w.SM_CXBORDER", "Window border width" ],

        MetricRec_t:[ w.SM_CYBORDER, "w.SM_CYBORDER", "Window border height" ],

        MetricRec_t:[ w.SM_CXDLGFRAME, "w.SM_CXDLGFRAME", "Dialog frame width" ],

        MetricRec_t:[ w.SM_CYDLGFRAME, "w.SM_CYDLGFRAME", "Dialog frame height" ],

        MetricRec_t:[ w.SM_CXHTHUMB, "w.SM_CXHTHUMB", "Horz scroll thumb width" ],

        MetricRec_t:[ w.SM_CYVTHUMB, "w.SM_CYVTHUMB", "Vert scroll thumb width" ],

        MetricRec_t:[ w.SM_CXICON, "w.SM_CXICON", "Icon width" ],

        MetricRec_t:[ w.SM_CYICON, "w.SM_CYICON", "Icon height" ],

        MetricRec_t:[ w.SM_CXCURSOR, "w.SM_CXCURSOR", "Cursor width" ],

        MetricRec_t:[ w.SM_CYCURSOR, "w.SM_CYCURSOR", "Cursor height" ],

        MetricRec_t:[ w.SM_CYMENU,  "w.SM_CYMENU", "Menu bar height" ],

        MetricRec_t:[ w.SM_CXFULLSCREEN, "w.SM_CXFULLSCREEN", "Largest client width" ],

        MetricRec_t:[ w.SM_CYFULLSCREEN, "w.SM_CYFULLSCREEN", "Largets client ht" ],

        MetricRec_t:[ w.SM_DEBUG, "w.SM_CDEBUG", "Debug version flag" ],

        MetricRec_t:[ w.SM_SWAPBUTTON, "w.SM_CSWAPBUTTON", "Mouse buttons swapped" ],

        MetricRec_t:[ w.SM_CXMIN, "w.SM_CXMIN", "Minimum window width" ],

        MetricRec_t:[ w.SM_CYMIN, "w.SM_CYMIN", "Minimum window height" ],

        MetricRec_t:[ w.SM_CXSIZE, "w.SM_CXSIZE", "Minimize/maximize icon width" ],

        MetricRec_t:[ w.SM_CYSIZE, "w.SM_CYSIZE", "Minimize/maximize icon height" ],

        MetricRec_t:[ w.SM_CXFRAME, "w.SM_CXFRAME", "Window frame width" ],

        MetricRec_t:[ w.SM_CYFRAME, "w.SM_CYFRAME", "Window frame height" ],

        MetricRec_t:[ w.SM_CXMINTRACK,  "w.SM_CXMINTRACK", "Minimum tracking width" ],

        MetricRec_t:[ w.SM_CXMAXTRACK,  "w.SM_CXMAXTRACK", "Maximum tracking width" ],

        MetricRec_t:[ w.SM_CYMINTRACK,  "w.SM_CYMINTRACK", "Minimum tracking ht" ],

        MetricRec_t:[ w.SM_CYMAXTRACK,  "w.SM_CYMAXTRACK", "Maximum tracking ht" ],

        MetricRec_t:[ w.SM_CXDOUBLECLK, "w.SM_CXDOUBLECLK", "Dbl-click X tolerance" ],

        MetricRec_t:[ w.SM_CYDOUBLECLK, "w.SM_CYDOUBLECLK", "Dbl-click Y tolerance" ],

        MetricRec_t:[ w.SM_CXICONSPACING, "w.SM_CXICONSPACING", "Horz icon spacing" ],

        MetricRec_t:[ w.SM_CYICONSPACING, "w.SM_CYICONSPACING", "Vert icon spacing" ],

        MetricRec_t:[ w.SM_CMOUSEBUTTONS, "w.SM_CMOUSEBUTTONS", " # of mouse btns" ]

    ];

const

    NumMetrics := @elements( MetricData );

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Create-

//

//  This procedure responds to the w.WM_CREATE message.

//  Windows sends this message once when it creates the

//  main window for the application.  We will use this

//  procedure to do any one-time initialization that

//  must take place in a message handler.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc:    dword;              // Handle to video display device context

    tm:     w.TEXTMETRIC;

begin Create;

    GetDC( hwnd, hdc );

        // Initialization:

        //

        //  Get the text metric information so we can compute

        //  the average character heights and widths.

        GetTextMetrics( tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        mov( eax, AverageCharHeight );

        mov( tm.tmAveCharWidth, eax );

        mov( eax, AverageCharWidth );

        // If bit #0 of tm.tmPitchAndFamily is set, then

        // we've got a proportional font.  In that case

        // set the average capital width value to 1.5 times

        // the average character width.  If bit #0 is clear,

        // then we've got a fixed-pitch font and the average

        // capital letter width is equal to the average

        // character width.

        mov( eax, ebx );

        shl( 1, tm.tmPitchAndFamily );

        if( @c ) then

            shl( 1, ebx );              // 2*AverageCharWidth

        endif;

        add( ebx, eax );                // Computes 2 or 3 times eax.

        shr( 1, eax );                  // Computes 1 or 1.5 times eax.

        mov( eax, AverageCapsWidth );

    ReleaseDC;

    intmul( 40, AverageCharWidth, eax );

    intmul( 25, AverageCapsWidth, ecx );

    add( ecx, eax );

    mov( eax, MaxWidth );

end Create;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

//  For this System Metrics program, the Paint procedure

//  displays three columns of text in the main window.

//  This procedure computes and displays the appropriate text.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    x           :int32;         // x-coordinate of start of output str.

    y           :int32;         // y-coordinate of start of output str.

    CurVar      :string;        // Current system metrics variable name.

    CVlen       :uns32;         // Length of CurVar string.

    CurDesc     :string;        // Current system metrics description.

    CDlen       :string;        // Length of the above.

    CDx         :int32;         // X position for CurDesc string.

    value       :string;

    valData     :char[32];

    CVx         :int32;         // X position for value string.

    vallen      :uns32;         // Length of value string.

    firstMet    :int32;         // Starting metric to begin drawing

    lastMet     :int32;         // Ending metric index to draw.

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Initialize the value->valData string object:

    mov( str.init( (type char valData), 32 ), value );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Figure out which metric we should start drawing

        // (firstMet =

        //      max( 0, VscrollPos + ps.rcPaint.top/AverageCharHeight - 1)):

        mov( ps.rcPaint.top, eax );

        cdq();

        idiv( AverageCharHeight );

        add( VscrollPos, eax );

        dec( eax );

        if( (type int32 eax) < 0 ) then

            xor( eax, eax );

        endif;

        mov( eax, firstMet );

        // Figure out the last metric we should be drawing

        // ( lastMet =

        //      min( NumMetrics,

        //           VscrollPos + ps.rcPaint.bottom/AverageCharHeight )):

        mov( ps.rcPaint.bottom, eax );

        cdq();

        idiv( AverageCharHeight );

        add( VscrollPos, eax );

        if( (type int32 eax) > NumMetrics ) then

            mov( NumMetrics, eax );

        endif;

        mov( eax, lastMet );

        // The following loop processes each entry in the

        // MetricData array.  The loop control variable (EDI)

        // also determines the Y-coordinate where this code

        // will display each line of text in the window.

        // Note that this loop counts on the fact that Windows

        // API calls preserve the EDI register.

        for( mov( firstMet, edi ); edi < lastMet; inc( edi )) do

            // Before making any Windows API calls (which have

            // a nasty habit of wiping out registers), compute

            // all the values we will need for these calls

            // and save those values in local variables.

            //

            //  A typical "high level language solution" would

            // be to compute these values as needed, immediately

            // before each Windows API calls.  By moving this

            // code here, we can take advantage of values previously

            // computed in registers without having to worry about

            // Windows wiping out the values in those registers.

            // Compute index into MetricData:

            intmul( @size( MetricRec_t ), edi, esi );

            // Grab the string from the current MetricData element:

            mov( MetricData.MetStr[ esi ], eax );

            mov( eax, CurVar );

            mov( (type str.strRec [eax]).length, eax );

            mov( eax, CVlen );

            mov( MetricData.MetDesc[ esi ], eax );

            mov( eax, CurDesc );

            mov( (type str.strRec [eax]).length, eax );

            mov( eax, CDlen );

            // Column one begins at X-position AverageCharWidth (ACW).

            // Col 2 begins at ACW + 25*AverageCapsWidth.

            // Col 3 begins at ACW + 25*AverageCapsWidth + 40*ACW.

            // Compute the Col 2 and Col 3 values here.

            mov( 1, eax );

            sub( HscrollPos, eax );

            intmul( AverageCharWidth, eax );

            mov( eax, x );

            intmul( 25, AverageCapsWidth, eax );

            add( x, eax );

            mov( eax, CDx );

            intmul( 40, AverageCharWidth, ecx );

            add( ecx, eax );

            mov( eax, CVx );

            // The Y-coordinate for the line of text we're writing

            // is computed as AverageCharHeight * (1-VscrollPos+edi).

            // Compute that value here:

            mov( 1, eax );

            sub( VscrollPos, eax );

            add( edi, eax );

            intmul( AverageCharHeight, eax );

            mov( eax, y );

            // Now generate the string we're going to print

            // as the value for the current metric variable:

            w.GetSystemMetrics( MetricData.MetConst[ esi ] );

            conv.i32ToStr( eax, 0, ' ', value );

            mov( str.length( value ), vallen );

            // First two columns have left-aligned text:

            SetTextAlign( w.TA_LEFT | w.TA_TOP );

            // Output the name of the metric variable:

            TextOut( x, y, CurVar, CVlen );

            // Output the description of the metric variable:

            TextOut( CDx, y, CurDesc, CDlen );

            // Output the metric's value in the third column.  This is

            // a numeric value, so we'll right align this data.

            SetTextAlign( w.TA_RIGHT | w.TA_TOP );

            TextOut( CVx, y, value, vallen );

            // Although not strictly necessary for this program,

            // it's a good idea to always restore the alignment

            // back to the default (top/left) after you done using

            // some other alignment.

            SetTextAlign( w.TA_LEFT | w.TA_TOP );

        endfor;

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//

//  L.O. word of lParam contains the X Size

//  H.O. word of lParam contains the Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    // VscrollMax = max( 0, NumMetrics+2 - ClientSizeY/AverageCharHeight )

    cdq();

    idiv( AverageCharHeight );

    mov( NumMetrics+2, ecx );

    sub( eax, ecx );

    if( @s ) then

        xor( ecx, ecx );

    endif;

    mov( ecx, VscrollMax );

    // VscrollPos = min( VscrollPos, VscrollMax )

    if( ecx > VscrollPos ) then

        mov( VscrollPos, ecx );

    endif;

    mov( ecx, VscrollPos );

    w.SetScrollRange( hwnd, w.SB_VERT, 0, VscrollMax, false );

    w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true );

    // HscrollMax =

    //  max( 0, 2 + (MaxWidth - ClientSizeX) / AverageCharWidth);

    mov( MaxWidth, eax );

    sub( ClientSizeX, eax );

    cdq();

    idiv( AverageCharWidth );

    add( 2, eax );

    if( @s ) then

        xor( eax, eax );

    endif;

    mov( eax, HscrollMax );

    // HscrollPos = min( HscrollMax, HscrollPos )

    if( eax > HscrollPos ) then

        mov( HscrollPos, eax );

    endif;

    mov( eax, HscrollPos );

    w.SetScrollRange( hwnd, w.SB_HORZ, 0, HscrollMax, false );

    w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true );

    xor( eax, eax ); // return success.

end Size;

// HScroll-

//

//  Handles w.WM_HSCROLL messages.

//  On entry, L.O. word of wParam contains the scroll bar activity.

procedure HScroll( hwnd: dword; wParam:dword; lParam:dword );

begin HScroll;

    // Convert 16-bit value in wParam to 32 bits so we can use the

    // switch macro:

    movzx( (type word wParam), eax );

    switch( eax )

        case( w.SB_LINEUP )

            mov( -1, eax );

        case( w.SB_LINEDOWN )

            mov( 1, eax );

        case( w.SB_PAGEUP )

            mov( -8, eax );

        case( w.SB_PAGEDOWN )

            mov( 8, eax );

        case( w.SB_THUMBPOSITION )

            movzx( (type word wParam[2]), eax );

            sub( HscrollPos, eax );

        default

            xor( eax, eax );

    endswitch;

    // eax =

    //  max( -HscrollPos, min( eax, HscrollMax - HscrollPos ))

    mov( HscrollPos, edx );

    neg( edx );

    mov( HscrollMax, ecx );

    add( edx, ecx );

    if( eax > (type int32 ecx) ) then

        mov( ecx, eax );

    endif;

    if( eax < (type int32 edx )) then

        mov( edx, eax );

    endif;

    if( eax <> 0 ) then

        add( eax, HscrollPos );

        imul( AverageCharWidth, eax );

        neg( eax );

        w.ScrollWindow( hwnd, eax, 0, NULL, NULL );

        w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true );

    endif;

    xor( eax, eax ); // return success

end HScroll;

// VScroll-

//

//  Handles the w.WM_VSCROLL messages from Windows.

//  The L.O. word of wParam contains the action/command to be taken.

//  The H.O. word of wParam contains a distance for the w.SB_THUMBTRACK

//  message.

procedure VScroll( hwnd: dword; wParam:dword; lParam:dword );

begin VScroll;

    movzx( (type word wParam), eax );

    switch( eax )

        case( w.SB_TOP )

            mov( VscrollPos, eax );

            neg( eax );

        case( w.SB_BOTTOM )

            mov( VscrollMax, eax );

            sub( VscrollPos, eax );

        case( w.SB_LINEUP )

            mov( -1, eax );

        case( w.SB_LINEDOWN )

            mov( 1, eax );

        case( w.SB_PAGEUP )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            neg( eax );

            if( (type int32 eax) > -1 ) then

                mov( -1, eax );

            endif;

        case( w.SB_PAGEDOWN )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            if( (type int32 eax) < 1 ) then

                mov( 1, eax );

            endif;

        case( w.SB_THUMBTRACK )

            movzx( (type word wParam[2]), eax );

            sub( VscrollPos, eax );

        default

            xor( eax, eax );

    endswitch;

    // eax = max( -VscrollPos, min( eax, VscrollMax - VscrollPos ))

    mov( VscrollPos, edx );

    neg( edx );

    mov( VscrollMax, ecx );

    add( edx, ecx );

    if( eax > (type int32 ecx) ) then

        mov( ecx, eax );

    endif;

    if( eax < (type int32 edx)) then

        mov( edx, eax );

    endif;

    if( eax <> 0 ) then

        add( eax, VscrollPos );

        intmul( AverageCharHeight, eax );

        neg( eax );

        w.ScrollWindow( hwnd, 0, eax, NULL, NULL );

        w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true );

        w.UpdateWindow( hwnd );

    endif;

    xor( eax, eax ); // return success.

end VScroll;

/**************************************************************************/

/*                   End of Application Specific Code                     */

/**************************************************************************/

// The window procedure.

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword );

    @stdcall;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin systemMetrics;

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    mov( hInstance, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW | w.WS_VSCROLL |  w.WS_HSCROLL,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end systemMetrics;

6.6: The DebugWindow Application

When moving from console applications to GUI applications, an important feature is lost to the application programmer - the ability to print a string of text from an arbitrary point in the program that display's the program's status and other debugging information. In this section we will explore how to write a simple terminal emulator application that displays text much like console applications process text. This simple application will demonstrate how to create console-like applications within a GUI environment. One feature of this particular application, however, is that it displays textual data sent to it from other applications. This allows you to write a GUI application that can send text messages to this simple console emulator application, allowing you to display debug (and other status messages) from a GUI application that doesn't normally support console-like output.

Writing a DebugWindow application that accepts messages from other applications makes use of some advanced message passing features in Windows. Few books on Windows programming would present this topic so early in the book. This book makes an exception to this rule for a couple of reasons. First, though the whole concept of message passing and multitasking applications is a bit advanced, you don't have to learn everything there is to know about this subject area to create the DebugWindow application. This section will only present a few of the API calls that you will need to implement this advanced form of message passing, so this shouldn't be a problem. Another reason for presenting this application here is that DebugWindow is, intrinsically, a text-based application. So describing the operation of this application in a chapter on text processing in Windows seems like a natural fit. Finally, one really good reason for presenting this application here is because you'll find it extremely useful and the sooner you have this application available, the sooner you'll be able to employ it in your own Windows projects. So the sooner the better...

6.6.1: Message Passing Under Windows

Windows communicates with your applications by passing them messages. Your window procedure is the code that Windows calls when it sends your application a message. Window procedures in code we've written up to this point have processed messages like w.WM_PAINT, w.WM_CREATE, and w.WM_DESTROY that Windows has passed to these applications. Although Windows is the most common source of messages to your applications, it's also possible for your application to send messages to itself or send messages to other processes in the system. For example, it's perfectly possible for you to send a w.WM_PAINT message to your program to force the window to repaint itself. Indeed, it's reasonable to manually send almost any message Windows sends to your window procedure. Messages provide a form of "deferred procedure call" that you can use to call the message handling procedures that your window procedure calls.

In addition to the stock messages that Windows defines, Windows also predefines a couple of sets of user-definable messages. These user-definable messages let your application create its own set of procedures that can be called via the message passing mechanism. Windows defines three ranges of user-definable messages. The first set of user-definable messages have values in the range w.WM_USER..$7FFF. Windows reserves messages in this range for private use by a window procedure. Note, however, that Window messages in this range are only meaningful within a single window class, they are not unique throughout an application. For example, some stock controls that Windows provides will use message values in this range. However, if you are sending a message to a specific window (which is a member of some specific window class), then you can define private messages that your window procedure will respond to using the values in this range.

If you need a range of messages that are unique throughout an application (e.g., you're going to broadcast a message to all active windows within a given application), then you'll want to use the message values in the range w.WM_APP..$BFFF. Windows guarantees that no system messages use these values, so if you broadcast a message whose message number is in this range, then none of the Windows' system window procedures (e.g., button, text edit boxes, list boxes, etc.) will inadvertently respond to messages in this range.

If two different processes want to pass messages between themselves, allocating hard-coded message numbers is not a good idea (because it's possible that some third process could accidentally intercept such messages if it also uses the same message number for inter-process communication). Therefore, Windows defines a final range of message values ($C000..$FFFF) that Windows explicitly manages via the w.RegisterWindowMessage API call. Here's the prototype for this API function:

static

    RegisterWindowMessage: procedure

    (

        lpString        :string

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__RegisterWindowMessageA@4" );

The parameter is an HLA string that uniquely identifies the message value to create. Windows will search for this string in an internal database and return whatever value is associated with that string. If no such string exists internal to Windows, then Windows will allocate a unique message number in the range $C000..$FFFF and return that value (w.RegisterWindowMessage returns the message value in the EAX register). The message number remains active until all programs that have registered it terminate execution. Note that unlike the other message values, that you can represent via constants, you must store message values that w.RegisterWindowMessage returns in a variable so you can test for these message values in your window procedure. The coding examples up to this point have always put their dispatch table in a readonly section. If you want to be able to process a message number that you obtain from w.RegisterWindowMessage, you'll have to put the dispatch table in a static section or modify the window procedure to test the message number that w.RegisterWindowMessage returns outside the normal loop that it uses to process the dispatch table entries. E.g.,

static

    Dispatch:   MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

    MyMsg: MsgProcPtr_t; @nostorage;

            MsgProcPtr_t:[ -1, &MyMsgHandler ],  // -1 is a dummy value.

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

        .

        .

        .

    // Get a unique message value from Windows and overwrite the dummy "-1" value

    // we're currently using as MyMsg's message value:

    w.RegisterWindowMessage( "MyMsg_Message_Value" );

    mov( eax, MyMsg.MessageValue );

Sending a message is really nothing more than a fancy way of calling a window procedure. Windows provides many different ways to send a message to a window procedure, but a very common API function that does this is w.SendMessage:

static

    SendMessage: procedure

    (

        hWnd            :dword;

        _Msg            :dword;

        _wParam         :dword;

        _lParam         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SendMessageA@16" );

The hWnd parameter specifies the handle of the window whose window procedure will receive the message. The _Msg parameter specifies the value of the message you want to pass to the window procedure (e.g., w.WM_PAINT or w.WM_USER). The _wParam and _lParam parameters are arbitrary 32-bit values that Windows passes on through to the window procedure; these parameters usually supply parameter data for the specific message. As you'll recall, the declaration of a window procedure takes the following form:

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword  );

As you can probably tell, Windows just passes the four w.SendMessage parameters on through to the appropriate Window procedure.

When an application is send a message to its own window procedure, knowing the window procedures handle is no big deal; Windows returns this handle when the application calls w.RegisterWindowEx from the main program. The application can save this value and use it when calling the window procedure via w.SendMessage. However, if an application wants to send a message to some other process in the system, the application will need to know the handle value for that other process' window procedure. To obtain the window handle for a window belonging to some other process in the system, an application can call the Windows w.FindWindow function:

static

    FindWindow: procedure

    (

        lpClassName     :string;

        lpWindowName    :string

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FindWindowA@8" );

The lpClassName parameter is a string containing the window procedure's class name, that is, the string that was registered with Windows in the w.RegisterClassEx call3. If you only have one instance of a given window class active in the system, then you can pass NULL as the value of the second parameter. However, if you've instantiated several different copies of a window, you can use the second parameter (which is the window title string) to further differentiate the windows. If you're going to be passing messages between different processes in the system, it's a real good idea to create a unique window class for the receiving process and only run one instance of that class at a time. That's the approach this book will take; doing so allows us to simply pass NULL as the value of the second parameter to w.FindWindow.

The w.FindWindow function returns the handle of the window you specify in the EAX register, assuming that window is presently active in the system. If the window class you specify is not available in the system, then w.FindWindow will return NULL (zero) in the EAX register. You can use this NULL return value to determine that there is no process waiting to receive the messages you want to send it, and take corrective action, as appropriate.

As a short example, suppose that you have the System Metrics program from the previous section running on your system. You can send this application a message from a different process and tell it to resize its window using a code sequence like the following:

    w.FindWindow( "SMWinClass", NULL );  // Get Sysmet's window handle in eax.

    w.SendMessage( eax, w.WM_SIZE, 0, 480 << 16 + 640 );  // 640X480 resize operation.

The w.SendMessage API function is synchronous. This means that it doesn't return to the caller until the window procedure it invokes finishes whatever it does with the message and returns control back to Windows. The only time w.SendMessage returns without the other process completing its task is when you pass w.SendMessage an illegal handle number. For example, if the process containing the window procedure you're invoking has quit (or was not executing in the first place), then the handle you're passing w.SendMessage is invalid. Otherwise, it's up to the message destination's window procedure to retrieve and process the message before whomever calls w.SendMessage will continue execution. If the target of the message never retrieves the message, or hangs up while processing the message, then the application that sent the message will hang up as well.

Synchronous behavior is perfect semantics for an intra-process message (that is, a message that an application sends to itself). This is very similar to a procedure call, which most programmers are comfortable using. Unfortunately, this behavior is not entirely appropriate for inter-process calls because the activities of that other process (e.g., whether or not that other process has "hung up") have a direct impact on the execution of the current process. To write more robust message passing applications, we need to relax the semantics of a synchronous call somewhat to avoid problems.

Windows provides several different API functions you can call to send a message through the message-handling system. Some of these additional functions address the problems with w.SendMessage hanging up if the receiving window procedure doesn't properly process the message. Examples of these API functions include w.PostMessage, w.SendMessageCallback, and w.SendMessageTimeout. These particular API functions have the following prototypes:

static

    PostMessage: procedure

    (

        hWnd            :dword;

        _Msg            :dword;

        wParam          :dword;

        lParam          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PostMessageA@16" );

    SendMessageCallback: procedure

    (

        hWnd            :dword;

        _Msg            :dword;

        _wParam         :dword;

        _lParam         :dword;

        lpCallBack      :SENDASYNCPROC;

        dwData          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SendMessageCallbackA@24" );

    SendMessageTimeout: procedure

    (

            hWnd        :dword;

            _Msg        :dword;

            _wParam     :dword;

            _lParam     :dword;

            fuFlags     :dword;

            uTimeout    :dword;

        var lpdwResult  :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SendMessageTimeoutA@28" );

The w.PostMessage function sends a message to some window procedure and immediately returns to the caller without waiting for the receiver to process the message. The parameters are identical to those for w.SendMessage. Although w.PostMessage avoids the problem with an application hanging up because the recipient of the message doesn't respond, there is a major drawback to using w.PostMessage - the caller doesn't know whether the target window procedure has actually received and processed the message. Therefore, if the caller requires some sort of proof of completion of the message, w.PostMessage is not an appropriate function to call. On the other hand, if the loss of the message (or the activity that the message causes) is no big deal, then w.PostMessage is a safe way to communicate with another window procedure4.

To receive confirmation that the target window procedure has processed (or has not processed) a message, without hanging up the calling process, Windows provides the w.SendMessageCallback function and the w.SendMessageTimeout function. These two functions take completely different approaches to dealing with the problems of synchronous message passing.

The w.SendMessageCallback API function behaves like w.PostMessage insofar as it immediately returns after sending the message to the target window procedure (that is, it doesn't wait for the window procedure to actually process the message). To receive notification that message processing has taken place, you pass w.SendMessageCallback the address of a callback function. Windows will call this function when the target window procedure completes execution. In the meantime, your application has continued execution. Because the call to the callback function is asynchronous, there are many issues involving concurrent programming that you must consider when using this technique. The issues of concurrent programming are a bit beyond the scope of this chapter, so we'll ignore this API function for the time being. See the Windows documentation for more details concerning the use of this function.

The second mechanism is to use a timeout to return control back to whomever calls w.SendMessageTimeout if the send message operation is taking too long (implying that the target window procedure is hung or is otherwise not going to process the message we sent it). Under normal circumstances, w.SendMessageTimeout behaves just like w.SendMessage - that is, the caller blocks (halts) until the recipient of the message processes the message and returns. The w.SendMessageTimeout function, however, will go ahead and return if the message's receiver does not respond within some time period (that you specify).

The w.SendMessageTimeout API function has three additional parameters (above and beyond those you supply to w.SendMessage): fuFlags, uTimeout, and lpdwResult. The fuFlags parameter should be one of the following values:

  • w.SMTO_ABORTIFHUNG - This tells w.SendMessageTimeout to immediately return (with a timeout error) if the receiving process appears to be hung. To Windows, a process is "hung" if it has not attempted to read a message from its message queue during the past five seconds.
  • w.SMTO_BLOCK - This value tells w.SendMessageTimeout to wait until the message returns (or times out). Windows will not send any other messages to the current process' window procedure when you use this value.
  • w.SMTO_NORMAL - The caller can process other messages that are sent to the window procedure while waiting for the w.SendMessageTimeout call to return.
  • w.SMTO_NOTIMEOUTIFNOTHUNG - (Windows 2000 and later) Does not return when the time-out period expires if the process receiving the message does not appear to be hung (that is, it is still processing messages on a regular basis).

Most of the time you'll want to pass w.SMTO_BLOCK or w.SMTO_NORMAL as the fuFlags parameter value.

The uTimeout parameter specifies the timeout period in milliseconds. Choosing a time-out period can be tricky. Too long a value may cause your application to pause for extended periods of time while waiting for a hung receiver, to short a timeout period may cause Windows to prematurely return a time-out error to your program because the message's receiver hasn't gotten around to processing the message. Because Windows defines a "hung program" as one that doesn't respond to any messages within five seconds, a five second timeout period is probably a good starting point unless you need much faster response from w.SendMessageTimeout. Presumably, if the system is functioning correctly and the target process is running, you should get a response within a couple of milliseconds.

6.6.2: Memory Protection and Messages

A Windows message provides only a 64-bit data payload (i.e., the values in the wParam and lParam parameters). If someone needs to pass more data via a message, the standard convention is to pass a pointer to a data structure containing that data through the lParam parameter. The window procedure then casts the lParam parameter as a pointer to that data structure and accesses the data indirectly. By limiting the data payload to 64 bits, Windows keeps the message passing mechanism lean and efficient. Unfortunately, this scheme creates some problems when we want to pass data from one process to another via a message.

The problem with passing information between two processes under (modern versions of) Windows is that each process runs in a separate address space. This means that Windows sets aside a four-gigabyte memory range for each running program that is independent of the four-gigabyte address space for other executing programs. The benefit of this scheme is that one process cannot inadvertently modify code or data in another process. The drawback is that two processes cannot easily share data in memory by passing pointers between themselves. Address $40_1200 in one process will not reference the same data as address $40_1200 in a second process. Therefore, passing data directly through memory from one process to another isn't going to work.

In order to copy a large amount of data (that is, anything beyond the 64-bit data payload that wParam and lParam provide), Windows has to make a copy of the data in the source process' address space in the address space of the destination process. For messages that Windows knows about (e.g., a w.WM_GETTEXT message that returns text associated with certain controls, like a text edit box), Windows handles all the details of copying data to and from buffers (typically pointed at by lParam) between two different processes. For user-defined messages, however, Windows has no clue whether the bit pattern in the lParam parameter to w.SendMessage is a pointer to some block of data in memory or a simple integer value. To overcome this problem, Windows provides a special message specifically geared towards passing a block of data via the w.SendMessage API function: the w.WM_COPYDATA message. A call to w.SendMessage passing the w.WM_COPYDATA message value takes the following form:

    w.SendMessage

    (

        hDestWindow,   // Handle of destination window

        w.WM_COPYDATA, // The copy data message command

        hWnd,          // Handle of the window passing the data

        cds            // (address of) a w.COPYDATASTRUCT object

    );

The w.COPYDATASTRUCT data structure takes the following form:

type

    COPYDATASTRUCT:

        record

            dwData    :dword;  // generic 32-bit value passed to receiver.

            cbData    :uns32;  // Number of bytes in block to transfer.

            lpData    :dword;  // Pointer to block of data to transfer.

        endrecord;

The dwData field is a generic 32-bit value that Windows will pass along to the application receiving the block of data. The primary purpose of this field is to pass along a user-defined message number (or command) to the receiver. Windows only provides a single w.WM_COPYDATA message; if you need to send multiple messages with large data payloads to some other process, you will have to send each of these messages using the single w.WM_COPYDATA message. To differentiate these messages, you can use the cds.dwData field to hold the actual message type. If your receiving application only needs to process a single user-defined message involving a block of data, it can switch off the w.WM_COPYDATA message directly and use the dwData field for any purpose it chooses.

The cbData field in the w.COPYDATASTRUCT data type specifies the size of the data block that is being transferred from the source application to the destination window. If you're passing a zero-terminated string from the source to the target application, don't forget to include the zero terminating byte in your length (i.e., don't simply pass the string length as the cbData value). If you're passing some other data structure, just take the size of that structure and pass it in the cbData field.

The lpData field in the w.COPYDATASTRUCT object is a pointer to the block of data to copy between the applications. This block of data must not contain any pointers to objects within the source application's address space (even if those pointers reference other objects in the data block you're copying). There is no guarantee that Windows will copy the block of data to the same address in the target address space and Windows will only copy data that is part of the data block (it will not copy data that pointers within the block reference, unless that data is also within the block). Therefore, any pointers appearing in the lpData area will be invalid once Windows copies this data into the address space of the target window procedure.

Given the complexity and overhead associated with passing a block of data from one application to another via w.SendMessage, you might question why we would want to use this scheme to create the DebugWindow application. After all, it's perfectly possible to open up a second window in an existing application and send all of your debugging output to that window. However, the problem with sending debug messages to an application's own debug window is that if the program crashes or hangs, that application's debug window may be destroyed (so you can't see the last output messages sent to the display just prior to the crash) or the program may freeze (so you can scroll through the messages sent to the debug window). Therefore, it's much better to have all the debug messages sent to a separate application whose browsing ability isn't tied to the execution of the application you're testing.

6.6.3: Coding the DebugWindow Application

Like any system involving inter-process communication, the DebugWindow application is one of two (or more) applications that have to work together. DebugWindow is responsible for displaying debug and status messages that other applications send. However, for DebugWindow to actually do something useful, you've actually got to have some other application send a debug message to the DebugWindow program. In this section we'll discuss the receiver end of this partnership (that is, the DebugWindow application), in the next section we'll discuss the transmitter (that is, the modifications necessary to applications that transmit debug messages).

The DebugWindow application itself will be a relatively straight-forward dumb terminal emulation. It will display text sent to it and it will buffer up some number of lines of text allowing the user to review those lines. By default, the version of DebugWindow we will develop in this section will save up to 1,024 lines of text, each line up to 255 characters long (though these values are easy enough to change in the source code). DebugWindow will make use of the vertical and horizontal scroll bars to allow the user to view all lines and columns of text displayable in the window. Once the transmitting application(s) send more than the maximum number of lines of text, DebugWindow will begin throwing away the oldest lines of text in the system. This keeps the program from chewing up valuable system resources (memory) if an application produces a large amount of output. The number of lines of text that DebugWindow remembers is controlled by the MaxLines_c constant appearing at the beginning of the source file:

const

MaxLines_c  := 1024;                // Maximum number of lines we will

                                    //  save up for review.

If you want to allow fewer or more lines of saved text, feel free to change this value.

DebugWindow will be capable of processing a small set of control characters embedded in the strings sent to it. Specifically, it will handle carriage returns, line feeds, and tab characters as special characters. All other characters it will physically write to the display window. Though it's fairly easy to extend DebugWindow to handle other control characters, there really is no need to do so. Most other control characters that have a widely respected meaning (e.g., backspace and formfeed) have a destructive nature and we don't want garbage output to the debugging window to accidentally erase any existing data (that could be used to determine why we got the garbage output).

DebugWindow ignores carriage returns and treats linefeeds as a new line character. Normal console applications under Windows use carriage return to move the cursor to the beginning of the current line. However, keeping in mind that we don't want DebugWindow to wipe out any existing data, we'll choose to quietly ignore carriage returns that appear in the character stream. The reason for considering carriage returns at all is because HLA's nl constant (newline) is the character sequence <carriage return><line feed> so it's quite likely that carriage returns will appear in the debug output because programmers are used to using the nl constant. Whenever a linefeed comes along, DebugWindow will emit the current line of text to the next output line in the output window and move the cursor to the beginning of the next line in the output window (DebugWindow will also save the line in an internal memory buffer so the user can review the line later, using the vertical scroll bar).

DebugWindow will interpret tab characters to adjust the output cursor so that it moves to the next "tab stop" on the output line. By default, DebugWindow sets tab stops every eight character positions. In order for tab positions to make any sense at all, DebugWindow uses a monospaced font ("Fixedsys") for character output. If a tab character appears in the data stream, DebugWindow moves the cursor to the next column that is an even multiple of eight, plus one. I.e., columns 9, 17, 25, 33, 41, etc., are tab positions. A tab character moves the cursor (blank filling) to the closest tab stop whose column number is greater than the current column position. DebugWindow uses a default of eight-column tab positions (this is a standard output for terminal output). You may, however, change this by modifying the tabCols constant at the beginning of the source file:

const

    tabCols     := 8;                   // Columns per tabstop position.

Like many of the applications you've seen in this chapter, DebugWindow uses the w.WM_CREATE message to determine when it can do program initialization of various global objects. The initialization that the Create procedure handles includes the following:

  • Initialization of the array of strings that maintain the output lines for later review by the user (i.e., sets all of these strings to the NULL pointer).
  • Selection of the "Fixedsys" font and determining font metrics so the application knows the height and width of characters in this font (so DebugWindows can determine how many characters will fit in the output window as well as deal with vertical and horizontal scrolling).
  • Output of an initial debug message to inform the user that DebugWindows is active.

Here's the code for the Create procedure that handles these tasks:

// Create-

//

//  This procedure responds to the w.WM_CREATE message.

//  Windows sends this message once when it creates the

//  main window for the application.  We will use this

//  procedure to do any one-time initialization that

//  must take place in a message handler.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

readonly

    s           :string :=

                    "Debug Windows begin execution:" nl nl;

    FixedsysName:byte; @nostorage;

                 byte "Fixedsys", 0;

static

    hDC         :dword;

    hOldFont    :dword;

    Fixedsys    :w.LOGFONT;

    tm          :w.TEXTMETRIC;

    cds         :w.COPYDATASTRUCT;

begin Create;

    push( edi );

    // Zero out our line index, count, and the array of pointers:

    xor( eax, eax );

    mov( eax, StartLine );

    mov( eax, LineCnt );

    mov( MaxLines_c, ecx );

    mov( &Lines, edi );

    rep.stosd();

    // Zero out the FONT structure:

    mov( @size( w.LOGFONT ), ecx );

    mov( &Fixedsys, edi );

    rep.stosb();

    // Create the font using the system's "Fixedsys" font and select

    // it into the device context:

    str.zcpy( FixedsysName, Fixedsys.lfFaceName );

    w.CreateFontIndirect( Fixedsys );

    mov( eax, FixedsysFontHandle );     // Save, so we can free this later.

    GetDC( hwnd, hDC );

        // Select in the fixed system font:

        SelectObject( FixedsysFontHandle );

        mov( eax, hOldFont );

        // Determine the sizes we need for the fixed system font:

        GetTextMetrics( tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        inc( eax );

        mov( eax, CharHeight );

        mov( tm.tmAveCharWidth, eax );

        mov( eax, CharWidth );

        SelectObject( hOldFont );

    ReleaseDC;

    // Just for fun, let's send ourselves a message to print the

    // first line in the debug window:

    mov( DebugMsg_c, cds.dwData );

    mov( s, eax );

    mov( eax, cds.lpData );

    mov( (type str.strRec [eax]).length, eax );

    inc( eax ); // Count zero byte, too!

    mov( eax, cds.cbData );

    w.SendMessage( hwnd, w.WM_COPYDATA, hwnd, &cds );

    pop( edi );

end Create;

The DebugWindow application will process several Windows messages and one user message. Here's the dispatch table for DebugWindow:

static

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_PAINT,      &Paint              ],

            MsgProcPtr_t:[ w.WM_COPYDATA,   &RcvMsg             ],

            MsgProcPtr_t:[ w.WM_CREATE,     &Create             ],

            MsgProcPtr_t:[ w.WM_HSCROLL,    &HScroll            ],

            MsgProcPtr_t:[ w.WM_VSCROLL,    &VScroll            ],

            MsgProcPtr_t:[ w.WM_SIZE,       &Size               ],

            MsgProcPtr_t:[ w.WM_DESTROY,    &QuitApplication    ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

The user message will actually be the w.WM_COPYDATA system message. This is because the one user-defined message that DebugWindow accepts is a "print" message that contains a zero-terminated string to display in the debug output window. The RcvMsg procedure (which handles the w.WM_COPYDATA message) defines the three w.COPYDATASTRUCT fields as follows:

  • dwData - This field contains the four bytes "dbug" (`d' is in the H.O. byte). DebugWindow only processes a single user message, so there is no need to use this field to differentiate messages. However, DebugWindow does check the value of this field just as a "sanity check" to avoid display garbage data.
  • cbData - This field contains the length of the string data (including the zero byte). DebugWindow actually ignores this information and simply processes the string data until it encounters a zero terminating byte.
  • lpData - This is a pointer to a zero-terminated string (note: this is not an HLA string!) that contains the data to display in the output window.

Note that the string at which lpData points isn't necessarily a single line of text. This string may contain multiple new line sequences (<carriage return><line feed>) that result in the display of several lines of text in the output window. Therefore, DebugWindow cannot simply copy this string to an element of the array of strings that maintain the lines of text in the display window. Instead, the RcvMsg procedure has to process each character in the message sent to DebugWindows and deal with the control characters (tabs, carriage returns, and line feeds) and construct one or more strings from the message of the text to place in the internal string list. Here's the RcvMsg procedure that does this processing:

// RcvMsg-

//

//  Receives a message from some other process and prints

// the zstring passed as the data payload for that message.

// Note that lParam points at a w.COPYDATASTRUCT object. The

// dwData field of that structure must contain DebugMsg_c if

// we are to process this message.

procedure RcvMsg( hwnd: dword; wParam:dword; lParam:dword );

var

    tabPosn :uns32;

    line    :char[256];

    // Note: addLine procedure actually goes here...

begin RcvMsg;

    push( esi );

    push( edi );

    // Process the incoming zero-terminated string.

    // Break it into separate lines (based on newline

    // sequences found in the string) and expand tabs.

    mov( lParam, esi );

    if( (type w.COPYDATASTRUCT [esi]).dwData = DebugMsg_c ) then

        // Okay, we've got the w.COPYDATASTRUCT type with a valid debug

        // message. Extract the data and print it.

        mov( (type w.COPYDATASTRUCT [esi]).lpData, esi );

        mov( 0, tabPosn );

        lea( edi, line );

        while( (type char [esi]) <> #0 ) do

            mov( [esi], al );

            // Ignore carriage returns:

            if( al <> stdio.cr ) then

                if( al = stdio.tab ) then

                    // Compute the number of spaces until the

                    // next tab stop position:

                    mov( tabPosn, eax );

                    cdq();

                    div( tabCols, edx:eax );

                    neg( edx );

                    add( tabCols, edx );

                    // Emit spaces up to the next tab stop:

                    repeat

                        mov( ' ', (type char [edi]) );

                        inc( edi );

                        inc( tabPosn );

                        if( tabPosn >= 255 ) then

                            dec( edi );

                        endif;

                        dec( edx );

                    until( @z );

                elseif( al = stdio.lf ) then

                    // We've just hit a new line character.

                    // Emit the line up to this point:

                    mov( #0, (type char [edi]) ); // Zero terminate.

                    lea( edi, line );       // Resets edi for next loop iteration.

                    str.a_cpyz( [edi] );    // Build HLA string from zstring.

                    addLine( eax );         // Add to our list of lines.

                    // Reset the column counter back to zero since

                    // we're starting a new line:

                    mov( 0, tabPosn );

                else

                    // If it's not a special control character we process,

                    // then add the character to the string:

                    mov( al, [edi] );

                    inc( edi );

                    inc( tabPosn );

                    // Don't allow more than 255 characters:

                    if( tabPosn >= 255 ) then

                        dec( edi );

                    endif;

                endif;

            endif;

            // Move on to next character in source string:

            inc( esi );

        endwhile;

        // If edi is not pointing at the beginning of "line", then we've

        // got some characters in the "line" string that need to be added

        // to our list of lines.

        lea( esi, line );

        if( edi <> esi ) then

            mov( #0, (type char [edi]) );   // Zero terminate the string.

            str.a_cpyz( [esi] );            // Make a copy of this zstring.

            addLine( eax );

        endif;

        // Because adding and removing lines can affect the

        // maximum line length, recompute the width and horizontal

        // scroll bar stuff here:

        ComputeWidth( hwnd );

        // Ditto for the vertical scroll bars:

        ComputeHeight( hwnd );

        // Tell Windows to tell us to repaint the screen:

        w.InvalidateRect( hwnd, NULL, true );

        w.UpdateWindow( hwnd );

    endif;

    pop( edi );

    pop( esi );

end RcvMsg;

This procedure begins by checking the dwData field of the w.COPYDATASTRUCT parameter passed by reference via the lParam parameter. If this field contains the constant DebugMsg_c ("dbug") then this procedure continues processing the data, otherwise it completely ignores the message (i.e., this is the sanity check to see if the message is valid).

If the message passes the sanity check, then the RcvMsg procedure begins processing the characters from the message one at a time, copying these characters to the local line character array if the characters aren't carriage returns, tabs, line feeds, or the zero terminating byte. If the individual character turns out to be a tab character, then RcvMsg expands the tab by writing the appropriate number of spaces to the line array. If it's a carriage return, then RcvMsg quietly ignores the character. If it's a line feed, then RcvMsg converts the characters in the line array to an HLA string and calls its local procedure addLine to add the current line of text to the list of lines that DebugWindow displays. If it's any other character besides a zero-terminating byte, then RcvMsg just appends the character to the end of the line array.

When RcvMsg encounters a zero-terminating byte it checks to see if there are any characters in the line array. If so, it converts those characters to an HLA string and adds them to the list of strings that DebugWindow will display. If there are no (more) characters in the input string, then RcvMsg doesn't bother creating another string. This process ensures that we don't lose any strings that consist of a sequence of characters ending with a new line sequence (on the other hand, it also means that RcvMsg automatically appends a new line sequence to the end of the message text if it doesn't end with this character sequence).

After adding the string(s) to the string list, the RcvMsg procedure calls the ComputeWidth and ComputeHeight functions (see the source code a little later for details). These functions compute the new maximum width of the file (the width of the list of strings could have gotten wider or narrowing depending on the lines we've added or delete) and the new height information (because we've added lines). These functions also update the horizontal and vertical scroll bars in an appropriate fashion.

The last thing that RcvMsg does is call the w.InvalidateRect and w.UpdateWindow API functions to force Windows to send a w.WM_PAINT message to the application. This causes DebugWindow to redraw the screen and display the text appearing in the debug window.

The RcvMsg procedure has a short local procedure (addLine) that adds HLA strings to the list of strings it maintains. To understand how this procedure operates, we need to take a quick look at the data structures that DebugWindows uses to keep track of the strings it has displayed. Here are the pertinent variables:

static

    LineAtTopOfScrn     :uns32 := 0;    // Tracks where we are in the document

    DisplayLines        :uns32 := 2;    // # of lines we can display.

    StartLine   :uns32;                 // Starting index into "Lines" array.

    LineCnt     :uns32;                 // Number of valid lines in "Lines".

    Lines       :string[MaxLines_c];    // Holds the text sent to us.

The fundamental data structure that keeps track of the text in the debug output window is an array of strings named Lines. This array holds up to MaxLines_c strings. Once this array fills up, DisplayWindows reuses the oldest strings in the array to hold incoming data. The StartLine and LineCnt variables maintain this "circular queue" of strings. LineCnt specifies how many strings are present in the Lines array. This variable starts with the value zero when DebugWindow first runs and increments by one with each incoming line of text until it reaches MaxLines_c. Once LineCnt reaches MaxLines_c the DebugWindows program stops adding new lines to the array and it starts reusing the existing entries in the lines array. The StartLine variable is what DebugWindows uses to maintain the circular buffer of strings. StartLine specifies the first array element of Lines to begin drawing in the debug window (assuming you're scrolled all the way to the top of the document). This variable will contain zero as long as there are fewer than MaxLines_c lines in the array. Once LineCnt hits MaxLines_c, however, DisplayWindow stops adding new lines to the Lines array and it no longer increments the LineCnt variable. Instead, it will first free the storage associated with the string at Lines[StartLine], it will store the new (incoming) string into Lines[StartLine], and then it will increment StartLine by one (wrapping back to zero whenever StartLine reaches MaxLines_c). This has the effect of reusing the oldest line still in the Lines array and setting the second oldest line to become the new "oldest line."

The LineAtTopOfScrn is an integer index into the Lines array that specifies the line that the Paint procedure will begin drawing at the top of the window. DisplayLines is a variable that holds the maximum number of lines that the program can display in the window at one time. Once the value of LineCnt exceeds the value of DisplayLines, the program will increment LineAtTopOfScrn with each incoming line of text so that the screen will scroll up with each incoming line (as you'd normally expect for a terminal).

In addition to these variables, there are a complementary set that control horizontal scrolling. The addLine procedure, however, doesn't use them so we'll ignore them for now.

Here's the actual addLine procedure (which is local to the RcvMsg procedure):

    procedure addLine( lineToAdd:string in eax ); @nodisplay; @noframe;

    begin addLine;

        if( LineCnt >= MaxLines_c ) then

            mov( StartLine, ecx );

            strfree( Lines[ ecx*4 ] ); // Free the oldest line of text.

            inc( StartLine );

            if( StartLine >= MaxLines_c ) then

                mov( 0, StartLine );

            endif;

        else

            mov( LineCnt, ecx );

            inc( LineCnt );

        endif;

        mov( eax, Lines[ ecx*4 ] );

        // If we've got more than "DisplayLines" lines in the

        // output, bump "LineAtTopOfScrn" to scroll the window up:

        mov( LineCnt, ecx );

        if( ecx >= DisplayLines ) then

            inc( LineAtTopOfScrn );

        endif;

        ret();

    end addLine;

The DebugWindow application also processes w.WM_SIZE, w.WM_VSCROLL, and w.WM_HSCROLL messages. However, the logic their corresponding message handling procedures use is nearly identical to that used by the Sysmets application given earlier in this chapter, so we won't rehash the description of these routines. The major differences between Sysmets and DebugWindow are mainly in the horizontal scrolling code. DebugWindow uses a monospaced font, so it can more easily handle horizontal scrolling as a function of some number of characters. Another difference is that horizontal scrolling in DebugWindows scrolls 1/4 of the screen when you do a page left or page right operation.

The Paint procedure in DebugWindow is also fairly straight-forward. A global variable (ColAtLeftOfScrn) tracks the amount of horizontal scrolling that takes place; DebugWindow uses this as an index into each line of text that it displays.

Here's the complete code to the DebugWindow application:

// DebugWindow.hla-

//

//  This program accepts "Debug Print" statements from other processes

// and displays that data in a "console-like" text window.

program DebugWindow;

#include( "stdio.hhf" )

#include( "conv.hhf" )

#include( "strings.hhf" )

#include( "memory.hhf" )

#include( "hll.hhf" )

#include( "w.hhf" )

#include( "wpa.hhf" )

#include( "excepts.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

const

    tabCols     := 8;                   // Columns per tabstop position.

    MaxLines_c  := 1024;                // Maximum number of lines we will

                                        //  save up for review.

static

    hInstance           :dword;         // "Instance Handle" Windows supplies.

    wc                  :w.WNDCLASSEX;  // Our "window class" data.

    msg                 :w.MSG;         // Windows messages go here.

    hwnd                :dword;         // Handle to our window.

    FixedsysFontHandle  :dword;         // Save this so we can free it later.

    WM_DebugPrint       :dword;         // Message number sent to us.

    CharWidth           :dword;

    CharHeight          :dword;

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

    LineAtTopOfScrn     :uns32 := 0;    // Tracks where we are in the document

    MaxLnAtTOS          :uns32 := 0;    // Max display position (vertical).

    DisplayLines        :uns32 := 2;    // # of lines we can display.

    ColAtLeftOfScrn     :uns32 := 0;    // Current Horz position.

    MaxColAtLeft        :uns32 := 0;    // Max Horz position.

    MaxWidth            :uns32 := 40;   // Maximum columns seen thus far

    StartLine   :uns32;                 // Starting index into "Lines" array.

    LineCnt     :uns32;                 // Number of valid lines in "Lines".

    Lines       :string[MaxLines_c];    // Holds the text sent to us.

// The following data type and DATA declaration

// defines the message handlers for this program.

type

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue:   dword;

            MessageHndlr:   MsgProc_t;

        endrecord;

readonly

    ClassName   :string := "DebugWindowClass";  // Window Class Name

    AppCaption  :string := "Debug Window";      // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    //

    // Note: the RcvMsg entry *must* be first as the Create code will

    // patch the entry for the message number (this is not a constant,

    // the message number gets assigned by the system).  The current

    // message number value for this entry is bogus; it must not, however,

    // be zero.

static

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_PAINT,      &Paint              ],

            MsgProcPtr_t:[ w.WM_COPYDATA,   &RcvMsg             ],

            MsgProcPtr_t:[ w.WM_CREATE,     &Create             ],

            MsgProcPtr_t:[ w.WM_HSCROLL,    &HScroll            ],

            MsgProcPtr_t:[ w.WM_VSCROLL,    &VScroll            ],

            MsgProcPtr_t:[ w.WM_SIZE,       &Size               ],

            MsgProcPtr_t:[ w.WM_DESTROY,    &QuitApplication    ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// ComputeWidth-

//

//  This procedure scans through all the lines of text we've saved

// up and finds the longest line in the list.  From this, it computes

// the maximum width we have and sets up the horizontal scroll bar

// accordingly.  This function also sets up the global variables

// MaxWidth,

// MaxColAtLeft, and

// ColAtLeftOfScrn.

procedure ComputeWidth( hwnd:dword );

begin ComputeWidth;

    push( eax );

    push( ecx );

    push( edx );

    // Need to scan through all the lines we've saved up and

    // find the maximum width of all the lines:

    mov( 0, ecx );

    for( mov( 0, edx ); edx < LineCnt; inc( edx )) do

        mov( Lines[ edx*4 ], eax );

        mov( (type str.strRec [eax]).length, eax );

        if( eax > ecx ) then

            mov( eax, ecx );

        endif;

    endfor;

    mov( ecx, MaxWidth );

    // MaxColAtLeft =

    //  max( 0, MaxWidth+1 - ClientSizeX / CharWidth);

    mov( ClientSizeX, eax );

    cdq();

    idiv( CharWidth );

    neg( eax );

    add( MaxWidth, eax );

    inc( eax );

    if( @s ) then

        xor( eax, eax );

    endif;

    mov( eax, MaxColAtLeft );

    // ColAtLeftOfScrn = min( MaxColAtLeft, ColAtLeftOfScrn )

    if( eax > ColAtLeftOfScrn ) then

        mov( ColAtLeftOfScrn, eax );

    endif;

    mov( eax, ColAtLeftOfScrn );

    w.SetScrollRange( hwnd, w.SB_HORZ, 0, MaxColAtLeft, false );

    w.SetScrollPos( hwnd, w.SB_HORZ, ColAtLeftOfScrn, true );

    pop( edx );

    pop( ecx );

    pop( eax );

end ComputeWidth;

// ComputeHeight-

//

// Computes the values for the following global variables:

//

//  DisplayLines,

//  MaxLnAtTOS, and

//  LineAtTopOfScrn

//

//  This procedure also redraws the vertical scroll bars, as necessary.

procedure ComputeHeight( hwnd:dword );

begin ComputeHeight;

    push( eax );

    push( ebx );

    push( ecx );

    // DisplayLines = ClientSizeY/CharHeight:

    mov( ClientSizeY, eax );

    cdq();

    idiv( CharHeight );

    mov( eax, DisplayLines );

    // MaxLnAtTOS = max( 0, LineCnt - DisplayLines )

    mov( LineCnt, ecx );

    sub( eax, ecx );

    if( @s ) then

        xor( ecx, ecx );

    endif;

    mov( ecx, MaxLnAtTOS );

    if( edx <> 0 ) then // EDX is remainder from ClientSizeY/CharHeight

        // If we can display a partial line, bump up the

        // DisplayLine value by one to display the partial line.

        inc( DisplayLines );

    endif;

    // LineAtTopOfScrn = min( LineAtTopOfScrn, MaxLnAtTOS )

    if( ecx > LineAtTopOfScrn ) then

        mov( LineAtTopOfScrn, ecx );

    endif;

    mov( ecx, LineAtTopOfScrn );

    w.SetScrollRange( hwnd, w.SB_VERT, 0, MaxLnAtTOS, false );

    w.SetScrollPos( hwnd, w.SB_VERT, LineAtTopOfScrn, true );

    pop( ecx );

    pop( ebx );

    pop( eax );

end ComputeHeight;

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Free the font we created in the Create procedure:

    w.DeleteObject( FixedsysFontHandle );

    w.PostQuitMessage( 0 );

end QuitApplication;

// Create-

//

//  This procedure responds to the w.WM_CREATE message.

//  Windows sends this message once when it creates the

//  main window for the application.  We will use this

//  procedure to do any one-time initialization that

//  must take place in a message handler.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

readonly

    s           :string :=

                    "Debug Windows begin execution:" nl nl;

    FixedsysName:byte; @nostorage;

                 byte "Fixedsys", 0;

static

    hDC         :dword;

    hOldFont    :dword;

    Fixedsys    :w.LOGFONT;

    tm          :w.TEXTMETRIC;

    cds         :w.COPYDATASTRUCT;

begin Create;

    push( edi );

    // Zero out our line index, count, and the array of pointers:

    xor( eax, eax );

    mov( eax, StartLine );

    mov( eax, LineCnt );

    mov( MaxLines_c, ecx );

    mov( &Lines, edi );

    rep.stosd();

    // Zero out the FONT structure:

    mov( @size( w.LOGFONT ), ecx );

    mov( &Fixedsys, edi );

    rep.stosb();

    // Create the font using the system's "Fixedsys" font and select

    // it into the device context:

    str.zcpy( FixedsysName, Fixedsys.lfFaceName );

    w.CreateFontIndirect( Fixedsys );

    mov( eax, FixedsysFontHandle );     // Save, so we can free this later.

    GetDC( hwnd, hDC );

        // Select in the fixed system font:

        SelectObject( FixedsysFontHandle );

        mov( eax, hOldFont );

        // Determine the sizes we need for the fixed system font:

        GetTextMetrics( tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        inc( eax );

        mov( eax, CharHeight );

        mov( tm.tmAveCharWidth, eax );

        mov( eax, CharWidth );

        SelectObject( hOldFont );

    ReleaseDC;

    // Just for fun, let's send ourselves a message to print the

    // first line in the debug window:

    mov( DebugMsg_c, cds.dwData );

    mov( s, eax );

    mov( eax, cds.lpData );

    mov( (type str.strRec [eax]).length, eax );

    inc( eax ); // Count zero byte, too!

    mov( eax, cds.cbData );

    w.SendMessage( hwnd, w.WM_COPYDATA, hwnd, &cds );

    pop( edi );

end Create;

// RcvMsg-

//

//  Receives a message from some other process and prints

// the zstring passed as the data payload for that message.

// Note that lParam points at a w.COPYDATASTRUCT object. The

// dwData field of that structure must contain DebugMsg_c if

// we are to process this message.

procedure RcvMsg( hwnd: dword; wParam:dword; lParam:dword );

var

    tabPosn :uns32;

    line    :char[256];

    procedure addLine( lineToAdd:string in eax ); @nodisplay; @noframe;

    begin addLine;

        if( LineCnt >= MaxLines_c ) then

            mov( StartLine, ecx );

            strfree( Lines[ ecx*4 ] ); // Free the oldest line of text.

            inc( StartLine );

            if( StartLine >= MaxLines_c ) then

                mov( 0, StartLine );

            endif;

        else

            mov( LineCnt, ecx );

            inc( LineCnt );

        endif;

        mov( eax, Lines[ ecx*4 ] );

        // If we've got more than "DisplayLines" lines in the

        // output, bump "LineAtTopOfScrn" to scroll the window up:

        mov( LineCnt, ecx );

        if( ecx >= DisplayLines ) then

            inc( LineAtTopOfScrn );

        endif;

        ret();

    end addLine;

begin RcvMsg;

    push( esi );

    push( edi );

    // Process the incoming zero-terminated string.

    // Break it into separate lines (based on newline

    // sequences found in the string) and expand tabs.

    mov( lParam, esi );

    if( (type w.COPYDATASTRUCT [esi]).dwData = DebugMsg_c ) then

        // Okay, we've got the w.COPYDATASTRUCT type with a valid debug

        // message. Extract the data and print it.

        mov( (type w.COPYDATASTRUCT [esi]).lpData, esi );

        mov( 0, tabPosn );

        lea( edi, line );

        while( (type char [esi]) <> #0 ) do

            mov( [esi], al );

            // Ignore carriage returns:

            if( al <> stdio.cr ) then

                if( al = stdio.tab ) then

                    // Compute the number of spaces until the

                    // next tab stop position:

                    mov( tabPosn, eax );

                    cdq();

                    div( tabCols, edx:eax );

                    neg( edx );

                    add( tabCols, edx );

                    // Emit spaces up to the next tab stop:

                    repeat

                        mov( ' ', (type char [edi]) );

                        inc( edi );

                        inc( tabPosn );

                        if( tabPosn >= 255 ) then

                            dec( edi );

                        endif;

                        dec( edx );

                    until( @z );

                elseif( al = stdio.lf ) then

                    // We've just hit a new line character.

                    // Emit the line up to this point:

                    mov( #0, (type char [edi]) ); // Zero terminate.

                    lea( edi, line );       // Resets edi for next loop iteration.

                    str.a_cpyz( [edi] );    // Build HLA string from zstring.

                    addLine( eax );         // Add to our list of lines.

                    // Reset the column counter back to zero since

                    // we're starting a new line:

                    mov( 0, tabPosn );

                else

                    // If it's not a special control character we process,

                    // then add the character to the string:

                    mov( al, [edi] );

                    inc( edi );

                    inc( tabPosn );

                    // Don't allow more than 255 characters:

                    if( tabPosn >= 255 ) then

                        dec( edi );

                    endif;

                endif;

            endif;

            // Move on to next character in source string:

            inc( esi );

        endwhile;

        // If edi is not pointing at the beginning of "line", then we've

        // got some characters in the "line" string that need to be added

        // to our list of lines.

        lea( esi, line );

        if( edi <> esi ) then

            mov( #0, (type char [edi]) );   // Zero terminate the string.

            str.a_cpyz( [esi] );            // Make a copy of this zstring.

            addLine( eax );

        endif;

        // Because adding and removing lines can affect the

        // maximum line length, recompute the width and horizontal

        // scroll bar stuff here:

        ComputeWidth( hwnd );

        // Ditto for the vertical scroll bars:

        ComputeHeight( hwnd );

        // Tell Windows to tell us to repaint the screen:

        w.InvalidateRect( hwnd, NULL, true );

        w.UpdateWindow( hwnd );

    endif;

    pop( edi );

    pop( esi );

end RcvMsg;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

//  For this program, the Paint procedure draws all the

// lines from the scroll position to the end of the window.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

    hOldFont    :dword;         // Saves old font handle.

    ps          :w.PAINTSTRUCT; // Used while painting text.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        SelectObject( FixedsysFontHandle );

        mov( eax, hOldFont );

        // Figure out how many lines to display;

        // It's either DisplayLines (the number of lines we can

        // physically put in the Window) or LineCnt (total number

        // of lines in the Lines array), whichever is less:

        mov( DisplayLines, esi );

        if( esi > LineCnt ) then

            mov( LineCnt, esi );

        endif;

        // Display each of the lines in the Window:

        for( mov( 0, ebx ); ebx < esi; inc( ebx )) do

            // Get the string to display (address to edi):

            mov( LineAtTopOfScrn, eax );

            add( ebx, eax );

            if( eax < LineCnt ) then

                // If we've got more than a buffer full of lines,

                // base our output at "StartLine" rather than at

                // index zero:

                add( StartLine, eax );

                if( eax >= LineCnt ) then

                    sub( LineCnt, eax );

                endif;

                // Get the current line to output:

                mov( Lines[ eax*4 ], edi );

                // Compute the y-coordinate in the window:

                mov( ebx, eax );

                intmul( CharHeight, eax );

                // Compute the starting column position into the

                // line and the length of the line (to handle

                // horizontal scrolling):

                mov( (type str.strRec [edi]).length, ecx );

                add( ColAtLeftOfScrn, edi );

                sub( ColAtLeftOfScrn, ecx );

                // Output the line of text:

                TextOut

                (

                    CharWidth,

                    eax,

                    edi,

                    ecx

                );

            endif;

        endfor;

        SelectObject( hOldFont );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    // Compute the new screen height info here:

    ComputeHeight( hwnd );

    // Compute screen width and MaxWidth values, and set up the

    // horizontal scroll bars:

    ComputeWidth( hwnd );

    xor( eax, eax ); // return success.

end Size;

// HScroll-

//

//  Handles w.WM_HSCROLL messages.

//  On entry, L.O. word of wParam contains the scroll bar activity.

procedure HScroll( hwnd: dword; wParam:dword; lParam:dword );

begin HScroll;

    movzx( (type word wParam), eax );

    mov( ColAtLeftOfScrn, ecx );

    if( eax = w.SB_LINELEFT ) then

        // Scrolling left means decrement ColAtLeftOfScrn by one:

        dec( ecx );

    elseif( eax = w.SB_LINERIGHT ) then

        // Scrolling right means increment ColAtLeftOfScrn by one:

        inc( ecx );

    elseif( eax = w.SB_PAGELEFT ) then

        // Page Left means decrement ColAtLeftOfScrn by 25% of screen width:

        mov( ClientSizeX, eax );

        cdq();

        div( CharWidth, edx:eax );  // Computes screen width in chars.

        shr( 2, eax );              // 25% of screen width.

        adc( 0, eax );

        sub( eax, ecx );

    elseif( eax = w.SB_PAGERIGHT ) then

        // Page Right means increment ColAtLeftOfScrn by 25% of screen width:

        mov( ClientSizeX, eax );

        cdq();

        div( CharWidth, edx:eax );  // Computes screen width in chars.

        shr( 2, eax );              // 25% of screen width.

        adc( 0, eax );

        add( eax, ecx );

    elseif( eax = w.SB_THUMBTRACK ) then

        // H.O. word of wParam contains new scroll thumb position:

        movzx( (type word wParam[2]), ecx );

    // else leave ColAtLeftOfScrn alone

    endif;

    // Make sure that the new ColAtLeftOfScrn value (in ecx) is within

    // a reasonable range (0..MaxColAtLeft):

    if( (type int32 ecx) < 0 ) then

        xor( ecx, ecx );

    elseif( ecx > MaxColAtLeft ) then

        mov( MaxColAtLeft, ecx );

    endif;

    mov( ColAtLeftOfScrn, eax );

    mov( ecx, ColAtLeftOfScrn );

    sub( ecx, eax );

    if( eax <> 0 ) then

        intmul( CharWidth, eax );

        w.ScrollWindow( hwnd, eax, 0, NULL, NULL );

        w.SetScrollPos( hwnd, w.SB_HORZ, ColAtLeftOfScrn, true );

        w.InvalidateRect( hwnd, NULL, true );

    endif;

    xor( eax, eax ); // return success

end HScroll;

// VScroll-

//

//  Handles the w.WM_VSCROLL messages from Windows.

//  The L.O. word of wParam contains the action/command to be taken.

//  The H.O. word of wParam contains a distance for the w.SB_THUMBTRACK

//  message.

procedure VScroll( hwnd: dword; wParam:dword; lParam:dword );

begin VScroll;

    movzx( (type word wParam), eax );

    mov( LineAtTopOfScrn, ecx );

    if( eax = w.SB_TOP ) then

        // Top of file means LATOS becomes zero:

        xor( ecx, ecx );

    elseif( eax = w.SB_BOTTOM ) then

        // Bottom of file means LATOS becomes MaxLnAtTOS:

        mov( MaxLnAtTOS, ecx );

    elseif( eax = w.SB_LINEUP ) then

        // LineUp - Decrement LATOS:

        dec( ecx );

    elseif( eax = w.SB_LINEDOWN ) then

        // LineDn - Increment LATOS:

        inc( ecx );

    elseif( eax = w.SB_PAGEUP ) then

        // PgUp - Subtract DisplayLines from LATOS:

        sub( DisplayLines, ecx );

    elseif( eax = w.SB_PAGEDOWN ) then

        // PgDn - Add DisplayLines to LATOS:

        add( DisplayLines, ecx );

    elseif( eax = w.SB_THUMBTRACK ) then

        // ThumbTrack - Set LATOS to L.O. word of wParam:

        movzx( (type word wParam[2]), ecx );

    // else - leave LATOS alone

    endif;

    // Make sure LATOS is in the range 0..MaxLnAtTOS-

    if( (type int32 ecx) < 0 ) then

        xor( ecx, ecx );

    elseif( ecx >= MaxLnAtTOS ) then

        mov( MaxLnAtTOS, ecx );

    endif;

    mov( LineAtTopOfScrn, eax );

    mov( ecx, LineAtTopOfScrn );

    sub( ecx, eax );

    if( eax <> 0 ) then

        intmul( CharHeight, eax );

        w.ScrollWindow( hwnd, 0, eax, NULL, NULL );

        w.SetScrollPos( hwnd, w.SB_VERT, LineAtTopOfScrn, true );

        w.InvalidateRect( hwnd, NULL, true );

        w.UpdateWindow( hwnd );

    endif;

    xor( eax, eax ); // return success.

end VScroll;

/**************************************************************************/

/*                   End of Application Specific Code                     */

/**************************************************************************/

// The window procedure.

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword );

    @stdcall;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin DebugWindow;

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    mov( hInstance, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW | w.WS_VSCROLL |  w.WS_HSCROLL,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end DebugWindow;

6.6.4: Using DebugWindow

By itself, the DebugWindow application isn't very interesting. When you run it, it displays an initial debug message and then waits for another message to arrive. In order to make practical use of this application, other applications must send messages to DebugWindow so it can display their debug output. In this section, we'll explore the additions you'll need to make to an application in order to send output to DebugWindow.

Of course, the obvious way to send output to the DebugWindow process is to explicitly build up w.COPYDATASTRUCT objects and make the w.SendMessage call yourself. There are two problems with this approach - first, it's a lot of work. Of course, it's easy enough to write a short procedure you can pass a string that will build the data structure and call w.SendMessage, so this problem is easily corrected. Another problem, not quite as easy to fix, is that HLA users (particularly those coming from a console application background) expect to be able to use a statement like "dbg.put( hwnd, ...);5" to print all kinds of data, not just string data. So this is what we want to shoot for, something like the stdout.put or str.put macros that will allow a DebugWindow user to easily print all sorts of data.

Another issue with the debug output statements that we place in our applications is the effect they have on the application's size and performance. Although debug statements are quite useful during the debugging and testing phases of program development, we don't want these statements taking up space or slowing down the program when we produce a production version of the software. That is, we want to easily enable all these statements during debugging, but we want to be able to immediately remove them all when compiling a production version of the software. Fortunately, HLA's compile-time language provides all the facilities we need to achieve this.

To control the emission of debug code in our applications, we'll use a tried and true mechanism: conditional assembly. HLA's #if statement allows use to easily activate or eliminate sequences of statements during a compile by defining certain symbols or setting certain constants true or false. For example, consider the following code sequence:

    #if( @defined( debug ))

        dbg.put( hwnd, "Reached Point A" nl ); // hwnd is this process' window handle

    #endif

This sequence of statements checks to see if the symbol debug is defined prior to the #if statement. If so, then HLA will compile the dbg.put statement; if not, then HLA will treat everything between the #if and #endif as a comment (and will ignore it). You may control the definition of the debug symbol a couple of different ways. One way is to explicitly place a statement like the following near the beginning of your program:

?debug := true;

(Note that the type and value of this symbol are irrelevant; the @defined compile-time function simply checks the symbol to see if it is previously defined; the convention for such symbols is to declare them as boolean constants and initialize them with true.)

Another way to define a symbol is from the HLA command line. Consider the following Windows cmd.exe command:

hla   -Ddebug   AppUsesDbgWin.hla

This command line will predefine the symbol debug as a boolean constant that is set to the value true. Note that the generic makefiles we've created for HLA applications include a "debug" option that defines this symbol when you specify "debug" from the command line.

The only problem with using the @defined compile-time function to control conditional assembly is that it's an all or nothing proposition. A symbol is either defined or it is not defined in HLA. You cannot arbitrarily undefine a symbol. This means that you cannot turn the debugging code on or off on a line-by-line basis in your files. A better solution is to use a boolean compile-time variable and set it to true or false. This allows you to activate or deactivate the debug code at will throughout your source file, e.g.,

?debug := true;

        .

        .

        .

    << During this section of the file, debugging is active >>

        .

        .

        .

?debug := false;

        .

        .

        .

    << During this section of the file, debugging is deactivated >>

        .

        .

        .

?debug := true;

        .

        .

        .

    << Here, the debugging code is once again active >>

        .

        .

        .

The only catch to this approach is that you must define debug before the first conditional assembly statement that tests the value of debug, or HLA will report an error. An easy way to ensure that debug is defined is to include code like this in the header file that contains the other definitions for the calls that output data to the debug window:

#if( !@defined( debug ))

    // If the symbol is not defined, default to a value of false-

    ?debug := false;

#endif

The combination of the definition of a symbol like debug and the use of conditional assembly (also called conditional compilation) in the program provides exactly what we need - the ability to quickly activate or deactivate the debugging code by defining (or not defining) a single symbol. There is, however, a minor problem with this approach - all those #if and #endif statements tend to clutter up the program and make it harder to read. Fortunately, there is an easy solution to this problem - make the dbg.put invocation a macro rather than a procedure call6. The dbg.put macro would then look something like the following:

namespace dbg; // This is where the "dbg." comes from...

    #macro put( hwnd, operand );

        #if( debug )

            <<Code that passes the string data to DebugWindow>>

        #endif

    #endmacro

end dbg;

With a macro definition like this, you can write code like the following and the system will automatically make the code associated with dbg.put go away if you've not set the debug symbol to true:

dbg.put( hwnd, "Some Message" nl );

Of course, we'd like dbg.put to be able to handle multiple operands and automatically convert non-string data types to their string format for output (just like stdout.put and str.put). We could copy the code for the stdout.put or str.put macros into the dbg namespace with the appropriate modification to construct a string to send to the DebugWindow application. However, this is a lot of code to copy and modify (these macros are several hundred lines long each). It turns out, however, that all we really need to do is allocate storage for a string, pass the dbg.put parameters to str.put (that will convert the data to string format), and then pass the resulting string on to the DebugWindow application. That is, we could get by with the following two statements:

    str.put( someStr, <<dbg.put parameters>> );

    sendStrToDebugWindow( hwnd, someStr );

The only major complication here is the need to allocate storage for a string (someStr) before executing these two statements. A minor issue is the fact that you cannot easily pass all the parameters from one variable-length parameter macro to another. Fortunately, with the HLA compile-time language and an understanding of the HLA string format, it's easy enough to write a macro that automates these two statements for us. Consider the following code:

namespace dbg;

    #macro put( _hwnd_dbgput_, _dbgput_parms_[] ):

            _dbgput_parmlist_,

            _cur_dbgput_parm_;

        #if( debug & @elements( _dbgput_parms_ ) <> 0 )

            sub( 4096, esp );       // Make room for string on stack.

            push( edi );            // We need a register, EDI is as good as any.

            lea( edi, [esp+12]);    // Turn EDI into a string pointer.

            // Initialize the maxlength field of our string (at [edi-8]) with 4084

            // (this string has 12 bytes of overhead).

            mov( 4084, (type str.strRec [edi]).MaxStrLen );

            // Initialize the current length to zero (empty string ):

            mov( 0, (type str.strRec [edi]).length );

            // Zero terminate the string (needed even for empty strings):

            mov( #0, (type char [edi]) );

            // Okay, invoke the str.put macro to process all the dbg.put

            // parameters passed to us. Note that we have to feed our parameters

            // to str.put one at a time:

            ?_dbgput_parmlist_ := "";

            #for( _cur_dbgput_parm_ in _dbgput_parms_ )

                ?_dbgput_parmlist_ :=

                    _dbgput_parmlist_ + "," + _cur_dbgput_parm_;

            #endfor

            str.put

            (

                (type string edi)   // Address of our destination string

                @text( _dbgput_parmlist_ )

            );  // End of str.put macro invocation

            dbg.sendStrToDebugWindow( _hwnd_dbgput_, (type string edi) );

            // Clean up the stack now that we're done:

            pop( edi );

            add( 4096, esp );

        #endif

    #endmacro

    #if( @defined( debug ))

        procedure sendStrToDebugWindow( hwnd:dword; s:string );

        begin sendStrToDebugWindow;

            << Code that sends s to DebugWindow >>

        end sendStrToDebugWindow;

    #endif

end dbg;

This code creates an HLA string on the stack (maximum of 4084 characters, which is probably more than enough for just about any debug message you can imagine7) and initializes it appropriately. Then it invokes the str.put macro to convert dbg.put's parameters to string form. Note how this code uses a compile-time #for loop to process all the dbg.put parameters (variable parameters supplied to a macro are translated to an array of strings by HLA; the #for loop processes each element of this array). Also note the use of the @text function to convert each of these strings into text for use by str.put (and also note the sneaky placement of the comma inside the #for loop so that we get exactly enough commas, in all the right places, when the #for loop executes during compile time). Effectively, the #for loop is simple injecting all of the dbg.put parameters into the str.put macro invocation after the first parameter (the destination string). This implementation of dbg.put requires a whole lot less code than implement it directly (i.e., by writing the same code that the str.put and stdout.put macros require).

The only thing left to implement is the sendStrToDebugWindow procedure. Here's the code for that procedure:

    #if( @global:debug )

        procedure sendStrToDebugWindow( hwnd:dword; s:string );

            @nodisplay;

            @noalignstack;

        var

            cds :@global:w.COPYDATASTRUCT;

        static

            GoodHandle :boolean := @global:true;

        begin sendStrToDebugWindow;

            push( eax );

            push( ebx );

            push( ecx );

            push( edx );

            mov( s, ebx );

            mov( (type @global:str.strRec [ebx]).length, eax );

            inc( eax ); // Account for zero terminating byte

            mov( ebx, cds.lpData );

            mov( eax, cds.cbData );

            mov( @global:DebugMsg_c, cds.dwData );

            @global:w.FindWindow( "DebugWindowClass", NULL );

            // Logic to bring up a dialog box complaining that

            // DebugWindow is not current running if we fail to

            // find the window.  Note that we don't want to display

            // this dialog box on each execution of a dbg.put call,

            // only on the first instance where we get a w.FindWindow

            // failure.

            if( GoodHandle ) then

                if( eax = NULL ) then

                    // Whenever GoodHandle goes from true to false,

                    // print the following error message in a dialog box:

                    @global:w.MessageBox

                    (

                        0,

                        "Debug Windows is not active!",

                        "dbg.put Error",

                        @global:w.MB_OK

                    );

                    mov( @global:false, GoodHandle ); // Only report this once.

                    xor( eax, eax );    // Set back to NULL

                endif;

            else

                // If the handle becomes good after it was bad,

                // reset the GoodHandle variable.  That way if

                // DebugWindow dies sometime later, we can once again

                // bring up the dialog box.

                if( eax <> NULL ) then

                    mov( @global:true, GoodHandle );

                endif;

            endif;

            lea( ebx, cds );

            @global:w.SendMessage( eax, @global:w.WM_COPYDATA, hwnd, ebx );

            pop( edx );

            pop( ecx );

            pop( ebx );

            pop( eax );

        end sendStrToDebugWindow;

    #endif

The @global: prefixes before several of the identifiers exist because these symbols are not local to the dbg namespace and HLA requires the @global: prefix when referencing symbols that are not local to the name space (this was not necessary in the macro put because HLA doesn't process the macro body until you actually expand the put macro, and usually this is outside the namespace). You'll also notice a quick check to see if w.FindWindow returns a valid handle. If this is the first call to sendStrToDebugWindow or the previous call to w.FindWindow returned a valid handle, and the current call returns an invalid handle, then this procedure brings up a dialog box to warn the user that the DebugWindow application is not active. To display this dialog box, sendStrToDebugWindow calls the w.MessageBox API function:

static

    MessageBox: procedure

    (

        hWnd            :dword;

        lpText          :string;

        lpCaption       :string;

        uType           :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__MessageBoxA@16" );

The hWnd parameter is the window handle for the calling procedure, the lpText parameter is a string that Windows displays within the dialog box, the lpCaption parameter is a string that Windows displays on the title bar of the dialog box, and the last parameter specifies the type of buttons to display in the dialog box (e.g., the w.MB_OK constant tells w.MessageBox to display an "OK" button). Note that this code doesn't display the message box on each failure by w.FindWindow. If the DebugWindow application is not running, it would be annoying to have this dialog box appear every time the application tried to send a debug message to DebugWindow. If the user starts up the DebugWindow application after the dialog box appears, then sendStrToDebugWindow "repairs" itself and begins sending the messages again.

The astute reader (and expert assembly language programmer) might notice a whole bunch of inefficiencies present in this code. For example, this code calls w.FindWindow for every debug message it processes. It wouldn't be too difficult to allocate a static variable inside sendStrToDebugWindow and initialize that variable on the very first call, saving the overhead of the call to w.FindWindow on future calls. Other inefficiencies include the fact that the put macro generates a ton of code to construct and destroy the string on each invocation of the dbg.put macro. Even within the DebugWindow application itself there are some situations where performance could be improved (e.g., this code could be smarter about how often it recomputes the maximum width and height values). On the other hand, keep in mind that the purpose of this code is to support debugging, not support a production version of an application. The minor inefficiencies present in this code are of little concern because such inefficiencies will not appear in the final code anyway.

We're not done with the DebugWindow application by any means. There are many, many, improvements we can make to this code. We'll revisit this application in later chapters when we cover features such as menus. At that time, we'll enhance the operation of this application to make it even more powerful and flexible.

6.7: The Windows Console API

A chapter on text manipulation in a Windows programming book isn't really complete without a brief look at the Win32 console API. Windows provides several features that let you manipulate text on the display from a console application (i.e., traditional text-based application), providing considerable control over the text display. Although the thrust of this book is to teach you how to write event-driven GUI applications under Windows, it's worth pointing out the console features that Windows provides because many applications run just fine in a text environment.

Note that, by default, HLA produces console applications when you link in and use the HLA Standard Library (e.g., when calling functions like HLA's stdout.put routine). In fact, HLA even provides a CONSOLE library module that provides a thin layer above the Win32 console API. The HLA Standard Library console module, however, doesn't provide anywhere near the total capabilities that are possible with the Win32 API (HLA's console module purposefully limits its capabilities to produce a modicum of compatibility with Linux console code). We'll take a look at many of the Win32 Console API capabilities in this section.

6.7.1: Windows' Dirty Secret - GUI Apps Can Do Console I/O!

A well-kept Microsoft secret, well, at least a seldom-used feature in Windows is that every application, including GUI applications, can have a console window associated with them. Conversely, every application can open up a graphics window. In fact, the only difference between a "console app" and a "GUI app" under Windows is that console applications automatically allocate and use a console window when they begin execution, whereas GUI applications do not automatically create a console window. However, there is nothing stopping you from allocating your own console window in a GUI app. This section will demonstrate how to do that.

Creating a console window for use by a GUI app is very simple - just call the w.AllocConsole function. This function has the following prototype:

static

    AllocConsole: procedure;

        @stdcall;

        @returns( "eax" ); // Zero if failure

        @external( "__imp__AllocConsole@0" );

You may call w.AllocConsole from just about anywhere in your program that you want (generally, calling it as the first statement of your main program is a good idea). However, you may only call this function once (unless you call the corresponding w.FreeConsole function to deallocate the console window prior to calling w.AllocConsole a second time; see the Windows documentation for more details about w.FreeConsole).

Once you call w.AllocConsole, Windows attaches the standard input, standard output, and standard error devices to that console window. This means that you can use any code that writes to the standard output (or standard error) or reads from the standard input to do I/O in the new console window you've created. Because HLA's stdout.XXXX and stdin.XXXX procedures read and write the standard input and standard output.

In the previous section, this chapter discussed the creation of the DebugWindow application that lets one application write debug information to a "console-like" text window. The DebugWindow application is really nice because if your main application crashes, the DebugWindow application is still functioning and you can scroll through the debug output your application produced to try and determine the root cause of your problem. However, if all you want to do is display some text while your program is operating normally, using an application like DebugWindow to capture the output of your application is overkill. A better solution is to allocate and open the console window associated with the application and then use HLA's Standard Library stdout.XXXX (and stdin.XXXX) functions to manipulate the console window. The following ConsoleHello program is very similar to the DBGHello program from the previous section, except it writes its output to the console window it opens up rather than sending this output to the DebugWindow application.

// ConsoleHello.hla:

//

// Displays "Hello World" in a window.

program ConsoleHello;

#include( "stdlib.hhf" )    // HLA Standard Library.

#include( "w.hhf" )         // Standard windows stuff.

#include( "wpa.hhf" )       // "Windows Programming in Assembly" specific stuff.

?@nodisplay := true;

?@nostackalign := true;

static

    hInstance:  dword;          // "Instance Handle" supplied by Windows.

    wc:     w.WNDCLASSEX;       // Our "window class" data.

    msg:    w.MSG;              // Windows messages go here.

    hwnd:   dword;              // Handle to our window.

readonly

    ClassName:  string := "TextAttrWinClass";       // Window Class Name

    AppCaption: string := "Text Attributes";        // Caption for Window

// The following data type and DATA declaration

// defines the message handlers for this program.

type

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue:   dword;

            MessageHndlr:   MsgProc_t;

        endrecord;

// The dispatch table:

//

//  This table is where you add new messages and message handlers

//  to the program.  Each entry in the table must be a tMsgProcPtr

//  record containing two entries: the message value (a constant,

//  typically one of the wm.***** constants found in windows.hhf)

//  and a pointer to a "tMsgProc" procedure that will handle the

//  message.

readonly

    Dispatch:   MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the "wm.Destroy" message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Create-

//

//  Just send a message to our console window when this message comes along:

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

begin Create;

    // When the create message comes along, send a message to

    // the console window:

    stdout.put( "Create was called" nl );

end Create;

// Size-

//

//  Display the window's new size whenever this message comes along.

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // When the size message comes along, send a message to

    // the console window:

    stdout.put

    (

        "Size was called, new X-size is ",

        (type uns16 lParam),

        " new Y-size is ",

        (type uns16 lParam[2]),

        nl

    );

end Size;

// Paint:

//

//  This procedure handles the "wm.Paint" message.

//  For this simple "Hello World" application, this

//  procedure simply displays "Hello World" centered in the

//  application's window.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword ); @nodisplay;

var

    hdc:    dword;              // Handle to video display device context

    ps:     w.PAINTSTRUCT;      // Used while painting text.

    rect:   w.RECT;             // Used to invalidate client rectangle.

static

    PaintCnt    :uns32 := 0;

begin Paint;

    // Display a count to the console window:

    inc( PaintCnt );

    stdout.put( "Paint was called (cnt:", PaintCnt, ")" nl );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    w.BeginPaint( hwnd, ps );

    mov( eax, hdc );

    w.GetClientRect( hwnd, rect );

    w.DrawText

    (

        hdc,

        "Hello World!",

        -1,

        rect,

        w.DT_SINGLELINE | w.DT_CENTER | w.DT_VCENTER

    );

    w.EndPaint( hwnd, ps );

end Paint;

/**************************************************************************/

/*                   End of Application Specific Code                     */

/**************************************************************************/

// The window procedure.  Since this gets called directly from

// windows we need to explicitly reverse the parameters (compared

// to the standard STDCALL declaration) in order to make HLA's

// Pascal calling convention compatible with Windows.

//

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword  );

    @stdcall;

    @nodisplay;

    @noalignstack;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin ConsoleHello;

    // Create a console window for this application:

    w.AllocConsole();

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    mov( eax, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Demonstrate printing to the console window:

    stdout.put( "Hello World to the console" nl );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // Read a newline from the console window in order to pause

    // before quitting this program:

    stdout.put( "Hit 'enter' to quit this program:" );

    stdin.readLn ();

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end ConsoleHello;

6.7.2: Win32 Console Functions

Whether you open your own console window within a GUI application or write a standard console app, Windows provides a large set of console related functions that let you manipulate the console window. Although there are far too many of these functions to describe in detail here, this section will attempt to describe the more useful functions available to console window programmers. For full details on the console window functions, check out the Microsoft document (e.g., on MSDN).

6.7.2.1: w.AllocConsole

static

    AllocConsole: procedure;

        @stdcall;

        @returns( "eax" ); // Zero if failure

        @external( "__imp__AllocConsole@0" );

The w.AllocConsole function will allocate a console for the current application. This function returns zero if the call fails, it returns a non-zero result if the function succeeds. An application may only have one allocated console associated with it. See the description of the w.FreeConsole function to learn how to deallocate a console window (so you can allocate another one with a second call to w.AllocConsole).

6.7.2.2: w.CreateConsoleScreenBuffer

static

    CreateConsoleScreenBuffer: procedure

    (

            dwDesiredAccess:        dword;

            dwShareMode:            dword;

        var lpSecurityAttributes:   SECURITY_ATTRIBUTES;

            dwFlags:                dword;

            lpScreenBufferData:     dword  // Should be NULL.

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateConsoleScreenBuffer@20" );

A console window can have multiple screen buffers associated with it, though only one screen buffer can be active at a time (see the description of w.SetConsoleActiveScreenBuffer for details on switching console buffers). By default, a console window as a single console buffer associated with it when you first create a console window (via w.AllocConsole). However, you may create additional console buffers by calling the w.CreateConsoleScreenBuffer API function.

The dwDesiredAccess parameter specifies the access rights the current process will have to the buffer. This should be the constant "w.GENERIC_READ | w.GENERIC_WRITE" for full access to the buffer (at the very least, this parameter should have w.GENERIC_WRITE, but read access is useful, too).

In general, the dwShareMode parameter should be zero. See the Microsoft documentation for more details about file sharing. File sharing isn't a particularly useful feature in console windows, so you'll normally set this parameter to zero.

The lpSecurityAttribute parameter should be set to NULL. See the Microsoft documentation if you want to implement secure access to a console window (not very useful, quite frankly).

The dwFlags parameter should be set to the value w.CONSOLE_TEXTMODE_BUFFER (this is currently the only legal value you can pass as this parameter's value).

The lpScreenBufferData parameter must be passed as NULL. Undoubtedly, Microsoft originally intended to use this parameter for something else, and then changed their mind about it's purpose.

If this function fails, it returns zero in the EAX register. If it succeeds, it returns a handle to the console buffer that Windows creates. You will need to save this handle so you can use it to switch to this console buffer using the w.SetConsoleActiveScreenBuffer API function.

6.7.2.3: w.FillConsoleOutputAttribute

static

    FillConsoleOutputAttribute: procedure

    (

            hConsoleOutput :dword; // Handle to screen buffer

            wAttribute     :dword; // Color attribute (in L.O. 16 bits)

            nLength        :dword; // Number of character positions to fill

            dwWriteCoord   :w.COORD; // Coordinates of first position to fill

        var lpNumberOfAttrsWritten :dword //Returns # of positions written here

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FillConsoleOutputAttribute@20" );

const

    FOREGROUND_BLUE := $1;

    FOREGROUND_GREEN := $2;

    FOREGROUND_RED := $4;

    FOREGROUND_INTENSITY := $8;

    BACKGROUND_BLUE := $10;

    BACKGROUND_GREEN := $20;

    BACKGROUND_RED := $40;

    BACKGROUND_INTENSITY := $80;

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

A character attribute in a console window is the combination of foreground and background colors for that character cell. The w.FillConsoleOutputAttribute function lets you set the foreground and background colors for a sequential set of characters on the screen without otherwise affecting the characters on the screen.

The hConsoleOutput parameter is the handle of a screen buffer (e.g., a handle returned by w.CreateConsoleScreenBuffer or by a call to w.GetStdHandle(w.STD_OUTPUT_HANDLE) ).

The wAttribute value is a bitmap containing some combination of the foreground and background colors defined in the windows.hhf header file (included by w.hhf). The base foreground colors are w.FOREGROUND_BLUE, w.FOREGROUND_GREEN, and w.FOREGROUND_RED. OR'ing these constants together forms additional colors, i.e.,

  • w.FOREGROUND_GREEN | w.FOREGROUND_BLUE produces cyan.
  • w.FOREGROUND_BLUE | w.FOREGROUND_RED produces magenta.
  • w.FOREGROUND_GREEN | w.FOREGROUND_RED produces yellow.
  • w.FOREGROUND_GREEN | w.FOREGROUND_BLUE | w.FOREGROUND_REG produces white.

Merging in w.FOREGROUND_INTENSITY with any of the individual colors or combination of colors produces a lighter (brighter) version of that color. Because you may combine four bits in any combination, it is possible to specify up to 16 different foreground colors with these values.

You set the background colors exactly the same way, except (of course) you use the w.BACKGROUND_xxx constants rather than the w.FOREGROUND_xxx constants. Note that you merge both the foreground and background colors into the same wAttribute parameter value. Also note that this bitmap consumes only eight of the 32 bits in the wAttribute parameter - the other bits must be zero.

The nLength parameter specifies the number of character positions to fill with the specified attribute. If this length would specify a character position beyond the end of the current line, then Windows will "wrap around" to the beginning of the next line and continue filling the attributes there. If the nLength parameter, along with the dwWriteCoord parameter, specifies a position beyond the end of the current screen buffer, Windows stops emitting attribute values at the end of the current screen buffer.

The dwWriteCoord parameter specifies the starting coordinate in the screen buffer to begin the attribute fill operation. Note that this is a record object consisting of a 16-bit X-coordinate and 16-bit Y-coordinate value pair. Generally, most people simply pass this parameter as a 32-bit value with the X-coordinate value appearing in the L.O. 32 bits of the double word and the Y-coordinate appearing in the upper 32-bits of the double word. You can use HLA's type coercion operation to pass a 32-bit register as this parameter value, e.g.,

    mov( $1_0002, edx );  // x=2, y=1

    w.FillConsoleOutputAttribute

    (

        hOutputWindow,

        w.BACKGROUND_GREEN | w.BACKKGROUND_BLUE | w.BACKGROUND_INTENSITY,

        (type w.COORD edx),

        charsWritten

    );

The last parameter, lpNumberOfAttrsWritten, is a pointer to an integer variable that receives the total number of attribute positions modified in the screen buffer by this function. The w.FillConsoleOutputAttr function stores a value different from nLength into the variable referenced by this address if nLength specifies output positions beyond the end of the screen buffer (forcing w.FillConsoleOutputAttr to write fewer than nLength attribute values to the screen buffer).

You would normally use this function to change the attributes (colors) of existing text on the display; you would not normally use this function to write new data to the screen (the w.SetConsoleTextAttribute function lets you set the default character attributes to use when writing characters to the console). You could use this function, for example, to highlight a line of existing text on the display (perhaps a menu selection) without otherwise affecting the text data.

The w.FillConsoleOutputAttribute function writes only a single foreground/background color combination to the console. If you want to write a sequence of different attribute values to the display, you should consider using the w.WriteConsoleOutputAttribute function, instead.

6.7.2.4: w.FillConsoleOutputCharacter

static

    FillConsoleOutputCharacter: procedure

    (

        hConsoleOutput:             dword;

        cCharacter:                 char;

        nLength:                    dword;

        dwWriteCoord:               COORD;

        var lpNumberOfAttrsWritten: dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FillConsoleOutputCharacterA@20" );

The w.FillConsoleOutputCharacter function fills a sequence of character cells on the console with a single character value. This function, for example, is useful for clearing a portion of the screen by writing a sequence of space characters (or some other background fill character). This function does not affect the attribute values at each character cell location; therefore, the characters that this function writes to the console will retain the foreground/background colors of the previous characters in those same character cell positions.

This function writes nLength copies of the character cCharacter to the console screen buffer specified by hConsoleOutput starting at the (x,y) coordinate position specified by dwWriteCoord. If nLength is sufficiently large that it will write characters beyond the end of a line, w.FillConsoleOutputCharacter will wrap around to the beginning of the next line. If w.FillConsoleOutputCharacter would write characters beyond the end of the screen buffer, then this function ignores the write request for those characters beyond the buffer's end.

6.7.2.5: w.FlushConsoleInputBuffer

static

    FlushConsoleInputBuffer: procedure

    (

        hConsoleInput:  dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FlushConsoleInputBuffer@4" );

A call to this function flushes all the input events from the input buffer. Input events include key presses, mouse events, and window resize events. Most console programs ignore mouse and resize events; such programs would call this function to flush any keystrokes from the type ahead buffer.

The hConsoleInput handle value is the handle to the console's input buffer. You may obtain the current console input buffer's handle using the API call w.GetStdHandle(w.STD_INPUT_HANDLE);

6.7.2.6: w.FreeConsole

static

    FreeConsole: procedure;

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FreeConsole@0" );

This API call frees (and detaches) the console window associated with the current process (this also closes that console window). Because you can only have one console window associated with an application, you must call this function if you want to create a new window via a w.AllocConsole call if the application already has a console window associated with it. Because Windows automatically frees the console window when an application terminates and you can only have one console window active at a time, few programs actually call this function.

6.7.2.7: w.GetConsoleCursorInfo

static

    GetConsoleCursorInfo: procedure

    (

            hConsoleOutput:         dword;

        var lpConsoleCursorInfo:    CONSOLE_CURSOR_INFO

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetConsoleCursorInfo@8" );

type

    CONSOLE_CURSOR_INFO:

        record

            dwSize: dword;

            bVisible: dword;

        endrecord;

This function returns the size and visibility of the console cursor in a w.CONSOLE_CURSOR_INFO data structure. The dwSize field of the record is a value in the range 1..100 and represents the percentage of the character cell that the cursor fills. This ranges from an underline at the bottom of the character cell to filling up the entire character cell (at 100%). The hConsoleOutput parameter must be the handle of the console's output buffer.

6.7.2.8: w.GetConsoleScreenBufferInfo

static

    GetConsoleScreenBufferInfo: procedure

    (

            handle: dword;

        var csbi:   CONSOLE_SCREEN_BUFFER_INFO

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetConsoleScreenBufferInfo@8" );

const

    FOREGROUND_BLUE := $1;

    FOREGROUND_GREEN := $2;

    FOREGROUND_RED := $4;

    FOREGROUND_INTENSITY := $8;

    BACKGROUND_BLUE := $10;

    BACKGROUND_GREEN := $20;

    BACKGROUND_RED := $40;

    BACKGROUND_INTENSITY := $80;

type

    CONSOLE_SCREEN_BUFFER_INFO:

        record

            dwSize: COORD;

            dwCursorPosition: COORD;

            wAttributes: word;

            srWindow: SMALL_RECT;

            dwMaximumWindowSize: COORD;

        endrecord;

    SMALL_RECT:

        record

            Left: word;

            Top: word;

            Right: word;

            Bottom: word;

        endrecord;

    COORD:

        record

            x: word;

            y: word;

        endrecord;

w.GetConsoleScreenBufferInfo retrieves information about the size of the screen buffer, the position of the cursor in the window, and the scrollable region of the console window. The handle parameter is the handle of the console screen buffer, the csbi parameter is where this function returns its data.

The dwSize field in the w.CONSOLE_SCREEN_BUFFER data structure specifies the size of the console screen buffer. Remember, these values specify the size of the buffer, not the physical size of the console window. Usually, the buffer is larger than the physical window. The dwSize.x field specifies the width of the screen buffer, the dwSize.y field specifies the height of the screen buffer. These values are in character cell coordinates.

The dwCursorPosition field specifies the (x,y) coordinate of the cursor within the screen buffer. Remember, this is the position of the cursor within the screen buffer, not in the console window.

The wAttribute field specifies the default foreground and background colors that Windows will use when you write a character to the display. This field may contain any combination of the w.FOREGROUND_xxx and w.BACKGROUND_xxx character constants. See the discussion of output attributes in the section on w.FillConsoleOutputAttribute for more details.

The srWindow field is a pair of coordinates that specify the position of the upper left hand corner and the lower right hand corner of the display window within the screen buffer. Note that you can scroll the screen buffer through the display window or even resize the display window by changing the srWindow coordinate values and calling w.SetConsoleScreenBufferInfo.

The dwMaximumWindowSize field specifies the maximum size of the console display window based on the current font in use, the physical screen size, and the screen buffer size.

6.7.2.9: w.GetConsoleTitle

static

    GetConsoleTitle: procedure

    (

        var lpConsoleTitle: var;

            nSize:          dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetConsoleTitleA@8" );

The w.GetConsoleTitle function returns a string containing the name found in the console window's title bar. The lpConsoletitle parameter must be the address of a buffer that will receive a zero-terminated string holding the window's title. The nSize parameter specifies the maximum number of characters (including the zero byte) that w.GetConsoleTitle will transfer. This function returns the actual length of the string (not counting the zero-terminating byte) in the EAX register.

There are two things to note about the lpConsoleTitle parameter. First of all, this is not the address of an HLA string, but the address of a buffer that will receive a zero-terminated string. Second, this is an untyped reference parameter, so HLA will automatically compute the address of whatever object you pass as this parameter. So if you pass a pointer variable (e.g., a string object), HLA will compute the address of the pointer, not the address of the buffer that the pointer references. You may override this behavior by using the VAL keyword to tell HLA to use the value of the pointer rather than its address. If you want this function to fill in an HLA string variable, you could use code like the following:

    mov( stringToUse, edx );

    w.GetConsoleTitle( [edx], (type str.strRec [edx]).MaxStrLen ) );

    mov( stringToUse, edx ); // Windows' API calls don't preserve EDX!

    mov( eax, (type str.strRec [edx]).length );

This code loads EDX with the address of the first byte of the character data buffer in the string referenced by stringToUse. This code passes the address of this character buffer in EDX to the function (brackets are necessary around EDX to tell HLA that this is a memory address rather than just a register value). This function passes the string's maximum length field as the buffer size to the w.GetConsoleTitle function.

On return, this function stores the actual string length (returned in EAX) into the HLA string's string length field. The end result is that stringToUse now contains valid HLA string data (the console window's title string).

6.7.2.10: w.GetConsoleWindow

static

    GetConsoleWindow: procedure;

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetConsoleWindow@0" );

The w.GetConsoleWindow function returns the window handle associated with the application's console window. Note that there is a big difference between a console screen buffer handle and a console window handle. The window handle is the same type of handle that GUI apps use when painting to the screen. Screen buffer handles are the handles you pass to console function to manipulate data in the screen buffer.

6.7.2.11: w.GetNumberOfConsoleInputEvents

static

    GetNumberOfConsoleInputEvents: procedure

    (

            hConsoleInput:      dword;

        var lpcNumberOfEvents:  dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetNumberOfConsoleInputEvents@8" );

The w.GetNumberOfConsoleInputEvents returns the total number of pending keystroke, mouse, or resize input events sent to the console window. This function stores the count at the address specified by the lpcNumberOfEvents parameter (note that this function returns success/failure in EAX, it does not return the count in EAX). There is, unfortunately, no way to directly check to see if there are any keystrokes sitting in the input queue. However, if w.GetNumberOfConsoleInputEvents tells you that there are pending input events, you can use w.PeekConsoleInput to determine if the input event at the front of the queue is keystroke waiting to be read. If not, you can call w.ReadConsolInput to remove that first input event from the queue. You can repeat the getnumber/peek/read sequence until you empty the input buffer or you peek at a keyboard event. By this processes, you can determine if there is at least one keyboard input event in the queue.

6.7.2.12: w.GetStdHandle

static

    GetStdHandle: procedure

    (

        nStdHandle:dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetStdHandle@4" );

const

    STD_INPUT_HANDLE := -10;

    STD_OUTPUT_HANDLE := -11;

    STD_ERROR_HANDLE := -12;

This function returns the current handle for the standard input, standard output, or standard error devices. The single parameter (nStdHandle) is one of the values w.STD_INPUT_HANDLE, w.STD_OUTPUT_HANDLE, or w.STD_ERROR_HANDLE, depending on which of these functions you want to retrieve. By default (meaning you've not redirected the standard input, standard output, or standard error devices), the standard output and standard error handles will be the same as the output screen buffer handle. The standard input, by default, will be the handle of the console's input buffer.

6.7.2.13: w.PeekConsoleInput

static

    PeekConsoleInput: procedure

    (

            hConsoleInput:          dword;

        var lpBuffer:               INPUT_RECORD;

            nLength:                dword;

        var lpNumberOfEventsRead:   dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PeekConsoleInputA@16" );

const

    KEY_EVENT := $1;

    mouse_eventC := $2;

    WINDOW_BUFFER_SIZE_EVENT := $4;

    MENU_EVENT := $8;

    FOCUS_EVENT := $10;

type

    INPUT_RECORD:

        record

            EventType:word;

            Event:

                union

                    KeyEvent                :KEY_EVENT_RECORD;

                    MouseEvent              :MOUSE_EVENT_RECORD;

                    WindowBufferSizeEvent   :WINDOW_BUFFER_SIZE_RECORD;

                    FocusEvent              :FOCUS_EVENT_RECORD;

                endunion;

        endrecord;

    KEY_EVENT_RECORD:

        record

            bKeyDown: dword;

            wRepeatCount: word;

            wVirtualKeyCode: word;

            wVirtualScanCode: word;

            _uChar: CHARTYPE;

            dwControlKeyState: dword;

        endrecord;

    MOUSE_EVENT_RECORD:

        record

            dwMousePosition: COORD;

            dwButtonState: dword;

            dwControlKeyState: dword;

            dwEventFlags: dword;

        endrecord;

     WINDOW_BUFFER_SIZE_RECORD:

        record

            dwSize: COORD;

       endrecord;

    FOCUS_EVENT_RECORD:

        record

            bSetFocus: dword;

        endrecord;

The w.PeekConsoleInput function reads one or more input records (if available) from the console's input queue without removing those records from the input queue. You may use the w.GetNumberOfConsoleInputEvents to determine how many pending events there are, allocate an array of w.INPUT_RECORD objects with the number of elements returned by w.GetNumberOfConsoleInputEvents, and then call w.PeekConsoleInput to read all of those events into the array you've allocated. Then you can scan through the array at your leisure to determine what type of input records are in the buffer.

The hConsoleInput parameter is the handle of the console input buffer you want to peek at. The lpBuffer parameter is a pointer to one or more w.INPUT_RECORD objects that will hold the returned records; note that you may specify a pointer, a scalar variable, or an array variable here, HLA will do the right thing with your parameter. The nLength parameter specifies the number of array elements you're passing via the lpBuffer parameter. If you're only passing a scalar w.INPUT_BUFFER object as the lpBuffer parameter, you should specify an nLength value of one. Note that the nLength parameter specifies the maximum number of input records that w.PeekConsoleInput will read; if there are fewer records than this number specifies, w.PeekConsoleInput will only read those records that are available. The lpNumberOfEventsRead parameter is a pointer to a 32-bit integer variable where w.PeekConsoleInput will store the actual number of events read from the input queue. This value will always be less than or equal to the value you pass in nLength. Note that it is perfectly possible (and reasonable) for w.PeekConsoleInput to read zero input records (if there are no pending input records) and return zero in the variable referenced by lpNumberOfEventsRead.

The lpBuffer parameter to w.PeekConsoleInput must contain the address of a w.INPUT_EVENT record. The first field of this record is the EventType field that specifies the type of the event that the record holds. This field will contain one of the following values: w.KEY_EVENT, w.mouse_eventC, w.WINDOW_BUFFER_SIZE_EVENT, w.MENU_EVENT, or w.FOCUS_EVENT. Yes, w.mouse_eventC is spelled funny and doesn't match the Window's documentation. This is one of those rare cases where Windows reused an identifier by simply changing the case of the identifier, thus creating a case conflict in HLA. The solution was to rename Windows's constant MOUSE_EVENT as w.mouse_eventC. The value of EventType determines whether the Event union's data is a w.KEY_EVENT_RECORD, w.MOUSE_EVENT_RECORD, w.WINDOW_BUFFER_SIZE_RECORD, or a w.FOCUS_EVENT_RECORD type.

If Event contains the value w.KEY_EVENT then the KeyEvent field of the w.INPUT_RECORD union contains data representing a keyboard event. The fields of the w.KEY_EVENT_RECORD data structure have the following meanings:

  • bKeyDown contains true (non-zero value) if the key was just pressed, false (0) if it was just released.
  • wRepeatCount may contain a "repeat count" for the key saying that the key auto-repeated this many times. You must treat this as "n" keypresses of the current key code.
  • wVirtualKeyCode specifies a "Windows virtual keycode" for this particular keypress. See the appendices for a list of the Windows virtual keycodes.
  • wVirtualScanCode specifies an OEM/Hardware dependent scan code for this particular keystroke. Most applications should ignore this value.
  • uChar is the translated ASCII/ANSI character code for this keystroke. Keystrokes that do not have ANSI character code equivalents generally return zero here and you have to use the wVirtualKeyCode field to determine which key was pressed (or released).
  • dwControlKeyState reports the state of the modifier keys when this key event occurred. This field is a bit map with a set bit indicating that a particular modifier key (e.g., control or shift) was held down. Here are the values associated with the various modifier keys on a PC's keyboard:
    w.RIGHT_ALT_PRESSED									$1

    w.LEFT_ALT_PRESSED									$2

    w.RIGHT_CTRL_PRESSED									$4

    w.LEFT_CTRL_PRESSED									$8

    w.SHIFT_PRESSED									$10

    w.NUMLOCK_ON									$20

    w.SCROLLLOCK_ON									$40

    w.CAPSLOCK_ON									$80

    w.ENHANCED_KEY									$100

The w.ENHANCED_KEY modifier specifies that the user has pressed an enhanced key on a keyboard. Enhanced keys are the INS, DEL, HOME, END, PAGEUP, PAGE DN, and arrow keys that are separate from the numeric keypad. The slash (/) and ENTER keys on the numeric keypad are also enhanced keys.

If Event contains the value w.MOUSE_EVENT then the KeyEvent field of the w.INPUT_RECORD union contains data representing a mouse event. The fields of the w.MOUSE_EVENT_RECORD data structure have the following meanings:

  • dwMousePosition specifies the location of the mouse cursor in screen buffer character cell coordinates. Note that the Y-coordinate appears in the H.O. word and the X-coordinate appears in the L.O. word of the w.COORD data type.
  • dwButtonState is a bitmap specifying the state of the buttons on the mouse. The w.hhf header files defines the following constants specifying button positions with in the bitmap:
    w.FROM_LEFT_1ST_BUTTON_PRESSED												$1

    w.RIGHTMOST_BUTTON_PRESSED												$2

    w.FROM_LEFT_2ND_BUTTON_PRESSED												$4

    w.FROM_LEFT_3RD_BUTTON_PRESSED												$8

    w.FROM_LEFT_4TH_BUTTON_PRESSED												$10
If a mouse supports more than five buttons, then the mouse driver will, undoubtedly, set additional H.O. bits in this bitmap.
  • dwControlKeyState is a bitmap specifying the current state of the modifier keys on the keyboard when this mouse event occurred. See the description of dwControlKeyState in the description of w.KEY_EVENT_RECORD for more details.
  • dwEventFlags specifies the type of mouse event that has occurred. If this field contains zero, it means that a mouse button has been pressed or released. If this field is not zero, it contains one of the following values:
    w.MOUSE_MOVED							$1

    w.DOUBLE_CLICK							$2

Console applications should ignore w.WINDOW_BUFFER_SIZE_RECORD and w.FOCUS_EVENT as these are intended for internal use by Windows.

6.7.2.14: w.ReadConsole

static

    ReadConsole: procedure

    (

            hConsoleInput:          dword;

        var lpBuffer:               var;

            nNumberOfCharsToRead:   dword;

        var lpNumberOfCharsRead:    dword;

        var lpReserved:             var

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ReadConsoleA@20" );

The w.ReadConsole function reads keyboard events from the console input buffer. The hConsoleInput parameter specifies the console input buffer to read from (this should be the standard input handle). The lpBuffer parameter is the address of a character array (buffer) that will receive the characters read from the console input buffer. The nNumberOfCharsToRead parameter specifies the number of keystrokes that will be read from the input buffer. By default, w.ReadConsole will return when the user types this many characters at the keyboard or when the user presses the enter key (prior to typing this many characters). Please see the Windows' documentation for details on the w.GetConsoleMode and w.SetConsoleMode functions that let you specify Windows' behavior with respect to console input. the lpNumberOfCharactersRead is a pointer to an integer variable; Windows will store the actual number of characters read from the keyboard into this integer variable. The lpReserved field is reserved and you must pass NULL in this parameter.

If the input buffer contains records other than keyboard events, w.ReadConsole removes those events from the buffer and ignores them. If you want to be able to read mouse events, use w.ReadConsoleInput instead of w.ReadConsole. Also note that w.ReadConsole is virtually the same as the w.ReadFile function. The "pure" Windows version of w.ReadConsole allows you to read Unicode characters, but the HLA prototype for w.ReadConsole only reads ANSI characters (there is nothing stopping you from creating your own prototype to call the actual Windows ReadConsole API function, though).

6.7.2.15: w.ReadConsoleInput

static

    ReadConsoleInput: procedure

    (

            hConsoleInput:          dword;

        var lpBuffer:               INPUT_RECORD;

            nLength:                dword;

        var lpNumberOfEventsRead:   dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ReadConsoleInputA@16" );

This function reads console input records from the console input buffer. This reads both keyboard and mouse events and stores them into a w.INPUT_RECORD array whose address you pass to this function.

The hConsoleInput parameter is the console input handle (e.g., the standard input handle). The lpBuffer parameter is the address of a w.INPUT_RECORD structure or an array of these structures (see the description of this data structure in the section on w.PeekConsoleInput). The nLength parameter specifies the number of input records to read from the console input queue. As with w.ReadConsole, Windows may actually read fewer than this number of input records if the console mode is set to return when the user presses the Enter key. In any case, w.ReadConsoleInput will store the actual number of input records at the address specified by the lpNumberOfEventsRead parameter. See the description of w.ReadConsole for more details. Also see the discussion of w.PeekConsoleInput for a discussion of the w.INPUT_RECORD type.

6.7.2.16: w.ReadConsoleOutput

static

    ReadConsoleOutput: procedure

    (

            hConsoleOutput: dword;

        var lpBuffer:       CHAR_INFO;

            dwBufferSize:   COORD;

            dwBufferCoord:  COORD;

        var lpReadRegion:   SMALL_RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ReadConsoleOutputA@20" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

    SMALL_RECT:

        record

            Left    :word;

            Top     :word;

            Right   :word;

            Bottom  :word;

         endrecord;

    CHARTYPE:

        union

            UnicodeChar :word;  // Note: HLA's prototype only returns ASCII characters.

            AsciiChar   :byte;

        endunion;

    CHAR_INFO:

        record

            CharVal    :CHARTYPE;

            Attributes :word;

        endrecord;

The w.ReadConsoleOutput function reads a rectangular region off the screen and stores the character data into a two-dimensional array of characters. The hConsoleOutput parameter is the handle of the console's output screen buffer (i.e., the standard output handle). The lpBuffer parameter is a pointer to a two-dimensional array of characters that will receive the character data read from the screen buffer. The dwBufferSize parameter specifies the size of the buffer that will receive the characters; the L.O. word (x) specifies the number of columns in the array, the H.O. word (y) specifies the number of rows in the array. The dwBufferCoord parameter specifies the (x,y) coordinates (in character cell positions) of the upper-leftmost character in the array specified by lpBuffer where w.ReadConsoleOutput will begin storing the character data. On entry, the values pointed at by the lpReadRegion parameter specify the upper left hand corner and the lower right hand corner of a rectangular region in the screen buffer to copy into the buffer specified by lpBuffer. On exit, the w.ReadConsoleOutput routine stores the actual coordinates used by the copy operation. The input and output values will be different if the input values exceeded the boundaries of the screen buffer (in which case, w.ReadConsoleOutput truncates the coordinates so that they remain within the screen buffer).

6.7.2.17: w.ReadConsoleOutputAttribute

static

    ReadConsoleOutputAttribute: procedure

    (

            hConsoleOutput:         dword;

        var lpAttribute:            word;

            nLength:                dword;

            dwReadCoord:            COORD;

        var lpNumberOfAttrsRead:    dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ReadConsoleOutputAttribute@20" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

This function is very similar to w.ReadConsoleOutput except it copies attribute values rather than characters to the lpAttribute array. The lpAttribute parameter should hold the address of a word array than contains at least nLength elements. The dwReadCoord specifies the coordinate in the screen console buffer where w.ReadConsoleOutputtAttribute begins reading attributes (the H.O. word holds the y-coordinate, the L.O. word holds the x-coordinate). The lpNumberOfAttrsRead parameter holds the address of an integer where w.ReadConsoleOutputAttribute will store the number of attribute values it reads from the screen buffer. The value this function returns will be less than nLength if the function attempts to read data beyond the end of the screen buffer.

Unlike w.ReadConsoleOutput, w.ReadConsoleOutputAttribute does not read a rectangular block of data into a two-dimensional array. Instead, this function simply reads a linear block of attributes from the screen buffer starting at the specified coordinate. If this function reaches the end of the current line and nLength specifies additional attributes to read, this function returns the attributes beginning at the start of the next line. It stops, however, at the end of the screen buffer.

6.7.2.18: w.ReadConsoleOutputCharacter

static

    ReadConsoleOutputCharacter: procedure

    (

            hConsoleOutput:         dword;

        var lpCharacter:            char;

            nLength:                dword;

            dwReadCoord:            COORD;

        var lpNumberOfCharsRead:    dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ReadConsoleOutputCharacterA@20" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

The w.ReadConsoleOutputCharacter function is like w.ReadConsoleOutput except it reads a linear sequence of characters from the screen buffer (like w.ReadConsoleOutputAttribute reads attributes) from the buffer rather than reading a rectangular block of characters. The hConsoleOutput parameter specifies the handle for the screen output buffer (i.e., the standard output handle). The lpCharacter parameter holds the address of the buffer to receive the characters; it should have room for at least nLength characters (the third parameter, that specifies the number of characters to read). The dwReadCoord parameter specifies the character cell coordinate in the screen buffer where this function begins reading characters. This function returns the number of characters actually read in the integer whose address you pass in the lpNumberOfCharsRead parameter; this number will match nLength unless you attempt to read beyond the end of the screen buffer.

6.7.2.19: w.ScrollConsoleScreenBuffer

static

    ScrollConsoleScreenBuffer: procedure

    (

            hConsoleOutput:         dword;

        var lpScrollRectangle:      SMALL_RECT;

        var lpClipRectangle:        SMALL_RECT;

            dwDestinationOrigin:    COORD;

        var lpFill:                 CHAR_INFO

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ScrollConsoleScreenBufferA@20" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

    CHARTYPE:

        union

            UnicodeChar :word;  // Note: HLA's prototype only returns ASCII characters.

            AsciiChar   :byte;

        endunion;

    CHAR_INFO:

        record

            CharVal    :CHARTYPE;

            Attributes :word;

        endrecord;

    SMALL_RECT:

        record

            Left    :word;

            Top     :word;

            Right   :word;

            Bottom  :word;

         endrecord;

The w.ScrollConsoleScreenBuffer function scrolls a rectangular region of a screen buffer. This function achieves this by moving a source rectangular region of the screen buffer to some other point in the screen buffer. The lpScrollRectangle parameter specifies the source rectangle (via the coordinates of the upper left hand and lower right hand corners of the rectangle). The dwDestinationOrigin parameter specifies the upper left hand corner of the target location where this function will move the source rectangle. Any locations in the source rectangle that do not overlap with the source rectangle wind up getting filled with the character and attribute specified by the lpFill parameter. The lpClipRectangle parameter specifies a "clipping rectangle". Actual scrolling only takes place within this clipping rectangle. For example, if you wanted to scroll the screen up one line, you would set the lpScrollRectangle parameter to encompass the whole screen (say, 80x25), set the destination origin to (0,-1), and then set the lpClipRectangle to the same values as the lpScrollRectangle value. You may also pass NULL as the value for lpClipRectangle, in which case w.ScrollConsoleScreenBuffer assumes that the clip region is equal to the source region (lpScrollRectangle).

6.7.2.20: w.SetConsoleActiveScreenBuffer

static

    SetConsoleActiveScreenBuffer: procedure

    (

        hConsoleOutput: dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetConsoleActiveScreenBuffer@4" );

This function displays the screen buffer whose handle you pass as the parameter in the console window. Although an application may have only one console window, it may have multiple screen buffers that it can display in that window. When the program first begins execution, the handle associated with the default console screen buffer is the standard output handle. You may create new screen buffers (and obtain their handles) via the w.CreateConsoleScreenBuffer call and then you can switch to these screen buffers using the w.SetConsoleActiveScreenBuffer API invocation.

6.7.2.21: w.SetConsoleCursorInfo

static

    SetConsoleCursorInfo: procedure

    (

            hConsoleOutput:         dword;

        var lpConsoleCursorInfo:    CONSOLE_CURSOR_INFO

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetConsoleCursorInfo@8" );

type

    CONSOLE_CURSOR_INFO:

        record

            dwSize    :dword;

            bVisible  :dword;

        endrecord;

The w.SetConsoleCursorInfo function lets you specify the size of the cursor in the console window and whether the cursor is visible or invisible. The hConsoleOutput parameter is the screen buffer handle of the screen buffer to which you wish the operation to apply. The lpConsoleCursorInfo parameter is a pointer to a w.CONSOLE_CURSOR_INFO data structure whose two fields (dwSize and bVisible) specify the size of the cursor and whether or not it is visible. The dwSize field must contain a value between 1 and 100 and specifies the percentage of a character cell that the cursor will fill. The cursor fills the cell from the bottom of the cell to the top, so a "1" would produce a cursor that is just an underline, "100" would produce a cursor that completely fills the character cell, and "50" would produce a cursor that fills the bottom half of the character cell. The bVisible field specifies whether the cursor will be visible in the console window. If this field contains true, then the cursor will be visible, if it contains false then Windows will not display the cursor.

6.7.2.22: w.SetConsoleCursorPosition

static

    SetConsoleCursorPosition: procedure

    (

        hConsoleOutput:     dword;

        dwCursorPosition:   COORD

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetConsoleCursorPosition@8" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

This function positions the cursor at the coordinate specified by the dwCursorPosition parameter. The hConsoleOutput parameter specifies the handle of the console screen buffer to which the cursor movement operation applies. If this function moves the cursor to a point in the screen buffer that is not displayed in the console window, then Windows will adjust the window to make the character cell specified by dwCursorPosition visible in the window.

6.7.2.23: w.SetConsoleScreenBufferSize

static

    SetConsoleScreenBufferSize: procedure

    (

        hConsoleOutput: dword;

        dwSize:         COORD

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetConsoleScreenBufferSize@8" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

The w.SetConsoleScreenBufferSize sets the size of the screen buffer. The hConsoleOutput parameter is the handle of the screen buffer whose size you want to change, the dwSize parameter specifies the new size of the buffer (the dwSize.x field specifies the new width, the dwSize.y field specifies the new height). This function fails if the screen buffer size (in either dimension) is smaller than the size of the console window or less than the minimum specified by the system (see the w.GetSystemMetrics call to find the minimally permissible size).

6.7.2.24: w.SetConsoleTextAttribute

static

    SetConsoleTextAttribute: procedure

    (

        hConsoleOutput: dword;

        wAttributes:    word

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetConsoleTextAttribute@8" );

const

    FOREGROUND_BLUE := $1;

    FOREGROUND_GREEN := $2;

    FOREGROUND_RED := $4;

    FOREGROUND_INTENSITY := $8;

    BACKGROUND_BLUE := $10;

    BACKGROUND_GREEN := $20;

    BACKGROUND_RED := $40;

    BACKGROUND_INTENSITY := $80;

The w.SetConsoleTextAttribute function lets you specify the default text attribute that Windows will use when writing text to the console window (e.g., via the HLA Standard Library "stdout.put" call). The hConsoleOutput parameter is the handle of the screen buffer whose default attribute you wish to set; the wAttributes parameter is the attribute value. This is a bitmap of color values created by combining the w.FOREGROUND_xxx and w.BACKGROUND_xxx values, e.g.,

w.SetConsoleTextAttribute( hndl, w.FOREGROUND_RED | w.BACKGROUND_BLUE );

6.7.2.25: w.SetConsoleTitle

static

    SetConsoleTitle: procedure

    (

        lpConsoleTitle: string

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetConsoleTitleA@4" );

The w.SetConsoleTitle function copies the string you pass as a parameter (lpConsoleTitle) to the title bar of the console window.

6.7.2.26: w.SetConsoleWindowInfo

static

    SetConsoleWindowInfo: procedure

    (

            hConsoleOutput:     dword;

            bAbsolute:          dword;

        var lpConsoleWindow:    SMALL_RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetConsoleWindowInfo@12" );

type

    SMALL_RECT:

        record

            Left    :word;

            Top     :word;

            Right   :word;

            Bottom  :word;

         endrecord;

The w.SetConsoleWindowInfo function sets the size and position of the console's window. The hConsoleOutput parameter specifies the handle of a screen buffer (to which this operation applies). The bAbsolute parameter is a boolean value (true/1 or false/0) that specifies whether the coordinates you're supplying are relative to the current coordinates of the window (that is, relative to the current upper left hand corner of the window within the screen buffer) or absolute (specifies the coordinates of the upper left hand corner of the window within the screen buffer). If bAbsolute is true, then the coordinates are absolute and specify a character cell position within the screen buffer for the upper-left hand corner of the window. Thee lpConsoleWindow parameter is the address of a w.SMALL_RECT structure that specifies the new origin and size of the console display window. This function fails if the new window size would exceed the boundaries of the screen buffer.

You may use this function to scroll through vertically the screen buffer by adjusting the Top and Bottom fields of the w.SMALL_RECT record by the same amount. Similarly, you can use this function to scroll horizontally through the screen buffer by adjusting the Left and Right fields by the same amount.

6.7.2.27: w.SetStdHandle

static

    SetStdHandle: procedure

    (

        nStdHandle: dword;

        hHandle:    dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetStdHandle@8" );

const

    STD_INPUT_HANDLE := 10;

    STD_OUTPUT_HANDLE := 11;

    STD_ERROR_HANDLE := 12;

This function redirects the standard input, standard output, or standard error devices to a handle you specify. The first parameter is one of the constant values w.STD_INPUT_HANDLE, w.STD_OUTPUT_HANDLE, or w.STD_ERROR_HANDLE that specifies which of the standard handles you want to redirect. The hHandle parameter is a file handle that specifies where you want the output redirected. For example, you may redirect the standard output to a specific console screen buffer by specifying that screen output buffer's handle.

6.7.2.28: w.WriteConsole

static

    WriteConsole: procedure

    (

            hConsoleOutput:         dword;

        var lpBuffer:               var;

            nNumberOfCharsToWrite:  dword;

        var lpNumberOfCharsWritten: dword;

        var lpReserved:             var

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__WriteConsoleA@20" );

The w.WriteConsole function writes a string to the console screen buffer specified by the hConsoleOutput handle. The lpBuffer parameter specifies the address of the character data to write to the console buffer. The nNumberOfCharsToWrite parameter specifies the character count. This function returns the actual number of characters written in the integer variable whose address you pass in the lpNumberOfCharsWritten parameter. The lpReserved parameter is reserved and you must pass NULL for its value.

Note that Windows actually provides two versions of this function - one for ASCII characters and one for Unicode characters. The HLA prototype specifies the ASCII version. If you need to call the Unicode version, you will need to create your own prototype for that function. Because the HLA version calls the ASCII version, this function is virtually identical to a w.WriteFile call (e.g., "stdout.put").

6.7.2.29: w.WriteConsoleInput

static

    WriteConsoleInput: procedure

    (

            hConsoleInput:              dword;

        var lpBuffer:                   INPUT_RECORD;

            nLength:                    dword;

        var lpNumberOfEventsWritten:    dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__WriteConsoleInputA@16" );

The w.WriteConsoleInput function writes one or more w.INPUT_RECORD structures to the console's input buffer. This function places any input records written at the end of the buffer, behind any pending input events. You may use this function to simulate keyboard or mouse input.

The hConsoleInput parameter specifies the handle of the console input buffer. You could, for example, supply the handle of the standard input device here. The lpBuffer parameter is a pointer to an array of one or more w.INPUT_RECORD objects (please see the discussion of input records in the section on the w.ReadConsoleInput function). The nLength parameter is the number of input records in the array that lpBuffer points at that you want this function to process. This function stores the number of input records processed in the integer variable whose address you pass in the lpNumberOfEventsWritten record.

6.7.2.30: w.WriteConsoleOutput

static

    WriteConsoleOutput: procedure

    (

            hConsoleOutput: dword;

        var lpBuffer:       CHAR_INFO;

            dwBufferSize:   COORD;

            dwBufferCoord:  COORD;

        VAR lpWriteRegion:  SMALL_RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__WriteConsoleOutputA@20" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

    SMALL_RECT:

        record

            Left    :word;

            Top     :word;

            Right   :word;

            Bottom  :word;

         endrecord;

    CHARTYPE:

        union

            UnicodeChar :word;  // Note: HLA's prototype only returns ASCII characters.

            AsciiChar   :byte;

        endunion;

    CHAR_INFO:

        record

            CharVal    :CHARTYPE;

            Attributes :word;

        endrecord;

The w.WriteConsoleOutput function writes a rectangular block of character and attribute data to a screen buffer. The hConsoleOutput parameter is the handle of the screen buffer where this function is to write the data. The lpBuffer parameter is the address of an array of character/attribute values that this function is to write to the screen. Note that each character/attribute element in lpBuffer consumes 32-bits (16 bits for the attribute and 16 bits for the character). The dwBufferSize parameter specifies the of the character/attribute data at which lpBuffer points; the dwBufferSize.x field specifies the number of columns, the dwBufferSize.y field specifies the number of rows of character/attribute data in the buffer area. Note that this parameter specifies the size of the source buffer, not the size of the data to write; the actual data written may be a subset of this two-dimensional array. The dwBufferCoord parameter specifies the coordinate of the upper-left hand corner of the rectangular area to write within the source buffer. The lpWriteRegion parameter is the address of a w.SMALL_RECT data structure; on input, these record specifies the region of the screen buffer where Windows is to write the data from the source buffer. When Windows returns, it writes the size of the actual rectangle written into this record structure (the actual rectangle may be smaller if the source rectangle's size violates the boundaries of the screen buffer).

6.7.2.31: w.WriteConsoleAttribute

static

    WriteConsoleOutputAttribute: procedure

    (

            hConsoleOutput:         dword;

        var lpAttribute:            word;

            nLength:                dword;

            dwWriteCoord:           COORD;

        var lpNumberOfAttrsWritten: dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__WriteConsoleOutputAttribute@20" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

The w.WriteConsoleAttribute function writes a linear sequence of attribute values to the console without affecting the character data at the cell positions where it writes those attributes. The hConsoleOutput parameter specifies the handle of a console screen buffer. The lpAttribute parameter is the address of an array of one or more 16-bit attribute values. The nLength parameter specifies the number of attribute values to write to the screen. The dwWriteCoord parameter specifies the (x,y) coordinate of the screen buffer where this function is to begin writing the attribute values (the dwWriteCoord.x field appears in the L.O. word and the dwWriteCoord.y field appears in the H.O. word of this double word parameter value). The lpNumberOfAttrsWritten parameter is the address of an integer variable that will receive the number of attributes actually written to the screen buffer. This number will be less than nLength if the function attempts to write data beyond the end of the screen buffer.

6.7.2.32: w.WriteConsoleOutputCharacter

static

    WriteConsoleOutputCharacter: procedure

    (

            hConsoleOutput:         dword;

            lpCharacter:            string;

            nLength:                dword;

            dwWriteCoord:           COORD;

        var lpNumberOfCharsWritten: dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__WriteConsoleOutputCharacterA@20" );

type

    COORD:

        record

            x: word;

            y: word;

        endrecord;

The w.WriteConsoleOutputCharacter function writes a linear sequence of attribute values to the console without affecting the character data at the cell positions where it writes those attributes. The hConsoleOutput parameter specifies the handle of a console screen buffer. The lpCharacter parameter is the address of an array of one or more character values. The nLength parameter specifies the number of character values to write to the screen. The dwWriteCoord parameter specifies the (x,y) coordinate of the screen buffer where this function is to begin writing the character values (the dwWriteCoord.x field appears in the L.O. word and the dwWriteCoord.y field appears in the H.O. word of this double word parameter value). The lpNumberOfCharsWritten parameter is the address of an integer variable that will receive the number of attributes actually written to the screen buffer. This number will be less than nLength if the function attempts to write data beyond the end of the screen buffer.

Note that Windows actually provides two versions of this function, one for Unicode and one for ASCII character data. The HLA Standard Library function prototypes the ASCII version. If you need the Unicode version, you can easily prototype that function yourself.

6.7.2.33: Plus More!

There are many, many, additional console related functions beyond the more common ones presented here. If you're interested in high-performance console application programming under Windows, you'll want to read the API documentation appearing on the accompanying CD-ROM or read up on console I/O on Microsoft's MSDN system.

6.7.3: HLA Standard Library Console Support

The HLA Standard Library provides a small console library module that simulates an ANSI terminal. Although this set of library functions provides but a small set of available Win32 console features, it is slightly easier to use for the more common operations (like cursor positioning, clearing portions of the screen, and things like that). Another advantage to the HLA Standard Library Console module is that it is portable - console applications that use this console module will compile and run, unchanged, under Linux. However, because this is a book on Windows programming, we won't bother exploring this portability issue any farther.

There is also a set of obsolete console library functions available with in the HLA Examples code (available on the accompanying CD-ROM or via download at http://webster.cs.ucr.edu). This set of library routines provided a much stronger set of console capabilities than the current HLA Standard Library (at lot of functionality was removed in order to achieve portability to Linux). These older functions are mostly wrappers around Win32 console API calls that make them more convenient to use. They're also great examples of calls to the Win32 API functions. The major drawback to this source code is that it is quite old - hailing from the days when HLA was missing several important features that make interfacing with Windows a lot easier; so the source code makes the calls to the Win32 API functions in some peculiar ways.

6.8: This Isn't the Final Word!

There is quite a bit more to text processing under Windows than a even a long chapter like this one can present. However, most people who are reading this text probably want to learn how to write GUI applications involving graphics, so it's time to set the discussion of text down for a while and start looking at how to do graphics in a Windows environment.

1For details on the foreach loop and iterators, please consult the HLA documentation or The Art of Assembly Language.
2See the Windows API documentation on the accompanying CD-ROM for details about the w.GetScrollInfo function. We will not discuss that function here.
3Windows also allows you to pass a small integer value, known as an atom, as this parameter's value. We will not consider atoms here.
4Actually, we'll see another big problem with w.PostMessage in the next section. But for most purposes, w.PostMessage is just fine if the application calling w.PostMessage can live with lost messages.
5The hwnd parameter is the application's window handle. dbg.put will need this value to pass on to the DebugWindow process via the wParam parameter of the w.SendMessage API function.
6It turns out, we have to do this anyway, so this is a trivial modification to make to the program.
7If you attempt to print more than 4084 characters with a single dbg.put macro, HLA will raise a string overflow exception.
TOC PREV NEXT INDEX


Chapter 7: Graphics

7.1: Graphics in a GUI World

In the last chapter we explored how to display text on the GUI display. Although text is unquestionably important in modern GUI applications, the first word in the phrase "graphical user interface" is graphical, not textual. Therefore, most people are probably a lot more interested in learning about how to do graphics under Windows rather than text. As it turns out, however, most GUI applications actually deal more with Windows' controls and text objects rather than pure graphical objects. Nevertheless, knowing how to draw graphical objects in a window is an critical skill to master. In this chapter we'll look at the facilities Windows provides for drawing graphical images on the screen.

7.2: Types of Graphic Objects You Can Draw in Windows

Windows attempts to present a device-independent view of output devices to your application software. This means that, to your programmer, a 24-bit video display card looks just like a printer, which looks just like a plotter, which looks just like a film recorder. Well, sort of. Though there are some very real-world differences between these types of output devices, differences of which your applications must be aware, for the most part you draw on the video display exactly the same way you "draw" on a printer. Indeed, in this chapter we will explore how to display graphic images on both a printer and a video display.

Before describing the types of images you can draw on these various devices, it is probably worthwhile to point out that not all Windows output APIs are so universal. For example, game and multimedia applications that use Microsoft's Direct-X subsystem use different mechanisms for displaying their information; these mechanisms are quite a bit different than the ones we'll explore in this chapter (and higher performance), but the drawback is that you cannot "print" such output to a printer device (though it may be perfectly possible to print such data to a video recorder device). So Windows does provide some special API functions for high-performance I/O devices (e.g., video input and output) that don't follow the standard GDI (Graphical Device Interface) model, but most Win32 applications will use the GDI model for output.

Windows' GDI model supports the ability to draw several primitive graphic objects on the display or a printer (we'll just use the term display from now on, but keep in mind that this discussion can apply to the printer as well). These primitives include lines, polylines, curves (arcs, chords, and bezier curves), rectangles, roundangles, ellipses and circles, filled regions, bitmaps, and text. We've already beaten text to death in the last chapter, we'll take a look at these other primitives in this chapter.

7.3: Facilities That the Windows GDI Provides

In addition to drawing, the Windows GDI provides several facilities to ease the creation of complex graphic images on the display. These facilities include the ability to change coordinate systems in use by the system (for example, using metric or English units rather than pixel units for string coordinates, the abilty to record a sequence of graphic drawing operations for later playback (as a metafile), the ability to maintain an outline (region or path) around an object, the ability to restrain drawing inside a given region (clipping), and the ability to control the color and fill pattern of objects drawn on the display. You'll see many of these features in action throughout this chapter.

7.4: The Device Context

In the last chapter you saw that Windows requires you to do all drawing through a device context. When responding to a Windows w.WM_PAINT message, you'd open a context with the BeginPaint macro invocation and you'd end the drawing context with an invocation of the EndPaint macro invocation. Between those two points you could draw to your heart's content. In the last chapter, we only drew text on the screen, but you use the same sequence of operations when drawing other graphic images to the display.

The last chapter introduced you to the BeginPaint, EndPaint, GetDC, GetWindowDC, and ReleaseDC macros found in the wpa.hhf header file. You'll use those macros with all of the graphic drawing functions in this chapter as well. However, in addition to those macros/functions, there are a couple of additional routines you can use when drawing data with the graphics primitives (including text). The first of these new functions to consider are w.CreateDC and w.DeleteDC:

static

    CreateDC: procedure

    (

                lpszDriver :string;

                lpszDevice :string;

                lpszOutput :string;

        var		lpInitData :DEVMODE

);

    @stdcall;

    @returns( "eax" );

    @external( "__imp__CreateDCA@16" );

    DeleteDC: procedure

    (

        hdc :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__DeleteDC@4" );

For the w.CreateDC API call, the lpszDriver parameter should be the string "DISPLAY" or "WINSPOOL" (or the name of some other Windows print provider on your system, though this is usually "WINSPOOL"), the other three parameters should normally be NULL (see the Win32 API documentation for details, but 99% of the time you're going to use "DISPLAY" as the first parameter and NULL as the remaining three parameter values). This call returns a handle to the entire display device (meaning you can draw anywhere on the display, including over the top of other windows on the display; obviously, you should only do this under special circumstances, well-behave applications don't arbitrarily draw on the display). When you are done using the device context you've created via a w.CreateDC call, you call w.DeleteDC to free up that resource. Mainly, we'll use the w.CreateDC call to obtain the device context of a printer device, not the display device.

When creating complex bitmap objects, it's often convenient to build the bitmap in memory first, and then transfer that bitmap to the display device (this, for example, prevents the object from flashing while you are drawing it). You use the w.CreateCompatibleDC function call to do this:

static

    CreateCompatibleDC: procedure

    (

        hdc :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateCompatibleDC@4" );

The w.CreateCompatibleDC requires a single parameter - the handle of an existing device context. This function creates an in-memory duplicate of that device context. You can draw to this context (by supplying the device context handle this function returns to the drawing routines) and Windows will store the image as a bitmap in memory. Later, you can transfer this bitmap to some actual output device. When you are done using the in-memory context you've created with w.CreateCompatibleDC, you must call w.DeleteDC (passing the handle that w.CreateCompatibleDC returns in EAX) to free up the resources associated with this device context. We'll return to the use of this function a little later in this book.

As noted earlier, Windows provides the ability to record all of your graphic output requests in a special data structure known as a metafile. The w.CreateMetaFile and w.CloseMetaFile functions provide a set of brackets between which Windows will record all GDI function calls to a file whose name you specify as the parameter to the w.CreateMetaFile function. The w.CloseMetaFile function gets passed the handle that w.CreateMetaFile returns in EAX, and w.CloseMetaFile returns a handle to the metafile that you can use in other API calls that expect such a handle.

7.5: Obtaining and Using Device Context Information

A Device Context is an internal Windows data structure that maintains information about the current state of the device. As the last chapter notes, a device context maintains information likethe current font in use, the current output color, and other attributes such as line width, fill pattern, output size, and other such features. Also in the last chapter, you learned about the w.GetDeviceCaps function that displays several of the values held in the device context structure and, in fact, the previous chapter presented a simple program to display various device capability values. In this section we'll explore the uses of some of this data.

As a reminder, here is the function prototype for the w.GetDeviceCaps API function:

static

    GetDeviceCaps: procedure

    (

        hdc             :dword;

        nIndex          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetDeviceCaps@8" );

The hdc parameter is the handle of the device context that you wish to query and the nIndex parameter is a special integer constant that specifies which value you wish this function to return in the EAX register (see Table 7-1 for the constant that w.GetDeviceCaps accepts).

Table 7-1: Common GetDeviceCaps nIndex Values
Index
Value Returned in EAX
w.HORZSIZE
The horizontal size, in millimeters, of the display (or other device).
w.VERTSIZE
The vertical size, in millimeters, of the display (or other device).
w.HORZRES
The display's width, in pixels.
w.VERTRES
The display's height, in pixels.
w.LOGPIXELSX
The resolution, in pixels per inch, along the displays' X-axis.
w.LOGPIXELSY
The resolution, in pixels per inch, along the displays' Y-axis.
w.BITSPIXEL
The number of bits per pixel (specifying color information).
w.PLANES
The number of color planes.
w.NUMBRUSHES
The number of device-specific brushes available.
w.NUMPENS
The number of device-specific pens available.
w.NUMFONTS
The number of device specific fonts available.
w.NUMCOLORS
Number of entries in the device's color table.
w.ASPECTX
Relative width of a device pixel used for drawing lines.
w.ASPECTY
Relative height of a device pixel used for drawing lines.
w.ASPECTXY
Diagonal width of the device pixel used for line drawing (45 degrees).
w.CLIPCAPS
This is a flag value that indicates whether the device can clip images to a rectangle. The w.GetDeviceCaps function returns one if the device supports clipping, zero otherwise.
w.SIZEPALETTE
Number of entries in the system palette (valid only if the display device uses a palette).
w.NUMRESERVED
Number of system reserved entries in the system palette (valid only if the display device uses a palette).
w.COLORRES
Actual color resolution of the device, in bits per pixel. For device drivers that use palettes.
w.PHYSICALWIDTH
For printing devices, the physical width of the output device in whatever units that device uses.
w.PHYSICALHEIGHT
For printing devices, the physical height of the output device.
w.PHYSICALOFFSETX
For printing devices, the horizontal margin.
w.PHYSICALOFFSETY
For printing devices, the vertical margin.
w.SCALINGFACTORX
For printing devices, the scaling factor along the X-axis.
w.SCALINGFACTORY
For printing devices, the scaling factor along the Y-axis.
w.VREFRESH
For display devices only: the current vertical refresh rate of the device, in Hz.
w.DESKTOPHORZRES
Width, in pixels, of the display device. May be larger than w.HORZRES if the display supports virtual windows or more than one display.
w.DESKTOPVERTRES
Height, in pixels, of the display device. May be larger than w.VERTRES if the display supports virtual windows or more than one display.
w.BITALIGNMENT
Preferred horizontal drawing alignment. Specifies the drawing alignment, in pixels, for best drawing performance. May be zero if the hardware is accelerated or the alignment doesn't matter.

The values that w.GetDeviceCaps returns when you specify the w.HORZRES and w.VERTRES constants are the width and height of the display device in pixels. For a display device, these values specify the maximum size of a window that will fit entirely on the display (includng the non-client areas like the title and scroll bars). For printer devices, these two return values specify the maximum printing area. A Windows application can use these values, for example, to determine some default window size to use when the application is creating its first window.

The w.HORZSIZE and w.VERTSIZE return values specify the the size of the display in millimeters1. These return values are quite useful for creating WYSIWYG (What You See Is What You Get) type applications where the dimensions on the display correspond, roughly, to real life. In fact, few applications present their output on the display in real-world dimension. The problem is that the resolution of the display is a little too low to display common character sizes (e.g., 10 point fonts) with a reasonable amount of fidelity. As a result, common fonts on the screen would be difficult to read. To overcome this problem, Windows defines a couple of values you may query via w.GetDeviceCaps with the w.LOGPIXELSX and w.LOGPIXELSY constants that define the number of pixels per logical inch (25.4 millimeters). A logical inch is about 50% larger than a real world inch. That is, if you draw a line segment on the display and make it w.LOGPIXELSX long, then it will actually be about 1.5 inches long if you measure it with a ruler on the display. Do keep in mind that these values that w.GetDeviceCaps returns are approximate. Different displays will have minor differences due to dot pitch differences and, of course, the values that w.GetDeviceCaps returns are rounded to the nearest integer value.

If you are writing a program that displays graphic objects on the screen, then the values that w.GetDeviceCaps returns for w.ASPECTX, w.ASPECTY and w.ASPECTXY will be very important to you. These values specify the relative width, height, and diagonal length of pixels on the screen. What this means is that if you draw a line segment that is n pixels long along the X-axis, you would need to draw a line segment with (n*w.ASPECTY)/w.ASPECTX pixels along the Y-axis to obtain a line that is physically the same size. You would use these calculations to ensure that the shapes you draw on the screen don't come out looking elongated or squashed. For example, when drawing a square on the display, you would not draw an object with n pixels on each edge of the rectangle; doing so would likely produce a rectangle whose sides have a different length than the top and bottom line segments (that is, you'd have a proper rectangle rather than a square). However, if you use the ratio w.ASPECTX:w.ASPECTY when drawing the sides of the rectangle then the image you create will physically look like a square on the display device. Generally, you'll only adjust one of the two coordinates via this ratio. For example, if you want to create a rectangle with the equivalent of n pixels on each side, you might use calculations like the following:

xWidth = n

yHeight = (n * logPixelsY) / logPixelsX

Or you could use

xWidth = (n * logPixelsX) / logPixelsY

yHeight = n

where logPixelsX and logPixelsY represent the values that w.GetDeviceCaps returns when you pass it the w.LOGPIXELSX and w.LOGPIXELSY constants, respectively.

The w.GetDeviceCaps API call also returns some interesting information about the color capabilities of the display device. For example, when supplying the w.NUMCOLORS index, w.GetDeviceCaps will return the current number of entries in the systems' color table. This is the number of different colors that Windows will guarantee that it can display simultaneously on the screen. This is not the maximum number of colors the display can handle, it is simply the number of colors that Windows can currently display on the adapter. For those display adapters with palettes, this number corresponds to the current number of initialized palette entries. Note that w.GetDeviceCaps returns -1 when you pass w.NUMCOLORS as the index value if the display supports more than 256 colors. To compute the number of colors the display adapter is capable of displaying, you have to use the w.PLANES and w.BITSPIXEL return values. The w.PLANES value specify the number of "bit planes" (or memory banks) present on the video display adapter. On most modern video display cards, this value is one; older technology video display cards used multiple memory banks in order to map a large amount of memory into the 128K data region allocated for video cards on the original IBM PC. However, this memory banking scheme is fairly slow, so most modern PCs map the video display card into linear memory. Nevertheless, it is possible to still find some PCs using this older technology (though such cards are rapidly fading away from the scene). It probably isn't absolutely safe to assume that w.GetDeviceCaps will always return one when you specify the w.PLANES index value, but it's probably rare for this API to return any other value for this index under any modern version of Windows.

The w.BITSPIXEL index tells w.GetDeviceCaps to return the number of bits per pixel, organized as a linear memory array, on the display. Note that both the w.PLANES and w.BITSPIXEL indexes tell w.GetDeviceCaps to return the number of bits per pixel. One of these return values will always be one and the other may be greater than one. The difference between the two is the underlying hardware technology the display adapter uses. Older video display cards split n bits of color information across n banks of memory (where n is usually eight or less). Newer video cards associate n contiguous bits in a linear memory space with each pixel on the display rather than spreading the bits for each pixel across multiple memory banks. Because bank switching requires a lot of additional work, manipulating pixels stored in contiguous bits in memory is far more efficient so almost all modern (high-performance) video cards use this scheme. Therefore, it's probably safe to assume that the value w.GetDeviceCaps returns for the w.BITSPIXEL index value is the number of bits per pixel on the display on any modern machine. However, if you expect your software to work on older machines, you'll have to grab both values to determine the number of bits per pixel. The total number of colors available on modern video display cards is 2bitsPerPixel. You can compute this value using the following expression:

Number_of_colors = 1 shl (bitsPerPixel * numPlanes);

bitsPerPixel is the value w.GetDeviceCaps returns for the w.BITSPIXEL index and numPlanes is the value it returns when you pass it the w.PLANES index. The shift left operation in this expression computes two raised to the power (bitsPerPixel * numPlanes). Because at least one of bitsPerPixel and numPlanes is the value one, we could also compute this as "1 shl (bitsPerPixel + numPlanes - 1)" if multiplication is a slow operation on the CPU you're using.

Note that w.GetDeviceCaps will return the value 16 in response to a w.BITSPIXEL query if the actual number of bits per pixel is 15. This is because many so-called 16-bit video display cards actually provide only a 15-bit color depth, yet their drivers return 16 bits as the color depth. Other cards returns 15 bits. To avoid confusion, Windows calls both 15-bit and 16-bit displays a 16 bit device. This means that you may only be capable of display half the actual number of colors that Windows reports when you're working with a 16-bit display.

Although the underlying display device may only be capable of displaying some limited number of colors, Windows actually uses 32-bit double word values to represent color throughout the system. In particular, Windows uses the w.COLORREF type to represent a color as a combination of the Red, Green, and Blue additive primary colors (an "RGB" value). The layout of an RGB value appears in Figure 7-1. The L.O. three bytes of this double word specify the eight-bit values (0..255) for the red, green, and blue components of the color. The H.O. byte of this double word contains zero.

Figure 7-1: Windows RGB Color Format

With 24 bits (one byte of each of the three additive primary colors), it is possible to represent over 16 million different colors. For example, you may obtain the color magenta by specifying 255 ($FF) for the blue and red components of an RGB color value and zero for the green component. That is, magenta is equal to the RGB value $FF_00FF. Similarly, you may obtain cyan by mixing blue and green (i.e., $FF_FF00) and yellow by mixing green and red (i.e., $00_FFFF). The color white is obtained by mixing all three colors ($FF_FFFF) and you get black by the absence of these three colors (i.e., $00_0000).

The HLA w.hhf header file simply defines w.COLORREF as a dword type. If you want to access the individual color bytes of this type you can easily do so (this is assembly language, after all). The "structured" way to do this is to create a record in your program, similar to the following:

type

    colorref_t:

        record

            red		:byte;

            green		:byte;

            blue		:byte;

            alpha		:byte; // "Alpha channel" is the name used for the fourth byte

        endrecord;

If you create an object of type colorref_t, you can access the individual bytes as fields of this record, e.g., colorVar.red, colorVar.green, and colorVar.blue.

The wpa.hhf header file includes a macro named RGB that makes it easy to create double-word (w.COLORREF) constants from the individual color component values. This macro is defined as follows:

    // RGB macro - Combines three constants to form an RGB constant.

    #macro RGB( _red_, _green_, _blue_ );

        ( _red_ | ( _green_ << 8 ) | ( _blue_ << 16 ))

    #endmacro

One very important thing to note about this macro is that its parameters must all be constants and it returns a constant value as its "result". You use this macro as the operand of some other instruction or assembler directive, you do not use this macro as though it were a function. Here's a typical use that defines the color magenta as a symbol in the const section of an HLA program:

const

    magenta :w.COLORREF := RGB( $FF, $00, $FF );

Again, and this is very important, the RGB operands must all be constants. You may not supply register or memory variables as arguments to this macro. If you would like a generic"function" that computes RGB values from eight-bit constants, registers, or memory locations, you could write a macro like the following:

// mkRGB - converts three eight-bit color values into an RGB value:

#macro mkRGB( _red_, _green_, _blue_ );

    movzx( _blue_, eax );

    shl( 16, eax );

    mov( _green_, ah );

    mov( _red_, al );

#endmacro

    .

    .

    .

    mkRGB( redByteVar, $f0, bl ); // RGB value is left in EAX.

Although Windows works with 24-bit color values throughout the system, not all display adapters are capable of displaying this many colors on the screen simultaneously (or, the user may have selected a smaller number of characters for Windows to use when accessing the display card). As a result, the fact that you've specified a 24-bit color value does not imply that Windows can actually display that color on the screen. To accomodate this descrepency, Windows will often use a technique known as dithering to increase the number of available colors. Dithering increases the number of colors by drawing adjacent pixels using different colors that, when blended, approximate the desired color. For example, if you draw an alternating sequence of red and blue pixels, and then view the result from a distance, the result looks like a solid magenta color. Though dithering increases the maximum number of colors the user perceives on the screen, there are several primary disadvantages of dithering. First, because it takes multiple pixels to do dithering, dithering effectively reduces your screen resolution. Also, dithering only works well when coloring large areas, it doesn't work well for small or thin objects and it doesn't work at all for single pixels. Dithering is fairly obvious when the viewer is close to the video display; the image looks coarse and grainy when you apply dithering. Another big disadvantage to dithering is that it is slow - Windows has to compute, on the fly, dithering values when it attempts to display a color that the video display mode doesn't directly support. This extra computation can slow down the rendering of some object by a fair amount.

In many cases, applications don't really require an exact color match. For example, some programmer might want to display some text with a color that is roughly 25% red and 0% blue and green (that is, a dark red color). Now most video modes are probably capable of displaying an object whose color is 25% red. However, is 25% red represented by the eight-bit value 63 or 64? Pick the wrong one and Windows will attempt to dither the color (producing a low-quality image and running slowly). How do you make sure you pick the "pure" color that Windows can efficiently render? Well, Windows provides a nifty API function that will tell you what the closest "pure" color is to an RGB value you've specified: w.GetNearestColor:

static

    GetNearestColor: procedure

    (

        hdc             :dword;

        crColor         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetNearestColor@8" );

As usual, hdc is the handle of the device context whose color values you wish to check. The crColor parameter is the RGB value whose nearest color you'd like to find. This function returns the nearest color in the EAX register. So if you want to set some color to (approximately) 25% red, you could do this with the following code:

    w.GetNearestColor( hDC, RGB( 64, 0, 0 ) ); // 64 is approximately 25% of 255.

    mov( eax, red25 );

    // Now use red25 as the color for 25% red...

Another couple of useful device context values are the Window Origin and Viewport Origin values. The window origin specifies where the logical display's (0,0) point falls in on the display device (the viewport). The viewport origin specifies where the physical screen's (0,0) point falls in the logical window. These two origin value provide different "views" of the same concept - how Windows maps the logical coordinate space to the physical display coordinate space. An application, should it choose to modify the coordinate systems, would normally modify either the window coordinates or the viewport coordinates, but not both (which would tend to get confusing). This discussion will center around the use of the Window Origin values but it generally applies to the Viewport Origin values as well.

By default, when Windows first opens an application's window, it maps logical coordinate (0,0) on the display to the upper-left hand corner of the physical display device. All coordinates within your application are relative to this mapping. By calling the w.SetWindowOrgEx API function, however, you can tell Windows to map all coordinate values relative to some other point on the display. For example, you can tell Windows that point (100,100) in your application's coordinate system should correspond to physical point (0,0) on the display. You might want to do this, for example, if it's more convenient to work with coordinates in your application that are 100 and above. Here are the functions you'll find useful for getting and setting the window origin:

type

    POINT:

        record

            x: dword;

            y: dword;

        endrecord;

static

    GetWindowOrgEx: procedure

    (

            hdc             :dword;

        var lpPoint         :POINT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetWindowOrgEx@8" );

    SetWindowOrgEx: procedure

    (

            hdc             :dword;

            X               :dword;

            Y               :dword;

        var lpPoint         :POINT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetWindowOrgEx@16" );

The w.GetWindowOrgEx function returns the current window origin for the device context provided by hdc in the variable you specify via the lpPoint parameter. The w.SetWindowOrgEx function sets the origin for the device context you specify via the X and Y parameters and returns the original origin in the lpPoint parameter (this function ignores lpPoint if you pass NULL in this parameter location). The following code fragment, for example, sets the window origin to (100,100) for use in our hypothetical example given earlier:

    w.SetWindowOrgEx( hDC, 100, 100, NULL );

The device context maintains several default drawing objects within the device context. For example, the default pen specifies how Windows will draw lines within that context; the default brush specifies how Windows will fill areas when drawing within the context; the default font specifies how Windows will render text to the device, the default colors to use, and so on. We'll discuss how to use the w.SelectObject functions to set these default values a little later in this chapter.

There is a minor problem you'll encounter when using device contexts - you create them in response to an API call such as BeginPaint and you destroy the context via a call to an API like EndPaint. Specifically, whenever you invoke BeginPaint, Windows creates a new device context complete with a set of default values. So if you change the default color Windows uses to display text, Windows will "forget" your change when you call EndPaint to finish the current output operation. This is convenient for certain values, but sometimes you'll want to set the default device context value that Windows uses. Windows provides an option that lets you tell Windows not to reset the device context every time you invoke something like BeginPaint. To do this, you include the w.CS_OWNDC constant as part of the window class style when creating the window in the first place, e.g., in an application's main program:

// Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW | w.CS_OWNDC, wc.style ); // Change this line

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

        .

        .

        .

With this modification, Windows will allocate a small amount of storage (under 1K) to maintain all the device context values between the EndPaint invocation and the next BeginPaint invocation. So now, for example, when you set the drawing color to red, it will remain red, even across calls to BeginPaint/EndPaint, until you explicitly change it to something else.

The drawback to using the w.CS_OWNDC style is the fact that Windows will preserve all default values you set, not just one or two that you'd like to preserve across BeginPaint/EndPaint invocations. If you need to be able to change only a few default values but make other changes temporary (i.e., within the bounds of the BeginPaint/EndPaint sequence), then you'll need some way of preserving the existing device context values and restoring them later. You can accomplish this with the w.SaveDC and w.RestoreDC calls:

static

    SaveDC: procedure

    (

        hdc                 :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SaveDC@4" );

    RestoreDC: procedure

    (

        hdc                 :dword;

        nSavedDC            :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__RestoreDC@8" );

You pass a handle to the device context whose values you want to save or restore to both of these routines. The w.SaveDC call will save a copy of the device context's values internal to Windows and return a handle by which you can restore those values in the EAX register. The w.RestoreDC function uses this handle returned by w.SaveDC as the second parameter to tell Windows which internal values to restore. To use these functions you'd typically write code like the following:

    BeginPaint( hdc );

        << code that changes device context values that you want to keep as the default>>

        w.SaveDC( hdc );

        mov( eax, SavedDCHandle ); // Save for later...

            << code that makes temporary changes to the device context >>

        w.RestoreDC( hdc, SavedDChandle ); // Restore the values we've changed

    EndPaint;

Note that w.SaveDC pushes a copy of the current context values onto a stack and w.RestoreDC pops the values off of that stack. The handle that w.SaveDC returns can be thought of as the saved context stack pointer. Therefore, if you call w.SaveDC multiple times without calling w.RestoreDC inbetween, each context is written to a separate section of memory (i.e., a new entry on the stack). You may pop the last entry pushed via the call w.RestoreDC( hdc, -1 ); This spares having to actually save the return value from w.SaveDC if you use w.SaveDC and w.RestoreDC in a stack like fashion. Note, however, that if you save two contexts on the stack and save the two handles that w.SaveDC returns, and then you pass the first (earliest) handle that w.SaveDC returns to w.RestoreDC, the w.RestoreDC function pops both contexts off the stack, you cannot restore a later-pushed-context after restoring an earlier-pushed-context.

As mentioned a little bit earlier, we'll return to the discussion of how to set certain device context values at appropriate moments in this chapter. In the meantime, however, it's time to begin discussing how to actually draw things into a device context (i.e., onto the screen).

7.6: Line Drawing Under Windows

Line drawing under Windows is accomplished using five different routines: w.MoveToEx, w.LineTo, w.PolyLine, w.PolyLineTo, and w.PolyPolyLine. Here are the prototypes for these five functions:

type

    POINT:

        record

            x: dword;

            y: dword;

        endrecord;

static

    LineTo: procedure

    (

        hdc             :dword;

        nXEnd           :dword;

        nYEnd           :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__LineTo@12" );

    MoveToEx: procedure

    (

            hdc         :dword;

            X           :dword;

            Y           :dword;

        var lpPoint     :POINT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__MoveToEx@16" );

    Polyline: procedure

    (

            hdc             :dword;

        var lppt            :POINT;

            cPoints         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__Polyline@12" );

    PolylineTo: procedure

    (

            hdc             :dword;

        var lppt            :POINT;

            cCount          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PolylineTo@12" );

    PolyPolyline: procedure

    (

            hdc             :dword;

        var lppt            :POINT;

        var lpdwPolyPoints  :var;

            cCount          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PolyPolyline@16" );

All of these functions exist as keyword macros in the BeginPaint/EndPaint multi-part macros. You invoke these macros between the BeginPaint/EndPaint pair; they have the following declarations:

      #keyword LineTo( _x_, _y_ );

      #keyword MoveToEx( _x_, _y_, _lpPoint_ );

      #keyword Polyline( _lppt_, _cPoints_ );

      #keyword PolylineTo( _lppt_, _cPoints_ );

      #keyword PolyPolylineTo( _lppt_, _lpdwPolyPoints_, _cCount_ );

The invocation sequence is similar to the Win32 API functions except you don't need the "w." prefix and you don't supply the hdc (first) parameter.

The unusual thing about Windows' line drawing routines is that most of them make use of the "current pen position" maintained in the device context. For example, consider the LineTo function that draws a line in the window specified by the hdc parameter to BeginPaint. You'll notice that this function has only two parameters, an x-coordinate value and a y-coordinate value; lines, however, are defined by two endpoints, not a single point. The LineTo API function will actually draw a line from the "current point" (that the device context maintains) to the point specified by the (x,y) coordinate value you pass to LineTo. After drawing the line, the LineTo function sets the internal "current point" value to the endpoint of the line (that is, the (x,y) coordinate we pass LineTo becomes the new "current point" after the line is drawn). This feature makes it very easy to draw complex connected shapes as a sequence of connected lines by making sequential calls to LineTo.

There are two problems with LineTo's behavior; specifically, "how do we set the initial starting point?" and "what happens when we want to draw two unconnected line segments?" Well, the MoveToEx macro comes to our rescue in this case. The MoveToEx macro (i.e., w.MoveToEx API function) sets the "current point" inside the device context but has no other effect on the display. You can use this function to set the initial endpoint of a line and then call LineTo to draw the line from that point to the line's end point, e.g.,

// Draw a line from (10, 10) to (100, 50):

    MoveToEx( 10, 10, NULL );

    LineTo( 100, 50 );

The third parameter in the MoveToEx invocation specifies a variable of type w.POINT where Windows will store the device context's "current point" value. If this argument is NULL (as in this example), then Windows will not bother storing the value.

Because drawing a complex object as a sequence of line segments is so common, the operation of the MoveToEx and LineTo functions is just what you want a large percentage of the time. However, if you decide to draw a fairly complex object made up of dozens, hundreds, or even thousands of line segments, the calls to LineTo can become a performance bottleneck,. The problem is that many modern display adapters provide hardware acceleration of various graphic primitives (including line drawing). As a result of this acceleration, the system actually winds up spending more time calling the LineTo function than actually drawing the line. To reduce the overhead of the LineTo function invocations when drawing objects made up of complex line segments, Windows provides the Polyline, PolylineTo, and PolyPolylineTo functions. These functions take an array of points (or an array of polylines, that is, an array of array of points) and pass that data on down to the GDI driver that is responsible for drawing lines. The line drawing code can quickly draw the sequence of line segments without the overhead of multiple calls to the line drawing function.

The execution of the PolylineTo function is roughly equivalent to a sequence of LineTo calls. It begins drawing line segments from the current position to the first point in an array of points you pass to this function. PolylineTo sets the "current position" to the coordinate of the last point appearing in the array of points. The lppt parameter is a pointer to an array of w.POINT records and the cCount parameter specifies the number of points in that array.

The Polyline and PolyPolyline functions neither use nor modify the "current pen position" value in the device context. The Polyline function (to which you pass an array of points and a count) draws a sequence of lines starting with the first point in the list through each of the remaining points in the list. We'll take a look at an example of a Polyline call a little later in this section. The PolyPolyline API function accepts an array of polylines and draws the object specified by all these points.

When drawing lines, Windows uses several default values in the device context to control how the line is drawn. This includes the color of the line, the width of the line, and the line's style (e.g., solid, dotted, dashed, etc.). The default pen setting is a black, solid pen whose width is one pixel. While this is probably the most common line style you will draw, there is often the need to draw lines of a different color, a different width, or with some other style besides solid. In order to do this, you must create a new pen and select that pen into the current device context. To create a new pen, you use the Windows w.CreatePen API call. Here's the prototype for that function:

static

    CreatePen: procedure

    (

        fnPenStyle  :dword;

        nWidth      :dword;

        crColor     :COLORREF

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreatePen@12" );

The fnPenStyle parameter specifies a style for the pen. This can be one of the values that Table 7-2 describes. The use of most of these constants is fairly self-explanatory. We'll discuss the purpose of the w.PS_INSIDEFRAME pen style in a later section of this chapter.

Table 7-2: Pen Styles
Pen Style
Description
w.PS_SOLID
Draw a solid line with the pen.
w.PS_DASH
The pen is dashed. This style is valid only when the pen width is one or less in device units.
w.PS_DOT
The pen is dotted. This style is valid only when the pen width is one or less in device units.
w.PS_DASHDOT
The pen has alternating dashes and dots. This style is valid only when the pen width is one or less in device units.
w.PS_DASHDOTDOT
The pen has alternating dashes and double dots. This style is valid only when the pen width is one or less in device units.
w.PS_NULL
Windows does not draw with the pen.
w.PS_INSIDEFRAME
The pen is solid. When this pen is used in any GDI drawing
function that takes a bounding rectangle, the dimensions of the
figure are shrunk so that it fits entirely within the bounding rectangle,
taking into account the width of the pen.

The w.CreatePen nWidth parameter is only valid for the w.PS_SOLID, w.PS_NULL, and w.PS_INSIDEFRAME pen styles. This specifies the width of the line in logical device units. If this field contains zero, the width of the line is always one pixel (which isn't necessarily true if the nWidth value is one).

The crColor parameter is an RGB value that specifies the color that the pen will draw on the screen. Note that you can use the wpa.hhf RGB macro to create an RGB value to pass as this parameter.

When you call w.CreatePen, Windows will create a pen GDI object internally and return a handle to that pen in the EAX register. Note that Windows does not automatically start using this pen. You must select this pen into the device context if you want to use it while drawing lines. This is done with the w.SelectObject API function (or SelectObject #keyword macro found in the wpa.hhf header file). When you select a pen into the current device context via SelectObject, Windows returns the handle to the previous pen that was selected. You can save this former pen value in order to restore the original pen when you are done using the current pen, e.g.,

    BeginPaint( hwnd, ps, hdc );

            .

            .

            .

        w.CreatePen( w.PS_SOLID, 0, RGB( $FF, $00, $FF )); // Solid, magenta, pen.

        mov( eax, magentaPen );									// Save, so we can delete later.

        SelectPen( eax );									// Select the magenta pen into the context.

        mov( eax, oldPen );									// Save, so we can restore.

            .

            .

            .

        SelectPen( oldPen );									// Restore original pen.

        w.DeleteObject( magentaPen );

    EndPaint;

Although this example might suggest this to be the case, understand that pens you create via w.CreatePen are not part of the device context. That is, you can create a pen outside the BeginPaint/EndPaint sequence and such pen values are persistent outside of the BeginPaint/EndPaint sequence. The calls to w.CreatePen and w.DeletePen appear inside the BeginPaint/EndPaint sequence here mainly for typographical convenience. Their presence in this sequence does not imply that these calls have to take place inside this sequence.

Because the w.CreatePen API function creates a resource inside Windows, you must be sure to delete that pen when you are done using it via a call to w.DeletePen. Because Windows has limited GDI resources available, failure to delete any GDI resources your program uses may lead to "resource leak" which will impact the overall system performance. Though you don't have to create and destroy a GDI object within your Paint procedure, you do need to make sure you delete all resources your program creates before your program quits. Many applications create the pens they need in their Create procedure and then delete those pens in the QuitApplication procedure. This sequence spares the application from having to constantly create and destroy often-used pens.

Windows provides three built-in, or stock, pens that you can use without calling w.CreatePen to create them: a black pen (the default), a white pen, and a null pen (which doesn't affect the display when you draw with it). You may obtain a handle to any of these stock pens by calling the w.GetStockObject API function:

static

    GetStockObject: procedure( fnObject:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetStockObject@4" );

This function requires a special value to tell Windows exactly what kind of object you want to create. The windows.hhf header file defines three constants that you can use to tell w.GetStockObject to return a handle to one of the stock pens: w.BLACK_PEN, w.WHITE_PEN, and w.NULL_PEN. For example, to quickly obtain the handle for a white pen, you can use the following call:

    w.GetStockObject( w.WHITE_PEN );

    mov( eax, whitePenHandle );

Note that you must not delete a stock object.

7.6.1: Background Mode and Color for Lines

When using a pen style that involves dashed or dotted lines, Windows uses the current background mode and color to determine how to draw the space between the dots and dashes that comprise the line. You can set the background color and mode by using the w.SetBkColor and w.SetBkMode API functions, respectively:

static

    SetBkColor: procedure( hdc:dword; crColor:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetBkColor@8" );

    SetBkMode: procedure( hdc:dword; iBkMode:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetBkMode@8" );

The hdc parameter to these functions is, obviously, the handle of the device context whose background color or mode you want to change. The crColor parameter to the w.SetBkColor function is an RGB color value that windows will use when drawing the gaps (if any) between the lines of an image it is rendering. The iBkMode parameter you supply to w.SetBkMode is either w.OPAQUE or w.TRANSPARENT. If you specify the w.OPAQUE mode, then Windows will fill the gaps in a dotted or dashed line with a solid color, the color you specify with the w.SetBkColor function call. Note that in the opaque mode, Windows overwrites whatever was previously on the screen using the solid color you've specified. On the other hand, if you specify w.TRANSPARENT as the background drawing mode, then Windows will ignore the background color and leave whatever image originallyl appeared on the screen in the gaps between the dashes and dots of a stylistic line.

7.6.2: Using the LineTo and MoveToEx API Functions

As a demonstration of the line drawing capabilities of Windows, the Lines.hla program repeatedly draws a set of pseudo-random lines on the display. This application is a typical Windows app with two wndproc procedures - Paint and Size. The Size procedure tracks window size changes so the Paint procedure can keep all lines within the boundaries of the window. The Paint procedure computes the endpoints of a "somewhat random" line to draw and then draws that line to the window.

Because the Size procedure is especially trivial, we'll take a look at it first. This procedure simply copies the values for the new width and height into a couple of global variables (ClientSizeX and ClientSizeY). Here's its code:

// Size-

//

//	This procedure handles the w.WM_SIZE message

//	Basically, it just saves the window's size so

//	the Paint procedure knows when a line goes out of

//	bounds.

//

//	L.O. word of lParam contains the new X Size

//	H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

The Paint procedure is a tiny bit more complicated. The first thing to consider are the variables that the Paint procedure uses to maintain the state of the pseudo-random lines it draws. Here are the declarations:

static

    hPen        :dword;                     // Pen handle.

    OldPen      :dword;                     // Old Pen Handle.

    lastRGB     :w.COLORREF;                // Last color we used

    x1          :int32 := 0;

    y1          :int32 := 0;

    x2          :int32 := 25;

    y2          :int32 := 25;

    lastDeltaX1 :int32 := 1;

    lastDeltaY1 :int32 := 1;

    lastDeltaX2 :int32 := 2;

    lastDeltaY2 :int32 := 2;

    changeCntr  :uns32 := 10;

The hPen variable holds the handle of the new pen this program creates (to control the color of the output); the OldPen variable maintains the handle of the old pen so that Paint can restore the original pen once Paint is finished drawing with the new pen. The lastRGB variable holds the color of the last pen. Paint increments this value to create a new color for each line it draws to the window. The x1, x2, y1, and y2 variables hold the coordinates of the end points of the line that Paint draws. The Paint procedure uses the lastDelta** and changeCntr variables to compute new endpoints for each line it produces. Here's how Paint uses these "delta" variables: on each pass through the Paint procedure, the code adds the value of lastDeltaX1 to the x1 variable, it adds lastDeltaY1 to y1, it adds lastDeltaX2 to x2, and it adds lastDeltaY2 to y2. This generates a new set of end points for the line. Should any one of those end points fall outside the boundaries of the screen, the Paint procedure clips the particular coordinate value so that it remains within the window's boundaries (and Paint also negates the corresponding lastDelta** value so that future adjustments continue to stay within the window's boudaries). The changeCntr variable determines how many line segments that Paint will draw with the current set of lastDelta** values before it chooses some new, semi-random, values for these variables. Here's how the Paint procedure uses changeCntr to determine when to set the lastDelta** values to semi-random values:

        // If the changeCntr variable counts down to zero, compute new

        // delta values:

        dec( changeCntr );

        if( @z ) then

            rand.range( 1, 100 ); // Compute a new, random, value for changeCntr

            mov( eax, changeCntr );

            // Compute new (random) values for the lastDelta** variables:

            rand.range( 0, 10 ); // We want a value in the range -5..+5

            sub( 5, eax );

            mov( eax, lastDeltaX1 );

            rand.range( 0, 10 );

            sub( 5, eax );

            mov( eax, lastDeltaY1 );

            rand.range( 0, 10 );

            sub( 5, eax );

            mov( eax, lastDeltaX2 );

            rand.range( 0, 10 );

            sub( 5, eax );

            mov( eax, lastDeltaY2 );

        endif;

Here's the code sequence that computes the new x1 value (computations for the x2, y1, and y2 values are nearly identical):

        // Compute a new starting X-coordinate for the

        // line we're about to draw (do this by adding

        // the appropriate delta to our current x-coordinate

        // value):

        mov( lastDeltaX1, eax );

        add( x1, eax );

        if( @s ) then

            // If we went off the left edge of the window,

            // then change the direction of travel for the

            // deltaX value:

            neg( lastDeltaX1 );

            add( lastDeltaX1, eax );

        endif;

        // Check to see if we went off the right edge of the window.

        // Reset the direction of deltaX if this happens:

        if( eax > ClientSizeX ) then

            neg( lastDeltaX1 );

            mov( ClientSizeX, eax );

            dec( eax );

        endif;

        mov( eax, x1 );

Note that on one pass through the Paint procedure this code draws only a single line segment.

Because the Paint procedure draws only a single line segment in response to a w.WM_PAINT message, it takes a continuous stream of w.WM_PAINT messages in order to generate the free-running display that Lines.hla produces in its window. The real trick to Lines.hla is how it continuously draws these line segments in its window. We can't stick an HLA forever loop inside our Paint procedure to continuously draw a sequence of lines over and over again; if we did that, the program wouldn't respond to messages and there would be no way to quite this program except by running the Windows task manager and killing the process. Because this is somewhat ill-behaved for an application, we've got to dream up a better way of telling the application to continuously draw lines. One sneaky way to do this is to tell Windows to send our application a w.WM_PAINT message just before leaving the Paint procedure. This action, of course, will cause Windows to call our Paint procedure again in short order, but through the standard message handling subsystem so our application can still respond to w.WM_DESTROY messages. The correct way to tell Windows to send our application a w.WM_PAINT message is to call the w.InvalidateRect API function, passing it a NULL rectangle to invalidate (which tells it to invalidate the whole window). Here's the call that does this (which appears at the end of the Paint procedure):

w.InvalidateRect( hwnd, NULL, false );

The last argument in the w.InvalidateRect parameter list tells this function to whether it should erase the screen before having Paint redraw it. Because we want to keep all the previous lines we've drawn in the window, this call passes false as the value of this parameter.

Here's the complete listing of the Lines.hla program:

// Lines.hla-

//

//  Simple Application the demonstrates line drawing.

program Lines;

#include( "rand.hhf" )

#include( "hll.hhf" )

#include( "w.hhf" )

#include( "wpa.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

type

    // Message and dispatch table related definitions:

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue    :dword;

            MessageHndlr    :MsgProc_t;

        endrecord;

static

    hInstance           :dword;         // "Instance Handle" Windows supplies.

    wc                  :w.WNDCLASSEX;  // Our "window class" data.

    msg                 :w.MSG;         // Windows messages go here.

    hwnd                :dword;         // Handle to our window.

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

readonly

    ClassName   :string := "LinesWinClass";         // Window Class Name

    AppCaption  :string := "Lines Program";         // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

static

    hPen        :dword;                     // Pen handle.

    OldPen      :dword;                     // Old Pen Handle.

    lastRGB     :w.COLORREF;                // Last color we used

    x1          :int32 := 0;

    y1          :int32 := 0;

    x2          :int32 := 25;

    y2          :int32 := 25;

    lastDeltaX1 :int32 := 1;

    lastDeltaY1 :int32 := 1;

    lastDeltaX2 :int32 := 2;

    lastDeltaY2 :int32 := 2;

    changeCntr  :uns32 := 10;

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        inc( lastRGB ); // Increment the color we're using.

        // If the changeCntr variable counts down to zero, compute new

        // delta values:

        dec( changeCntr );

        if( @z ) then

            rand.range( 1, 100 );

            mov( eax, changeCntr );

            // Compute new (random) values for the lastDelta* variables:

            rand.range( 1, 10 );

            sub( 5, eax );

            mov( eax, lastDeltaX1 );

            rand.range( 1, 10 );

            sub( 5, eax );

            mov( eax, lastDeltaY1 );

            rand.range( 1, 10 );

            sub( 5, eax );

            mov( eax, lastDeltaX2 );

            rand.range( 1, 10 );

            sub( 5, eax );

            mov( eax, lastDeltaY2 );

        endif;

        // Compute a new starting X-coordinate for the

        // line we're about to draw (do this by adding

        // the appropriate delta to our current x-coordinate

        // value):

        mov( lastDeltaX1, eax );

        add( x1, eax );

        if( @s ) then

            // If we went off the left edge of the window,

            // then change the direction of travel for the

            // deltaX value:

            neg( lastDeltaX1 );

            add( lastDeltaX1, eax );

        endif;

        // Check to see if we went off the right edge of the window.

        // Reset the direction of deltaX if this happens:

        if( eax > ClientSizeX ) then

            neg( lastDeltaX1 );

            mov( ClientSizeX, eax );

            dec( eax );

        endif;

        mov( eax, x1 );

        // Same as the above code, except for the Y coordinate

        mov( lastDeltaY1, eax );

        add( y1, eax );

        if( @s ) then

            neg( lastDeltaY1 );

            add( lastDeltaY1, eax );

        endif;

        if( eax > ClientSizeY ) then

            neg( lastDeltaY1 );

            mov( ClientSizeY, eax );

            dec( eax );

        endif;

        mov( eax, y1 );

        // Same as all the above code, but for the end point

        // (rather than the starting point) of the line:

        mov( lastDeltaX2, eax );

        add( x2, eax );

        if( @s ) then

            neg( lastDeltaX2 );

            add( lastDeltaX2, eax );

        endif;

        if( eax > ClientSizeX ) then

            neg( lastDeltaX2 );

            mov( ClientSizeX, eax );

            dec( eax );

        endif;

        mov( eax, x2 );

        mov( lastDeltaY2, eax );

        add( y2, eax );

        if( @s ) then

            neg( lastDeltaY2 );

            add( lastDeltaY2, eax );

        endif;

        if( eax > ClientSizeY ) then

            neg( lastDeltaY2 );

            mov( ClientSizeY, eax );

            dec( eax );

        endif;

        mov( eax, y2 );

        // Create a pen with the current color we're using:

        w.CreatePen( w.PS_SOLID, 0, lastRGB );

        mov( eax, hPen );

        SelectObject( eax );

        mov( eax, OldPen );

        // Draw the line:

        MoveToEx( x1, y1, NULL );

        LineTo( x2, y2 );

        // Restore the old pen and delete the current one:

        SelectObject( OldPen );

        w.DeleteObject( hPen );

    EndPaint;

    // Force Windows to redraw this window without erasing

    // it so that we get constant feedback in the window:

    w.InvalidateRect( hwnd, NULL, false );

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

/**************************************************************************/

/*                   End of Application Specific Code                     */

/**************************************************************************/

// The window procedure.

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword );

    @stdcall;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Here's the main program for the application.

begin Lines;

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    mov( hInstance, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW | w.WS_VSCROLL |  w.WS_HSCROLL,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end Lines;

7.6.3: Using the PolyLineTo API Function

The PolylineTo (and Polyline and PolyPolyline) functions provide a high-performance way to draw a large number of connected line sequences. At first glance it might appear that these functions are useful on occasion but don't have a tremendous amount of utility - after all, how many different objects can you draw with a sequence of connected straight lines? However, such an attitude is a bit naive. It turns out that these poly line functions are great for drawing arbitrary curves. It might not seem reasonable to draw arbitrary curves using straight lines, but keep in mind that if the line segments are short enough, the fact that a curve is built up of straight lines won't be noticable. In this section we'll explore this fact by writing a function that graphs a set of arbitrary functions.

The basic premise for the function plotting program is that we'll compute the values for some function y=f(x) with x ranging over some reasonable (though closely spaced) set of values. By feeding this function a set of monotonically increasing values along the x axis and then drawing a line betwen the last (x,y) pair produced and the current (x,y) produced, we can obtain a plot of that function. By spacing the x values close together, we can produce a finely detailed plot and it won't be at all apparent that the curves in that plot are made up of straight line segments.

The FtoX.hla program actually plots several functions simultaneously in the window. To differentiate the plots for each of the different functions, this program uses a different pen (and color) for each of the graphs. This particular program plots the graphs for the sine, cosine, log, and tangent functions. Because the sine and cosine functions return a value between -1.0 and +1.0, this function scales their values so that they fit within the middle 80% of the window. Because the log function produces values that are completely out of range, this program clips out of range values. Note that the graphs for each of these functions do not use the same scaling functions - the intent is to draw a pretty picture and demonstrate the use of the PolyLine function, not provide a mathematically correct image.

These functions create one line segment for each pixel along the x-axis. To plot the graph of one of these functions the program runs a for loop from zero to the width of the window (incrementing by one) and passes the loop index to one of the functions as the value for x in the function y=f(x), with f being sine, cosine, tangent, or log. The functions return a value between zero and the current window's height. The Paint procedure (that contains this for loop) saves the (x,y) pair as the next line end point to plot for that function.

In order to save all these (x,y) end points, the Paint procedure needs an array of w.POINT values for each of the functions (that is, a polyline data structure). Because the user can resize the window at any time, we cannot deetermine the size of this array at compile time. The number of elements in these arrays is going to be a function of the window's size at run-time. While we could do something really gross like overallocate storage for the arrays (i.e., make them so large that we don't have to worry about overrunning their bounds), the correct way to deal with arrays of this form is to dynamically allocate their storage at run-time based on the exact number of elements we need.

Because we only need these arrays for the duration of Paint's execution, allocating the storage for these arrays using malloc, or using HLA's dynamic array library, is almost as disgusting as over-allocating storage for the arrays. Dynamic allocation is not particularly fast, even if it is memory efficient. A better solution is to simply allocate storage for the dynamic arrays on the stack so that the storage is automatically deallocated when the Paint procedure returns to Windows. We can reserve storage on the stack using the HLA Standard Library talloc function. You pass this function a block size and it returns a pointer to a block of memory at least that large in the EAX register. This function allocates that storage on the stack.

Warning: talloc allocates its storage by dropping the stack down by some number of bytes (possibly larger than you've requested, to keep the stack double-word aligned). This means that if you've recently pushed data onto the stack, you'll not be able to access that data unless you've saved a pointer to that data on the stack. Exiting from the Paint procedure will automatically deallocate this storage, but anything you've pushed onto the stack before allocation may become inaccessible. Therefore, it's a good idea to allocate storage with talloc before pushing registers or other values on the stack in the Paint procedure.

The Size procedure in the FofX.hla program tracks any changes to the window's size (including the original window creation) and stores the width and height in the global ClientX and ClientY variables. Upon entry into the Paint procedure, the program can allocate sufficient storage to hold the end points Polyline must draw.

Although this program only plots four different functions, the number of functions is not hard-coded into the Paint procedure. Instead, this application uses an array of pens, and array of procedure pointers, and an array of pointers to the endpoint array data to make it very easy to change the number of functions that this program handles. By simply changing a constant at the beginning of the source file and adding (or removing) an appropriate mathematical function, you can easily change this program to plot additional (or fewer) functions without a major rewrite of the code. The numFuncs constant declaration controls the number of functions this application will plot:

// The following constant defintion defines how many functions we're going

// to plot with this program:

const

    numFuncs := 4;

To differentiate the plots in the window, the FofX.hla application uses a different pen style for each function. In order to reduce the number of different things you have to change in the program in order to add or remove functions, the FofX.hla program uses an array of records to hold each function's address and the particular pen style to plot that function. Here's the data structure that holds this information:

// The following data type is used to hold the pertinent values needed to

// plot a single function - including the function's address and pen type.

type

    plotRec:

        record

            f :procedure( x:uns32); @returns( "eax" );

            lineType :dword;

            lineWidth :dword;

            lineColor :w.COLORREF;

        endrecord;

// The following table has one entry for each of the functions we're

// going to plot. It specifies the function to call and the pen info.

// The number of entries in this table must match the numFuncs constant

// value (this table must be maintained manually!).

readonly

    plotInfo : plotRec[ numFuncs ] :=

        [

            plotRec:[ &sin, w.PS_SOLID, 0, RGB( $F0, $70, $F0) ],

            plotRec:[ &cos, w.PS_SOLID, 0, RGB( $00, $C0, $C0) ],

            plotRec:[ &tan, w.PS_SOLID, 0, RGB( $C0, $C0, $00) ],

            plotRec:[ &log, w.PS_SOLID, 0, RGB( $FF, $C0, $80) ]

        ];

To add a new function to plot, you would add a new entry to the plotInfo array that provides the address of the function and the pen style, width, and color values (of course, you need to supply the actual function as well). These are the three changes you need to make in order to plot a new function: change the value of the numFuncs constant, add an entry to the plotInfo array, and then write the actual function that computes y=f(x) for a given value of x.

The individual functions take a single integer parameter and return an integer result in the EAX register. When computing the data to plot, the FofX.hla program sequences through each of the x-coordinate values in the window and passes these values to the individual functions; whatever values they return, the FofX.hla program uses as the corresponding y-coordinate of the next point to plot. Here is a typical example - the sin function computes the sin of the angle (in degrees) passed in as the parameter and returns a scaled y-coordinate value (to plot the sin curve such that it fills the window):

// sin- plots the sine of the angle (in degrees) passed as the parameter:

procedure sin( x:uns32 ); @returns( "eax" );

var

    WinHt   :uns32;

begin sin;

    // Note: fsin wants radians, so convert x to radians as

    // radians = degrees*pi/180:

    fild( x );

    fld( 180.0 );

    fdiv();

    fldpi();

    fmul();

    // Compute the sine of the angle:

    fsin();

    // Sine produces a result between -1..+1, scale to within 90% of

    // the top and bottom of our window:

    mov( ClientY, eax );

    shr( 1, eax );

    mov( eax, WinHt );

    fild( WinHt );

    fmul();

    fld( 0.9 );

    fmul();

    fild( WinHt );

    fadd();

    // Return an integer result as our Y-axis value:

    fistp( WinHt );

    mov( WinHt, eax );

end sin;

The remaining functions compute similar results. The cos function computes the cosine, the tan function computes the trigonmetric tangent, and the log function computes a combination logarithm/sine function (log by itself is a rather boring curve, combining log and sine produces something a bit more interesting). See the program listing a little later for the exact implementation of these functions.

When the FofX.hla application plots each of the curves for the numFuncs functions, it uses a different color for each plot in order to make it easier to differentiate the different graphs in the window. This means that the program needs to create a different pen object for each graph it draws. Athough the program could create (and destroy) these pens on the fly, this application actually creates an array of pen objects and initializes this array in the Create procedure and then destroys each of these pens in the QuitApplication procedure. Here's the pertinent code to deal with these pens:

static

    pens                :dword[ numFuncs ]; // Pens used for each plot.

    axisPen             :dword;

// The Create procedure creates all the pens we're going to use

// in this application to plot the various functions

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

begin Create;

    // Create pens for each of the functions:

    #for( i := 0 to numFuncs-1 )

        w.CreatePen

        (

            plotInfo.lineType [ i*@size( plotRec ) ],

            plotInfo.lineWidth[ i*@size( plotRec ) ],

            plotInfo.lineColor[ i*@size( plotRec ) ]

        );

        mov( eax, pens[ i*4 ] );

    #endfor

    // Create a thicker, gray, pen for drawing the axis:

    w.CreatePen( w.PS_SOLID, 2, RGB( $20, $20, $20) );

    mov( eax, axisPen );

end Create;

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Delete the pens we created in the Create procedure:

    #for( i := 0 to numFuncs-1 )

        w.DeleteObject( pens[ i*4 ] );

    #endfor

    w.DeleteObject( axisPen );

    w.PostQuitMessage( 0 );

end QuitApplication;

Note how the Create and QuitApplication procedures use a compile-time loop to automatically generate all the code needed to initialize numFuncs pens so that you don't have to change these procedures should you ever decide to add or remove functions that this program plots.

The FofX.hla program redraws its window everytime you resize the window. Of course, resizing means that the program has more (or less) window space to draw, so the program has to keep track of the window's size when a redraw actually occurs. FofX.hla does this by saving the window's width and height values passed along with the w.WM_SIZE message that Windows sends along whenever the user resizes the screen:

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientY );

    xor( eax, eax ); // return success.

end Size;

Of course, the Paint procedure is where all the "real" work takes place. Whenever Windows sends FofX.hla a w.WM_PAINT message, the application computes a new set of points for each graph and calls the Polyline function to plot this set of points. There are two important things to note about this procedure: first, notice how it dynamically allocates storage for the poly line array of points (using talloc, as described earlier) and, second, not how it automatically handles any number of plotting functions using a (run-time) for loop. Here's the code for Paint:

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

    oldPen      :dword;

    polyLinePts :dword;

begin Paint;

    // We need to allocate a dynamic array to hold the points we're going

    // to plot. The ClientX global specifies the number of array elements.

    // Note that we must allocate this storage before pushing any registers

    // (or anything else) onto the stack. We rely upon the activation record

    // clean-up on return to deallocate this storage.

    intmul( @size( w.POINT ), ClientX, eax );

    talloc( eax );

    mov( eax, polyLinePts );

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Draw an axis in the window:

        SelectObject( axisPen ); // Select the gray pen.

        mov( eax, oldPen );      // Save, so we can restore later

        MoveToEx( 1, 0, NULL );  // Draw the vertical axis

        LineTo( 1, ClientY );

        mov( ClientY, ebx );     // Draw the horizontal axis.

        shr( 1, ebx );

        MoveToEx( 0, ebx, NULL );

        LineTo( ClientX, ebx );

        // For each of the functions, generate an array of points (the

        // polyline) and plot those points:

        mov( polyLinePts, ebx );

        for( mov( 0, edi ); edi < numFuncs; inc( edi )) do

            for( mov( 0, esi ); esi < ClientX; inc( esi )) do

                intmul( @size( plotRec ), edi, eax );

                plotInfo.f[eax]( esi );

                mov( esi, (type w.POINT [ebx]).x[ esi*8 ] );

                mov( eax, (type w.POINT [ebx]).y[ esi*8 ] );

            endfor;

            SelectObject( pens[ edi*4 ] );

            Polyline( [ebx], ClientX );

        endfor;

        // Restore original pen:

        SelectObject( oldPen );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

As you can see by scanning through this code, most of the work takes place in the two nested for loops. The outermost for loop repeats once for each of the mathematical functions this program plots, the inner-most loop generates a set of w.POINT values (the poly line array) that the call to Polyline draws on the screen. The whole point of this section (beyond demonstrating more Windows code) is to show you you can draw arbitrary curves by calling Polyline to draw short line segments making up the curves. Just so you don't get the impression that we're going to draw some coarse graphs because we're drawing the curves using a line-drawing function, Figure 7-2 shows the output (sans color) from this application.

Figure 7-2: Output From the FofX.hla Program

As you can see in Figure 7-2, the curves are nicely drawn and it's not apparent at all that they are drawn using straight-line segments. The reason this is not apparent is because the line segments we're drawing are only one unit apart on the x-axis. As such, most of these "lines" that FofX.hla draws are actually only a couple pixels long.

7.6.4: A Software Engineering Break

Before actually presenting the complete FofX.hla program, it's worthwhile to take a step back and look at a problem that exists in the applications presented up to this point. As you may have noticed, most of the Win32 applications we've written up to this point contain a considerable amount of common code. Indeed, the main program and the WndProc procedures have been nearly identical in all of the programs we've written. Although it's easy enough to "cut and paste" this common code from application to application as we've been doing, anyone with more than a few months' programming experience is probably wondering if there isn't some way to put all this common code into a library module so we don't have to constantly clutter up our source files with the same code over and over again. The answer is a qualified "yes". And that's what we'll explore in this section - how to create a library module from this common code2.

At first blush, creating a library module of all this common code seems like a trivial project - put all the common code into a unit and then link it into the final application. There are two problems that get in our way, however. First of all, although the code is almost identical in the main program and WndProc procedure, it is not exactly identical. Because of this, we need to work in some way to allow our applications to parameterize those values (generally the window class values) that often vary between different Win32 apps. Another problem is that a fair chunk of the common code is found in the application's main program; it's not like it's in some procedure we can easily move into some HLA unit to call from our program; it's the code that would normally call us. Sure, we could take a bunch of code out of the main program and put it in a procedure, but we'd still have that common code in the main program of every application we write that calls this new function we've created. Though we've reduced the amount of common code, it's not intuitively satisfying that we've still got to place common code in all of our applications.

To handle the latter problem, a common main program, is actually not too difficult. It just requires a small change in perspective. In most (non-Win32) software projects, the main program is the unique piece of software that calls a set of standardized library modules (i.e., HLA units). In a Win32 application this situation is reversed and the main program is the common code that calls a set of unique functions (e.g., Paint, Create, QuitApplication, and Size). To solve this problem is trivial - all we do is write a common main program module in HLA and link it with application specific units that contain code unique to that application. There is nothing that prevents us from linking a common main program with our application-specific units. So that's exactly the approach we'll use - we'll create a common "main program" module and include that in our library.

The first problem mentioned here, the fact that the code in the main program isn't exactly the same in all Win32 applications, is easily handled by doing two things: first, we export all the static (and usually read-only) variables that commonly change in applications (such as the window caption, the message procedure table, and other such common variables). The second thing we'll do is call a generic initialization function to give an application the opportunity to do any necessary application-specific initialization once the generic initialization is complete.

Software Engineering Note: this whole process begs for the use of classes and object-oriented programming. This book is explicitly avoiding the use of object-oriented programming because many assembly language programmers are unfamiliar with this programming paradigm. However, if you are comfortable with object-oriented programming techniques, you might want to explore this option in your own code.

In addition to revising the structure of our applications' source files, these modifications to eliminate redundant code from our Win32 source files will also impact the makefile and RadASM project files. Up to this point, we've only had to deal with one HLA source file and one header file (wpa.hhf). When we break out the main program and the WndProc procedure, we're going to have to explicitly link in a new library module (wpa.lib). Communication of public and external symbols will take place through a new winmain.hhf header file. However, we'll need to create this library module and modify the makefiles we create to link in appropriate code from this library module. The contents of this library module and how we build it will be the subject of the rest of this section.

As it turns out, the WndProc procedure that has appeared in each of the applications up to this point is perfectly generic - there have been no changes whatsover to that procedure in any of the source files we've considered. Furthermore, no code except the main program in these applications have actually referred to this procedure (remember, we initialize the wc.lpfnWndProc field with the address of this procedure and that's typically the only reference to the procedure in the whole application). This limited global exposure make WndProc a perfect candidate for a library routine. In fact, the only issue here is the fact that WndProc needs to refer to application-specific data (i.e., the message dispatch table). This, however, is easily handle by making that data external and requiring the application to specifically supply the data that WndProc needs. Because there are absolutely no changes needed the the WndProc code, we'll not waste space here (it appears in the full WinMain.hla listing that appears a little later in this section).

The main programs in several of our applications have had minor differences, particularly with respect to the way they've initialized the wc (window class) variable. Because we want a consistent main program in our library module (that is, every application will use exactly the same main program) this is going to present a minor problem: how do we have common code yet allow each application to make minor customizations to the code? The answer is that we'll have the main program call a couple of different procedures, that each specific application must provide, to deal with the customization issues. In particular, the generic main program will set up the global wc variable with some reasonable values and then call an external procedure, initWC, that the individual application must provide; this initWC function can be empty (i.e., it immediately returns) if the generic wc initialization is sufficient, or we can stick some application-specific initialization directly in this code. Here's the relevant code sequence in the generic main program that calls initWC:

// Here's the main program for the application.

begin WinMain;

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

// Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    mov( hInstance, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Allow application-specific initialization of wc:

    initWC();

Although the name initWC suggests that this function initializes the wc variable (which is its primary purpose), the application can actually stick any sort of initialization it likes into that function. The only catch is to realize that immediately upon return from initWC, the program is going to register the window class, create and show the window, and then enter the message processing loop of the main program. Here's the rest of the generic main program that follows the call to initWC:

// Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    // Have the application actually create the window -

    // It may want to supply different parameters to

    // w.CreateWindowEx, etc.

    appCreateWindow();

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end WinMain;

Most of this could should be familiar, the only thing out of the ordinary is the call to the appCreateWindow procedure that replaces the code in the original main program to create and display the application's main window. Like initWC, appCreateWindow is a call to a procedure that the specific application must provide; this procedure must create and show the window (if appropriate). Most of the time, this little procedure will simply jump to the defaultCreateWindow procedure which is a small procedure that does what the original code in our main program used to do, i.e.,

// Provide a defaultCreateWindow procedure that the

// application can call if it doesn't need to worry

// about passing different parameters to w.CreateWindowEX

// or change the calls to w.ShowWindow/w.UpdateWindow.

procedure defaultCreateWindow;

begin defaultCreateWindow;

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    push( eax );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    pop( eax ); // Return handle in EAX.

end defaultCreateWindow;

For many applications, here's what a typical implementation of initWC and appCreateWindow will look like:

// initWC - Application-specific initialization (no specific initialization here):

procedure initWC; @noframe;

begin initWC;

ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

Without further ado, here's the entire WinMain.hla program:

// WinMain.hla-

//

// This is the main program "stub" that we'll use as a "library" module

// for win32 development (encapsulates all the common code present in

// a typical win32 main program).

program WinMain;

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    hInstance           :dword;         // "Instance Handle" Windows supplies.

    wc                  :w.WNDCLASSEX;  // Our "window class" data.

    msg                 :w.MSG;         // Windows messages go here.

    hwnd                :dword;         // Handle to our window.

// The window procedure.

// This is actually a function that returns a return result in

// EAX.  If this function returns zero in EAX, then the event

// loop terminates program execution.

procedure WndProc( hwnd:dword; uMsg:uns32; wParam:dword; lParam:dword );

    @stdcall;

begin WndProc;

    // uMsg contains the current message Windows is passing along to

    // us.  Scan through the "Dispatch" table searching for a handler

    // for this message.  If we find one, then call the associated

    // handler procedure.  If we don't have a specific handler for this

    // message, then call the default window procedure handler function.

    mov( uMsg, eax );

    mov( &Dispatch, edx );

    forever

        mov( (type MsgProcPtr_t [edx]).MessageHndlr, ecx );

        if( ecx = 0 ) then

            // If an unhandled message comes along,

            // let the default window handler process the

            // message.  Whatever (non-zero) value this function

            // returns is the return result passed on to the

            // event loop.

            w.DefWindowProc( hwnd, uMsg, wParam, lParam );

            exit WndProc;

        elseif( eax = (type MsgProcPtr_t [edx]).MessageValue ) then

            // If the current message matches one of the values

            // in the message dispatch table, then call the

            // appropriate routine.  Note that the routine address

            // is still in ECX from the test above.

            push( hwnd );   // (type tMsgProc ecx)(hwnd, wParam, lParam)

            push( wParam ); //  This calls the associated routine after

            push( lParam ); //  pushing the necessary parameters.

            call( ecx );

            sub( eax, eax ); // Return value for function is zero.

            break;

        endif;

        add( @size( MsgProcPtr_t ), edx );

    endfor;

end WndProc;

// Provide a defaultCreateWindow procedure that the

// application can call if it doesn't need to worry

// about passing different parameters to w.CreateWindowEX

// or change the calls to w.ShowWindow/w.UpdateWindow.

procedure defaultCreateWindow;

begin defaultCreateWindow;

    w.CreateWindowEx

    (

        NULL,

        ClassName,

        AppCaption,

        w.WS_OVERLAPPEDWINDOW,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        w.CW_USEDEFAULT,

        NULL,

        NULL,

        hInstance,

        NULL

    );

    mov( eax, hwnd );

    push( eax );

    w.ShowWindow( hwnd, w.SW_SHOWNORMAL );

    w.UpdateWindow( hwnd );

    pop( eax ); // Return handle in EAX.

end defaultCreateWindow;

// Here's the main program for the application.

begin WinMain;

    // Get this process' handle:

    w.GetModuleHandle( NULL );

    mov( eax, hInstance );

    // Set up the window class (wc) object:

    mov( @size( w.WNDCLASSEX ), wc.cbSize );

    mov( w.CS_HREDRAW | w.CS_VREDRAW, wc.style );

    mov( &WndProc, wc.lpfnWndProc );

    mov( NULL, wc.cbClsExtra );

    mov( NULL, wc.cbWndExtra );

    mov( w.COLOR_WINDOW+1, wc.hbrBackground );

    mov( NULL, wc.lpszMenuName );

    mov( ClassName, wc.lpszClassName );

    mov( hInstance, wc.hInstance );

    // Get the icons and cursor for this application:

    w.LoadIcon( NULL, val w.IDI_APPLICATION );

    mov( eax, wc.hIcon );

    mov( eax, wc.hIconSm );

    w.LoadCursor( NULL, val w.IDC_ARROW );

    mov( eax, wc.hCursor );

    // Allow application-specific initialization of wc:

    initWC();

    // Okay, register this window with Windows so it

    // will start passing messages our way.  Once this

    // is accomplished, create the window and display it.

    w.RegisterClassEx( wc );

    // Have the application actually create the window -

    // It may want to supply different parameters to

    // w.CreateWindowEx, etc.

    appCreateWindow();

    // Here's the event loop that processes messages

    // sent to our window.  On return from GetMessage,

    // break if EAX contains false and then quit the

    // program.

    forever

        w.GetMessage( msg, NULL, 0, 0 );

        breakif( !eax );

        w.TranslateMessage( msg );

        w.DispatchMessage( msg );

    endfor;

    // The message handling inside Windows has stored

    // the program's return code in the wParam field

    // of the message.  Extract this and return it

    // as the program's return code.

    mov( msg.wParam, eax );

    w.ExitProcess( eax );

end WinMain;

If you try to compile this program to an executable with HLA, you will be disappointed by the results. HLA will complained bitterly about a whole host of undefined symbols. Names like initWC, appCreateWindow, ClassName, and AppCaption (among others) are not defined anywhere in this file. That is the purpose of the WinMain.hhf header file- to create external declarations for these symbols that the specific application must provide (WinMain.hhf also exports some symbols from WinMain.hla that the application will need). Here's the source file for the WinMain.hhf header file:

#if( ! @defined( winmain_hhf ))

?winmain_hhf := 1;

#includeonce( "w.hhf" )

type

    // Message and dispatch table related definitions:

    MsgProc_t:  procedure( hwnd:dword; wParam:dword; lParam:dword );

    MsgProcPtr_t:

        record

            MessageValue    :dword;

            MessageHndlr    :MsgProc_t;

        endrecord;

static

    hInstance   :dword; @external;

    wc          :w.WNDCLASSEX; @external;

    msg         :w.MSG; @external;

    hwnd        :dword; @external;

readonly

    ClassName   :string; @external;

    AppCaption  :string; @external;

    Dispatch    :MsgProcPtr_t; @external;

    procedure initWC; @external;

    procedure appCreateWindow; @external;

    procedure defaultCreateWindow; @returns( "eax" ); @external;

#endif

The WinMain.hla file exports the hInstance, wc, msg, and hwnd variables; the application must export the ClassName, AppCaption, and Dispatch variables. Similarly, the WinMain.hla program exports the defaultCreateWindow procedure while the user application must export the initWC and appCreateWindow procedures.

In order to use WinMain as a piece of library code, we need to compile it to an object (.OBJ) file and, optionally, add that object file to a library module. The following makefile will build WinMain.lib from WinMain.hla. Note: this makefile assumes that you have a copy of Microsoft's LIB.EXE program handy. If you don't have an appropriate librarian program available, just compile WinMain.hla to WinMain.obj and link in the object file with your programs rather than the ".lib" file.

build: winmain.lib

buildall: clean winmain.lib

compilerc:

    echo No Resource Files to Process!

syntax:

    hla -s -p:tmp winmain.hla

run:

    echo This module is a library, not an executable program

clean:

    delete tmp

    delete *.exe

    delete *.obj

    delete *.link

    delete *.inc

    delete *.asm

    delete *.map

    delete *.lib

winmain.lib: winmain.obj

    lib /out:winmain.lib winmain.obj

    copy winmain.lib ..

winmain.obj: winmain.hla wpa.hhf winmain.hhf

    hla -c -p:tmp winmain.hla

Of course, the WinMain.lib file appears on the CD-ROM accompanying this book, so even if you don't have a librarian program available you can use this library file.

7.6.5: The FofX.hla Application

Now that we've described how to eliminate a bunch of redundant code from our Win32 applications, we can take a look at the FofX.hla source file (minus all that redundant code). The best place to start is with the makefile for this project:

build: fofx.exe

buildall: clean fofx.exe

compilerc:

    echo No Resource Files to Process!

syntax:

    hla -s -p:tmp lines.hla

run: fofx.exe

    fofx

clean:

    delete tmp

    delete *.exe

    delete *.obj

    delete *.link

    delete *.inc

    delete *.asm

    delete *.map

fofx.exe: fofx.hla wpa.hhf winmain.hhf

    hla $(debug) -p:tmp -w fofx winmain.lib

The big difference between this makefile and the usual makefiles we've created up to this point is the fact that the command associated with the fofx.exe dependency links in the winmain.lib library module containing the generic win32 main program.

When creating a new RadASM project for an application that uses the generic WinMain main program, the steps you use are identical to those you've used before except that you also add the WinMain.lib file to the project and modify the makefile so that it links in the WinMain.lib file (by adding WinMain.lib to the end of the HLA command line in the makefile). Of course, there is one other important difference in the project as well - the main source file for the application must be an HLA unit rather than an HLA program. This is because HLA program files always create a main program and an application can only have one of these; the WinMain.lib module supplies the application's main program, so you can't link in a second compiled program file. This, however, is a trivial change to the way you write your code - just put the application within an HLA unit and you're in business.

Here's the HLA source code for the FofX.hla application (note that this is an HLA unit, that we must compile to object code and link with the generic main program we supply in the WinMain.lib library file):

// FofX.hla-

//

//  Program that demonstrates the use of Polyline to plot a set of function

//  values. Demonstrates drawing curves via Polyline.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit FofX;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

// The following constant defintion defines how many functions we're going

// to plot with this program:

const

    numFuncs := 4;

// The following data type is used to hold the pertinent values needed to

// plot a single function - including the function's address and pen type.

type

    plotRec:

        record

            f           :procedure( x:uns32); @returns( "eax" );

            lineType    :dword;

            lineWidth   :dword;

            lineColor   :w.COLORREF;

        endrecord;

// The following table has one entry for each of the functions we're

// going to plot. It specifies the function to call and the pen info.

// The number of entries in this table must match the numFuncs constant

// value (this table must be maintained manually!).

readonly

    plotInfo : plotRec[ numFuncs ] :=

        [

            plotRec:[ &sin, w.PS_SOLID, 0, RGB( $F0, $70, $F0) ],

            plotRec:[ &cos, w.PS_SOLID, 0, RGB( $00, $C0, $C0) ],

            plotRec:[ &tan, w.PS_SOLID, 0, RGB( $C0, $C0, $00) ],

            plotRec:[ &log, w.PS_SOLID, 0, RGB( $FF, $C0, $80) ]

        ];

static

    ClientX             :uns32;

    ClientY             :uns32;

    pens                :dword[ numFuncs ]; // Pens used for each plot.

    axisPen             :dword;

readonly

    ClassName   :string := "FofXWinClass";          // Window Class Name

    AppCaption  :string := "F(x) Program";          // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - Just for fun, let's set the background color to a dark gray

//          to demonstrate how we use the initWC procedure:

procedure initWC; @noframe;

begin initWC;

    mov( w.COLOR_GRAYTEXT+1, wc.hbrBackground );

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// Here are the functions we're going to plot:

//

//

// sin- plots the sine of the angle (in degrees) passed as the parameter:

procedure sin( x:uns32 ); @returns( "eax" );

var

    WinHt   :uns32;

begin sin;

    // Note: fsin wants radians, so convert x to radians as

    // radians = degrees*pi/180:

    fild( x );

    fld( 180.0 );

    fdiv();

    fldpi();

    fmul();

    // Compute the sine of the angle:

    fsin();

    // Sine produces a result between -1..+1, scale to within 90% of

    // the top and bottom of our window:

    mov( ClientY, eax );

    shr( 1, eax );

    mov( eax, WinHt );

    fild( WinHt );

    fmul();

    fld( 0.9 );

    fmul();

    fild( WinHt );

    fadd();

    // Return an integer result as our Y-axis value:

    fistp( WinHt );

    mov( WinHt, eax );

end sin;

// cos- plots the cosine of the angle (in degrees) passed as the parameter:

procedure cos( x:uns32 ); @returns( "eax" );

var

    WinHt   :uns32;

begin cos;

    // See sin for comments. Same exact code except we use fcos rather than

    // fsin to compute the function result.

    fild( x );

    fld( 180.0 );

    fdiv();

    fldpi();

    fmul();

    fcos();

    mov( ClientY, eax );

    shr( 1, eax );

    mov( eax, WinHt );

    fild( WinHt );

    fmul();

    fld( 0.9 );

    fmul();

    fild( WinHt );

    fadd();

    fistp( WinHt );

    mov( WinHt, eax );

end cos;

// tan- plots the tangent of the angle (in degrees) passed as the parameter:

procedure tan( x:uns32 ); @returns( "eax" );

var

    WinHt   :uns32;

begin tan;

    // See "sin" for comments. Same code except we use fsincos and fdiv

    // to compute the tangent:

    fild( x );

    fld( 180.0 );

    fdiv();

    fldpi();

    fmul();

    fsincos();

    fdiv();

    mov( ClientY, eax );

    shr( 1, eax );

    mov( eax, WinHt );

    fild( WinHt );

    fmul();

    fld( 0.1 );

    fmul();

    fild( WinHt );

    fadd();

    fistp( WinHt );

    mov( WinHt, eax );

end tan;

// log- Well, this isn't really a log function. It computes a combination

//      of log and sin the produces a nice looking curve.

procedure log( x:uns32 ); @returns( "eax" );

var

    WinHt   :uns32;

begin log;

    if( x = 0 ) then

        xor( eax, eax );

    else

        fild( x );

        math.log();

        fsin();

        // Scale to within 90% of our window height (note that the fsin

        // instruction returns a value between -1..+1).

        fild( ClientY );

        fmul();

        fld( 0.9 );

        fmul();

        fistp( WinHt );

        mov( WinHt, eax );

    endif;

end log;

// The Create procedure creates all the pens we're going to use

// in this application to plot the various functions

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

begin Create;

    // Create pens for each of the functions:

    #for( i := 0 to numFuncs-1 )

        w.CreatePen

        (

            plotInfo.lineType [ i*@size( plotRec ) ],

            plotInfo.lineWidth[ i*@size( plotRec ) ],

            plotInfo.lineColor[ i*@size( plotRec ) ]

        );

        mov( eax, pens[ i*4 ] );

    #endfor

    // Create a thicker, gray, pen for drawing the axis:

    w.CreatePen( w.PS_SOLID, 2, RGB( $20, $20, $20) );

    mov( eax, axisPen );

end Create;

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Delete the pens we created in the Create procedure:

    #for( i := 0 to numFuncs-1 )

        w.DeleteObject( pens[ i*4 ] );

    #endfor

    w.DeleteObject( axisPen );

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

    oldPen      :dword;

    polyLinePts :dword;

begin Paint;

    // We need to allocate a dynamic array to hold the points we're going

    // to plot. The ClientX global specifies the number of array elements.

    // Note that we must allocate this storage before pushing any registers

    // (or anything else) onto the stack. We rely upon the activation record

    // clean-up on return to deallocate this storage.

    intmul( @size( w.POINT ), ClientX, eax );

    talloc( eax );

    mov( eax, polyLinePts );

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Draw an axis in the window:

        SelectObject( axisPen ); // Select the gray pen.

        mov( eax, oldPen );      // Save, so we can restore later

        MoveToEx( 1, 0, NULL );  // Draw the vertical axis

        LineTo( 1, ClientY );

        mov( ClientY, ebx );     // Draw the horizontal axis.

        shr( 1, ebx );

        MoveToEx( 0, ebx, NULL );

        LineTo( ClientX, ebx );

        // For each of the functions, generate an array of points (the

        // polyline) and plot those points:

        mov( polyLinePts, ebx );

        for( mov( 0, edi ); edi < numFuncs; inc( edi )) do

            for( mov( 0, esi ); esi < ClientX; inc( esi )) do

                intmul( @size( plotRec ), edi, eax );

                plotInfo.f[eax]( esi );

                mov( esi, (type w.POINT [ebx]).x[ esi*8 ] );

                mov( eax, (type w.POINT [ebx]).y[ esi*8 ] );

            endfor;

            SelectObject( pens[ edi*4 ] );

            Polyline( [ebx], ClientX );

        endfor;

        // Restore original pen:

        SelectObject( oldPen );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientY );

    xor( eax, eax ); // return success.

end Size;

end FofX;

7.6.6: Pen Drawing Modes

When Windows draws a line from (x1,y1) to (x2,y2), it plots a pixel at (x1,y1) and fills in the pixels up to but not including (x2,y2), see Figure 7-3 for details. Though this might seem somewhat bizarre, there is a very good reason that Windows doesn't actually fill in the last pixel of a line segment that it draws: the drawing mode and raster operations.

Figure 7-3: Line Plotting in Windows

Whenever Windows draws a line on the screen, it doesn't simply force the pixels to a given color at the points on the display where the line is drawn. Instead, Windows uses one of 16 different logical operations to merge the pixel that would be drawn for the line with the pixel that is already on the screen. The exact mechanism used to place the pixel on the display is called a "binary raster operation" or "ROP2" and the exact drawing mode is kept as part of the device context data structure. Windows supports 16 different functions for transfering pixel data to the display based on the pixel to be draw and the pixel already on the display3. Table 7-3 lists these 16 operations.

Table 7-3 also shows the results of the boolean calculations assuming you're plotting black (0) or white (1) pixels. In fact, most PCs operate with a full-color display. So the result won't be simply black or white, but some combination of colors produced by running these boolean calculations on the n-bit-deep color pixels (in a bitwise fashion).

Table 7-3: Windows Pen Drawing Modes
Pen
1
1
0
0
Mathematical Description
Drawing Mode
Display
1
0
1
0
0
0
0
0
0
R2_BLACK
0
0
0
1
not (pen or display)
R2_NOTMERGEPEN
0
0
1
0
(not pen )& display
R2_MASKNOTPEN
0
0
1
1
not pen
R2_NOTCOPYPEN
0
1
0
0
pen and (not display)
R2_MASKPENNOT
0
1
0
1
not display
R2_NOT
0
1
1
0
pen xor display
R2_XORPEN
0
1
1
1
not(pen and display)
R2_NOTMASKPEN
1
0
0
0
pen and display
R2_MASKPEN
1
0
0
1
not (pen xor display)
R2_NOTXORPEN
1
0
1
0
display
R2_NOP
1
0
1
1
(not pen) or display
R2_MERGENOTPEN
1
1
0
0
pen
R2_COPYPEN (default)
1
1
0
1
pen or (not display)
R2_MERGEPENNOT
1
1
1
0
pen or display
R2_MERGEPEN
1
1
1
1
1
R2_WHITE

Table 7-3 shows the results of the boolean calculations assuming you're plotting black (0) or white (1) pixels. In fact, most PCs operate with a full-color display. So the result won't be simply black or white, but some combination of colors produced by running these boolean calculations on the n-bit-deep color pixels (in a bitwise fashion).

The CopyModes.hla application demonstrates the various Window pen copying modes when using black and white on the display (sorry, but reproduction as well as logistical problems prevent the presentation of a version that uses color; however, feel free to modify CopyModes.hla appropriately to see what happens when you use different colors beyond black and white). This program begins by drawing a set of 16 wide black lines on the display, then it draws two lines (one black, one white) over the top of each of these 16 background lines, using a different copy mode in each of the 16 cases (Figure 7-4 shows the output this program produces).

Figure 7-4: CopyModes.hla Program Output

Note that the CopyModes.hla program uses the WinMain library developed earlier to reduce the size of our Win32 assembly programs.

// CopyModes.hla-

//

//  Program that demonstrates the use of the SetROP2 API function to set

// the Windows copy mode (for drawing with a pen).

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit CopyModes;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

// copy_t-

// This record type holds a caption string and a copy mode

// value for use when demostrating the 16 different copy

// modes on the display.

type

    copy_t:

        record

            msg :string;

            cm  :dword;

        endrecord;

// cmm-

//  This is a utility macro used to save some typing when

// filling in an array of 16 "copy_t" array elements. An

// invocation of the form "cmm( xyz )" produces the following:

//

//  copy_t:[ "xyz", w.xyz ]

//

// The intent is to supply a Windows Drawing mode as the macro

// argument (see the copyData array, below).

#macro cmm( mode );

    copy_t:[ @string( mode ), @text( "w." + @string( mode )) ]

#endmacro

static

    bkgndPen    :dword;

    whitePen    :dword;

    fgndPen     :dword;

    copyData    :copy_t[16] :=

        [

            cmm( R2_BLACK ),

            cmm( R2_NOTMERGEPEN ),

            cmm( R2_MASKNOTPEN ),

            cmm( R2_NOTCOPYPEN ),

            cmm( R2_MASKPENNOT ),

            cmm( R2_NOT ),

            cmm( R2_XORPEN ),

            cmm( R2_NOTMASKPEN ),

            cmm( R2_MASKPEN ),

            cmm( R2_NOTXORPEN ),

            cmm( R2_NOP ),

            cmm( R2_MERGENOTPEN ),

            cmm( R2_COPYPEN ),

            cmm( R2_MERGEPENNOT ),

            cmm( R2_MERGEPEN ),

            cmm( R2_WHITE )

        ];

readonly

    ClassName   :string := "CopyModesWinClass";     // Window Class Name

    AppCaption  :string := "CopyModes Program";     // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// The Create procedure creates all the pens we're going to use

// in this application.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

begin Create;

    // Create a thick, black, pen for drawing the axis:

    w.CreatePen( w.PS_SOLID, 24, RGB( $0, $0, $0) );

    mov( eax, bkgndPen );

    // Create a thin, black, pen for demonstrating the copy modes:

    w.CreatePen( w.PS_SOLID, 12, RGB( $0, $0, $0) );

    mov( eax, fgndPen );

    // Create a thin, white, pen for demonstrating the copy modes:

    w.CreatePen( w.PS_SOLID, 12, RGB( $FF, $FF, $FF) );

    mov( eax, whitePen );

end Create;

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Delete the pens we created in the Create procedure:

    w.DeleteObject( bkgndPen );

    w.DeleteObject( whitePen );

    w.DeleteObject( fgndPen );

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

    oldPen      :dword;

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Draw a set of black lines in the window:

        SelectObject( bkgndPen );  // Select the fat black pen.

        mov( eax, oldPen );      // Save, so we can restore later

        // Demonstrate each of the 16 pen/copy/draw modes,

        // one mode per iteration of the following loop:

        for( mov( 0, esi ); esi < 16; inc( esi )) do

            // Compute the Y-offset for each line

            // on the display:

            intmul( 32, esi, edi );

            add( 8, edi );

            // Print a caption for each line:

            TextOut

            (

                32,

                edi,

                copyData.msg[ esi*8 ],

                str.length( copyData.msg[ esi*8] )

            );

            add( 8, edi );

            // Display a background black line

            // to draw against:

            SetROP2( w.R2_COPYPEN );

            SelectObject( bkgndPen );  // Select the fat black pen.

            MoveToEx( 240, edi, NULL );

            LineTo( 440, edi );

            // Change the current copy mode to demonstrate each

            // of the copy modes:

            SetROP2( copyData.cm[ esi*8 ] );

            // Draw a white line, using the curren copy mode,

            // over the first half of the background line:

            SelectObject( whitePen );  // Select the fat black pen.

            MoveToEx( 200, edi, NULL );

            LineTo( 330, edi );

            // Draw a black line, using the current copy mode,

            // over the second half of the background line.

            SelectObject( fgndPen );  // Select the fat black pen.

            MoveToEx( 350, edi, NULL );

            LineTo( 480, edi );

        endfor;

        // Restore original pen:

        SelectObject( oldPen );

        SetROP2( w.R2_COPYPEN );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end CopyModes;

7.6.7: Paths

Lines are useful graphic primitives from which you can draw many other types of objects. For example, with four lines you can create a rectangle on the display. As you'll see in a moment, however, Windows treats connected objects like rectangles a little differently than it does an arbitrary set of four lines drawn on the screen. For example, you can fill rectangular objects with some pattern or color whereas no such concept like "fill" exists for a set of four arbitrary lines appearing on the display; even if the position of those four lines just happen to visually form a rectangle on the display. What is needed is some mechanism for telling Windows that a collection of lines form a special object. That special mechanism is the path.

A path in Windows is a recording. That is, you record a sequence of line drawing operations within Windows and then "play that recording back" to draw those sequence of lines as a single object (this is known as rendering the path). To record a path, you use the w.BeginPath and w.EndPath API functions:

static

    BeginPath: procedure( hdc:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__BeginPath@4" );

    EndPath: procedure( hdc :dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__EndPath@4" );

Calling w.BeginPath (passing the device context handle that BeginPaint produces) tells Windows to start recording all calls that draw lines (e.g., LineTo, PolylineTo, BezierTo, and so on) into an internal buffer rather than rendering them to the output device. When you call w.EndPath (also passing the device context handle), Windows finishes the recording. You may then draw the path you've recorded as a single object using the w.StrokePath API call:

static

    StrokePath: procedure( hdc:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__StrokePath@4" );

Here is a typical example of such a w.BeginPath/w.Endpath/w.StrokePath calling sequence:

            w.BeginPath( hdc );

                // Draw the path that forms a triangle:

                MoveToEx( edi, 270, NULL );

                add( 40, edi );

                LineTo( edi, 270 );

                LineTo( edi, 230 );

                sub( 40, edi );

                LineTo( edi, 270 );

            w.EndPath( hdc );

            w.StrokePath( hdc );

As these are all GDI calls, they must appear inside a BeginPath/EndPaint or comparable sequence.

Now it may seem like a lot of work to go through the process of creating a path. After all, you can just as easily draw these lines on the display using the line drawing APIs without recording and rendering the path. However, you should note that Windows will draw paths a little differently than it draws arbitrary lines (you'll see the difference in a moment). Another advantage of using paths is that you can tell Windows to fill the enclosed regions of a path with some pattern (we'll take a look at this capability a little later in this chapter). Rest assured, there are some very good reasons for recording a sequence of lines as a path rather than drawing them directly.

One advantage of creating a path is that you can create a closed object when drawing a path, something that may not be so easy to do when drawing the objects outside a path. You can close a path you're recording with a call to the w.CloseFigure API call:

static

    CloseFigure: procedure( hdc:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CloseFigure@4" );

The w.CloseFigure API function draws a straight line from the endpoint of the last line you've drawn, to the beginning of the first line you've drawn in the path. Note that simply drawing a line to the same coordinate as the starting point of your line does not create a closed figure. Remember, LineTo and other line drawing functions draw up to, but not including, the end point you specify. From Window's perspective, this is not necessarily a closed object. Therefore, calling the w.CloseFigure function is a good way to finish drawing a path before closing the path with a call to w.EndPath.

Technically, a path inside a w.BeginPath..w.EndPath sequence consists of all the connected lines that you've drawn (that is, the sequence of LineTo, Polylineto, BezierTo, and so on, calls you've made). If you call MoveToEx within a path sequence, Windows treats the following calls as a separate path (a "subpath") and renders those objects separately. Usually, you'd create a single, connected, object inside a w.BeginPath..w.EndPath sequence.

7.6.8: Extended Pen Styles

When you draw lines that are one pixel in width, connecting lines together (e.g., as part of a path) is relatively straight-forward - just draw the first pixel of the second line in some sequence just beyond the last pixel of the first line in some sequence (and so on for each additional line) and, presto, the two lines mate and look like a single contiguous object. However, when you create a pen whose width is greater than one pixel, attempts to draw two connected lines via a sequence of LineTo (or similar) calls produces some annoying artifacts on the display. to alleviate these problems, Windows provides a second pen creation routine, w.ExtCreatePen, that lets you specify some additional pen styles specifically for wide pens. The prototype for w.ExtCreatePen is

static

    ExtCreatePen: procedure

    (

            dwPenStyle      :dword;

            dwWidth         :dword;

        var lplb            :w.LOGBRUSH;

            dwStyleCount    :dword;

        var lpStyle         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ExtCreatePen@20" );

The dwPenStyle parameter is where you specify the new styles (we'll get to these in a moment), the dwWidth parameter specifies the width (in pixels or logical units) of the line, the lplb parameter is where you specify a pattern and color for the pen to draw. The dwStyleCount and lpStyle parameters we'll just set to zero (NULL) for the time being.

The dwPenStyle parameter can have all the bit values allowed for the styles that are legal for the w.CreatePen API call. In addition to those styles, you may also specify the styles shown in Table 7-4. As with the style values you specify for w.CreatePen, these are all single bit values and you may combine these values using the HLA compile-time bitwise-OR operator ("|").

Table 7-4: dwPenStyle Values for the w.ExtCreatePen API Function
Pen Style
Description
w.PS_COSMETIC
Generally used with dashed or dotted pens (though this was a restriction of Windows 95 that has long since been removed).
w.PS_GEOMETRIC
This tells Windows to draw the line using a width specified in logical units).
w.PS_ENDCAP_ROUND
This style tells Windows to "cap" each end of the line with a half-circle. Note that the cap extends beyond the endpoints of the line. See Figure 7-5 for an example.
w.PS_ENDCAP_SQUARE
This style tells Windows to "cap" each end of the line with a rectangle whose width is half the line's width. This rectangular cap extends beyond the endpoints of the line. See Figure 7-5 for an example.
w.PS_ENDCAP_FLAT
This style tells Windows to skip capping each end of the line. The line is drawn as a rectangle between the two endpoints the caller passes to the line drawing routine. See Figure 7-5 for an example.
w.PS_JOIN_ROUND
This style defines how Windows will connect two lines that are drawn in sequence (that is, the endpoint of one line is the starting point of the second line). This particular style specifies a rounded edge between the two lines when drawing lines in a sequence. See Figure 7-6 for an example.
w.PS_JOIN_BEVEL
This style defines how Windows will connect two lines that are drawn in sequence (that is, the endpoint of one line is the starting point of the second line). This particular style specifies a straight edge between the edges of the two lines when drawing lines in a sequence. See Figure 7-6 for an example.
w.PS_JOIN_MITER
This style defines how Windows will connect two lines that are drawn in sequence (that is, the endpoint of one line is the starting point of the second line). This particular style tells Windows to extend the outside edges of two lines until those edges intersect. See Figure 7-6 for an example.

Figure 7-5: Endcaps on a Line Drawn by Windows

Figure 7-6: Line Joins Drawn by Windows

Note that the w.PS_ENDCAP_* styles are mutually exclusive (that is, you may only select one of the endcap styles). Likewise, the w.PS_JOIN_* styles are all mutually exclusive and you may only select one of them.

Windows draws lines using the w.PS_JOIN_* styles only when it is rendering a path. If you set these styles for a given pen and then draw a sequence of lines without first creating a path, Windows will not bother to join the lines as you would expect.

The lplb parameter you pass to w.ExtCreatePen specifies a brush that Windows will use when drawing the line. We'll talk about brushes a little later in this chapter. For now, just realize that this parameter is where you specify the color of the line that you will draw with the pen. The lplb parameter is of type w.LOGBRUSH (a Windows "brush" type) and this is a record object that has three fields we'll need to initialize before calling w.ExtCreatePen: the lbStyle field, the lbColor field, and the lbHatch field. For normal pens, you'll set the lbStyle field to w.BS_SOLID (brush style solid), the lbColor field to whatever RGB value you want to draw the line with, and the lbHatch field we'll set to zero (no hatch markings). We'll take another look at brush style values when we talk about brushes later in this chapter.

The ExtendedPens.hla application demonstrates the use of the w.ExtCreatePen API function. It draws a sequence of lines with various endcaps and then draws a series of triangles to demonstrate the path operations and the pen join modes. Figure 7-7 shows the output from this application.

Figure 7-7: ExtendedPens.hla Program Output

As usual, now, the ExtendedPen.hla application uses the WinMain.lib library files to reduce the size of the source file.

// ExtendedPens.hla-

//

//  Program that demonstrates the use of the ExtCreatePen API function.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit ExtendedPens;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

readonly

    ClassName   :string := "ExtendedPensWinClass";      // Window Class Name

    AppCaption  :string := "ExtendedPens Program";      // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    oldPen      :dword;         // Used to save the original pen.

    grayPen     :dword;

    roundPen    :dword;

    squarePen   :dword;

    flatPen     :dword;

    tempPen     :dword;

    ps          :w.PAINTSTRUCT; // Used while painting text.

    lb          :w.LOGBRUSH;    // Used to create pens.

readonly

    penStyles: dword[ 9 ] :=

        [

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_ROUND | w.PS_JOIN_ROUND,

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_ROUND | w.PS_JOIN_BEVEL,

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_ROUND | w.PS_JOIN_MITER,

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_SQUARE | w.PS_JOIN_ROUND,

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_SQUARE | w.PS_JOIN_BEVEL,

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_SQUARE | w.PS_JOIN_MITER,

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_FLAT | w.PS_JOIN_ROUND,

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_FLAT | w.PS_JOIN_BEVEL,

            w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_FLAT | w.PS_JOIN_MITER

        ];

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Create a thin, gray, pen for annotation purposes:

    w.CreatePen( w.PS_SOLID, 0, RGB( $A0, $A0, $A0) );

    mov( eax, grayPen );

    // Set up "lb" for use when creating the following pens:

    mov( w.BS_SOLID, lb.lbStyle );

    mov( RGB( 0, 0, 0 ), lb.lbColor );

    mov( 0, lb.lbHatch );

    // Create a thick, black, pen for drawing the axis:

    w.ExtCreatePen

    (

        w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_ROUND,

        16,

        lb,

        0,

        NULL

    );

    mov( eax, roundPen );

    w.ExtCreatePen

    (

        w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_SQUARE,

        16,

        lb,

        0,

        NULL

    );

    mov( eax, squarePen );

    w.ExtCreatePen

    (

        w.PS_SOLID | w.PS_GEOMETRIC | w.PS_ENDCAP_FLAT,

        16,

        lb,

        0,

        NULL

    );

    mov( eax, flatPen );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        SelectObject( roundPen );  // Select the round pen.

        mov( eax, oldPen );        // Save, so we can restore later

        TextOut( 5, 20, "Round Pen", 9 );

        MoveToEx( 100, 25, NULL );

        LineTo( 200, 25 );

        TextOut( 5, 70, "Square Pen", 10 );

        SelectObject( squarePen );  // Select the square pen.

        MoveToEx( 100, 75, NULL );

        LineTo( 200, 75 );

        TextOut( 5, 120, "Flat Pen", 8 );

        SelectObject( flatPen );   // Select the flat pen.

        MoveToEx( 100, 125, NULL );

        LineTo( 200, 125 );

        SelectObject( grayPen );

        MoveToEx( 100, 10, NULL );

        LineTo( 100, 150 );

        MoveToEx( 200, 10, NULL );

        LineTo( 200, 150 );

        // Draw a sequence of triangles to demonstrate the join mode

        // when drawing (non-stroked) lines:

        for( mov( 0, esi ); esi < 9; inc( esi )) do

            // Select one of the nine pen styles here:

            mov( penStyles[ esi*4 ], eax );

            w.ExtCreatePen

            (

                eax,

                12,

                lb,

                0,

                NULL

            );

            mov( eax, tempPen );

            SelectObject( eax );

            // Draw a triangle using the selected pen:

            intmul( 64, esi, edi );

            add( 20, edi );

            MoveToEx( edi, 200, NULL );

            add( 40, edi );

            LineTo( edi, 200 );

            LineTo( edi, 160 );

            sub( 40, edi );

            LineTo( edi, 200 );

            // Draw a triangle using the thin, gray, pen

            // over the top of the existing triangle to

            // show where we've drawn:

            SelectObject( grayPen );

            add( 40, edi );

            LineTo( edi, 200 );

            LineTo( edi, 160 );

            sub( 40, edi );

            LineTo( edi, 200 );

            // Delete the pen we created above:

            w.DeleteObject( tempPen );

        endfor;

        // Draw a sequence of triangles to demonstrate

        // the join mode when drawing stroked lines:

        for( mov( 0, esi ); esi < 9; inc( esi )) do

            // Select one of the nine pen styles here:

            mov( penStyles[ esi*4 ], eax );

            w.ExtCreatePen

            (

                eax,

                12,

                lb,

                0,

                NULL

            );

            mov( eax, tempPen );

            SelectObject( eax );

            // Capture all the strokes:

            w.BeginPath( hdc );

                // Draw a triangle using the selected pen:

                //

                // Draw the triangles 64 units apart:

                intmul( 64, esi, edi );

                // Start at x-coordinate 20 for the first triangle:

                add( 20, edi );

                // Draw the path that forms the triangle:

                MoveToEx( edi, 270, NULL );

                add( 40, edi );

                LineTo( edi, 270 );

                LineTo( edi, 230 );

                sub( 40, edi );

                LineTo( edi, 270 );

                // Create a closed path

                // (so 'end' joins with 'start' properly):

                w.CloseFigure( hdc );

            w.EndPath( hdc );

            // Actually draw the triangle based on the path

            // we've captured above:

            w.StrokePath( hdc );

            // Draw a triangle using the thin, gray, pen

            // over the top of the existing triangle to

            // show where we've drawn:

            SelectObject( grayPen );

            intmul( 64, esi, edi );

            add( 20, edi );

            MoveToEx( edi, 270, NULL );

            add( 40, edi );

            LineTo( edi, 270 );

            LineTo( edi, 230 );

            sub( 40, edi );

            LineTo( edi, 270 );

            // Delete the pen we created above:

            w.DeleteObject( tempPen );

        endfor;

        // Restore original pen:

        SelectObject( oldPen );

    EndPaint;

    // Delete the pens we created:

    w.DeleteObject( grayPen );

    w.DeleteObject( roundPen );

    w.DeleteObject( squarePen );

    w.DeleteObject( flatPen );

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end ExtendedPens;

7.7: Bounding Boxes and Rectangles in Windows

Once you get past lines, that have a very simple mathematical definition (two points!), the way people define other objects varies tremendously. Windows, like the Macintosh before it, uses the concept of a "bounding box" to descibe how to draw complex objects. A bounding box is the smallest rectangular box (realized with horizontal and vertical lines) that completely surrounds an object. In Windows, you'll usually specify a bounding box using a w.RECT object. Under Windows, a w.RECT object takes the following form:

type

    RECT:

        record

            left:   dword;

            top:    dword;

            right:  dword;

            bottom: dword;

        endrecord;

The left field specifies the x-coordinate of the left-hand side of the bounding rectangle (in local coordinate units). The top field specifies the y-coordinate of the top of the bounding rectangle. The right field specifies the x-coordinate of the right-hand side of the bounding box. The bottom field specifies the y-coordinate of the bottom of the bounding rectangle (see Figure 7-8). Some people also like to define a bounding box by two points at opposite corners of the rectangle. Usually they use the two points specified by (left, top) and (right, bottom), see Figure 7-8, but (right, top) and (left, bottom) could just as easily describe the rectangle.

Figure 7-8: Bounding Rectangle Relationship with a w.RECT Object

One problem with the bounding box model is that a bounding box rectangle and a rectangle you actually render on the screen are similar in a visual sense. However, there is a big difference between the two. A bounding box is a mathematical rectangle made up of mathematical lines. In mathematics, a line has no width. This is also true for a bounding box, as a bounding box is a mathematical concept made up of mathematical lines. A rectangle we render on the display, however, is not a mathematical rectangle because the lines we use to draw it must have some width (i.e., at least one pixel) in order to be visible on the display. So the question that should come up is "How does one reconcile the differences between these two types of rectangles when physically drawing objects (that use a bounding box) in a device context?" The answer is that Windows always draws from the top coordinate to, but not including, the bottom coordinate and from the left coordinate to, but not including, the right coordinate. The reasons for doing it this way are the same reasons Windows draws a line from the line's starting point up to, but not including, the end point (see Figure 7-9).

Figure 7-9: How the Bounding Box Circumscribes an Object

The Windows API provides several different functions you can call to manipulate rectangles. A typical example is the w.SetRect function:

static

    SetRect: procedure

    (

        var lprc        :RECT;

            xLeft       :dword;

            yTop        :dword;

            xRight      :dword;

            yBottom     :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetRect@20" );

The w.SetRect function initializes the w.RECT object you pass as its first parameter with the coordinate values you pass in the remaining four parameters. While this function does offer a fair amount of convenience, I would hope you realize that you're paying a heavy price whenever you call this API function to initialize a w.RECT structure. If you want the convenience of this function, you should create your own macro that does the same job, e.g.:

#macro SetRect( theRect, left, top, right, bottom );

    mov( left, (type w.RECT theRect).left );

    mov( top, (type w.RECT theRect).top );

    mov( bottom, (type w.RECT theRect).bottom );

    mov( right, (type w.RECT theRect).right );

#endmacro

        .

        .

        .

    SetRect( SomeRectangle, 0, 0, 5, 10 );

This macro coerced its first parameter to type w.RECT to allow you to pass parameters like the following:

    SetRect( [ebx], 0, 1, 2, 3 );

Of course, it's perfectly legal to pass an actual w.RECT object as the first parameter, as well.

Windows provides several additional rectangle related functions that may prove useful when dealing with objects of type w.RECT (e.g., bounding boxes). Though these routines are usable anyplace you need to manipulate w.RECT objects in Windows, this seems like a good place to introduce these functions as many of them are quite useful when working with bounding boxes.

    RectVisible: procedure( hdc:dword; var lprc:RECT );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__RectVisible@8" );

The w.RectVisible function checks to see if the rectangle specified by lprc is within the clipping region of the device context whose handle is the first parameter to this call. This function returns false (zero) if no part of the rectangle is within the clipping region. This function returns a non-zero value indicating true if some part of the rectangle is within the device context's clipping region.

    CopyRect: procedure

    (

        var lprcDst :RECT;

        var lprcSrc :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CopyRect@8" );

The w.CopyRect function will make a copy of the source rectangle into the destination rectangle. Note that copying a rectangle (which is just four dwords) is a fairly trivial operation and writing a macro to do this operation would be a far more efficient solution, e.g.,

#macro CopyRect( src, dest ); // Note the reversal of the parameters

    mov( (type w.RECT src).top, eax );

    mov( eax, (type w.RECT dest).top );

    mov( (type w.RECT src).left, eax );

    mov( eax, (type w.RECT dest).left );

    mov( (type w.RECT src).bottom, eax );

    mov( eax, (type w.RECT dest).bottom );

    mov( (type w.RECT src).right, eax );

    mov( eax, (type w.RECT dest).right );

#endmacro

Note that this macro will wipe out the value previously held in the EAX register (of course, the w.CopyRect API function also changes the value in EAX).

    EqualRect: procedure( var lprc1:RECT; var lprc2:RECT );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__EqualRect@8" );

The w.EqualRect function compares the coordinates of two rectangles and returns true (non-zero) in EAX if the two rectangles have the same coordinates; it returns false (zero) if the coordinates are not the same. As for many of the simler rectangle routines, you'll get better performance if you compare the coordinate directly, yourself (or write a macro to do this).

    InflateRect: procedure

    (

        var lprc    :RECT;

            _dx     :dword;

            _dy     :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__InflateRect@12" );

The w.InflateRect function increases the width of the rectangle specified by the signed integer value found in _dx. It also increases the height of the rectangle by the value found in _dy. The _dx and _dy values may be negative, which will shrink the size by the specified amount.

    IntersectRect: procedure

    (

        var lprcDst     :RECT;

        var lprcSrc1    :RECT;

        var lprcSrc2    :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__IntersectRect@12" );

The w.IntersectRect API function computes the intersection of two rectangles and stores the resulting rectangle into the rectangle object you pass for the lprcDst parameter. If the intersection of the two source rectangles is empty, then this function returns an empty rectangle (which is a w.RECT value with all coordinates set to zero).

    OffsetRect: procedure

    (

        var lprc        :RECT;

            _dx         :dword;

            _dy         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__OffsetRect@12" );

The w.OffsetRect function adds the signed integer value found in _dx to the left and right fields of the lprc w.RECT object you pass as a parameter. It also adds the value in _dy to the top and bottom fields. The _dx and _dy values may be negative, which will shrink the coordinates by the specified amount. Note that if _dx and _dy are both constant values, you can do this same task with only four add instructions:

    add( _dx, lprc.left ); // _dx must be a constant!

    add( _dx, lprc.right );

    add( _dy, lprc.top );  // _dy must be a constant!

    add( _dy, lprc.bottom );

    SetRectEmpty: procedure( var lprc:RECT );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetRectEmpty@4" );

The w.SetRectEmpty function sets all the coordinates of the specified rectangle to zero
(the empty rectangle). Of course, four simple mov instructions will also do the same trick.
Recommendation: make a macro for this operation if you do it frequently.

    SubtractRect: procedure

    (

        var lprcDst     :RECT;

        var lprcSrc1    :RECT;

        var lprcSrc2    :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SubtractRect@12" );

The w.SubtractRect function creates a new rectangle by subtracting the coordinates in lprcSrc2 from the corresponding coordinates in lprcSrc1. This function stores the resulting rectangle in the object pointed at by lprcDst.

    UnionRect: procedure

    (

        var lprcDst     :RECT;

        var lprcSrc1    :RECT;

        var lprcSrc2    :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__UnionRect@12" );

The w.UnionRect API function takes the union of two rectangles and returns that value as the destination rectangle. The union of two rectangles is the smallest rectangle that complete surrounds the two source rectangles. This is the minimum of the two left and top values, and the maximum of thetwo right and bottom values in the source rectangles.

7.8: Drawing Objects in Windows

Although you can use Windows' line drawing primitives to draw just about any shape you please, there are a couple of problems with this approach:

  • You actually have to know the mathematical details behind the creation of such objects (and they aren't all as trivial as drawing rectangles).
  • Some objects cannot be drawn efficiently using only line drawing primitives (e.g., filled objects).
  • You'll have to write and test a lot of code.

Fortunately, you don't have to drop down to this level of detail. Windows provides lot of additional graphics primitives you can use to draw shapes in a device context. Even for those application specific objects that Windows does not provide for you, you're better off creating those objects using the full set of graphic primitives in Windows rather than attempting to draw everything using lines.

7.8.1: Filling Objects and Windows' Brushes

One big differece between simple line objects and the more complex objects that Windows can render is the fact that many graphical objects can enclose a region. For example, consider a rectangle that consists of four lines; those four lines enclose a rectangular region. When drawing in black and white, a typical rectangle might consist of four black lines enclosing a white rectangular area. However, it's also quite possible for those same four lines to enclose a black rectangular area. The difference is how you've chosen to fill that area (see Figure 7-10).

Figure 7-10: Filling a Rectangular Area

Although the images in Figure 7-10 give the impression that you can fill an object with a single color (e.g., black or white), Windows is actually far more sophisiticated than this. Of course, you can fill a rectangle with any solid color you like, not just black and white. Beyond this, you can even specify a specific fill pattern to use (rather than specifying that all pixels be filled with the same solid color).

Windows uses the current device context brush to fill the interior of an object. A brush is an eight-by-eight bitmap that is repeated in a "tiled" fashion to fill in an area. Windows provides several functions that create brushes for you, they are:

    CreateSolidBrush: procedure( crColor:COLORREF );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateSolidBrush@4" );

The w.CreateSolidBrush function creates a solid brush that draws what Windows believes is a solid color. You specify an RGB color value as the parameter and this function returns a handle to the brush that you can use to draw that solid color. Note that not all display devices can render 24-bit RGB values, Windows may attempt to draw the "solid" color using dithering (which doesn't look very solid up close).

    CreateHatchBrush: procedure( fnStyle:dword; clrref:COLORREF );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateHatchBrush@8" );

The w.CreateHatchBrush function creates a brush that will draw a hatch pattern. The first parameter is a special Windows defined constant that specifies the hatch style (see Figure 7-11). The second parameter is an RGB color value that Windows will use to draw that hatch pattern lines. If the current background mode (i.e., w.SetBkMode) is w.TRANSPARENT, then Windows will draw the lines on top of the existing background and that background will show between the hatch mark lines. If the current background mode is w.OPAQUE, then Windows will fill in the gaps between the lines in the hatch pattern using the background fill color (i.e., w.SetBkColor).

Figure 7-11: Windows Hatch Patterns

    CreatePatternBrush: procedure(  hbmp:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreatePatternBrush@4" );

The w.CreatePatternBrush API function creates a brush from a bitmap you supply. The single parameter is a handle of an eight-by-eight bitmap specifying the pattern you want to draw for the brush. We'll talk about bitmaps a little later in this chapter.

    CreateBrushIndirect: procedure( var lplb:LOGBRUSH );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateBrushIndirect@4" );

The w.CreateBrushIndirect API function combines the functionality of the other three brush creation functions into a single API call. This call gets passed a pointer to a w.LOGBRUSH record, that has the following definition:

type

    LOGBRUSH:

        record

            lbStyle: dword;

            lbColor: dword;

            lbHatch: dword;

        endrecord;

The lbStyle field specifies the brush's style and directly controls the meaning of the following two fields of this structure (see Table 7-5)

Table 7-5: lbStyle Values in the w.LOGBRUSH Record
lbStyle
lbColor
lbHatch
w.BS_SOLID
Color of brush (solid color).
N/A
w.BS_HOLLOW
N/A
N/A
w.BS_HATCHED
Color of hatch lines
Hatch style
w.BS_PATTERN
Not used
Handle to bitmap

You saw an example of the use of the w.LOGBRUSH record earlier, in the example involving the w.ExtCreatePen function. Table 7-5 provides the purpose of the values we assigned to each of the w.LOGBRUSH fields.

These "create brush" functions all return a handle you can use to reference the brush that Windows creates internally. In order to activate this brush in a device context, you use the SelectObject function (well, macro actually) within the BeginPaint/EndPaint block4 and pass SelectObject the handle of the brush you want to activate. Also, because these "create brush" functions create resources inside of Windows, don't forget to call w.DeleteObject to delete the brush resource when you're through with it (but be careful not to delete a brush you've currently got selected into some device context).

7.8.2: Filling and Framing Paths

In the section that described the use of the w.ExtCreatePen function, you saw how to use paths to record and draw an object made up from a sequence of lines. In the example code that section presents, you saw how to draw triangle objects as paths and actually render those triangles to a device context using the w.StrokePath API function. The w.StrokePath draws the outline of some object described by a sequence of lines (triangles in the case of the code you're seen already). In addition to drawing the outline of some object, you can also fill that object with some brush value by using the w.FillPath API function and you can draw the outline and fill its interior region in a single operation by calling the w.StrokeAndFillPath function:

static

    FillPath:procedure( hdc:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FillPath@4" );

    StrokeAndFillPath:procedure( hdc:dword );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__StrokeAndFillPath@4" );

The following sample program demonstrates the use of the w.StrokeAndFillPaths API function. It draws a series of triangles by recording their paths and the filling them with the six different hatch values. This program's output appears in figure.

Figure 7-12: FillPaths Program Output

// FillPaths.hla-

//

//  Program that demonstrates object filling via the

// FillPaths and StrokeAndFillPaths API functions.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit FillPaths;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

readonly

    ClassName   :string := "FillPathsWinClass";     // Window Class Name

    AppCaption  :string := "FillPaths Program";     // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    oldPen      :dword;         // Used to save the original pen.

    curHatch    :dword;         // Handle to the Hatch we're using.

    ps          :w.PAINTSTRUCT; // Used while painting text.

readonly

    Hatches : dword[6] :=

        [

            w.HS_HORIZONTAL,

            w.HS_VERTICAL,

            w.HS_FDIAGONAL,

            w.HS_BDIAGONAL,

            w.HS_CROSS,

            w.HS_DIAGCROSS

        ];

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Draw a sequence of triangles to demonstrate

        // the w.StrokeAndFill API function:

        for( mov( 0, esi ); esi < @elements( Hatches ); inc( esi )) do

            // Select one of the six hatch styles here:

            mov( Hatches[ esi*4 ], eax );

            w.CreateHatchBrush( Hatches[ esi*4 ], RGB( $80, $80, $80 ));

            mov( eax, curHatch );

            SelectObject( curHatch );

            // Capture all the strokes:

            w.BeginPath( hdc );

                // Draw a triangle using the default pen:

                intmul( 80, esi, edi );

                add( 40, edi );

                MoveToEx( edi, 50, NULL );

                add( 40, edi );

                LineTo( edi, 50 );

                LineTo( edi, 10 );

                // Create a closed path

                // (so 'end' joins with 'start' properly):

                w.CloseFigure( hdc );

            w.EndPath( hdc );

            // Actually draw the triangle based on the path

            // we've captured above:

            w.StrokeAndFillPath( hdc );

            // Delete the Hatch we created above:

            w.DeleteObject( curHatch );

        endfor;

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end FillPaths;

7.8.3: Drawing Rectangles

After lines, perhaps the least complex graphic object that Windows will render is the rectangle. You can draw a rectangle and fill in a rectangle using the following Win32 API functions:

static

    FillRect: procedure

    (

            hDC         :dword;

        var lprc        :RECT;

            hbr         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FillRect@12" );

    FrameRect: procedure

    (

            hDC         :dword;

        var lprc        :RECT;

            hbr         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FrameRect@12" );

    InvertRect: procedure

    (

            hDC     :dword;

        var lprc    :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__InvertRect@8" );

    Rectangle: procedure

    (

            hdc             :dword;

            nLeftRect       :dword;

            nTopRect        :dword;

            nRightRect      :dword;

            nBottomRect     :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__Rectangle@20" );

These four GDI functions all have #keyword macros that are part of the BeginPaint/EndPaint (etc.) macros in the wpa.hhf header file. The macro declarations for each of these functions follows that standard form (that is, they're similar to the Windows function, minus the hdc parameter):

FillRect( lprc, hbr );

FrameRect( lprc, hbr );

InvertRect( lprc );

Rectangle( left, top, right bottom );

The FillRect call fills the interior of the rectangle. The lprc parameter specifies the rectangle to fill and the hbr parameter (a handle to a brush value) specifies the brush that Windows will use when filling the rectangle. Alternately, you can specify one of the standard system colors as the brush value by using the corresponding Windows constant, plus one, in place of the brush handle. For example, "FillRect( someRect, w.COLOR_WINDOW+1);" fills the rectangle with the standard Windows' window color.

The FrameRect function draws a line around the rectangle. The lprc parameter specifies the outline of the rectangle to draw and the hbr parameter is a handle to a brush that Windows will use as the pen's brush when drawing the line around the rectangle. Like FillRect, the FrameRect function allows you to specify one of the standard window colors in place of the brush handle if you want one of the standard, solid, colors.

The InvertRect function inverts the color of each pixel in the specified rectangle's interior.

The Rectangle function provides a quick way to draw and fill a rectangle without setting up a brush or w.RECT record. It draws the rectangle specified by the coordinates you directly pass it as parameters and it uses the current pen to draw the outline of the rectangle, and the current device context brush to fill the rectangle.

In his first Programming Windows books, Charles Petzold introduced the "Random Rectangles" program that continuously draws a set of random rectangles on the screen (much like the earlier "Lines" program that appeared in this chapter). The following code is an example of such a random rectangles program written in assembly language.

// Rectangles.hla-

//

//  Simple Application the demonstrates line drawing.

unit Rectangles;

#include( "rand.hhf" )

#include( "hll.hhf" )

#include( "w.hhf" )

#include( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

readonly

    ClassName   :string := "RectanglesWinClass";    // Window Class Name

    AppCaption  :string := "Rectangles Program";    // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

static

    hBrush      :dword;                     // Brush handle.

    blackBrush  :dword;                     // Handle for the stock white brush

    lastRGB     :w.COLORREF;                // Last color we used

    theRect     :w.RECT;                    // Rectangle Coordinates

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Get the stock black brush object so we outline the rectangles

    // we draw:

    w.GetStockObject( w.BLACK_BRUSH );

    mov( eax, blackBrush );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        rand.range( 0, $FF_FFFF ); // Generate a random RGB value.

        mov( eax, lastRGB );

        // Create a brush with the current color we're using:

        w.CreateSolidBrush( lastRGB );

        mov( eax, hBrush );

        // Draw a random rectangle using the brush we just created:

        rand.range( 0, ClientSizeX );

        mov( eax, theRect.left );

        rand.range( 0, ClientSizeY );

        mov( eax, theRect.top );

        mov( ClientSizeX, eax );

        sub( theRect.left, eax );

        rand.range( 0, eax );

        mov( eax, theRect.right );

        mov( ClientSizeY, eax );

        sub( theRect.top, eax );

        rand.range( 0, eax );

        mov( eax, theRect.bottom );

        FillRect( theRect, hBrush );

        FrameRect( theRect, blackBrush );

        // Delete the brush we created:

        w.DeleteObject( hBrush );

    EndPaint;

    // Force Windows to redraw this window without erasing

    // it so that we get constant feedback in the window:

    w.InvalidateRect( hwnd, NULL, false );

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

end Rectangles;

7.8.4: Drawing Circles and Ellipses

Windows provides the w.Ellipse function for drawing circles and ellipses (a circle is a special case of an ellipse, which is why there is only one function for this purpose). The calling sequence for the w.Ellipse function is identical to that of the w.Rectangle API function. Because drawing circles and ellipses is not quite a common as drawing rectangles, you don't get as wide a variety of API functions as are available for rectangles (i.e., no FrameEllipse, no FillEllipse, and no InvertEllipse functions). Nevertheless, it is possible to do everything these functions do (for ellipses), even if it's not quite as convenient.

Here's the prototype for the w.Ellipse function:

static

    Ellipse: procedure

    (

        hdc         :dword;

        nLeftRect   :dword;

        nTopRect    :dword;

        nRightRect  :dword;

        nBottomRect :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__Ellipse@20" );

The prototype for the BeginPaint/EndPaint #keyword macro is the following:

Ellipse( LeftRect, TopRect, RightRect, BottomRect )

As usual, the Ellipse invocation macro is identical to the w.Ellipse API function with the exception that you don't supply the device context handle as the first parameter.

The left, top, right, and bottom coordinate values you pass to the w.Ellipse function specifies a bounding box that just surrounds the ellipse you want to draw (see Figure 7-13). In order to draw a circle, the bounding rectangle must be a square; that is, (bottom - top) must be equal to (right - left).

Figure 7-13: Specifying an Ellipse with a Bounding Box

The w.Ellipse function frames the ellipse using the current device context's pen and it fills the ellipse using the device context's current brush value. If you want to change these values, you'll have to select a new pen or brush into the device context before drawing an ellipse. Of course, if specifying a different pen or fill value is something you commonly do, you might want to consider writing your own function that lets you specify these values as parameters.

The following sample program, ellipses.hla, is the ellipse version of the "random rectangles" program of the previous section. This program draws random ellipses to the window on a continual basis (until the user quits the program). One big difference between the ellipses.hla and the rectangle.hla programs is the fact that the ellipses program actually draws a sequence of ellipses at each point, with each successive ellipse shrunken in a small amount and the fill color slightly brightened for each ellipse. This creates a nice looking shade effect (of course, the program draws the ellipses so fast that it's hard to appreciate the shading, but if you look at the ellipses drawn near the edge of the window, you'll be able to see this effect).

// Ellipses.hla-

//

//  Simple Application the demonstrates drawing ellipses.

unit Ellipses;

#include( "rand.hhf" )

#include( "hll.hhf" )

#include( "w.hhf" )

#include( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

readonly

    ClassName   :string := "EllipsesWinClass";  // Window Class Name

    AppCaption  :string := "Ellipses Program";  // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

static

    hBrush      :dword;                     // Brush handle.

    blackBrush  :dword;                     // Handle for the stock white brush

    nullPen     :dword;                     // Handle for the null pen.

    oldPen      :dword;                     // Handle for the original pen

    lastRGB     :w.COLORREF;                // Last color we used

    theRect     :w.RECT;                    // Rectangle Coordinates

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Create a null pen so we don't have an outline on our ellipse:

    w.CreatePen( w.PS_NULL, 0, 0 );

    mov( eax, nullPen );

    // Get the stock black brush object so we outline the rectangles

    // we draw:

    w.GetStockObject( w.BLACK_BRUSH );

    mov( eax, blackBrush );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        rand.range( 0, $FF_FFFF ); // Generate a random RGB value.

        mov( eax, lastRGB );

        // Select the null pen into the context so we don't get

        // a black outline around the ellipse:

        SelectObject( nullPen );

        mov( eax, oldPen );

        // Create a random rectangle to surround the first ellipse

        // we're going to draw:

        rand.range( 0, ClientSizeX );

        mov( eax, theRect.left );

        rand.range( 0, ClientSizeY );

        mov( eax, theRect.top );

        mov( ClientSizeX, eax );

        sub( theRect.left, eax );

        rand.range( 0, eax );

        add( theRect.left, eax );

        mov( eax, theRect.right );

        mov( ClientSizeY, eax );

        sub( theRect.top, eax );

        rand.range( 0, eax );

        add( theRect.top, eax );

        mov( eax, theRect.bottom );

        // Draw a sequence of ellipses, each successive one inside the

        // previous one, until the ellipse closes up on itself.

        forever

            // Create a brush with the current color we're using.

            // On each iteration of the loop, we'll lighten the color

            // a small amount so that it creates a shading effect.

            w.CreateSolidBrush( lastRGB );

            add( $03_0303, lastRGB );       // Lighten the color for next loop.

            and( $FF_FFFF, lastRGB );

            mov( eax, hBrush );

            SelectObject( eax );

            // Draw the current ellipse:

            Ellipse( theRect.left, theRect.top, theRect.right, theRect.bottom );

            // Shrink the Ellipse in by two pixels:

            add( 2, theRect.top );

            add( 2, theRect.left );

            sub( 2, theRect.bottom );

            sub( 2, theRect.right );

            // Quit the loop when the ellipse collapses on itself:

            mov( theRect.top, eax );

            mov( theRect.left, ecx );

            breakif( eax >= theRect.bottom  ||  ecx >= theRect.right );

            // Delete the brush we created:

            SelectObject( blackBrush ); // First, select a different brush.

            w.DeleteObject( hBrush );

        endfor;

        // Restore the original pen:

        SelectObject( oldPen );

    EndPaint;

    // Force Windows to redraw this window without erasing

    // it so that we get constant feedback in the window:

    w.InvalidateRect( hwnd, NULL, false );

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

end Ellipses;

7.8.5: Drawing Roundangles, Arcs, Chords, and Pies

The Windows GDI provides several additional graphic primitives that are useful for building complex graphical objects on the display: roundangles, arcs, chords, and pie sections. Here are the descriptions of these functions:

static

    AngleArc: procedure

    (

        hdc:        dword;

        x:          dword;

        y:          dword;

        dwRadius:   dword;

        eStartAngle:real32;

        eSweepAngle:real32

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__AngleArc@24" );

// #keyword AngleArc( x, y, dwRadius, eStartAngle, eSweepAngle );

The w.AngleArc function is unique in this set insofar as it doesn't use the standard "bounding box" paradigm to descibe the graphical object. Instead, this particular function uses a standard definition for a circle in order to describe the object it draws; specifically, you provide the coordinates of a center point (the x and y coordinates) and you provide a radius for the circle. This function draws a line segment and an arc. Specifically, it draws a line from the current pen position to the beginning of the arc and then it draws an arc along the perimeter of a circle from a start angle (in degrees) through the number of degrees specified by the sweep angle. The circle on which this function draws the arc is specified by the center point (x,y) and the radius (dwRadius) parameters. The principle purpose for this function is to draw a rounded edge that connects to line segments. This function leaves the current pen position sitting at the end of the arc it draws. This function draws the arc using the current device context pen setting, it does not fill the arc.

Like other GDI functions, you can only call this function while you hold a valid device context. the AngleArc macro in the wpa.hhf header file lets you easily call this function from within a BeginPaint/EndPaint sequence.

static

    Arc: procedure

    (

        hdc:            dword;

        nLeftRect:      dword;

        nTopRect:       dword;

        nRightRect:     dword;

        nBottomRect:    dword;

        nXStartArc:     dword;

        nYStartArc:     dword;

        nXEndArc:       dword;

        nYEndArc:       dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__Arc@36" );

The w.Arc function (#keyword macro: Arc) lets you draw an arc along the perimeter of an ellipse. For this function, you supply a standard bounding box that surrounds the ellipse on whose perimeter you want to draw the arc. You also supply two points that are outside the bounding box. Where the line described by the starting point (specified by nXStartArc and nYStartArc) and the center of the ellipse intersects the perimeter of the ellipse, Windows begins drawing the arc; where the line described by the ending point (nXEndArc and nYEndArc) and the center of the ellipse intersect the ellipse's perimeter, Windows stops drawing the arc (see Figure 7-14).

Figure 7-14: An Arc Drawn with the w.Arc Function

    ArcTo: procedure

    (

        hdc:            dword;

        nLeftRect:      dword;

        nTopRect:       dword;

        nRightRect:     dword;

        nBottomRect:    dword;

        nXRadial1:      dword;

        nYRadial1:      dword;

        nXRadial2:      dword;

        nYRadial2:      dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ArcTo@36" );

The w.ArcTo function is a cross between the w.AngleArc function and the w.Arc function. The parameters and geometry are identical to the w.Arc function (though the names have changed, the function still uses a bounding box and two points to specify where the arc is to be drawn, just like w.Arc). Like the w.AngleArc function, w.ArcTo draws a line from the current pen position to the start of the arc and leaves the current pen position at the end of the arc that it draws.

static

    Chord: procedure

    (

        hdc         :dword;

        nLeftRect   :dword;

        nTopRect    :dword;

        nRightRect  :dword;

        nBottomRect :dword;

        nXRadial1   :dword;

        nYRadial1   :dword;

        nXRadial2   :dword;

        nYRadial2   :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__Chord@36" );

// #keyword Chord( left, top, right, bottom, x1, y1, x2, y2 );

A chord is a closed object that consists of an arc with a line drawn between the two endpoints of the arc (see Figure 7-15). The chord function draws the outline of the chord using the device context's current pen and it fills the interior of the chord using the current device context brush.

Figure 7-15: Drawing a Chord with w.Chord

    Pie: procedure

    (

        hdc                 :dword;

        nLeftRect           :dword;

        nTopRect            :dword;

        nRightRect          :dword;

        nBottomRect         :dword;

        nXRadial1           :dword;

        nYRadial1           :dword;

        nXRadial2           :dword;

        nYRadial2           :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__Pie@36" );

// #keyword Pie( left, top, right, bottom, x1, y1, x2, y2 );

The w.Pie function is very similar to the w.Chord function except it draws a "pie slice" by filling in the area of the ellipse between the endpoints of the arc you specify and the center of the ellipse (see Figure 7-16).

Figure 7-16: Drawing a Pie Slice with w.Pie

    RoundRect: procedure

    (

        hdc                 :dword;

        nLeftRect           :dword;

        nTopRect            :dword;

        nRightRect          :dword;

        nBottomRect         :dword;

        nWidth              :dword;

        nHeight             :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__RoundRect@28" );

// #keyword RoundRect( left, top, right, bottom, width, height );

The w.RoundRect API function draws a "rounded rectangle" (or "roundangle" for short). This is a rectangle with rounded corners. The usual bounding box specifies the outline of the rectangular portion of the image, the nWidth and nHeight parameters specify the amount of rounding that occurs at the four corners by specifying the bounds for an ellipse on each corner (see Figure 7-17).

Figure 7-17: Drawing a Rounded Rectangle using w.RoundRect

7.8.6: Bezier Curves

Bezier curves are a mathematical description of a curved line segment that models a spline (a spline is a varying curve instrument that draftsmen use to create a curved line between a set of points). Windows provides two API functions that you can use to draw Bezier curves:

static

    PolyBezier: procedure

    (

            hdc             :dword;

        var lppt            :POINT;

            cPoints         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PolyBezier@12" );

    PolyBezierTo: procedure

    (

            hdc             :dword;

        var lppt            :POINT;

            cCount          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PolyBezierTo@12" );

// #keyword PolyBezier( lppt, cPoints );

// #keyword PolyBezierTo( lppt, cPoints );

The difference between these two functions is that w.PolyBezierTo updates the current pen position (to point at the endpoint of the Bezier curve it draws) wherease w.PolyBezier does not affect the current pen position. As usual for GDI drawing functions, you may only call these functions while you've got a valid device context open. If you use the wpa.hhf macros (like BeginPaint/EndPaint) you can use the PolyBezier and PolyBezierTo macros to streamline calls to these API functions.

A "cubic Bezier curve" is defined by four points: two endpoints of a line and two control points that describe how to warp that line to form a curve. As their "Poly" prefix suggests, the w.PolyBezier and w.PolyBezierTo API functions actually draw a sequence of Bezier curves (much like w.PolyLine draws a sequece of straight line segments). The lppt parameter you supply to these functions is a pointer to an array of endpoints and control points, that are organized as follows:

[initial point] [control point 1] [control point 2] [end point/start point]

        [control point 1] [control point 2] [end point/start point] ...

An important thing to notice here is that each endpoint is also the start point for the next Bezier curve in the list. Only the initial end point and the last endpoint in the list do not serve double duty. The cCount parameter specifies the total number of points found in this array. Because each Bezier curve in the list requires three points (except for the first one, which requires four points), the cCount value must be a value that, once you subtract one, is evenly divisible by three.

Generally, applications don't compute the values of the endpoints and control points for a Bezier curve in order to draw a curved shape. Instead, most applications allow the user to specify the control and end points via some sort of "graphic editor" and the program simple records those points for later playback. Therefore, it's rare to find a program that actually computes the values for the control points of a Bezier curve in order to display some generic curve. Nevertheless, a short demo application is called for here, just to see how you can fill in the points in the points array you pass to the w.PolyBezier and w.PolyBezierTo API functions. Figure 7-18 shows the output of the bezier.hla source file (actually, this shows one possible output because the program actually generates the curves in a random fashion and will generate a new display whenever you resize or otherwise force a redraw of the window).

Figure 7-18: Output from the Bezier.hla Program

// Bezier.hla-

//

//  Program that demonstrates the use of the w.PolyBezier function.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit Bezier;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

readonly

    ClassName   :string := "BezierWinClass";        // Window Class Name

    AppCaption  :string := "Bezier Program";        // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

    bzeaCurves  :w.POINT[16];   // Holds five Bezier curves.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Fill in the bzeaCurves array with a set of end and control points:

    for

    (

        mov( 0, esi );

        esi < 16 * @size( w.POINT);

        add( @size( w.POINT ), esi )

    ) do

        rand.range( 0, ClientSizeX );

        mov( eax, (type w.POINT bzeaCurves[esi]).x );

        rand.range( 0, ClientSizeY );

        mov( eax, (type w.POINT bzeaCurves[esi]).y );

    endfor;

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Draw the curves we've created:

        PolyBezier( bzeaCurves, 16 );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

end Bezier;

7.8.7: Drawing with the Polygon Function

Windows provides an API function that draws polygons as a set of connected points. The operating of this function is almost identical to w.Polyline except that Windows will automatically draw a closing line from the last point to the first point in the list you provide (if these aren't the same point). The calling sequence and prototypes are also very similar:

static

    Polygon: procedure

    (

            hdc             :dword;

        var lpPoints        :POINT;

            nCount          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__Polygon@12" );

    PolyPolygon: procedure

    (

            hdc             :dword;

        var lpPoints        :POINT;

        var lpPolyCounts    :var;

            nCount          :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PolyPolygon@16" );

// #keyword Polygon( lpPoints, ncount );

// #keyword PolyPolygon( lpPoints, lpPolyPoints, nCount );

The w.PolyPolygon procedure draws a series of polygons (just as w.PolyPolyline draws a series of poly lines). The main differences between the polyline functions and the polygon functions are that the polygon functions always create a close object (that is, they connect the endpoint and the start point) and the polygon function fill the interior of their shapes with the current device context brush (poly line objects are not considered fillable shapes by Windows).

For the w.Polygon function, the lpPoints parameter is an array of w.POINT objects that specify the vertices of the polygon you wish to draw. The nCount parameter specifies the number of points that are in this array of points. The w.Polygon function will draw lines between these points using the current device context pen and will fill the resulting shape with the currently selected device context brush. As with all GDI drawing functions, you must have a current device context selected (and if you use the BeginPaint/EndPaint macros from wpa.hhf, you can use the streamlined Polygon macro to invoke this function).

The w.PolyPolygon function draws a sequence of polygons whose vertices all appear in a list. The lpPoints parameter specifies all the vertices for the polygons, the lpPolyCounts parameter is an array of integers that specifies the number of points for each polygon, and the nCount parameter specifies the total number of polygons (i.e., the number of integer entries in the lpPolyCounts array). The lpPoints array must have the number of points specified by the sum of all the elements in the lpPolyCounts array. Windows does not connect these individual polygons.

Because you can create some very complex polygonal objects, Windows offers a couple of different modes you can specify when filling the polygon. By default, Windows uses an "alternate" mode. In the alternate mode, Windows fills those closed sections of the polygon that it can reach by going over an odd number of lines in the shape; any interiors you reach by traversing an even number of lines do not get filled. Sometimes, this is an inappropriate fill behavior, so Windows also offers a "winding" mode that tells Windows to fill all the interior areas. You can select between these two modes by passing the w.ALTERNATE or w.WINDING constant values as the iPolyFillMode parameter to the w.SetPolyFillMode function:

static

    SetPolyFillMode: procedure

    (

        hdc                 :dword;

        iPolyFillMode       :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetPolyFillMode@8" );

// #keyword SetPolyFillMode( iPolyFillMode );

The following program listing demonstrates the use of the w.Polygon and w.SetPolyFillMode functions, as well as the use of the w.ALTERNATE and w.WINDING fill modes. See Figure 7-19 for the sample output from this program. The image on the left hand side of the window was drawn with the w.ALTERNATE fill style, while the object on the right was drawn with the w.WINDING fill style.

Figure 7-19: Output from the Polygons.hla Program

// Polygons.hla-

//

//  Program that demonstrates the use of the w.Polygon and w.SetPolyFillMode

// functions.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit Polygons;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

readonly

    ClassName   :string := "PolygonsWinClass";      // Window Class Name

    AppCaption  :string := "Polygons Program";      // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

    windingPoly :w.POINT[8];    // Holds the points for two overlapping rects.

    alterPoly   :w.POINT[8];    // Holds the points for two overlapping rects.

    #macro index(i);

        (i*@size(w.POINT))

    #endmacro

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Fill in the polygon array to form two overlapping rectangles:

    mov( 0, windingPoly.x[ index(7) ] );

    mov( ClientSizeX, eax );

    shr( 2, eax );  // 1/4 of the window's width

    mov( eax, windingPoly.x[ index(5) ] );

    mov( eax, ecx );

    shr( 1, eax );  // 1/8 of the window width

    mov( eax, windingPoly.x[ index(0) ] );

    mov( eax, windingPoly.x[ index(3) ] );

    mov( eax, windingPoly.x[ index(4) ] );

    mov( eax, windingPoly.x[ index(6) ] );

    add( ecx, eax );    // 3/8 of the window's width

    mov( eax, windingPoly.x[ index(1) ] );

    mov( eax, windingPoly.x[ index(2) ] );

    mov( 5, eax );

    mov( eax, windingPoly.y[ index(0) ] );

    mov( eax, windingPoly.y[ index(1) ] );

    mov( eax, windingPoly.y[ index(4) ] );

    mov( ClientSizeY, eax );

    shr( 1, eax );

    mov( eax, ecx );

    shr( 1, ecx );

    add( ecx, eax );    // 3/4 of ClientSizeY

    mov( eax, windingPoly.y[ index(2) ] );

    mov( eax, windingPoly.y[ index(3) ] );

    mov( eax, windingPoly.y[ index(6) ] );

    shr( 1, eax );  // 3/8 of ClientSizeY

    mov( eax, windingPoly.y[ index(5) ] );

    mov( eax, windingPoly.y[ index(7) ] );

    // Create the alterPoly array here by offseting the

    // winding polygon by half the screen:

    mov( ClientSizeX, ecx );

    shr( 1, ecx );

    for( xor( esi, esi ); esi < 8; inc( esi )) do

        mov( windingPoly.x[ esi*8], eax );

        add( ecx, eax );

        mov( eax, alterPoly.x[ esi*8 ] );

        mov( windingPoly.y[ esi*8], eax );

        mov( eax, alterPoly.y[ esi*8 ] );

    endfor;

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        SelectObject( w.GetStockObject( w.GRAY_BRUSH ));

        // Draw the curves we've created:

        SetPolyFillMode( w.ALTERNATE );

        Polygon( windingPoly, 8 );

        SetPolyFillMode( w.WINDING );

        Polygon( alterPoly, 8 );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

end Polygons;

7.9: Regions

A region in Windows is an area that is defined using rectangles, roundangles (rounded rectangles), ellipses, and polygons. While you can use regions for many different purposes in Windows, their primary purposes are to support masking (or clipping) when drawing objects to the display and creating specialized objects to draw or fill. We'll take a look at drawing complex regions in this section and look at clipping in the next section.

Windows' regions provide a mechanism for merging two distinct objects into a single shape. For example, consider the overlapping rectangles produced by the Polygons.hla program of the previous section (see Figure 7-20). Unless you fill this object with a very dark (e.g., black) brush, it's pretty obivious that what we have here are two rectangles rather than a single object. Perhaps you really wanted to create the shape that Figure 7-21 shows (for the time being, we'll ignore the fact that we can easily create this new shape as a polygon, indeed with less work than creating the overlapping rectangles from Polygons.hla; use your imagination and assume that this is a complex shape). Well, Windows' region facilities provide a way to combine (or merge) two separate shapes, producing a new, distinct shape.

Figure 7-20: The Overlapping Rectangles that Polygon.hla Produces

Figure 7-21: A Desired Shape

To create a simple region, you use one of the followingWindows API functions:

static

    CreateEllipticRgn: procedure

    (

        nLeftRect   :dword;

        nTopRect    :dword;

        nRightRect  :dword;

        nBottomRect :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateEllipticRgn@16" );

    CreateEllipticRgnIndirect: procedure

    (

        var lprc    :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateEllipticRgnIndirect@4" );

    CreatePolyPolygonRgn: procedure

    (

        var lppt            :POINT;

        var lpPolyCounts    :dword;

            nCount          :dword;

            fnPolyFillMode  :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreatePolyPolygonRgn@16" );

    CreatePolygonRgn: procedure

    (

        var lppt            :POINT;

            cPoints         :dword;

            fnPolyFillMode  :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreatePolygonRgn@12" );

    CreateRectRgn: procedure

    (

        nLeftRect   :dword;

        nTopRect    :dword;

        nRightRect  :dword;

        nBottomRect :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateRectRgn@16" );

    CreateRectRgnIndirect: procedure

    (

        VAR lprc    :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateRectRgnIndirect@4" );

    CreateRoundRectRgn: procedure

    (

        nLeftRect       :dword;

        nTopRect        :dword;

        nRightRect      :dword;

        nBottomRect     :dword;

        nWidthEllipse   :dword;

        nHeightEllipse  :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateRoundRectRgn@24" );

These calls have the same parameters as the w.Rectangle, w.RoundRect, w.Ellipse, w.Polygon, and w.PolyPolygon API functions. However, instead of rendering one of these objects, these functions create a region. The "indirect" versions let you pass a bounding rectangle that defines the shape of the object from which Windows will create a region.

A region is an internal Windows resource. When you create a region with one of these calls, Windows will construct an internal data structure to represent that region and return a handle (in EAX) that you can use to refer to that structure. Like all GDI resources you create, you must make sure that you delete any region objects you create before your application quits. If you don't, those regions will continue to consume Windows' resources. Therefore, it's important for you to save the handle that Windows returns from these functions so you can delete the resource later.

You can also change an existing region to a simple rectangular region via the API function:

    SetRectRgn: procedure

    (

        hrgn                :dword;

        nLeftRect           :dword;

        nTopRect            :dword;

        nRightRect          :dword;

        nBottomRect         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetRectRgn@20" );

This function does not return a handle (it modifies the region whose handle you pass to this function). Instead, this function simply returns success/error in EAX.

The real power behind regions only becomes apparent when you consider the w.CombineRgn API function:

static

    CombineRgn: procedure

    (

        hrgnDest        :dword;

        hrgnSrc1        :dword;

        hrgnSrc2        :dword;

        fnCombineMode   :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CombineRgn@16" );

The first three parameters must be handles of valid regions that you've previously created (this is even true for the hrgnDest parameter, which w.CombineRgn will destroy before creating a new region by combining the second two parameters - weird semantics, but this is the way Windows works). This function combines the two regions specified by the hrgnSrc1 and hrgnSrc2 parameter using the combine mode specified by the fourth parameter (fnCombineMode); this function creates a new region and replaces the hrgnDest region by this new region. The hrgnDest parameter supports the modes described in Table 7-6.

Table 7-6: fnCombineMode Values for w.CombineRgn
fnCombine
How w.CombineRgn Creates the new Region
w.RGN_AND
The new region is the intersection of the source regions (i.e., the portions of the source regions that overlap).
w.RGN_OR
The new region is the union of the two source regions (i.e., all the area described by both regions).
w.RGN_XOR
The new region is the union of the two source regions, minus any overlap between the two regions.
w.RGN_DIFF
The new region is all of the area in the first source region that is not also in the second source region.
w.RGN_COPY
The new region is all of the first source region, ignoring the second source region.

Once you've created a region, you can draw and fill that region in the current device context. Here are the functions you can use to render regions:

static

    FillRgn: procedure

    (

        hdc             :dword;

        hrgn            :dword;

        hbr             :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FillRgn@12" );

// #keyword FillRgn( hrgn, hbr );

The w.FillRgn function fills a region (passed as the hrgn parameter) using the brush you pass as a parameter). Like all GDI drawing routines, you must have a valid device context before calling this routine.

static

    FrameRgn: procedure

    (

        hdc             :dword;

        hrgn            :dword;

        hbr             :dword;

        nWidth          :dword;

        nHeight         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__FrameRgn@20" );

// #keyword FrameRgn( hrgn, hbr, nWidth, nHeight );

The w.FrameRgn function draws an outline around a region. The hrgn parameter specifies the region to frame, the hbr parameter specifies the handle of a brush to use when drawing the outline, the nWidth parameter specifies the width, in logical units, of vertical brush strokes, and the nHeight parameter specifies the height of horizontal brush strokes.

static

    InvertRgn: procedure

    (

        hdc                 :dword;

        hrgn                :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__InvertRgn@8" );

// #keyword InvertRgn( hrgn );

The w.InvertRgn function inverts the colors within the region specified by the handle you pass this function.

static

    PaintRgn: procedure

    (

        hdc                 :dword;

        hrgn                :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PaintRgn@8" );

// #keyword PaintRgn( hrgn );

The w.PaintRgn is very similar to w.FillRgn. The difference is that w.PaintRgn fills the region using the currently selected brush in the device context (as opposed to w.FillRgn, to whom you pass a brush handle).

In addition to the functions that frame and paint regions, there are several additional utility functions that are useful when using regions:

static

    EqualRgn: procedure

    (

        hSrcRgn1    :dword;

        hSrcRgn2    :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__EqualRgn@8" );

The w.EqualRgn function returns true (non-zero) in EAX if the two regions are identical in size and shape. It returns false (zero) otherwise.

static

    GetRgnBox: procedure

    (

            hrgn            :dword;

        var lprc            :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetRgnBox@8" );

The w.GetRgnBox function stores the coordinates of a bounding box that surrounds the region whose handle you pass as the first parameter. The second parameter, lprc, receives the bounding rectangle. The return value in EAX specifies the type of the region, it is one of the values in Table 7-7.

Table 7-7: w.GetRgnBox Return Values
Value Returned in EAX
Description
w.NULLREGION
Region is empty.
w.SIMPLEREGION
Region is a simple rectangle
w.COMPLEXREGION
Region is a complex object that is more than a single rectangle.

static

    OffsetRgn: procedure

    (

        hrgn            :dword;

        nXOffset        :dword;

        nYOffset        :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__OffsetRgn@12" );

The w.OffsetRgn function API function adds the nXOffset and nYOffset values to the region coordinates, moving the region by the specified amount. Note that these are signed integer values (despite the declaration to the contrary) and you can move the image up, down, left, or right by an appropriate amount by specifying positive and negative numbers.

static

    PtInRegion: procedure

    (

        hrgn                :dword;

        X                   :dword;

        Y                   :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PtInRegion@12" );

The w.PtInRegion function returns true (non-zero) in EAX if the point specified by the (X,Y) parameters is within the region whose handle you pass as the hrgn parameter. This function returns false (zero) if the point is outside the region.

static

    RectInRegion: procedure

    (

            hrgn            :dword;

        var lprc            :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__RectInRegion@8" );

The w.RectInRegion function returns true (non-zero) or false (zero) in EAX depending upon whether any part of the rectangle you specify via the lprc parameter is within the region whose handle you pass as the hrgn parameter. Note that this function returns false only if the rectangle is completely outside the region.

7.10: Clipping

The second primary use for regions is to support clipping under Windows. You can use a region as a mask, or template, that protects portions of the client area of your window when drawing to that window. To use a region as a clipping region, you simply select the region into a device context. From that point forward, Windows will only allow you to draw within the area described by that region. You can select a region you've created into the current device context by using either of the following two functions:

static

    SelectObject: procedure

    (

        hdc                 :dword;

        hgdiobj             :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SelectObject@8" );

// #keyword SelectObject( hgdiobj ); // use a region handle for clipping!

    SelectClipRgn: procedure

    (

        hdc                 :dword;

        hrgn                :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SelectClipRgn@8" );

// #keyword SelectClipRegion( hrgn );

Once you execute w.SelectObject or w.SelectClipRegion, then all future output to the current device context is limited to the region whose handle you pass as the hrgn or hgdiobj parameter.

Charles Petzold's "Windows Programming" book has another cute example worth repeating here: the "clover" application. This program demonstrates clipping in elliptical regions by creating four overlapping elliptical regions, combining them, and then using them as a clipping region for the application's window. Once the clipping region is set up, the program draws a serials a radial lines from the center of the screen towards the outer edges. The following clover.hla program is an HLA adaptation of this idea. Figure shows the output for this program.

Figure 7-22: Output from the Clover.hla Program

// Clover.hla-

//

//  Program that demonstrates the use of the clipping by drawing a

// simple "Clover" pattern.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit Clover;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

readonly

    ClassName   :string := "CloverWinClass";        // Window Class Name

    AppCaption  :string := "Clover Program";        // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

    leafs       :dword[4];      // Handles for our four-leaf clover

    Clover      :dword;         // Four-leaf clover region handle.

    temp1       :dword;

    temp2       :dword;

    x           :dword;

    y           :dword;

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Create the four regions we're going to use as a mask:

    // Region one - ellipse specified by (0, y/4):(x/2, y*3/4)

    mov( ClientSizeX, esi );

    shr( 1, esi );      // 1/2 ClientSizeX

    mov( ClientSizeY, edi );

    shr( 1, edi );      // 1/2 ClientSizeY

    mov( edi, eax );

    shr( 1, eax );      // 1/4 ClientSizeY

    add( eax, edi );    // 3/4 ClientSizeY

    w.CreateEllipticRgn( 0, eax, esi, edi );

    mov( eax, leafs[0*4] );

    // Region two - ellipse specified by (x/2, y/4):(x, y*3/4)

    mov( ClientSizeY, edi );

    shr( 1, edi );      // 1/2 ClientSizeY

    mov( edi, eax );

    shr( 1, eax );      // 1/4 ClientSizeY

    add( eax, edi );    // 3/4 ClientSizeY

    w.CreateEllipticRgn( esi, eax, ClientSizeX, edi );

    mov( eax, leafs[1*4] );

    // Region three - ellipse specified by (x/4, 0):(x*3/4, y/2)

    mov( ClientSizeY, esi );

    shr( 1, esi );      // 1/2 ClientSizeY

    mov( ClientSizeX, edi );

    shr( 1, edi );      // 1/2 ClientSizeX

    mov( edi, eax );

    shr( 1, eax );      // 1/4 ClientSizeX

    add( eax, edi );    // 3/4 ClientSizeX

    w.CreateEllipticRgn( eax, 0, edi, esi );

    mov( eax, leafs[2*4] );

    // Region four - ellipse specified by (x/4, y/2):(x*3/4, y)

    mov( ClientSizeX, edi );

    shr( 1, edi );      // 1/2 ClientSizeX

    mov( edi, eax );

    shr( 1, eax );      // 1/4 ClientSizeX

    add( eax, edi );    // 3/4 ClientSizeX

    w.CreateEllipticRgn( eax, esi, edi, ClientSizeY );

    mov( eax, leafs[3*4] );

    // Combine the regions using the XOR combination:

    w.CreateRectRgn( 0, 0, 0, 0 );

    mov( eax, temp1 );

    w.CreateRectRgn( 0, 0, 0, 0 );

    mov( eax, temp2 );

    w.CreateRectRgn( 0, 0, 0, 0 );

    mov( eax, Clover );

    w.CombineRgn( temp1, leafs[0*4], leafs[1*4], w.RGN_OR );

    w.CombineRgn( temp2, leafs[2*4], leafs[3*4], w.RGN_OR );

    w.CombineRgn( Clover, temp1, temp2, w.RGN_XOR );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Select the Clover region in to use as our clipping rectangle:

        w.SelectClipRgn( hdc, Clover );

        // Now draw a sequence of lines from the center of the display

        // to the edges in order to demonstrate the clipping:

        mov( ClientSizeX, eax );    // Compute the coordinate of the

        shr( 1, eax );              //  screen's center point.

        mov( eax, x );

        mov( ClientSizeY, eax );

        shr( 1, eax );

        mov( eax, y );

        // Draw the lines that intersect the top of the window:

        for( mov( 0, esi ); esi < ClientSizeX; add( 4, esi )) do

            MoveToEx( x, y, NULL );

            LineTo( esi, 0 );

        endfor;

        // Draw the lines that intersect the right side of the window:

        for( mov( 0, esi ); esi < ClientSizeY; add( 4, esi )) do

            MoveToEx( x, y, NULL );

            LineTo( ClientSizeX, esi );

        endfor;

        // Draw the lines that intersect the bottom of the window:

        for( mov( 0, esi ); esi < ClientSizeX; add( 4, esi )) do

            MoveToEx( x, y, NULL );

            LineTo( esi, ClientSizeY );

        endfor;

        // Draw the lines that intersect the left side of the window:

        for( mov( 0, esi ); esi < ClientSizeY; add( 4, esi )) do

            MoveToEx( x, y, NULL );

            LineTo( 0, esi );

        endfor;

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

end Clover;

7.11: Bitmaps

Bitmaps are probably the most common thing that people recognize as graphic images. A bit map is a two-dimensional array of pixels that Windows can render on the screen. Digital photographs, icons, and cursors are good examples of typical bitmaps in a Windows system. In this section, we'll explore ways to create and display bitmaps on a device context.

Unfortunately, dealing with bitmaps is not a trivial process. Bitmaps, because of their pixel nature, are somewhat device dependent. Indeed, the earliest versions of Windows only supported "device-dependent bitmaps" meaning that if you move a graphic image represented by a bitmap from one machine to the next you might get some spectacularly weird results. Later versions of Windows support "device independent bitmaps" but this device independence comes at a price - complexity. Working with device independent bitmaps is definitely a lot more work than dealing with device dependent bitmaps. Nevertheless, device independence is the only way to go, so you're going to have to deal with the additional complexity with working with bitmaps.

Device independent bitmaps, or DIBs as they are known in Windows' terminology, is a well-defined file format. Indeed, the ".BMP" suffix that programs like Windows Paint uses indicate a DIB format. Windows, itself, doesn't actually work with the DIB file format. Instead, you've got to convert a DIB format to a device dependent format in order to display the DIB.

A DIB begins with the device independent bitmap file header. This is a record, named w.BITMAPFILEHEADER, that takes the following form:

type

    BITMAPFILEHEADER:

        record

            bfType: word;

            bfSize: dword;

            bfReserved1: word;

            bfReserved2: word;

            bfOffBits: dword;

        endrecord;

The bfType field always contains "BM" (for bitmap). The bfSize field specifies the total size of the bitmap file. The bfOffBits file specifies the offset, in bytes, from the beginnig of the file to the actual bitmap data. The other two fields are reserved and will be set to zero.

The bitmap file header is followed by another record of type w.BITMAPINFOHEADER that takes the following form:

type

    BITMAPINFOHEADER:

        record

            biSize: dword;

            biWidth: dword;

            biHeight: dword;

            biPlanes: word;

            biBitCount: word;

            biCompression: dword;

            biSizeImage: dword;

            biXPelsPerMeter: dword;

            biYPelsPerMeter: dword;

            biClrUsed: dword;

            biClrImportant: dword;

        endrecord;

The biSize field contains the size of the w.BITMAPINFOHEADER, in bytes. This field exists to allow changes to this structure format in the future (the application can use this field to detemine how many fields are present in the bitmap info header record). Examples of such extensions are the w.BITMAPV4HEADER and w.BITMAPV5HEADER records used by win95 and win2K/win98, respectively:

type

    BITMAPV4HEADER:

        record

            bV4Size: dword;

            bV4Width: dword;

            bV4Height: dword;

            bV4Planes: word;

            bV4BitCount: word;

            bV4V4Compression: dword;

            bV4SizeImage: dword;

            bV4XPelsPerMeter: dword;

            bV4YPelsPerMeter: dword;

            bV4ClrUsed: dword;

            bV4ClrImportant: dword;

            bV4RedMask: dword;

            bV4GreenMask: dword;

            bV4BlueMask: dword;

            bV4AlphaMask: dword;

            bV4CSType: dword;

            bV4Endpoints: CIEXYZTRIPLE;

            bV4GammaRed: dword;

            bV4GammaGreen: dword;

            bV4GammaBlue: dword;

        endrecord;

    BITMAPV5HEADER:

        record

            bV5Size: dword;

            bV5Width: dword;

            bV5Height: dword;

            bV5Planes: word;

            bV5BitCount: word;

            bV5V4Compression: dword;

            bV5SizeImage: dword;

            bV5XPelsPerMeter: dword;

            bV5YPelsPerMeter: dword;

            bV5ClrUsed: dword;

            bV5ClrImportant: dword;

            bV5RedMask: dword;

            bV5GreenMask: dword;

            bV5BlueMask: dword;

            bV5AlphaMask: dword;

            bV5CSType: dword;

            bV5Endpoints: CIEXYZTRIPLE;

            bV5GammaRed: dword;

            bV5GammaGreen: dword;

            bV5GammaBlue: dword;

            bV5Intent:dword;

            bV5ProfileData:dword;

            bV5ProfileSize:dword;

            bV5Reserved:dword;

        endrecord;

The biWidth and biHeight fields specify the width and height of the bitmap, in pixels, respective.

The biPlanes field is always set to one on modern machines (this is device dependent information for really old video display cards).

The biBitCount field specifies the number of bits per pixel used to specify the number of colors. This field will contain 1, 4, 8, 16, or 24.

The biCompression field specifies whether this bitmap uses data compression. It will contain zero for no compression. Windows supports no compression, run length encoding, JPEG, and PNG compression formats. Compressed bitmaps are beyond the scope of these book and we won't consider them here. Please see the Microsoft documentation for more details. The biSizeImage contains the size of the bitmap image if compression is in use. This field can be set to zero if you're not using compression.

The biXPelsPerMeter and biYPelsPerMeter specifies the expected display resolution in pixels per meter.

The biClrUsed field specifies thenumber of colors that the image uses and the biClrImportant field specifies the minimum number of colors that should be used to properly display the image. If the biClrUsed field is zero and the biBitCount field is 1, 4, or 8, then a set of two, 16, or 256 RGB values immediately follow the w.BITMAPINFOHEADER record. These RGB values specify the palette to use when drawing the bitmap image. If the biClrUsed field is non-zero, then this field specifies how many RGB values immediately follow the w.BITMAPINFOHEADER record.

Note that the fields beyond biBitCount are optional. The biSize field's value determines if these fields are actually present in the bitmap info header. Therefore, you must explicitly check the value of biSize before accessing these fields. If a field is not present, you can assume its value is zero or any other reasonable default value you want to use.

Immediately following the set of RGB values (if any) comes the bitmap itself. The first byte of the bitmap data corresponds to the lower left hand corner of the image to be rendered. Successive bits in the bitmap correspond to pixels appearing to the right and, upon hitting the end of a row, lines of pixels appearing above the current line.

For a monocrhome bitmap, where there is one bit per pixel, the pixels are packed into the bytes such that the high order bit of a byte is the first pixel to be drawn, bit six of a byte is the pixel just to the right of the H.O. pixel, and so on down to the L.O. bit. Upon crossing a byte boundary, the process repeats again with the H.O. bit.

For a 16-color (four-bit) bitmap, the H.O. four bits of the first byte correspond to the first pixel on the display (in the lower left hand corner), the L.O. four bits of the first byte correspond to the second pixel on the display, and so on.

For an eight-bit (256 color) bitmap, each byte corresponds to a single pixel. For 16-bit bitmaps, a pair of bytes constitutes each pixel. For 24-bit bitmaps, a set of three bytes is used for each pixel.

Every row in a bitmap, no matter what the actual width in pixels of the bitmap happens to be, is padded with zero to ensure that each row is a multiple of four bytes long.

Creating and drawing a bitmap is a relatively straight-forward process. The first step is to fill in a w.BITMAPINFOHEADER structure and use this structure to create a device independent bitmap. Windows actually provides several was of creating the bitmap, a simple one is:

static

    CreateDIBitmap: procedure

    (

            hdc         :dword;

        var lpbmih      :BITMAPINFOHEADER;

            fdwInit     :dword;

        var lpbInit     :var;

        var lpbmi       :BITMAPINFO;

            fuUsage     :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateDIBitmap@24" );

// #keyword CreateDIBitmap( lpbmih, fdwInit, lpbInit, lpbmi, fuUsage );

The hdc parameter (which is typically your display device context) provides a template from which Windows will extract several default values for the bitmap. For example, the bitmap will have the same number of colors as the device context whose handle you supply as the first parameter. In general, if you intend to render the bitmap on your display, you should supply the display's device context handle here.

The second parameter, lpbmih, is a pointer to a w.BITMAPINFOHEADER record object. Windows extracts the bitmap information from this object.

The fdwInit parameter is either zero or should contain the value w.CBM_INIT. If this field is zero, then w.CreateDIBitmap ignores the lpbInit and lpbmi parameters. If this field contains w.CBM_INIT then the w.CreateDIBitmap function uses the values in these two parameters (see the descriptions that follow).

If the fdwInit parameter has the value w.CBM_INIT, then Windows will use the data found at the address passed in lpbInit as the initial set of bits for the bitmap data. The format of this data depends upon the values found in the w.BITMAPINFOHEADER pointer passed in the lpbmih parameter. See the description given earlier for details.

If the fdwInit parameter has the value w.CBM_INIT, then the lpbmi parameter contains the address of a w.BITMAPINFO record that specifies the color information for the initial bitmap. The w.BITMAPINFO record takes the following form:

type

    BITMAPINFO:

        record

            bmiHeader: BITMAPINFOHEADER;

            bmiColors: RGBQUAD;  // This is an array, bounds specified by bimHeader.

        endrecord;

The fuUsage parameter should contain zero (technically, it should contain the constant w.DIB_RGB_COLORS, but this happens to be the value zero). See the Microsoft documentation for more details on this parameter's value.

Once you create a bitmap with w.CreateDIBitmap, there really isn't a whole lot you can do with it. This function returns a handle to the bitmap, but that bitmap is internal to Windows, so you cannot play around with it directly. In order to manipulate the bitmap, you've got to create a memory-based device context and select the bitmap into this new device context. You can create a memory-based device context using the w.CreateCompatibleDC function:

static

    CreateCompatibleDC: procedure

    (

        hdc :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateCompatibleDC@4" );

This function requires a handle to an existing device context (e.g., your display context) and it will create a new, memory-based, device context with the same attributes as the device context whose handle you pass as the parameter. For example, if you pass your application window's device context handle as the parameter, the memory-based device context this function creates will have the same attributes (e.g., number of colors and size) as the application's window. Because you're probably going to render the bitmap you're creating directly in the application's window, it's not a bad idea for your memory-based device context to have the same attributes.

Okay, so what exactly does "memory-based device context" mean, anyway? Well, quite literally, this means that Windows uses system RAM to maintain the image you draw. It isn't connect to any display output device, so you can't see what you've draw to a memory-based device context, but beyond this minor point a memory-based device context is identical to a standard device context. Of course, if you can't see what's in a memory-based device context, you might wonder why anyone would want to use it. Well, as it turns out, although you cannot directly view what is written to a memory-based device context, you can copy part or all of a memory-based device context to some other device context (e.g., your window's device context).

The w.CreateCompatibleDC function returns a handle to the device context it creates. You will use this handle to refer to the bitmap object you're creating. Also, don't forget that device contexts are resources and you must delete system resources when you are done using them. In particular, you will need to call w.DeleteDC to delete the device context from memory when you are finished with the device context, else you'll cause a resourceleak inside Windows.

Once you've created a bitmap with w.CreateDIBitmap and you've created a device context with w.CreateCompatibleDC, the next step is to connect the two by selecting the bitmap into the device context. You accomplish this using the w.SelectObject function, e.g.,

    w.SelectObject( memDeviceContextHandle, bitmapHandle );

When you first create a device context via w.CreateCompatibleDC, Windows sets aside a 1x1 pixel drawing area for that device context. This isn't large enough to do anything of interest. Selecting a bitmap into the device context expands the drawing canvas for that device context (that is, it uses the bitmap you've selected as the drawing area). This is specifically why we created a bitmap in the first place, to define the drawing area for use by the device context.

After selecting the bitmap into the memory-based device context, you can write to that device context using thenormal GDI drawing commands and everything you draw will wind up in the bitmap you've created. Note that because you're not using the application's standard Window device context, you cannot use the BeginPaint/EndPaint macros found in the wpa.hhf header file. Instead, you'll have to call the Win32 API drawing functions directly and pass them your memory-based device context's handle.

The last piece in the puzzle is "how do we transfer our bitmap from the memory-based device context to the window's device context?" Well, this is accomplished using Windows' bitblt operations (bitblt, pronounced "bit blit" standards for "bit block transfer" ). Two very useful functions in this category are w.BitBlt and w.StretchBlt:

static

    BitBlt: procedure

    (

        hdcDest     :dword;

        nXDest      :dword;

        nYDest      :dword;

        nWidth      :dword;

        nHeight     :dword;

        hdcSrc      :dword;

        nXSrc       :dword;

        nYSrc       :dword;

        dwRop       :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__BitBlt@36" );

    StretchBlt: procedure

    (

        hdcDest             :dword;

        nXOriginDest        :dword;

        nYOriginDest        :dword;

        nWidthDest          :dword;

        nHeightDest         :dword;

        hdcSrc              :dword;

        nXSrc               :dword;

        nYSrc               :dword;

        nWidthSrc           :dword;

        nHeightSrc          :dword;

        dwRop               :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__StretchBlt@44" );

The w.BitBlt API function copies a set of pixels from one device context to another. The hdcDest parameter specifies the target device context (e.g., your application window's device context). The nXDest, nYDest, nWidth, and nHeight parameter specify the location and size of the bitmap when w.BitBlt draws it to the device context. Note that this function will not scale the bitmap if you specify a larger size than is present in the source bitmap. It will simply fill any unused area with the background or truncate the image it is copying if the source image is larger than the destination size you've specified.

The hdcSrc parameter specifies the source device context from which the w.BitBlt function will transfer some pixels. The nXSrc and nYSrc parameters specify the starting pixel position in the source device context. The height and width of the source bitmap this function copies is the same as the destination (with the note about truncation or filling given earlier).

The dwROP (raster op) parameter specifies how the w.BitBlt function will copy the data from the source device context to the destination device context. Generally, you'll specify w.COPYSRC here, which means "copy the source bits to the destination." However, there are several additional possibilities you can use (actually, ther eare 256 of them, but only 15 of them have been given names). See Table 7-8 for the details. 99% of the time, you're probably going to use the w.SRCCOPY value for the dwROP parameter.

Table 7-8: ROP Values for the w.BitBlt Function.
Name
Copies these bits (S-source, D-Destination, P-context brush)
w.BLACKNESS
0
w.NOTSRCERASE
not ( S or D )
w.NOTSRCCOPY
not S
w.SRCERASE
S and not D
w.DSTINVERT
not D
w.PATINVERT
not P
w.SRCINVERT
not S
w.SRCAND
S and D
w.MERGEPAINT
not S or D
w.MERGECOPY
P and S
w.SRCCOPY
S
w.SRCPAINT
S or D
w.PATCOPY
P
w.PATPAINT
P or not S or D
w.WHITENESS
1

The w.StretchBits API function allows you to scale the bitmap while you're copying it from one device context to another. The parameters are the same as for w.BitBlt with the addition of the nWidthSrc and nHeightSrc parameters. Windows will automatically scale the source bitmap (of the given width and height) so that it fits into the destination bitmap area. You should note that w.StretchBlt runs signficantly slower than w.BitBlt, so you should try to use this operation as infrequently as possible.

The following sample program, Bitmaps.hla, is a simple demonstrate of the bitmap manipulation functions we've seen thus far. This program creates a single bit map in a memory-based device context and then draws three copies of that bitmap using w.BitBlt and w.StretchBlt (see Figure 7-23).

Figure 7-23: Bitmaps.hla Program Output

// Bitmaps.hla-

//

//  Program that demonstrates how to use a simple bitmap.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit Bitmaps;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

readonly

    ClassName   :string := "BitmapsWinClass";       // Window Class Name

    AppCaption  :string := "Bitmaps Program";       // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hBitmap     :dword;         // Handle to hold our bitmap handle.

    hdcMem      :dword;         // Handle for Memory device context.

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

    bmih        :w.BITMAPINFOHEADER;

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Fill in the fields of the bitmap info header we're gonna use:

    mov( @size( w.BITMAPINFOHEADER), bmih.biSize );

    mov( 100, bmih.biWidth );

    mov( 100, bmih.biHeight );

    mov( 1, bmih.biPlanes );

    mov( 24, bmih.biBitCount );

    mov( 0, bmih.biCompression );

    mov( 0, bmih.biSizeImage );

    mov( 96*2540, bmih.biXPelsPerMeter ); //96 pixels per inch

    mov( 96*2540, bmih.biYPelsPerMeter );

    mov( 0, bmih.biClrUsed );

    mov( 0, bmih.biClrImportant );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Create an uninitialized bitmap:

        CreateDIBitmap( bmih, 0, NULL, NULL, 0 );

        mov( eax, hBitmap );

        // Now create a new device context (memory device context)

        // and give it the same attributes as our bitmap we just created:

        w.CreateCompatibleDC( hdc );

        mov( eax, hdcMem );

        w.SelectObject( hdcMem, hBitmap );

        //  Okay, let's draw on this bitmap we've created.

        //

        // First, fill the background with 50% gray:

        w.SelectObject( hdcMem, w.GetStockObject( w.GRAY_BRUSH ));

        w.Rectangle( hdcMem, 0, 0, 100, 100 );

        // Now draw an "X" in the middle of the bitmap:

        w.SelectObject( hdcMem, w.GetStockObject( w.WHITE_PEN ));

        w.MoveToEx( hdcMem, 20, 20, NULL );

        w.LineTo( hdcMem, 80, 80 );

        w.MoveToEx( hdcMem, 80, 20, NULL );

        w.LineTo( hdcMem, 20, 80 );

        // Copy the bitmap to our display context so we can see it:

        w.BitBlt( hdc, 20, 20, 100, 100, hdcMem, 0, 0, w.SRCCOPY );

        // Draw the bitmap and stretch it out:

        w.StretchBlt

        (

            hdc,

            20, 170,    // (x,y) position

            200, 100,   // width/height

            hdcMem,

            0, 0,       // From (x,y) in bitmap

            100, 100,   // width/height of bitmap

            w.SRCCOPY

        );

        w.StretchBlt

        (

            hdc,

            220, 20,

            100, 250,

            hdcMem,

            0, 0,

            100, 100,

            w.SRCCOPY

        );

        // Delete the memory-based device context we created because

        // we're now done with it:

        w.DeleteDC( hdcMem );

        // Delete the bitmap object we created, because we're done

        // with it:

        w.DeleteObject( hBitmap );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

end Bitmaps;

This section barely touches on the functionality of Windows with respect to handling bitmaps. Alas, space limitations prevent spending as much time as we should on this important Windows facility. For more details, consult the Windows documentation for the GDI functions.

7.12: Metafiles

A metafile is a recording of a sequence of GDI commands that you can play back later. In this sense, a metafile is not unlike a path; however, metafiles are far more powerful than paths because you can record any drawings, not just lines in a metafile. Furthermore, once you've recorded a metafile, you can play that metafile back and rescale the image. And unlike bitmaps, the scaling produces a very high-fidelity image. Perhaps the greatest drawback to using metafiles (compared to bitmaps) is that they require more time to render an image.

Using metafiles is actually quite easy. There are are only four API functions you need to worry about: w.CreateEnhMetafile, w.CloseEnhMetafile, w.PlayEnhMetafile, and w.DeleteEnhMetaFile:

static

    CreateEnhMetaFile: procedure

    (

            hdcRef          :dword;

            lpFilename      :string;

        var lpRect          :RECT;

            lpDescription   :string

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateEnhMetaFileA@16" );

    CloseEnhMetaFile: procedure

    (

        hdc:dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CloseEnhMetaFile@4" );

    PlayEnhMetaFile: procedure

    (

            hdc             :dword;

            hemf            :dword;

        var lpRect          :RECT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__PlayEnhMetaFile@12" );

    DeleteEnhMetaFile: procedure

    (

        hdc:dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__DeleteEnhMetaFile@4" );

The w.CreateEnhMetaFile function is the function you call to begin recording a metafile. The first parameter, hdcRef, is the handle of a device context that this function will use as a template for the device context it is creating. If you pass NULL here, Windows will use the current display device as the DC template. The second parameter is a pointer to a string specifying the filename on disk where Windows will store the metafile information. If you specify NULL as this parameter, Windows will create an in-memory meta file. The third parameter is a rectangle that specifies the dimensions (in 0.01 millimeter units) of the picture to be stored in the metafile. If you specify NULL in this field, Windows will automatically compute this value for you; however, note that this computation runs rather slowly (especially for complex objects) and you should attempt to supply this value if you can figure it out yourself. The fourth parameter is a pointer to a sequence of two zero-terminated strings that specify the application's name and the pictures name (with a single null character between them and two null characters at the end of the string). If this parameter has the value NULL, then there will be no such name associated with the metafile. For most memory-based metafiles you'll want to create, you can set all four parameters to NULL.

The w.CreateEnhMetaFile function returns a handle to a device context. Drawing via GDI functions to this device context will record all the drawing operations for later playback.

The w.CloseEnhMetaFile closes the device context opened by w.CreateMetaFile and returns a handle to the metafile itself. You will supply this handle to the w.PlayEnhMetaFile and w.DeleteEnhMetaFile functions.

Calling w.PlayEnhMetaFile renders the image to some device context. The first parameter to this function is the output device context to where you want the image drawn. The second parameter to this function is the handle that was returned by w.CloseEnhMetaFile when you finished the creation of the metafile. The last parameter to w.PlayEnhMetaFile is a rectangle that provides a bounding box for the image to be drawn. Windows will scale the actual image to fit in this rectangle.

You must call the w.DeleteEnhMetaFile API function when you are done using the metafile. This deletes the internal Windows resources that the metafile uses so other applications in the system can reuse those resources.

The Metafiles.hla application on the CD-ROM accompanying this book provides a simple demonstration of the use of metafiles. This short application records a metafile consisting of a square, a circle, and an "x" and then plays this metafile back repeatedly in the application's window. The output from this program appears in Figure 7-24.

Figure 7-24: Output from the Metafiles.hhf Program

// Metafiles.hla-

//

//  Program that demonstrates how to use a metafile.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit Metafiles;

#includeonce( "rand.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "math.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

readonly

    ClassName   :string := "MetafilesWinClass";     // Window Class Name

    AppCaption  :string := "Metafiles Program";     // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hMetafile   :dword;         // Handle for our metafile.

    hmf         :dword;         // Handle for completed metafile.

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

    rect        :w.RECT;

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Create the metafile here:

    w.CreateEnhMetaFile( NULL, NULL, NULL, NULL );

    mov( eax, hMetafile );

    // Draw the objects we're recording to the metafile:

    w.Rectangle( hMetafile, 0, 0, 20, 20 );

    w.Ellipse( hMetafile, 0, 0, 20, 20 );

    w.MoveToEx( hMetafile, 0, 0, NULL );

    w.LineTo( hMetafile, 20, 20 );

    w.MoveToEx( hMetafile, 20, 0, NULL );

    w.LineTo( hMetafile, 0, 20 );

    // Close the metafile and save the handle away:

    w.CloseEnhMetaFile( hMetafile );

    mov( eax, hmf );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Play the meta file back several times across the screen:

        for( mov( 0, esi ); esi < 20; inc( esi )) do

            for( mov( 0, edi ); edi < 20; inc( edi )) do

                intmul( 20, esi, ecx );

                intmul( 20, edi, edx );

                mov( ecx, rect.left );

                add( 20, ecx );

                mov( ecx, rect.right );

                mov( edx, rect.top );

                add( 20, edx );

                mov( edx, rect.bottom );

                w.PlayEnhMetaFile( hdc, hmf, rect );

            endfor;

        endfor;

    EndPaint;

    w.DeleteEnhMetaFile( hmf );

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//  Basically, it just saves the window's size so

//  the Paint procedure knows when a line goes out of

//  bounds.

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    xor( eax, eax ); // return success.

end Size;

end Metafiles;

7.13: For More Information

Although this chapter is fairly long, it only begins to cover the GDI functions that are present in Microsoft Windows. In particular, this chapter doesn't do the discussion of bitmaps justice and there are many GDI functions that aren't even mentioned in this chapter. For more information, you'll want to consult the Microsoft GDI documentation (e.g., MSDN or the HLA/Windows function descriptions appearing on the accompanying CD-ROM).

1Note that one inch is 25.4 millimeters, so conversion from the metric to the English system is fairly trivial, involving only a single division (or multiplication if converting in the opposite direction).
2If nothing else, putting all this common code into a library module will reduce the size of the listings in this book, making this book easier to read and far less redundant.
3For those who know a little digital logic, the number 16 comes from the fact that there are 16 different possible functions of two boolean variables.
4Or other sequence during which you've obtained a handle to a device context. You can also call the w.SelectObject API function directly, passing it the device context handle and the brush's handle.
TOC PREV NEXT INDEX


Chapter 8: Event-Driven Input

8.1: Event-Driven Input Versus "Hurry Up and Wait"

Immediately after a new Win32 programmer gets over the shock of discovering that event-driven output is nothing like the type of programming they're used to, they take a look at event-driven input and go into shock again. Most programmers are comfortable using statement like "readln" and "gets" that they can call to read whole lines of text, lists of values, etc. This has become known as the "hurry up and wait" model because the programmer writes code that runs as quickly as possible between two input operations (so the user doesn't notice a delay between inputs) and then the computer sits idle for a very long time waiting for theuser to enter data from the keyboard.

The event-driven input model is far more efficient from the CPU's point of view. The operating system sends your application messages whenever there is some input to process. Therefore, the system isn't just sitting around waiting for the input to arrive - it can be doing other things (including passing other messages on to your application for processing).

Unfortunately, from the application's point of view, event-driven input is far more complex. With a statement like "readln( i32);" in Pascal or "cin >> i32;" in C++, you're telling the operating system to take charge until the user enters an integer value (assuming i32 is an integer variable). The application doesn't have to deal with collecting the individual characters together that make up the string that represents this integer. Nor does the application have to deal with the conversion of that string to an integer value. More importantly, the application doesn't have to worry about dealing with other possible inputs while waiting for the user to input this integer value. All of these simplifications go away when working with input in an event-driven system like Windows.

The input model in Windows is actually quite simple. If an input event occurs (that is associated with your application), then Windows sends your application a message specifying the type of input event. The bad news is that these are very simple events, like "someone just pressed a key on the keyboard" or "someone just pressed a button on the mouse." It is up to your application to interpret these events in an abstract manner (e.g., the user has pressed a sequence of keystrokes whose ASCII codes form a numeric string that the application can convert to an integer value).

While the concept is simple, there is a major problem with the Windows' model: the user of your application can decide to pause the input of some integer value (for example) half-way through the input operation, switch to some other input box and enter data there, then return and finish the original input string. This fact adds conceptual complexity on top of programming complexity (that is, it's hard to visualize how the end-user might behave when designing the application, thus increasing the likelihood that your program may not consider some possible input sequence by the user). In this chapter, we're going to consider how to deal with these complexities.

Although the primary focus of this chapter is going to be on the keyboard, we're also going to look at mouse input and timer events in this chapter. Mouse (or pointer device) input is probably the second-most common form of input in use under Windows. Timer events are less common, but very important. A timer event lets you wake up an application by sending it a periodic (or timed) message even in the absence of any other events or messages directed at the application. We'll consider these three forms of input in this chapter.

8.2: Focus on Focus

Because a typical PC has only a single set of input devices, applications must share the use of these input devices. In particular, the operating system will typically send all input events some device generates to a single application. For example, the operating system only sends keystrokes to a single application's window (imagine the confusion that would ensue if the OS sent the same keystroke messages to a word processor, a spreadsheet, and a database application simultaneously). To control which application (or, more specifically, which window in an application) receives input messages, Windows uses the concept of input focus.

Focus is associated with the currently active Window. This is the window to which Windows will send all keyboard (and possibly other) input events. If the user switches between active windows on their desktop, then the input focus switches to the new active window and all keyboard messages are sent to the new active window rather than the original window.

When the Windows makes some window the active window, it sends that application's window procedure a w.WM_SETFOCUS message so the application is made aware that it might be receiving keyboard messages before too much longer. Conversely, when the user switches from one window to another, Windows first sends the window that originally had the keyboard focus a w.WM_KILLFOCUS message to let it know that a focus change is about to occur and should take appropriate action.

8.3: Stick and Caret

Whenever a window is expecting keyboard input, it usually displays a flashing block, underline, vertical bar, or some other symbol to indicate the position on the window where the application will place any characters input via the keyboard. Although the common term for this symbol is cursor, Windows actually uses the term caret to describe the keyboard input position1. In this section we'll take a look at the functions that control the display of the caret within your application's window.

Because only one window can have the input focus, that is, only one application window can receive keyboard input, there is only one system-wide caret. It wouldn't do for two open application windows to be displaying that blinking caret - the poor user wouldn't be able to tell which window would receive the next input character that they type. Therefore, your applications can display the caret when they are given the input focus and they must relinquish the input caret when they give up the input focus. As noted above, Windows sends your application's window procedure the w.WM_SETFOCUS and w.WM_KILLFOCUS messages when it gives or retracts the input focus (respectively). By handling these messages, your applications can properly obtain and release the caret as needed.

When an application is given the focus (i.e., it receives a w.WM_SETFOCUS message) it should call the w.CreateCaret function to create a caret specifically for the application. The prototype for this function is the following:

static

    CreateCaret: procedure

    (

        hWnd            :dword;

        hBitmap         :dword;

        nWidth          :dword;

        nHeight         :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__CreateCaret@16" );

The hWnd parameter is the handle of the window that will own the caret (e.g., your application's main window). The hBitmap parameter is the handle of a bitmap object that Windows will use for the caret. If this parameter is NULL, Windows will not use a bitmap for the caret and will, instead, use the nHeight and nWidth parameters to define the caret. The nHeight and nWidth parameters specify a block-style cursor that is nWidth pixels wide and nHeight pixels high.

When your program gives up the focus (i.e., when you receive a w.WM_KILLFOCUS message), you must destroy the caret you've created with a w.DestroyCaret API call. Here's the prototype for that function:

static

    DestroyCaret: procedure;

        @stdcall;

        @returns( "eax" );

        @external( "__imp__DestroyCaret@0" );

Note that this function doesn't require any parameters. Because there is only one system caret, Windows will destroy the only one in existence (the one associated with the window handle you originally passed to w.CreateCaret).

Whenever you create a caret via the w.CreateCaret API call, Windows creates an invisible caret. In order to actually display the caret you need to call the w.ShowCaret API function. While your application is holding the focus (and the caret), you can make the caret invisible again by calling the w.HideCaret API function. Here are their prototypes:

static

    ShowCaret: procedure

    (

        hWnd        :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__ShowCaret@4" );

    HideCaret: procedure

    (

        hwnd            :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__HideCaret@4" );

For both procedures, the hwnd parameter is the handle of the window that currently has the caret attached to it. This must be the same handle you originally passed to the w.CreateCaret function.

Windows maintains an internal "caret visible" counter that w.ShowCaret increments and w.HideCaret decrements. While this counter is positive, Windows shows the caret. When this value is zero, Windows hides the cursor. Therefore, if you call w.ShowCaret several times without a comparable number of calls to w.HideCaret, Windows will keep the caret visible until you've made the corresponding number of calls to w.HideCaret.

Once you enable the display of the caret in your application's window, you control the position of the caret via the w.SetCaretPos function call. This function has the following prototype:

static

    SetCaretPos: procedure

    (

        X           :dword;

        Y           :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetCaretPos@8" );

The (X,Y) parameters specify the x-coordinate and y-coordinate of the caret in the client area of your window. In particular, these coordinates specify the upper-left hand corner of the caret bitmap or block in your window.

Armed with this information about the caret and the w.WM_SETFOCUS and w.WM_KILLFOCUS messages, it's now possible to write some code that will automatically show the caret whenever your application gains the focus and hides the caret whenever it loses the focus. Here are a couple of routines, SetFocus and KillFocus, that handle these messages and take the appropriate actions:

// SetFocus-

//

//  This procedure gets called whenever this application gains the

// input focus.

procedure SetFocus( hwnd: dword; wParam:dword; lParam:dword );

begin SetFocus;

    w.CreateCaret( hwnd, NULL, AverageCharWidth, AverageCharHeight );

    w.SetCaretPos( 0, 0 ); // "Home" the cursor

    w.ShowCaret( hwnd );

    xor( eax, eax );  // Return success

end SetFocus;

// KillFocus-

//

//  Processes the WM_KILLFOCUS message that gets sent whenever this

// application is losing the input focus.

procedure KillFocus( hwnd: dword; wParam:dword; lParam:dword );

begin KillFocus;

    w.HideCaret( hwnd );

    w.DestroyCaret();

    xor( eax, eax );  // Return success

end KillFocus;

These routines are not completely general. First of all, the SetFocus function always homes the cursor to position (0, 0). Second, you might not always want to show the caret whenever you get the focus (e.g., the application may not be prepared to accept keyboard input just because it has received the focus). Nonetheless, these two message handling procedures demonstrate how to call these five different API functions.

There are a couple of additional API functions related to the caret that you might find useful. The first of these is w.GetCaretPos which returns the current caret position in the window. The prototype for this function is the following:

static

    GetCaretPos: procedure

    (

        var lpPoint     :POINT

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetCaretPos@4" );

This function returns the current caret position in the lpPoint parameter you pass by reference (w.POINT objects have an x and a y field that receive the caret coordinates).

The w.GetCaretBlinkTime and the w.SetCaretBlinkTime functions have the following prototypes:

static

    GetCaretBlinkTime: procedure;

        @stdcall;

        @returns( "eax" );

        @external( "__imp__GetCaretBlinkTime@0" );

    SetCaretBlinkTime: procedure

    (

        uMSeconds       :dword

    );

        @stdcall;

        @returns( "eax" );

        @external( "__imp__SetCaretBlinkTime@4" );

The w.GetCaretBlinkTime function returns the number of milliseconds that pass between inversions of the caret on the display (the alternating inversions of the bitmap are what causes the caret to "blink"). The uMSeconds parameter you pass to w.SetCaretBlinkTime specifies the number of milliseconds between inversions of the caret bitmap.

8.4: Keyboard Messages

Windows sends several different keyboard related messages to your applications. In fact, a single keystroke typically winds up sending three different messages to your application's window procedure. Fortunately, you can ignore many of the messages Windows sends to your applications. Most of the time, there are only two types of messages to which you will normally respond. Nevertheless, it's important to understand the purpose of each of these messages in order to properly process the keyboard messages that are important to you.

Windows will send the following keyboard messages to your application:

  • w.WM_KEYDOWN
  • w.WM_KEYUP
  • w.WM_SYSKEYDOWN
  • w.WM_SYSKEYUP
  • w.WM_CHAR
  • w.WM_SYSCHAR
  • w.WM_DEADCHAR
  • w.WM_SYSDEADCHAR

Most of these messages you can ignore. In fact, the vast majority of the time you can get by processing only w.WM_KEYDOWN and w.WM_CHAR messages.

To understand the purpose of each of these messages, a brief discussion of the keyboard's operation is necessary. The standard PC keyboard does not produce ASCII character code whenever you process a key. Instead, the keyboard sends out one of two numeric codes (known as "scan codes"). One scan code indicates that the user has just pressed the key (a down code), a second scan code indicates that the user has released the key (an up code). Usually, you will get one up code for each down code (i.e., the user presses and releases a key, generating the two key codes). The exception is when the user holds down a key long enough for it to being autorepeating. In this case you will get a sequence of down codes without a corresponding up code. When the user finally releases the key, you will get a single up code for that key.

Note that a user can hold a key down while pressing other keys. The system can use the down and up codes to determine if you are holding down one key (e.g., a shift or control key) while pressing and releasing other keys. This allows the system to translate a scan code for some key like "A" into different ASCII codes, based on whether you're holding down other keys while pressing the "A". For example, "A" without a modifier key like shift, control, or alt, produces the `a' character; holding down shift while pressing "A" produces the "A" character; holding down the control or alt key produces CTRL-A or ALT-A, respectively.

An important thing to realize is that keyboard scan codes are not the same thing as ASCII codes. These are simply some numeric values that the hardware manufacturer chose when desiging the keyboard. Indeed, different manufacturers have been known to use different scan code sets for their computer keyboards. To eliminate problems with different scan code sets, Windows defines a "virtual keycode" set. People who write keyboard drivers translate their "OEM scan codes" into virtual keycodes and pass those keycodes on to applications. This allows applications to deal with a single set of codes rather than having to worry about differences in the underlying hardware. Table 8-1 lists the standard Windows virtual key codes.

Table 8-1: Windows Virtual Keycodes
Value
windows.hhf Constant
Description
$01
w.VK_LBUTTON
Left mouse button
$02
w.VK_RBUTTON
Right mouse button
$03
w.VK_CANCEL
Ctrl-Break
$04
w.VK_MBUTTON
Middle mouse button
$05
w.VK_XBUTTON1
Win 2K X1 mouse button
$06
w.VK_XBUTTON2
Win 2K X2 mouse button
$08
w.VK_BACK
Backspace
$09
w.VK_TAB
Tab
$0C
w.VK_CLEAR
5 on numeric keypad with Numlock off
$0D
w.VK_RETURN
Carriage Return (Enter) key
$10
w.VK_SHIFT
Either shift key
$11
w.VK_CONTROL
Either control key
$12
w.VK_MENU
Alt key
$13
w.VK_PAUSE
Pause key
$14
w.VK_CAPITAL
Capslock key
$1B
w.VK_ESCAPE
Escape key
$20
w.VK_SPACE
Space key
$21
w.VK_PGUP
PgUp key
$21
w.VK_PRIOR
PgUp key
$22
w.VK_NEXT
PgDn key
$22
w.VK_PGDN
PgDn key
$23
w.VK_END
End key
$24
w.VK_HOME
Home key
$25
w.VK_LEFT
Left arrow key
$26
w.VK_UP
Up arrow key
$27
w.VK_RIGHT
Right arrow key
$28
w.VK_DOWN
Down arrow key
$29
w.VK_SELECT
Special key on certain keyboards
$2A
w.VK_PRINT
Special key on certain keyboards (e.g., WinCE devices)
$2B
w.VK_EXECUTE
Special key on certain keyboards (e.g., WinCE devices)
$2C
w.VK_SNAPSHOT
Print Screen key
$2D
w.VK_INSERT
Ins key
$2E
w.VK_DELETE
Del key
$2F
w.VK_HELP
Special key on certain keyboards (e.g., WinCE)
$30
w.VK_0
0
$31
w.VK_1
1
$32
w.VK_2
2
$33
w.VK_3
3
$34
w.VK_4
4
$35
w.VK_5
5
$36
w.VK_6
6
$37
w.VK_7
7
$38
w.VK_8
8
$39
w.VK_9
9
$41
w.VK_A
A
$42
w.VK_B
B
$43
w.VK_C
C
$44
w.VK_D
D
$45
w.VK_E
E
$46
w.VK_F
F
$47
w.VK_G
G
$48
w.VK_H
H
$49
w.VK_I
I
$4A
w.VK_J
J
$4B
w.VK_K
K
$4C
w.VK_L
L
$4D
w.VK_M
M
$4E
w.VK_N
N
$4F
w.VK_O
O
$50
w.VK_P
P
$51
w.VK_Q
Q
$52
w.VK_R
R
$53
w.VK_S
S
$54
w.VK_T
T
$55
w.VK_U
U
$56
w.VK_V
V
$57
w.VK_W
W
$58
w.VK_X
X
$59
w.VK_Y
Y
$5A
w.VK_Z
Z
$60
w.VK_NUMPAD0
0 on the numeric keypad
$61
w.VK_NUMPAD1
1 on the numeric keypad
$62
w.VK_NUMPAD2
2 on the numeric keypad
$63
w.VK_NUMPAD3
3 on the numeric keypad
$64
w.VK_NUMPAD4
4 on the numeric keypad
$65
w.VK_NUMPAD5
5 on the numeric keypad
$66
w.VK_NUMPAD6
6 on the numeric keypad
$67
w.VK_NUMPAD7
7 on the numeric keypad
$68
w.VK_NUMPAD8
8 on the numeric keypad
$69
w.VK_NUMPAD9
9 on the numeric keypad
$6A
w.VK_MULTIPLY
* on the numeric keypad
$6B
w.VK_ADD
+ on the numeric keypad
$6C
w.VK_SEPARATOR
Special key on certain keyboards
$6D
w.VK_SUBTRACT
- on the numeric keypad
$6E
w.VK_DECIMAL
. on the numeric keypad
$6F
w.VK_DIVIDE
/ on the numeric keypad
$70
w.VK_F1
F1
$71
w.VK_F2
F2
$72
w.VK_F3
F3
$73
w.VK_F4
F4
$74
w.VK_F5
F5
$75
w.VK_F6
F6
$76
w.VK_F7
F7
$77
w.VK_F8
F8
$78
w.VK_F9
F9
$79
w.VK_F10
F10
$7A
w.VK_F11
F11
$7B
w.VK_F12
F12
$7C
w.VK_F13
F13
$7D
w.VK_F14
F14
$7E
w.VK_F15
F15
$7F
w.VK_F16
F16
$80
w.VK_F17
F17
$81
w.VK_F18
F18
$82
w.VK_F19
F19
$83
w.VK_F20
F20
$84
w.VK_F21
F21
$85
w.VK_F22
F22
$86
w.VK_F23
F23
$87
w.VK_F24
F24
$90
w.VK_NUMLOCK
Numlock key
$91
w.VK_SCROLL
Scroll lock key
$A0
w.VK_LSHIFT
Left shift key
$A1
w.VK_RSHIFT
Right shift key
$A2
w.VK_LCONTROL
Left control key
$A3
w.VK_RCONTROL
Right control key
$A4
w.VK_LMENU
Left alt key
$A5
w.VK_RMENU
Right alt key
$BA
w.VK_SEMICOLON
";:" key (US keyboard)
$BB
w.VK_EQUAL
"=+" key (US keyboard)
$BC
w.VK_COMMA
",<" key (US keyboard)
$BD
w.VK_MINUS
"-_" key (US keyboard)
$BE
w.VK_PERIOD
".>" key (US keyboard)
$BF
w.VK_SLASH
"/?" key (US keyboard)
$C0
w.VK_GRAVEACCENT
"`~" key (US keyboard)
$DB
w.VK_LBRACKET
"[{" key (US keyboard)
$DC
w.VK_BSLASH
"\|" key (US keyboard)
$DD
w.VK_RBRACKET
"]}" key (US keyboard)
$DE
w.VK_APOST
"`"" key (US keyboard)

The first pair of messages to look at are the w.WM_KEYDOWN and w.WM_KEYUP messages. Windows sends these two messages whenever the user presses or releases an application key (versus a system key), respectively. The wParam field of the message payload (i.e., the wParam parameter in the window procedure call) specifies the Windows' virtual keycode for the message (see Table 8-1). The lParam parameter contains the information found in Table 8-2.

Table 8-2: lParam Data in a w.WM_KEYDOWN or w.WM_KEYUP Message
Bit Positions
Description
0..15
Repeat Count. This field specifies the number of keyboard messages (all the same) received for the current virtual keycode. Note that there is no guarantee that you won't see two consecutive keyboard messages with the same information (rather than merging them together into one message with an incremented repeat count). Windows uses this repeat count to minimize messages in the message queue during long auto-repeat operations while the application isn't reading messages. Note that the repeat count only applies to w.WM_KEYDOWN messages. The repeat count is always one for w.WM_KEYUP messages.
16-23
The is the OEM keyboard scan code. You should probably ignore this value as it may vary for keyboards from different manufacturers.
24
Specifies whether the key is an extended key (e.g., the right CTRL or ALT key). This bit contains one if the message is due to an extended key press or release.
25-28
Unused. Ignore these bits.
29
Context code. This code specifies whether the ALT key was pressed when this keyboard message was sent. This bit is always zero for up-key events.
30
This bit specifies the previous key state. It contains zero if the key was previously up and one if the key was previously down. Note that this bit will always contain one for w.WM_KEYUP messages, though it may contain zero or one for w.WM_KEYDOWN messages (because of the possibility of autorepeat).
31
This bit contains one if there was a down-to-up or up-to-down transition.

The w.WM_KEYDOWN and w.WM_KEYUP messages are useful for determining when the user presses a function key, cursor control key, or other special (non-ASCII) key on the keyboard. In fact, Windows will send these messages for every key you press on the keyboard if you are not holding the ALT key down simultaneously. If you are holding down the ALT key when you press a key on the keyboard, Windows will actually send w.WM_SYSKEYDOWN and w.WM_SYSKEYUP messages to your application. In general, however, your application should simply ignore these messages and let the default message handler process them. Windows will convert system keyboard messages into other message types and may pass those new messages on to your application for further processing.

To demonstrate how you would use the w.WM_KEYDOWN and w.WM_KEYUP messages in your applications to process virtual scan codes, we'll modify the System Metrics application that originally appeared in the chapter on Windows' text processing. This kbSysmet program extends the sysmet program by handling w.WM_KEYDOWN messages and translating certain cursor control keys (the arrow keys, page up, page down, home, and end) into messages that will cause the application to scroll the window in appropriate directions. The following is a typical case in a switch statement:

        // If they press the "HOME" key, scroll to the top of the window.

        // Do this by sending a w.WM_VSCROLL message to do the scrolling

        // routines to reposition the window to the beginning.

        case( w.VK_HOME )

            w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_TOP, 0 );

Other cases handle the other cursor control operations by translating the key press message into a corresponding mouse event on the scroll bar.

// kbSysmet.hla-

//

//  System metrics display program that supports keyboard messages.

unit kbSysmets;

// Set the following to true to display interesting information

// during program operation. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "excepts.hhf" )

#includeonce( "conv.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

type

    // Data type for the system metrics data array:

    MetricRec_t:

        record

            MetConst    :uns32;

            MetStr      :string;

            MetDesc     :string;

        endrecord;

static

    AverageCapsWidth    :dword;         // Font metric values.

    AverageCharWidth    :dword;

    AverageCharHeight   :dword;

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

    MaxWidth            :int32 := 0;    // Maximum output width

    VscrollPos          :int32 := 0;    // Tracks where we are in the document

    VscrollMax          :int32 := 0;    // Max display position (vertical).

    HscrollPos          :int32 := 0;    // Current Horz position.

    HscrollMax          :int32 := 0;    // Max Horz position.

readonly

    ClassName   :string := "kbSysmetsWinClass";     // Window Class Name

    AppCaption  :string := "kbSysmets Program";     // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY, &QuitApplication   ],

            MsgProcPtr_t:[ w.WM_PAINT,   &Paint             ],

            MsgProcPtr_t:[ w.WM_CREATE,  &Create            ],

            MsgProcPtr_t:[ w.WM_HSCROLL, &HScroll           ],

            MsgProcPtr_t:[ w.WM_VSCROLL, &VScroll           ],

            MsgProcPtr_t:[ w.WM_SIZE,    &Size              ],

            MsgProcPtr_t:[ w.WM_KEYDOWN, &KeyDown           ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

readonly

    MetricData: MetricRec_t[] :=

    [

        MetricRec_t:[ w.SM_CXSCREEN, "w.SM_CXSCREEN", "Screen width" ],

        MetricRec_t:[ w.SM_CYSCREEN, "w.SM_CYSCREEN", "Screen height" ],

        MetricRec_t:[ w.SM_CXVSCROLL, "w.SM_CXVSCROLL", "Vert scroll arrow width" ],

        MetricRec_t:[ w.SM_CYVSCROLL, "w.SM_CYVSCROLL", "Vert scroll arrow ht" ],

        MetricRec_t:[ w.SM_CXHSCROLL, "w.SM_CXHSCROLL", "Horz scroll arrow width" ],

        MetricRec_t:[ w.SM_CYHSCROLL, "w.SM_CYHSCROLL", "Horz scroll arrow ht" ],

        MetricRec_t:[ w.SM_CYCAPTION, "w.SM_CYCAPTION", "Caption bar ht" ],

        MetricRec_t:[ w.SM_CXBORDER, "w.SM_CXBORDER", "Window border width" ],

        MetricRec_t:[ w.SM_CYBORDER, "w.SM_CYBORDER", "Window border height" ],

        MetricRec_t:[ w.SM_CXDLGFRAME, "w.SM_CXDLGFRAME", "Dialog frame width" ],

        MetricRec_t:[ w.SM_CYDLGFRAME, "w.SM_CYDLGFRAME", "Dialog frame height" ],

        MetricRec_t:[ w.SM_CXHTHUMB, "w.SM_CXHTHUMB", "Horz scroll thumb width" ],

        MetricRec_t:[ w.SM_CYVTHUMB, "w.SM_CYVTHUMB", "Vert scroll thumb width" ],

        MetricRec_t:[ w.SM_CXICON, "w.SM_CXICON", "Icon width" ],

        MetricRec_t:[ w.SM_CYICON, "w.SM_CYICON", "Icon height" ],

        MetricRec_t:[ w.SM_CXCURSOR, "w.SM_CXCURSOR", "Cursor width" ],

        MetricRec_t:[ w.SM_CYCURSOR, "w.SM_CYCURSOR", "Cursor height" ],

        MetricRec_t:[ w.SM_CYMENU,  "w.SM_CYMENU", "Menu bar height" ],

        MetricRec_t:[ w.SM_CXFULLSCREEN, "w.SM_CXFULLSCREEN", "Largest client width" ],

        MetricRec_t:[ w.SM_CYFULLSCREEN, "w.SM_CYFULLSCREEN", "Largets client ht" ],

        MetricRec_t:[ w.SM_DEBUG, "w.SM_CDEBUG", "Debug version flag" ],

        MetricRec_t:[ w.SM_SWAPBUTTON, "w.SM_CSWAPBUTTON", "Mouse buttons swapped" ],

        MetricRec_t:[ w.SM_CXMIN, "w.SM_CXMIN", "Minimum window width" ],

        MetricRec_t:[ w.SM_CYMIN, "w.SM_CYMIN", "Minimum window height" ],

        MetricRec_t:[ w.SM_CXSIZE, "w.SM_CXSIZE", "Minimize/maximize icon width" ],

        MetricRec_t:[ w.SM_CYSIZE, "w.SM_CYSIZE", "Minimize/maximize icon height" ],

        MetricRec_t:[ w.SM_CXFRAME, "w.SM_CXFRAME", "Window frame width" ],

        MetricRec_t:[ w.SM_CYFRAME, "w.SM_CYFRAME", "Window frame height" ],

        MetricRec_t:[ w.SM_CXMINTRACK,  "w.SM_CXMINTRACK", "Minimum tracking width" ],

        MetricRec_t:[ w.SM_CXMAXTRACK,  "w.SM_CXMAXTRACK", "Maximum tracking width" ],

        MetricRec_t:[ w.SM_CYMINTRACK,  "w.SM_CYMINTRACK", "Minimum tracking ht" ],

        MetricRec_t:[ w.SM_CYMAXTRACK,  "w.SM_CYMAXTRACK", "Maximum tracking ht" ],

        MetricRec_t:[ w.SM_CXDOUBLECLK, "w.SM_CXDOUBLECLK", "Dbl-click X tolerance" ],

        MetricRec_t:[ w.SM_CYDOUBLECLK, "w.SM_CYDOUBLECLK", "Dbl-click Y tolerance" ],

        MetricRec_t:[ w.SM_CXICONSPACING, "w.SM_CXICONSPACING", "Horz icon spacing" ],

        MetricRec_t:[ w.SM_CYICONSPACING, "w.SM_CYICONSPACING", "Vert icon spacing" ],

        MetricRec_t:[ w.SM_CMOUSEBUTTONS, "w.SM_CMOUSEBUTTONS", " # of mouse btns" ]

    ];

const

    NumMetrics := @elements( MetricData );

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    dbg.put( hwnd, nl "Bitmaps3----------------", nl );

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Create-

//

//  This procedure responds to the w.WM_CREATE message.

//  Windows sends this message once when it creates the

//  main window for the application.  We will use this

//  procedure to do any one-time initialization that

//  must take place in a message handler.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc:    dword;              // Handle to video display device context

    tm:     w.TEXTMETRIC;

begin Create;

    GetDC( hwnd, hdc );

        // Initialization:

        //

        //  Get the text metric information so we can compute

        //  the average character heights and widths.

        GetTextMetrics( tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        mov( eax, AverageCharHeight );

        mov( tm.tmAveCharWidth, eax );

        mov( eax, AverageCharWidth );

        // If bit #0 of tm.tmPitchAndFamily is set, then

        // we've got a proportional font.  In that case

        // set the average capital width value to 1.5 times

        // the average character width.  If bit #0 is clear,

        // then we've got a fixed-pitch font and the average

        // capital letter width is equal to the average

        // character width.

        mov( eax, ebx );

        shl( 1, tm.tmPitchAndFamily );

        if( @c ) then

            shl( 1, ebx );              // 2*AverageCharWidth

        endif;

        add( ebx, eax );                // Computes 2 or 3 times eax.

        shr( 1, eax );                  // Computes 1 or 1.5 times eax.

        mov( eax, AverageCapsWidth );

    ReleaseDC;

    intmul( 40, AverageCharWidth, eax );

    intmul( 25, AverageCapsWidth, ecx );

    add( ecx, eax );

    mov( eax, MaxWidth );

end Create;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

//  For this System Metrics program, the Paint procedure

//  displays three columns of text in the main window.

//  This procedure computes and displays the appropriate text.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    x           :int32;         // x-coordinate of start of output str.

    y           :int32;         // y-coordinate of start of output str.

    CurVar      :string;        // Current system metrics variable name.

    CVlen       :uns32;         // Length of CurVar string.

    CurDesc     :string;        // Current system metrics description.

    CDlen       :string;        // Length of the above.

    CDx         :int32;         // X position for CurDesc string.

    value       :string;

    valData     :char[32];

    CVx         :int32;         // X position for value string.

    vallen      :uns32;         // Length of value string.

    firstMet    :int32;         // Starting metric to begin drawing

    lastMet     :int32;         // Ending metric index to draw.

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Initialize the value->valData string object:

    mov( str.init( (type char valData), 32 ), value );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Figure out which metric we should start drawing

        // (firstMet =

        //      max( 0, VscrollPos + ps.rcPaint.top/AverageCharHeight - 1)):

        mov( ps.rcPaint.top, eax );

        cdq();

        idiv( AverageCharHeight );

        add( VscrollPos, eax );

        dec( eax );

        if( (type int32 eax) < 0 ) then

            xor( eax, eax );

        endif;

        mov( eax, firstMet );

        // Figure out the last metric we should be drawing

        // ( lastMet =

        //      min( NumMetrics,

        //           VscrollPos + ps.rcPaint.bottom/AverageCharHeight )):

        mov( ps.rcPaint.bottom, eax );

        cdq();

        idiv( AverageCharHeight );

        add( VscrollPos, eax );

        if( (type int32 eax) > NumMetrics ) then

            mov( NumMetrics, eax );

        endif;

        mov( eax, lastMet );

        // The following loop processes each entry in the

        // MetricData array.  The loop control variable (EDI)

        // also determines the Y-coordinate where this code

        // will display each line of text in the window.

        // Note that this loop counts on the fact that Windows

        // API calls preserve the EDI register.

        for( mov( firstMet, edi ); edi < lastMet; inc( edi )) do

            // Before making any Windows API calls (which have

            // a nasty habit of wiping out registers), compute

            // all the values we will need for these calls

            // and save those values in local variables.

            //

            //  A typical "high level language solution" would

            // be to compute these values as needed, immediately

            // before each Windows API calls.  By moving this

            // code here, we can take advantage of values previously

            // computed in registers without having to worry about

            // Windows wiping out the values in those registers.

            // Compute index into MetricData:

            intmul( @size( MetricRec_t ), edi, esi );

            // Grab the string from the current MetricData element:

            mov( MetricData.MetStr[ esi ], eax );

            mov( eax, CurVar );

            mov( (type str.strRec [eax]).length, eax );

            mov( eax, CVlen );

            mov( MetricData.MetDesc[ esi ], eax );

            mov( eax, CurDesc );

            mov( (type str.strRec [eax]).length, eax );

            mov( eax, CDlen );

            // Column one begins at X-position AverageCharWidth (ACW).

            // Col 2 begins at ACW + 25*AverageCapsWidth.

            // Col 3 begins at ACW + 25*AverageCapsWidth + 40*ACW.

            // Compute the Col 2 and Col 3 values here.

            mov( 1, eax );

            sub( HscrollPos, eax );

            intmul( AverageCharWidth, eax );

            mov( eax, x );

            intmul( 25, AverageCapsWidth, eax );

            add( x, eax );

            mov( eax, CDx );

            intmul( 40, AverageCharWidth, ecx );

            add( ecx, eax );

            mov( eax, CVx );

            // The Y-coordinate for the line of text we're writing

            // is computed as AverageCharHeight * (1-VscrollPos+edi).

            // Compute that value here:

            mov( 1, eax );

            sub( VscrollPos, eax );

            add( edi, eax );

            intmul( AverageCharHeight, eax );

            mov( eax, y );

            // Now generate the string we're going to print

            // as the value for the current metric variable:

            w.GetSystemMetrics( MetricData.MetConst[ esi ] );

            conv.i32ToStr( eax, 0, ' ', value );

            mov( str.length( value ), vallen );

            // First two columns have left-aligned text:

            SetTextAlign( w.TA_LEFT | w.TA_TOP );

            // Output the name of the metric variable:

            TextOut( x, y, CurVar, CVlen );

            // Output the description of the metric variable:

            TextOut( CDx, y, CurDesc, CDlen );

            // Output the metric's value in the third column.  This is

            // a numeric value, so we'll right align this data.

            SetTextAlign( w.TA_RIGHT | w.TA_TOP );

            TextOut( CVx, y, value, vallen );

            // Although not strictly necessary for this program,

            // it's a good idea to always restore the alignment

            // back to the default (top/left) after you done using

            // some other alignment.

            SetTextAlign( w.TA_LEFT | w.TA_TOP );

        endfor;

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    // VscrollMax = max( 0, NumMetrics+2 - ClientSizeY/AverageCharHeight )

    cdq();

    idiv( AverageCharHeight );

    mov( NumMetrics+2, ecx );

    sub( eax, ecx );

    if( @s ) then

        xor( ecx, ecx );

    endif;

    mov( ecx, VscrollMax );

    // VscrollPos = min( VscrollPos, VscrollMax )

    if( ecx > VscrollPos ) then

        mov( VscrollPos, ecx );

    endif;

    mov( ecx, VscrollPos );

    w.SetScrollRange( hwnd, w.SB_VERT, 0, VscrollMax, false );

    w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true );

    // HscrollMax =

    //  max( 0, 2 + (MaxWidth - ClientSizeX) / AverageCharWidth);

    mov( MaxWidth, eax );

    sub( ClientSizeX, eax );

    cdq();

    idiv( AverageCharWidth );

    add( 2, eax );

    if( @s ) then

        xor( eax, eax );

    endif;

    mov( eax, HscrollMax );

    // HscrollPos = min( HscrollMax, HscrollPos )

    if( eax > HscrollPos ) then

        mov( HscrollPos, eax );

    endif;

    mov( eax, HscrollPos );

    w.SetScrollRange( hwnd, w.SB_HORZ, 0, HscrollMax, false );

    w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true );

    xor( eax, eax ); // return success.

end Size;

// HScroll-

//

//  Handles w.WM_HSCROLL messages.

//  On entry, L.O. word of wParam contains the scroll bar activity.

procedure HScroll( hwnd: dword; wParam:dword; lParam:dword );

begin HScroll;

    // Convert 16-bit command to 32 bits so we can use switch macro:

    movzx( (type word wParam), eax );

    switch( eax )

        case( w.SB_LINELEFT )

            mov( -1, eax );

        case( w.SB_LINERIGHT )

            mov( 1, eax );

        case( w.SB_PAGELEFT )

            mov( -8, eax );

        case( w.SB_PAGERIGHT )

            mov( 8, eax );

        case( w.SB_THUMBPOSITION )

            movzx( (type word wParam[2]), eax );

            sub( HscrollPos, eax );

        default

            xor( eax, eax );

    endswitch;

    // eax =

    //  max( -HscrollPos, min( eax, HscrollMax - HscrollPos ))

    mov( HscrollPos, edx );

    neg( edx );

    mov( HscrollMax, ecx );

    add( edx, ecx );

    if( eax > (type int32 ecx) ) then

        mov( ecx, eax );

    endif;

    if( eax < (type int32 edx )) then

        mov( edx, eax );

    endif;

    if( eax <> 0 ) then

        add( eax, HscrollPos );

        imul( AverageCharWidth, eax );

        neg( eax );

        w.ScrollWindow( hwnd, eax, 0, NULL, NULL );

        w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true );

    endif;

    xor( eax, eax ); // return success

end HScroll;

// VScroll-

//

//  Handles the w.WM_VSCROLL messages from Windows.

//  The L.O. word of wParam contains the action/command to be taken.

//  The H.O. word of wParam contains a distance for the w.SB_THUMBTRACK

//  message.

procedure VScroll( hwnd: dword; wParam:dword; lParam:dword );

begin VScroll;

    movzx( (type word wParam), eax );

    switch( eax )

        case( w.SB_TOP )

            mov( VscrollPos, eax );

            neg( eax );

        case( w.SB_BOTTOM )

            mov( VscrollMax, eax );

            sub( VscrollPos, eax );

        case( w.SB_LINEUP )

            mov( -1, eax );

        case( w.SB_LINEDOWN )

            mov( 1, eax );

        case( w.SB_PAGEUP )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            neg( eax );

            if( (type int32 eax) > -1 ) then

                mov( -1, eax );

            endif;

        case( w.SB_PAGEDOWN )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            if( (type int32 eax) < 1 ) then

                mov( 1, eax );

            endif;

        case( w.SB_THUMBTRACK )

            movzx( (type word wParam[2]), eax );

            sub( VscrollPos, eax );

        default

            xor( eax, eax );

    endswitch;

    // eax = max( -VscrollPos, min( eax, VscrollMax - VscrollPos ))

    mov( VscrollPos, edx );

    neg( edx );

    mov( VscrollMax, ecx );

    add( edx, ecx );

    if( eax > (type int32 ecx) ) then

        mov( ecx, eax );

    endif;

    if( eax < (type int32 edx)) then

        mov( edx, eax );

    endif;

    if( eax <> 0 ) then

        add( eax, VscrollPos );

        intmul( AverageCharHeight, eax );

        neg( eax );

        w.ScrollWindow( hwnd, 0, eax, NULL, NULL );

        w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true );

        w.UpdateWindow( hwnd );

    endif;

    xor( eax, eax ); // return success.

end VScroll;

// KeyDown-

//

//  Handles the w.WM_KEYDOWN messages from Windows.

//  The L.O. word of wParam contains the action/command to be taken.

//  The H.O. word of wParam contains a distance for the w.SB_THUMBTRACK

//  message.

procedure KeyDown( hwnd: dword; wParam:dword; lParam:dword );

begin KeyDown;

    mov( wParam, eax );

    switch( eax )

        // If they press the "HOME" key, scroll to the top of the window:

        case( w.VK_HOME )

            w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_TOP, 0 );

        // If they press the "END" key, scroll to the bottom of the window:

        case( w.VK_END )

            w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_BOTTOM, 0 );

        // If they press the "PgUp" key, scroll up one page:

        case( w.VK_PRIOR )

            w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_PAGEUP, 0 );

        // If they press the "PgDn" key, scroll down one page:

        case( w.VK_NEXT )

            w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_PAGEDOWN, 0 );

        // If they press the "Up" key, scroll up one line:

        case( w.VK_UP )

            w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_LINEUP, 0 );

        // If they press the "Down" key, scroll down one line:

        case( w.VK_DOWN )

            w.SendMessage( hwnd, w.WM_VSCROLL, w.SB_LINEDOWN, 0 );

        // If they press the "Left" key, scroll text to the right:

        case( w.VK_LEFT )

            w.SendMessage( hwnd, w.WM_HSCROLL, w.SB_PAGEUP, 0 );

        // If they press the "Right" key, scroll text to the left:

        case( w.VK_RIGHT )

            w.SendMessage( hwnd, w.WM_HSCROLL, w.SB_PAGEDOWN, 0 );

    endswitch;

end KeyDown;

end kbSysmets;

The kbSysmets program demonstrates how to process raw virtual keycodes in your program. Most of the time, however, you're not interested in the virtual keycodes; what you really want are ASCII codes for whatever keys the user presses. The only time you'll really want to deal with virtual key codes is when reading keystrokes from the user that have no corresponding ASCII codes.

In theory, you could convert virtual keycodes into ASCII codes yourself. The catch is that you must maintain certain state information, such as which modifier keys (shift, control, alt, capslock, numlock, etc.) are currently active and use this information to translate a virtual key code into the corresponding ASCII code. For example, if you get the virtual keycode $41 (`A'), you would have to translate this code to $61 (`a'), $41 (`A'), or $01 (ctrl-A), depending on the state of the shift and control keys (note that Windows treats the alt modifier key specially, you wouldn't normally have to deal with this). There are a couple of problems with this translation - first of all it's a lot of work. Second, and more important, there is no single translation you can do. Different Windows systems in different countries do the translation differently. Trying to handle the translation of virtual key codes for the dozens of different keyboards that exist today would be overwhelming. Fortunately, you don't have to do this translation yourself (nor should you attempt it): Windows will automatically do the translation for you. Consider the main message processing loop in the Winmain.hla module:

        forever

            w.GetMessage( msg, NULL, 0, 0 );

            breakif( eax == 0 );

            if( LocalProcessMsg( msg ) == 0) then

                w.TranslateMessage( msg );

            endif;

            w.DispatchMessage( msg );

        endfor;

The w.TranslateMessage API call intercepts w.WM_KEYDOWN and w.WM_KEYUP messages and generates a new message to send to your application if the key message corresponds to some ASCII character. Note that upon return, the message processing loop still sends the w.WM_KEYDOWN or w.WM_KEYUP message to your application; the new message that w.TranslateMessage creates will follow shortly. Therefore, when the w.TranslateMessage function does a translation, your application will actually receive two messages.

Before looking at these new message types, let's first discuss the LocalProcessMsg function call that appears in the loop above. This is not a Windows' API function call (note the lack of a "w." prefix). Instead, this is a call to an application-supplied function that determines whether any local message processing should take place. LocalProcessMsg returns zero/not zero in EAX to determine whether the main message processing loop should call the w.TranslateMessage API function (EAX is a "skip translation" flag, zero indicates that translation should take place and non-zero means to skip the translation operation). The vast majority of the time, you'll want to call the Windows' w.TranslateMessage API function and not mess around with the message. Therefore, the typical default LocalProcessMessage function looks like this:

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

That is, the function simply returns with EAX zero so that the main message processing loop will call the w.TranslateMessage API function. The single parameter is a pointer to a message structure, this object takes the following form:

type

    MSG:

        record

            hwnd:    dword;

            message: dword;

            wParam:  dword;

            lParam:  dword;

            time:    dword;

            pt:      POINT;

        endrecord;

The three fields of particular interest are the message, wParam, and lParam fields. These fields contain the values that Windows will pass as parameters to your Window procedure (i.e., the parameters that wind up being passed to your message handling procedures in your application). You can peek at the message field's valueto determine if you want to do any special processing on the message. If so, you can fetch other values from the wParam and lParam fields. Any changes you make to the fields of this structure will be passed along to w.TranslateMessage (if you set EAX to zero before returning) and to the w.DispatchMessage API function (that winds up calling your window procedure). Therefore, you can do some sophisticated translation of your own, should you choose to do so, within the LocalProcessMsg function. The vast majority of the time, however, you'll not bother with doing any translations inside this code.

If the message processing loop sends w.TranslateMessage a w.WM_KEYDOWN or w.WM_KEYUP message, then the w.TranslateMessage function may inject a new message into the message queue containing an ASCII translation of the keypress. This new message is a w.WM_CHAR message. The w.WM_CHAR message contains the same information in the lParam field as the w.WM_KEYDOWN and w.WM_KEYUP messages, the wParam field contains an ASCII key code rather than a Windows virtual key code.

Whenever you press (and release) a key that has a corresponding ASCII key code, Windows will actually send three messages to your application: a w.WM_KEYDOWN message, a w.WM_CHAR message, and then a w.WM_KEYUP message, in that order. Generally, you will ignore the keydown and key up messages and process only the w.WM_CHAR messages within your application (in fact, most applications also ignore all w.WM_KEYUP messages, as well).

Windows also sends a couple of other keyboard-related messages to your applications. These messages are w.WM_DEADCHAR, w.WM_SYSCHAR, and w.WM_SYSDEADCHAR. The "...SYS..." messages correspond to system keystrokes. You application can safely ignore these messages. Windows sends a w.WM_DEADCHAR message whenever you press an "accent key prefix" key on the keyboard that will produce an accented character (e.g., on non-U.S. keyboards). You can usually ignore all dead character messages as the following w.WM_CHAR message will incorporate the dead key information. The only reason for looking at these messages is to create your own, special, accented characters that Windows doesn't normally support. As such a need is rare, we won't consider the dead codes here any farther.

The following application, keytest.hla, is another application inspired by a program in Petzold's book. This program processes the keyboard messages and writes their data payloads to the application's window. This short application lets you view all the keyboard messages that come along whenever you press a key on the PC's keyboard.

// keytest.hla-

//

//  This program reads keystroke messages from the system and displays them.

unit keytest;

// Set the following to true to display interesting information

// during program operation. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "excepts.hhf" )

#includeonce( "conv.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

type

    // Data type for the keyboard message:

    keymsgPtr_t     :pointer to keymsg_t;

    keymsg_t:

        record

            // Maintains list of records:

            Next        :keymsgPtr_t;

            // MsgStr points at a string specifying the

            // message type: "WM_CHAR", "WM_KEYDOWN", etc:

            MsgStr      :string;

            // Virtual key code in WM_KEYDOWN/WM_KEYUP

            // messages:

            VirtKey     :dword;

            // Repeat count in message (# of autorepeated

            // keys passed on this message):

            RepeatCnt   :uns16;

            // ASCII code from WM_CHAR message:

            ASCIIcode   :byte;

            // OEM scan code in messages:

            ScanCode    :byte;

            // 1 indicates down->up transition, 0 indicates

            // an up->down transition:

            Transition  :uns8;

            // 1 indicates key previously down, 0 indicates

            // key was previously up:

            PrevKeyState:uns8;

            // 1 indicates that ALT was down when key

            // was pressed:

            Context     :uns8;

            // 1 indicates extended (non-keypad) cursor control

            // key was pressed:

            ExtendedKey :uns8;

            // 1 indicates left shift was down:

            LshiftState :uns8;

            // 1 indicates right shift was down:

            RshiftState :uns8;

            // 1 indicates that the left control

            // key was down:

            LctrlState  :uns8;

            // 1 indicates that the right control

            // key was down:

            RctrlState  :uns8;

            // 1 indicates that the left ALT key

            // was down:

            LaltState   :uns8;

            // 1 indicates that the right ALT key

            // was down:

            RaltState   :uns8;

            // 1 indicates that the keyboard is in the

            // "capslock" mode (caps are on):

            CapsLock    :uns8;

            // 1 indicates that the keyboard is in the

            // "numlock" mode:

            NumLock     :uns8;

            // 1 indicates that the keyboard is in the

            // "scroll lock" mode:

            ScrlLock    :uns8;

        endrecord;

static

    KeyPressList        :keymsgPtr_t := NULL;   // Ptr to list of key events.

    EndKeyPressList     :keymsgPtr_t := NULL;   // Ptr to last entry

    KeyPressCnt         :uns32 := 0;            // # of key event entries.

    TheFont             :dword;         // Font we'll use.

    AverageCapsWidth    :dword;         // Font metric values.

    AverageCharWidth    :dword;

    AverageCharHeight   :dword;

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

    VscrollPos          :int32 := 0;    // Tracks where we are in the document

    VscrollMax          :int32 := 0;    // Max display position (vertical).

    HscrollPos          :int32 := 0;    // Current Horz position.

    HscrollMax          :int32 := 0;    // Max Horz position.

    MaxWidth            :int32 := 0;    // Max # of chars on a line.

readonly

    ClassName   :string := "keytestWinClass";       // Window Class Name

    AppCaption  :string := "keytest Program";       // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY,    &QuitApplication    ],

            MsgProcPtr_t:[ w.WM_PAINT,      &Paint              ],

            MsgProcPtr_t:[ w.WM_CREATE,     &Create             ],

            MsgProcPtr_t:[ w.WM_HSCROLL,    &HScroll            ],

            MsgProcPtr_t:[ w.WM_VSCROLL,    &VScroll            ],

            MsgProcPtr_t:[ w.WM_SIZE,       &Size               ],

            MsgProcPtr_t:[ w.WM_KEYDOWN,    &KeyDown            ],

            MsgProcPtr_t:[ w.WM_KEYUP,      &KeyUp              ],

            MsgProcPtr_t:[ w.WM_SYSKEYDOWN, &SysKeyDown         ],

            MsgProcPtr_t:[ w.WM_SYSKEYUP,   &SysKeyUp           ],

            MsgProcPtr_t:[ w.WM_CHAR,       &CharMsg            ],

            MsgProcPtr_t:[ w.WM_DEADCHAR,   &DeadCharMsg        ],

            MsgProcPtr_t:[ w.WM_SYSCHAR,    &SysCharMsg         ],

            MsgProcPtr_t:[ w.WM_SYSDEADCHAR,&SysDeadCharMsg     ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Create-

//

//  This procedure responds to the w.WM_CREATE message.

//  Windows sends this message once when it creates the

//  main window for the application.  We will use this

//  procedure to do any one-time initialization that

//  must take place in a message handler.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc:    dword;              // Handle to video display device context

    tm:     w.TEXTMETRIC;

begin Create;

    // First, create a useful font (fixed pitch):

    w.GetStockObject( w.SYSTEM_FIXED_FONT );

    mov( eax, TheFont );

    GetDC( hwnd, hdc );

        // Initialization:

        //

        //  Get the text metric information so we can compute

        //  the average character heights and widths.

        SelectObject( TheFont );

        GetTextMetrics( tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        mov( eax, AverageCharHeight );

        mov( tm.tmAveCharWidth, eax );

        mov( eax, AverageCharWidth );

        // If bit #0 of tm.tmPitchAndFamily is set, then

        // we've got a proportional font.  In that case

        // set the average capital width value to 1.5 times

        // the average character width.  If bit #0 is clear,

        // then we've got a fixed-pitch font and the average

        // capital letter width is equal to the average

        // character width.

        mov( eax, ebx );

        shl( 1, tm.tmPitchAndFamily );

        if( @c ) then

            shl( 1, ebx );              // 2*AverageCharWidth

        endif;

        add( ebx, eax );                // Computes 2 or 3 times eax.

        shr( 1, eax );                  // Computes 1 or 1.5 times eax.

        mov( eax, AverageCapsWidth );

    ReleaseDC;

end Create;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

//  For this keytest program, the Paint procedure

//  displays the list of key events we've saved in memory.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    value       :string;

    valData     :char[256];

    vallen      :uns32;         // Length of value string.

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Initialize the value->valData string object:

    mov( str.init( (type char valData), 256 ), value );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Select the fixed-pitch font into our context:

        SelectObject( TheFont );

        // Figure out which keypress record we should start drawing

        // Begin by computing the number of lines of text we can

        // can draw in the window. This is

        //

        //          (ClientSizeY/AverageCharHeight)

        //

        // The number of keyboard events we can display is the

        // the minimum of this value and the number of events

        // we've seen thus far:

        mov( ClientSizeY, eax );

        cdq();

        idiv( AverageCharHeight );

        if( eax > KeyPressCnt ) then

            mov( KeyPressCnt, eax );

        endif;

        mov( eax, esi );

        // VscrollPos specifies the starting event record number

        // we're supposed to display. Search for that entry in

        // the keyboard event list:

        mov( KeyPressList, ebx );

        for( mov( 1, edi ); edi < VscrollPos; inc( edi )) do

            mov( (type keymsg_t [ebx]).Next, ebx );

        endfor;

        // Okay, draw all the records (esi currently holds the

        // number of records to draw):

        mov( 0, edi );  // Holds output y-coordinate.

        while( esi > 0 ) do

            // Create the output string for this line:

            str.put

            (

                value,

                (type keymsg_t [ebx]).MsgStr:-16,

                "VK=",

                (type byte (type keymsg_t [ebx]).VirtKey):2,

                " ASCII=",

                (type byte (type keymsg_t [ebx]).ASCIIcode):3,

                " SC=",

                (type byte (type keymsg_t [ebx]).ScanCode):3,

                " Trans=",

                (type keymsg_t [ebx]).Transition:2,

                " Prev=",

                (type keymsg_t [ebx]).PrevKeyState:2,

                " Context=",

                (type keymsg_t [ebx]).Context:2,

                " Ext=",

                (type keymsg_t [ebx]).ExtendedKey:2,

                " LS=",

                (type keymsg_t [ebx]).LshiftState:2,

                " RS=",

                (type keymsg_t [ebx]).RshiftState:2,

                " LC=",

                (type keymsg_t [ebx]).LctrlState:2,

                " RC=",

                (type keymsg_t [ebx]).RctrlState:2,

                " LA=",

                (type keymsg_t [ebx]).LaltState:2,

                " RA=",

                (type keymsg_t [ebx]).RaltState:2,

                " CL=",

                (type keymsg_t [ebx]).CapsLock:2,

                " NL=",

                (type keymsg_t [ebx]).NumLock:2,

                " SL=",

                (type keymsg_t [ebx]).ScrlLock:2

            );

            mov( value, eax );

            mov( (type str.strRec [eax]).length, ecx );

            // Activate the horizontal scroll bars if we emit a line

            // that is wider than the current window:

            if( ecx > MaxWidth ) then

                mov( ecx, MaxWidth );

                push( eax );

                mov( MaxWidth, eax );

                intmul( AverageCharWidth, eax );

                sub( ClientSizeX, eax );

                cdq();

                idiv( AverageCharWidth );

                add( 2, eax );

                if( @s ) then

                    xor( eax, eax );

                endif;

                mov( eax, HscrollMax );

                w.SetScrollRange( hwnd, w.SB_HORZ, 0, HscrollMax, false );

                pop( eax );

            endif;

            // Emit the current line to the display:

            add( HscrollPos, eax );     // Add in offset to 1st char in line

            sub( HscrollPos, ecx );     // Decrease line length by like amount.

            TextOut

            (

                5,

                edi,

                eax,

                ecx

            );

            // Move on to the next line of text to display:

            add( AverageCharHeight, edi );

            dec( esi );

            mov( (type keymsg_t [ebx]).Next, ebx );

        endwhile;

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    // Compute new bounds values for VscrollMax and VscrollPos based

    // on the new size of the window:

    //

    // VscrollMax = max( 0, KeyPressCnt - ClientSizeY/AverageCharHeight )

    mov( ClientSizeY, eax );

    cdq();

    idiv( AverageCharHeight );

    neg( eax );

    add( KeyPressCnt, eax );

    if( @s ) then

        xor( eax, eax );

    endif;

    mov( eax, VscrollMax );

    // VscrollPos = min( VscrollPos, VscrollMax )

    if( eax > VscrollPos ) then

        mov( VscrollPos, eax );

    endif;

    mov( eax, VscrollPos );

    // Set the new scrolling range and position based on the

    // new VscrollMax and VscrollPos values:

    w.SetScrollRange( hwnd, w.SB_VERT, 0, VscrollMax, false );

    w.SetScrollPos( hwnd, w.SB_VERT, VscrollPos, true );

    // Repeat the above for the horizontal scroll items:

    //

    // HscrollMax =

    //  max( 0, 2 + (MaxWidth - ClientSizeX) / AverageCharWidth);

    mov( MaxWidth, eax );

    intmul( AverageCharWidth, eax );

    sub( ClientSizeX, eax );

    cdq();

    idiv( AverageCharWidth );

    add( 2, eax );

    if( @s ) then

        xor( eax, eax );

    endif;

    mov( eax, HscrollMax );

    // HscrollPos = min( HscrollMax, HscrollPos )

    if( eax > HscrollPos ) then

        mov( HscrollPos, eax );

    endif;

    mov( eax, HscrollPos );

    w.SetScrollRange( hwnd, w.SB_HORZ, 0, HscrollMax, false );

    w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true );

    xor( eax, eax ); // return success.

end Size;

// HScroll-

//

//  Handles w.WM_HSCROLL messages.

//  On entry, L.O. word of wParam contains the scroll bar activity.

procedure HScroll( hwnd: dword; wParam:dword; lParam:dword );

begin HScroll;

    // Convert 16-bit command to 32 bits so we can use switch macro:

    movzx( (type word wParam), eax );

    switch( eax )

        case( w.SB_LINELEFT )

            mov( -1, eax );

        case( w.SB_LINERIGHT )

            mov( 1, eax );

        case( w.SB_PAGELEFT )

            mov( -8, eax );

        case( w.SB_PAGERIGHT )

            mov( 8, eax );

        case( w.SB_THUMBPOSITION )

            movzx( (type word wParam[2]), eax );

            sub( HscrollPos, eax );

        default

            xor( eax, eax );

    endswitch;

    // eax =

    //  max( -HscrollPos, min( eax, HscrollMax - HscrollPos ))

    mov( HscrollPos, edx );

    neg( edx );

    mov( HscrollMax, ecx );

    add( edx, ecx );

    if( eax > (type int32 ecx) ) then

        mov( ecx, eax );

    endif;

    if( eax < (type int32 edx )) then

        mov( edx, eax );

    endif;

    if( eax <> 0 ) then

        add( eax, HscrollPos );

        neg( eax );

        w.SetScrollPos( hwnd, w.SB_HORZ, HscrollPos, true );

    endif;

    w.InvalidateRect( hwnd, NULL, false );

    xor( eax, eax ); // return success

end HScroll;

// VScroll-

//

//  Handles the w.WM_VSCROLL messages from Windows.

//  The L.O. word of wParam contains the action/command to be taken.

//  The H.O. word of wParam contains a distance for the w.SB_THUMBTRACK

//  message.

procedure VScroll( hwnd: dword; wParam:dword; lParam:dword );

begin VScroll;

    movzx( (type word wParam), eax );

    switch( eax )

        case( w.SB_TOP )

            mov( VscrollPos, eax );

            neg( eax );

        case( w.SB_BOTTOM )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            neg( eax );

            add( VscrollMax, eax );

            sub( VscrollPos, eax );

        case( w.SB_LINEUP )

            mov( -1, eax );

        case( w.SB_LINEDOWN )

            mov( 1, eax );

        case( w.SB_PAGEUP )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            neg( eax );

            if( (type int32 eax) > -1 ) then

                mov( -1, eax );

            endif;

        case( w.SB_PAGEDOWN )

            mov( ClientSizeY, eax );

            cdq();

            idiv( AverageCharHeight );

            if( (type int32 eax) < 1 ) then

                mov( 1, eax );

            endif;

        case( w.SB_THUMBTRACK )

            movzx( (type word wParam[2]), eax );

            sub( VscrollPos, eax );

        default

            xor( eax, eax );

    endswitch;

    // VscrollPos += min( eax, VscrollMax - VscrollPos )

    mov( VscrollMax, ecx );

    sub( VscrollPos, ecx );

    if( eax > (type int32 ecx)) then

        mov( ecx, eax );

    endif;

    add( VscrollPos, eax );

    if( @s ) then

        xor( eax, eax );

    endif;

    mov( eax, VscrollPos );

    w.SetScrollPos( hwnd, w.SB_VERT, eax, true );

    w.InvalidateRect( hwnd, NULL, false );

    xor( eax, eax ); // return success.

end VScroll;

// KeyMsg-

//

//  Handles the keyboard messages from Windows.

//  This routine creates a keyboard message record and adds it to the

//  list we're building in memory.

procedure KeyMsg

(

    hwnd:dword;

    msg:string;

    wParam:dword;

    lParam:dword;

    ASCII:dword

);

const

    msgPtr :text := "(type keymsg_t [ebx])";

var

    ThisMsg :keymsgPtr_t;

begin KeyMsg;

    push( ebx );

    // Allocate storage for a new node in our

    // keymsg list:

    malloc( @size( keymsg_t ) );

    mov( eax, ThisMsg );

    mov( eax, ebx );

    // Build the current key message record that we've

    // just allocated:

    mov( NULL, msgPtr.Next );

    mov( msg, eax );

    mov( eax, msgPtr.MsgStr );

    mov( wParam, eax );

    mov( eax, msgPtr.VirtKey );

    // The repeat count is bits 0..16 of lParam:

    mov( lParam, eax );

    mov( ax, msgPtr.RepeatCnt );

    // The transition flag is in bit 31:

    test( eax, eax );

    sets( cl );

    mov( cl, msgPtr.Transition );

    // The PrevKeyState flag is in bit 30:

    bt( 30, eax );

    setc( cl );

    mov( cl, msgPtr.PrevKeyState );

    // The Context flag is in bit 29:

    bt( 29, eax );

    setc( cl );

    mov( cl, msgPtr.Context );

    // The extended key flag is in bit 24:

    bt( 24, eax );

    setc( cl );

    mov( cl, msgPtr.ExtendedKey );

    // The eight-bit scan code is in bits 16..23:

    shr( 16, eax );

    mov( al, msgPtr.ScanCode );

    // Save the ASCII code in the record entry:

    mov( (type byte ASCII), al );

    mov( al, msgPtr.ASCIIcode );

    // Call GetKeyState to get the remaining

    // flags. Most of these set bit 31 to 1 if

    // the current state is true:

    w.GetKeyState( w.VK_LSHIFT );

    shr( 31, eax );

    mov( al, msgPtr.LshiftState );

    w.GetKeyState( w.VK_RSHIFT );

    shr( 31, eax );

    mov( al, msgPtr.RshiftState );

    w.GetKeyState( w.VK_LCONTROL );

    shr( 31, eax );

    mov( al, msgPtr.LctrlState );

    w.GetKeyState( w.VK_RCONTROL );

    shr( 31, eax );

    mov( al, msgPtr.RctrlState );

    w.GetKeyState( w.VK_LMENU );

    shr( 31, eax );

    mov( al, msgPtr.LaltState );

    w.GetKeyState( w.VK_RMENU );

    shr( 31, eax );

    mov( al, msgPtr.RaltState );

    // For the "lock" keys, bit zero

    // determines if the keyboard is

    // in the "locked" state:

    w.GetKeyState( w.VK_CAPITAL );

    and( 1, eax );

    mov( al, msgPtr.CapsLock );

    w.GetKeyState( w.VK_NUMLOCK );

    and( 1, eax );

    mov( al, msgPtr.NumLock );

    w.GetKeyState( w.VK_SCROLL );

    and( 1, eax );

    mov( al, msgPtr.ScrlLock );

    // Bump out key event counter and

    // add the current entry to our list

    // of key events.

    inc( KeyPressCnt );

    if( EndKeyPressList = NULL ) then

        mov( ebx, KeyPressList );

        mov( ebx, EndKeyPressList );

    else

        mov( EndKeyPressList, eax );

        mov( ebx, (type keymsg_t [eax]).Next );

        mov( ebx, EndKeyPressList );

    endif;

    // Whenever they press a key, automatically move to the end of the

    // display list so that the new line is displayed:

    mov( KeyPressCnt, ebx );

    mov( ClientSizeY, eax );    // Compute size of window.

    cdq();

    idiv( AverageCharHeight );

    sub( eax, ebx );

    if( @s ) then

        xor( ebx, ebx );

    endif;

    mov( ebx, VscrollMax );     // VscrollMax = max( 0, KeyPressCnt-scrnsize)

    push( eax );

    w.SetScrollRange( hwnd, w.SB_VERT, 0, ebx, false );

    pop( ebx );

    // Move the thumb on the scroll bar to the appropriate

    // position:

    mov( KeyPressCnt, eax );

    sub( ebx, eax );

    if( @s ) then

        xor( eax, eax );

    endif;

    mov( eax, VscrollPos );

    w.SetScrollPos( hwnd, w.SB_VERT, eax, true );

    // Force Windows to redraw this window without erasing

    // it so that we get feedback in the window:

    w.InvalidateRect( hwnd, NULL, false );

    pop( ebx );

end KeyMsg;

// KeyDown, KeyUp, SysKeyDown, SysKeyUp-

// These procedures handle the actual Windows' keyboard messages

// and pass them on to KeyMsg to do the actual work.

procedure KeyDown( hwnd: dword; wParam:dword; lParam:dword );

begin KeyDown;

    KeyMsg( hwnd, "WM_KEYDOWN", wParam, lParam, 0 );

end KeyDown;

procedure KeyUp( hwnd: dword; wParam:dword; lParam:dword );

begin KeyUp;

    KeyMsg( hwnd, "WM_KEYUP", wParam, lParam, 0 );

end KeyUp;

procedure SysKeyDown( hwnd: dword; wParam:dword; lParam:dword );

begin SysKeyDown;

    KeyMsg( hwnd, "WM_SYSKEYDOWN", wParam, lParam, 0 );

end SysKeyDown;

procedure SysKeyUp( hwnd: dword; wParam:dword; lParam:dword );

begin SysKeyUp;

    KeyMsg( hwnd, "WM_SYSKEYUP", wParam, lParam, 0 );

end SysKeyUp;

procedure CharMsg( hwnd: dword; wParam:dword; lParam:dword );

begin CharMsg;

    KeyMsg( hwnd, "WM_CHAR", 0, lParam, wParam );

end CharMsg;

procedure DeadCharMsg( hwnd: dword; wParam:dword; lParam:dword );

begin DeadCharMsg;

    KeyMsg( hwnd, "WM_DEADCHAR", 0, lParam, wParam );

end DeadCharMsg;

procedure SysCharMsg( hwnd: dword; wParam:dword; lParam:dword );

begin SysCharMsg;

    KeyMsg( hwnd, "WM_SYSCHAR", 0, lParam, wParam );

end SysCharMsg;

procedure SysDeadCharMsg( hwnd: dword; wParam:dword; lParam:dword );

begin SysDeadCharMsg;

    KeyMsg( hwnd, "WM_SYSDEADCHAR", 0, lParam, wParam );

end SysDeadCharMsg;

end keytest;

Before concluding the discussion of keyboard messages and events, we're going to take a look at a more practical application - one that lets you enter text from the keyboard into a window. The following application is a very simple "typewriter" application that displays the characters you type on the keyboard in the application's window. In addition to the standard ASCII characters (that this program writes to the display), this application also handles the following special keystrokes:

  • Left arrow: moves the caret one position to the left unless the caret is already in the left-most column.
  • Right arrow: moves the caret one position to the right unless the caret is in the right-most column.
  • Up arrow: moves the caret up one line (preserving the horizontal position) unless the caret is already on the top line of the screen.
  • Down arrow: moves the caret down on line on the screen unless the caret is on the bottom line.
  • End: moves the caret to the first position beyond the last non-blank character on the current line.
  • Home: moves the caret to the beginning of the current line.
  • PgUp: positions the caret on the top line of the window.
  • PgDn: positions the caret on the bottom line of the window.
  • Del: deletes the character under the caret, sliding all characters to the right of the caret one position to the left (filling the last character position on the line with a space).
  • Backspace: deletes the character to the left of the caret by moving all the characters on the current line, from the caret position to the end of the line, one position to the left.
  • Tab: writes spaces to fill out to the start of the next tab position on the line. Tabstops occur every eight character positions.
  • Line feed: behaves just like the down arrow key.
  • Carriage return: positions the caret to the beginning of the next line.
  • Escape: clears the window and homes the cursor.

If the keyboard message is not one of the above special codes and it is an ASCII character code (i.e., the application receives a w.WM_CHAR message), then this program writes the arriving character to the window at the current caret position.

In order to significantly reduce the effort needed to write this program, the typewriter.hla application uses a fixed system font. With a fixed pitch system font, each character consumes the same amount of horizontal space on a line. This signficantly reduces the effort needed to write this application because we can use the caret's physical position on the Window (divided by an appropriate value) as an index into an in-memory buffer. Were we to have used a proportional font, computing an index into each new line (that would preserve the caret's physical position on the screen) would have been quite a bit more work. Therefore, one of the first things this application does in the Create procedure (that handles w.WM_CREATE messages) is to get a handle to the fixed system font. You could create a new fixed system font via a call to a function like "w.GetFontIndirect" but an easier solution is to grab a handle to the system's fixed font with a call to the w.GetStockObject API function:

// Create-

//

//  This procedure responds to the w.WM_CREATE message.

//  Windows sends this message once when it creates the

//  main window for the application.  We will use this

//  procedure to do any one-time initialization that

//  must take place in a message handler.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc:    dword;              // Handle to video display device context

    tm:     w.TEXTMETRIC;

begin Create;

    // First, create a useful font (fixed pitch):

    w.GetStockObject( w.SYSTEM_FIXED_FONT );

    mov( eax, TheFont );

        .

        .

        .

Using the fixed system font stock object is a good idea as this font is available on all systems and generally looks very good.

Once the Create procedure obtains a handle to the fixed system font, the next step is to get the font metric information for that font. This is necessary because we'll position the caret at pixel offsets on the screen and we'll need to convert those pixel offsets to character offsets. To do that, we'll need to divide the caret's (pixel) location by the width and height of the character. A quick call to GetTextMetrics supplies this information for us. The following code completes the Create procedure:

    .

    .

    .

GetDC( hwnd, hdc );

        // Initialization:

        //

        //  Get the text metric information so we can compute

        //  the average character heights and widths.

        SelectObject( TheFont );

        GetTextMetrics( tm );

        mov( tm.tmHeight, eax );

        add( tm.tmExternalLeading, eax );

        mov( eax, AverageCharHeight );

        mov( tm.tmAveCharWidth, eax );

        mov( eax, AverageCharWidth );

        // If bit #0 of tm.tmPitchAndFamily is set, then

        // we've got a proportional font.  In that case

        // set the average capital width value to 1.5 times

        // the average character width.  If bit #0 is clear,

        // then we've got a fixed-pitch font and the average

        // capital letter width is equal to the average

        // character width.

        mov( eax, ebx );

        shl( 1, tm.tmPitchAndFamily );

        if( @c ) then

            shl( 1, ebx );              // 2*AverageCharWidth

        endif;

        add( ebx, eax );                // Computes 2 or 3 times eax.

        shr( 1, eax );                  // Computes 1 or 1.5 times eax.

        mov( eax, AverageCapsWidth );

    ReleaseDC;

end Create;

The Paint procedure in the typewriter.hla application is fairly simple. Whenever Windows requests a screen redraw, this program simply redraws the text from an internal buffer where the keyboard handling procedures stuff all the arriving characters. See the program listing a little bit later for the details.

The typewriter application maintains a buffer large enough to hold all the characters that will fit on the window (i.e., one screen buffer full of data). Should the user decide to resize the window, we've got a small problem - if the user makes the window larger the current data buffer will not be large enough to hold the data; if the user makes the window smaller, then the buffer will be too large (which isn't as bad as being too small, but an issue nonetheless). To solve this problem, the typewriter.hla application clears the window, deallocates the existing buffer (if there is one), and then reallocates storage for a new buffer whenever the user resizes the window. This code also initializes the buffer with spaces and homes the caret to the (0,0) position. See the Size procedure in the full source code a little later for the details of this procedure.

One big difference between the typewriter.hla application and the previous two applications in this chapter is that the typewriter.hla application has to maintain the caret. As you'll recall, there is only one system-wide caret and the application must properly maintain the caret by watching for w.WM_FOCUS and w.WM_KILLFOCUS messages. Whenever a w.WM_FOCUS message comes along, the application should create a caret and make it visible in the window. Similarly, whenever the application loses the focus (i.e., a w.WM_KILLFOCUS message arrives) it needs to hide the caret and destory it. Here's the code that does this:

// SetFocus-

//

//  This procedure gets called whenever this application gains the

// input focus.

procedure SetFocus( hwnd: dword; wParam:dword; lParam:dword );

begin SetFocus;

    w.CreateCaret( hwnd, NULL, AverageCharWidth, AverageCharHeight );

    gotoxy( x, y );

    w.ShowCaret( hwnd );

    sub( eax, eax );  // Return success

end SetFocus;

// KillFocus-

//

//  Processes the WM_KILLFOCUS message that gets sent whenever this

// application is losing the input focus.

procedure KillFocus( hwnd: dword; wParam:dword; lParam:dword );

begin KillFocus;

    w.HideCaret( hwnd );

    w.DestroyCaret();

    sub( eax, eax );  // Return success

end KillFocus;

Beyond these simple routines, the majority of the remaining effort consists of dealing with the keyboard messages that come along and, if appropriate, shoving the ASCII codes for the keyboard characters into the memory buffer or performing whatever activity the (control) keystrokes require. Here's the complete code for the application:

// typewriter.hla-

//

//  This program simulates a "typewriter" by copying keystrokes to the display.

unit typewriter;

// Set the following to true to display interesting information

// during program operation. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "excepts.hhf" )

#includeonce( "conv.hhf" )

#includeonce( "hll.hhf" )

#includeonce( "memory.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

const

    ESC := #$1b;    // Escape character code.

static

    TheFont             :dword;         // Font we'll use.

    AverageCapsWidth    :dword;         // Font metric values.

    AverageCharWidth    :dword;

    AverageCharHeight   :dword;

    maxCharWidth        :dword;

    maxCharHeight       :dword;

    ClientSizeX         :int32 := 0;    // Size of the client area

    ClientSizeY         :int32 := 0;    //  where we can paint.

    x                   :int32 := 0;    // Caret x-coordinate.

    y                   :int32 := 0;    // Caret y-coordinate.

    scrnBuf             :pointer to char := NULL;

readonly

    ClassName   :string := "typewriterWinClass";        // Window Class Name

    AppCaption  :string := "typewriter Program";        // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_PAINT,      &Paint              ],

            MsgProcPtr_t:[ w.WM_KEYDOWN,    &KeyDown            ],

            MsgProcPtr_t:[ w.WM_CHAR,       &CharMsg            ],

            MsgProcPtr_t:[ w.WM_SETFOCUS,   &SetFocus           ],

            MsgProcPtr_t:[ w.WM_KILLFOCUS,  &KillFocus          ],

            MsgProcPtr_t:[ w.WM_SIZE,       &Size               ],

            MsgProcPtr_t:[ w.WM_CREATE,     &Create             ],

            MsgProcPtr_t:[ w.WM_DESTROY,    &QuitApplication    ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// gotoxy-

//

// Positions the cursor at the specified (x,y) *character* coordinate.

procedure gotoxy( x:dword in eax; y:dword in edx );

begin gotoxy;

    intmul( AverageCharWidth, eax );

    intmul( AverageCharHeight, edx );

    w.SetCaretPos( eax, edx );

end gotoxy;

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    w.PostQuitMessage( 0 );

end QuitApplication;

// Create-

//

//  This procedure responds to the w.WM_CREATE message.

//  Windows sends this message once when it creates the

//  main window for the application.  We will use this

//  procedure to do any one-time initialization that

//  must take place in a message handler.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc:    dword;              // Handle to video display device context

    tm:     w.TEXTMETRIC;

begin Create;

    // First, create a useful font (fixed pitch):

    w.GetStockObject( w.SYSTEM_FIXED_FONT );

    mov( eax, TheFont );

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

//  For this keytest program, the Paint procedure

//  displays the list of key events we've saved in memory.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    value       :string;

    valData     :char[256];

    vallen      :uns32;         // Length of value string.

    hdc         :dword;         // Handle to video display device context

    ps          :w.PAINTSTRUCT; // Used while painting text.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Initialize the value->valData string object:

    mov( str.init( (type char valData), 256 ), value );

    // When Windows requests that we draw the window,

    // fill in the string in the center of the screen.

    // Note that all GDI calls (e.g., w.DrawText) must

    // appear within a BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Select the fixed-pitch font into our context:

        SelectObject( TheFont );

        mov( y, edi );

        intmul( maxCharHeight, edi );

        mov( scrnBuf, ebx );

        for( mov( 0, esi ); esi <= edi; add( AverageCharHeight, esi )) do

            TextOut( 0, esi, ebx, maxCharWidth );

            add( maxCharWidth, ebx );

        endfor;

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

    xor( eax, eax ); // Return success

end Paint;

// Size-

//

//  This procedure handles the w.WM_SIZE message

//

//  L.O. word of lParam contains the new X Size

//  H.O. word of lParam contains the new Y Size

procedure Size( hwnd: dword; wParam:dword; lParam:dword );

begin Size;

    // Convert new X size to 32 bits and save:

    movzx( (type word lParam), eax );

    mov( eax, ClientSizeX );

    cdq();

    idiv( AverageCharWidth );

    mov( eax, maxCharWidth );

    // Convert new Y size to 32 bits and save:

    movzx( (type word lParam[2]), eax );

    mov( eax, ClientSizeY );

    cdq();

    idiv( AverageCharHeight );

    mov( eax, maxCharHeight );

    // Allocate storage for a screen buffer here:

    if( scrnBuf <> NULL ) then

        free( scrnBuf );

    endif;

    mov( maxCharWidth, eax );

    intmul( maxCharHeight, eax );

    mov( eax, ecx );

    malloc( eax );

    mov( eax, scrnBuf );

    // Fill the buffer with spaces:

    push( edi );

    mov( eax, edi );

    mov( ' ', al );

    rep.stosb();

    pop( edi );

    xor( eax, eax );

    mov( eax, x );   // Home caret to (0,0)

    mov( eax, y );

    w.GetFocus();

    if( eax = hwnd ) then

        gotoxy( 0, 0 );

    endif;

    xor( eax, eax ); // Return success.

end Size;

// SetFocus-

//

//  This procedure gets called whenever this application gains the

// input focus.

procedure SetFocus( hwnd: dword; wParam:dword; lParam:dword );

begin SetFocus;

    w.CreateCaret( hwnd, NULL, AverageCharWidth, AverageCharHeight );

    gotoxy( x, y );

    w.ShowCaret( hwnd );

    sub( eax, eax );  // Return success

end SetFocus;

// KillFocus-

//

//  Processes the WM_KILLFOCUS message that gets sent whenever this

// application is losing the input focus.

procedure KillFocus( hwnd: dword; wParam:dword; lParam:dword );

begin KillFocus;

    w.HideCaret( hwnd );

    w.DestroyCaret();

    sub( eax, eax );  // Return success

end KillFocus;

// KeyDown-

//

//  This procedure handles WM_KEYDOWN messages. Mainly, we process

// cursor control keystrokes here.

procedure KeyDown( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

begin KeyDown;

    mov( wParam, eax );

    switch( eax )

        case( w.VK_LEFT )

            // Move the cursor one position to the left.

            // However, if we are already in column zero,

            // then ignore the request.

            dec( x );

            if( @s ) then

                mov( 0, x );

            endif;

        case( w.VK_RIGHT )

            // Bump the cursor position one spot to the right.

            // If we are already on the right hand side of the

            // screen, then ignore this request.

            inc( x );

            mov( x, eax );

            if( eax > maxCharWidth ) then

                mov( maxCharWidth, eax );

                mov( eax, x );

            endif;

        case( w.VK_UP )

            // Move the cursor up one line unless we're

            // already at line zero:

            dec( y );

            if( @s ) then

                mov( 0, y );

            endif;

        case( w.VK_DOWN )

            // Move the cursor down one line unless

            // we're already on the bottom line of the display.

            inc( y );

            mov( y, eax );

            if( eax > maxCharHeight ) then

                mov( maxCharHeight, eax );

                mov( eax, y );

            endif;

        case( w.VK_END )

            // Move the cursor to the first spot beyond the last non-space

            // character on the line.

            //

            // Calculate the end of the line as the last non-blank

            // character in the buffer on the current line:

            mov( y, edx );                  // Calculate start of buffer adrs

            mov( maxCharWidth, ecx );

            intmul( ecx, edx );

            add( scrnBuf, edx );

            add( edx, ecx );                // Point ecx at end of buffer.

            repeat

                dec( ecx );

            until( ecx = edx || (type char [ecx]) <> ' ');

            sub( edx, ecx );

            inc( ecx );     // Move just beyond character

            if( ecx > maxCharWidth ) then

                // If last position contains a character, don't

                // increment beyond that point.

                mov( maxCharWidth, ecx );

            endif;

            mov( ecx, x );

        case( w.VK_HOME )

            // Pressing the HOME key moves the cursor to the

            // beginning of the current line.

            mov( 0, x );

        case( w.VK_PRIOR )

            // PgUp positions the caret on the top line

            // of the display:

            mov( 0, y );

        case( w.VK_NEXT )

            // PgDn positions the caret on the bottom line of

            // the display:

            mov( maxCharHeight, eax );

            dec( eax );

            mov( eax, y );

        case( w.VK_DELETE )

            // Delete the key under the cursor.

            push( esi );

            push( edi );

            // First, we've got to determine the address

            // of this character:

            mov( maxCharWidth, esi );

            mov( y, edi );

            intmul( esi, edi );

            add( scrnBuf, edi );        // Adrs of start of line

            push( edi );                // Save for later

            add( x, edi );              // Address of character position

            lea( esi, [edi+1] );        // Start copying with next char

            // Now, copy the remaining characters on the line

            // over the current character:

            mov( maxCharWidth, ecx );   // Compute chars remaining on line

            sub( x, ecx );

            rep.movsb();                // Copy chars over char to delete.

            mov( ' ', (type char [edi])); // Put a space in last position.

            // Redraw the line of text so we can see the effect

            // of the deletion on the display:

            pop( edi );                 // Retrieve address of line.

            w.HideCaret( hwnd );

            GetDC( hwnd, hdc );

                SelectObject( TheFont );

                mov( y, eax );

                intmul( AverageCharHeight, eax );

                TextOut( 0, eax, edi, maxCharWidth );

            ReleaseDC;

            w.ShowCaret( hwnd );

            pop( edi );

            pop( esi );

    endswitch;

    gotoxy( x, y );

    sub( eax, eax );  // Return success

end KeyDown;

// CharMsg-

//

//  This procedure handles the WM_CHAR messages that come along:

procedure CharMsg( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context

    charAdrs    :dword;

begin CharMsg;

    mov( wParam, eax );     // Get ASCII code

    if( al = stdio.bs ) then

        // Handle the backspace key.

        // Just like the DEL key, except we delete the

        // character to the left of the cursor rather than

        // the character under the cursor. If in column zero,

        // do nothing. (See the comments for the DEL key for

        // more details.)

        if( x <> 0 ) then

            push( esi );

            push( edi );

            mov( maxCharWidth, ecx );

            mov( y, esi );              // esi = y*maxCharWidth+scrnBuf

            intmul( ecx, esi );         //  which is the address of the

            add( scrnBuf, esi );        //  start of the line.

            push( esi );                // Save start address for later.

            add( x, esi );              // Address of character position

            lea( edi, [esi-1] );        // Start copying with this char

            sub( x, ecx );              // Compute remaining chars

            rep.movsb();                // Copy chars over char to delete.

            mov( ' ', (type char [edi])); // Put a space in last position.

            // Reprint the line we've just modified:

            pop( edi );

            w.HideCaret( hwnd );

            GetDC( hwnd, hdc );

                SelectObject( TheFont );

                mov( y, eax );

                intmul( AverageCharHeight, eax );

                TextOut( 0, eax, edi, maxCharWidth );

            ReleaseDC;

            w.ShowCaret( hwnd );

            // We need to move the cursor to the left one

            // spot to move it over the character we

            // just deleted:

            dec( x );

            if( @s ) then

                mov( 0, x );

            endif;

            pop( edi );

            pop( esi );

        endif;

    elseif( al = stdio.tab ) then

        // If the user presses the TAB key, then write spaces

        // from the current caret position up to the next

        // x-coordinate that is an even multiple of eight

        // characters (assume 8-character tab stop positions).

        push( ebx );

        mov( x, ebx );

        repeat

            w.SendMessage( hwnd, w.WM_CHAR, ' ', 1 );

            inc( bl );

            test( %111, bl );

        until( @z );

    elseif( al = stdio.lf ) then

        // Treat LF just like the "down" key:

        w.SendMessage( hwnd, w.WM_KEYDOWN, w.VK_DOWN, 1 );

        w.SendMessage( hwnd, w.WM_KEYUP, w.VK_DOWN, 1 );

    elseif( al = stdio.cr ) then

        // When the user presses enter, go to the beginning

        // of the next line:

        mov( 0, x );

        w.SendMessage( hwnd, w.WM_CHAR, stdio.lf, 1 );

    elseif( al = ESC ) then

        // If the user presses the ESC key, then clear the

        // screen:

        mov( maxCharWidth, eax );

        intmul( maxCharHeight, eax );

        mov( eax, ecx );

        push( edi );

        mov( scrnBuf, edi );

        mov( ' ', al );

        rep.stosb();

        pop( edi );

        xor( eax, eax );

        mov( eax, x );   // Home caret to (0,0)

        mov( eax, y );

        // Force Windows to redraw this window.

        w.InvalidateRect( hwnd, NULL, true );

    else // we've got a normal character - print it

        // Compute index into buffer:

        mov( y, ecx );

        intmul( maxCharWidth, ecx );

        add( x, ecx );

        add( scrnBuf, ecx );

        mov( al, [ecx] );

        mov( ecx, charAdrs );

        // Display the character on the screen:

        w.HideCaret( hwnd );

        GetDC( hwnd, hdc );

            SelectObject( TheFont );

            mov( y, eax );

            intmul( AverageCharHeight, eax );

            mov( x, ecx );

            intmul( AverageCharWidth, ecx );

            TextOut( ecx, eax, charAdrs, 1 );

        ReleaseDC;

        w.ShowCaret( hwnd );

        // Update the caret position:

        inc( x );

        mov( x, eax );

        if( eax > maxCharWidth ) then

            mov( 0, x );

            inc( y );

            mov( y, eax );

            if( eax > maxCharHeight ) then

                mov( 0, y );

            endif;

        endif;

    endif;

    gotoxy( x, y );

    xor( eax, eax );  // Return success

end CharMsg;

end typewriter;

8.5: Mouse Messages

Although the mouse is a central input device for Windows, it is actually a fairly simple device and the messages Windows sends to your applications concerning the mouse are very straight-forward.

Windows sends two types of mouse messages to an application's window procedure: client area messages and non-client area messages. Client area messages are sent to an application whenever there is a mouse event within the client area of the application's window, non-client messages are sent whenever there is a mouse event outside the client area, but still within the bounds of the window (e.g., in the title bar or along the window's border). Note that an application does not normally receive messages for mouse events outside the window (though there are a couple of exceptions to this rule).

Table 8-3 lists the messages that Windows sends an application whenever a mouse event occurs within the client area.

When a mouse event message arrives, the lParam parameter contains the current mouse (x,y) position using client-window coordinates. The L.O. word of lParam contains the x-coordinate and the H.O. word contains the y-coordinate. Note that these two values are 16-bit signed integers. If you want to convert these values to 32-bit integers (as is usually the case) you will need to sign extend them, e.g.,

        movsx( (type word lParam), eax );

        movsx( (type word lParam[2]), ecx );

Under certain circumstances, mouse coordinates can actually be negative. If you simply zero extend these values to 32-bits such negative values will start looking like really large positive values.

The wParam parameter that Windows passes to your mouse message handling procedure is a set of bits specifying the state of the various mouse buttons in addition to the shift and control keys on the keyboard. Windows provides a set of constants (see Table 8-4) that you can use to test the bits in the wParam parameter.

Table 8-3:
Message
Description
lParam
wParam
w.WM_LBUTTONDBLCLK
Sent after the second click of a double-click sequence by the left mouse button.
L.O. word holds the mouse cursor's x-coordinate, H.O. word holds the cursor's y-coordinate.
Flags (bits) containing modifier key and mouse button status (see text for details)
w.WM_LBUTTONDOWN
Sent when the user presses the left mouse button.
w.WM_LBUTTONUP
Sent when the user releases the left mouse button.
w.WM_MBUTTONDBLCLK
Sent after the second click of a double-click sequence by the middle mouse button.
w.WM_MBUTTONDOWN
Sent when the user presses the middle mouse button
w.WM_MBUTTONUP
Sent when the user releases the middle mouse button.
w.WM_RBUTTONDBLCLK
Sent after the second click of a double-click sequence by the right mouse button.
w.WM_RBUTTONDOWN
Sent when the user presses the right mouse button
w.WM_RBUTTONUP
Sent when the user releases the right mouse button.
w.WM_MOUSEMOVE
Sent whenever the user moves the mouse in the client area.
Client Area Mouse Message

Table 8-4: wParam Flags for a Mouse Event
Flag to AND with wParam
Meaning (if bit set)
w.MK_CONTROL
Control key on keyboard was down during mouse event.
w.MK_LBUTTON
Left button on mouse was down during mouse event.
w.MK_MBUTTON
Middle button on mouse was down during mouse event.
w.MK_RBUTTON
Right button on mouse was down during mouse event.
w.MK_SHIFT
Shift key on keyboard was down during mouse event (either shift key).

While you are moving the mouse through the client area of your application's window, Windows will send a constant stream of w.WM_MOUSEMOVE messages to your application. The exact number and rate of mouse movement messages is dependent upon your hardware and OS settings, but suffice to say that you will not get a mouse movement message for every pixel the mouse cursor traverses; if you rapidly move the mouse, Windows will accelerate the mouse movements and skip over several pixels with each message it sends to your application. The sample program appearing in a little bit will demonstrate this.

Windows sends the mouse button up and down messages to your application whenever the user presses one of the three mouse buttons within the client area of your application's window (see Table 8-3). Note that these messages are not guaranteed to come in pairs. Though most of the time you will receive a mouse down and then a mouse up message, it is quite possible to receive a mouse down message and then never receive the corresponding mouse up; for example, if the user presses the mouse button in your client area, drags the mouse cursor outside the window's client area, and then releases the mouse, you will not receive a mouse up event. Other scenerios are also possible. So don't count on these messages always occuring in pairs.

Double-click messages are special. First of all, Windows will not automatically send double-click messages to your application's window procedure - you have to specifically request that it do this. To make this request, you will have to logically OR in the constant w.CS_DBLCLKS into the wc.style field of the window class. As you may recall, we've buried the initialization of the wc variable into the winmain library module, so we don't have direct access to the initialization of this structure. Fortunately, the winmain code does call a procedure in our code, initWC, that allows us to make any changes to the wc object prior to actually registering the window class. Therefore, we can add the w.CS_DBLCLKS style to our window class by adding the following code to the initWC procedure:

procedure initWC; @noframe;

begin initWC;

    or( w.CS_DBLCLKS, wc.style );	// Activate double-clicks

    ret();

end initWC;

Once you've done this, Windows will send the various double-click messages to your application.

An important thing to note about double click messages is that you don't get a single double-click message when the user double-clicks the mouse. You will actually get a mouse down message, a mouse up message, a dou ble-click message and then a final mouse up message (assuming the user double clicked the mouse button within the application's client window). In other words, Windows simply replaces the second mouse down message with a double-click message.

Without further ado, here's a quick program that demonstrates the use of the mouse within an application. This simple program, MousePts, simply tracks movements throughout a window while the left mouse button is down (drawing pixels at each w.WM_MOUSEMOVE event) and then drawing lines between all the points when the user releases the mouse button. Figure 8-1 shows some sample output just before releasing the mouse button, Figure 8-2 shows the output immediately after releasing the mouse button.

//

//  A program that demonstrates the use of the mouse.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit MousePoints;

// Set the following to true to display interesting information

// about the bitmap file this program opens. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "hll.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

const

    maxPoints   := 2048;

static

    PointCnt:       uns32 := 0;

    PointsArray:    w.POINT[ maxPoints ];

readonly

    ClassName   :string := "MousePts2WinClass";      // Window Class Name

    AppCaption  :string := "MousePts2 Program";      // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY,        &QuitApplication    ],

            MsgProcPtr_t:[ w.WM_PAINT,          &Paint              ],

            MsgProcPtr_t:[ w.WM_LBUTTONDOWN,    &LButtonDown        ],

            MsgProcPtr_t:[ w.WM_LBUTTONUP,      &LButtonUp          ],

            MsgProcPtr_t:[ w.WM_MOUSEMOVE,      &MouseMove          ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Tell the application to quit:

    w.PostQuitMessage( 0 );

end QuitApplication;

// LButtonDown:

//

//  This procedure handles the w.WM_LBUTTONDOWN message.

procedure LButtonDown( hwnd: dword; wParam:dword; lParam:dword );

begin LButtonDown;

    mov( 0, PointCnt );

    w.InvalidateRect( hwnd, NULL, true );

    xor( eax, eax ); // Return zero to indicate success.

end LButtonDown;

// LButtonUp:

//

//  This procedure handles the w.WM_LBUTTONUP message.

procedure LButtonUp( hwnd: dword; wParam:dword; lParam:dword );

begin LButtonUp;

    w.InvalidateRect( hwnd, NULL, true );

    xor( eax, eax ); // Return zero to indicate success.

end LButtonUp;

// MouseMove:

//

//  This procedure handles the w.WM_MOUSEMOVE message.

procedure MouseMove( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc: dword;

begin MouseMove;

    test( w.MK_LBUTTON, wParam );

    if( @nz && PointCnt < maxPoints ) then

        GetDC( hwnd, hdc );

            mov( PointCnt, ecx );

            movzx( (type word lParam), eax );

            mov( eax, PointsArray.x[ ecx*8 ] );

            movzx( (type word lParam[2]), edx );

            mov( edx, PointsArray.y[ ecx*8 ] );

            SetPixel( eax, edx, 0 );

            inc( PointCnt );

        ReleaseDC;

    endif;

    xor( eax, eax );

end MouseMove;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        w.SetCursor( w.LoadCursor( NULL, @string( w.IDC_WAIT ) ));

        w.ShowCursor( true );

        MoveTo( PointsArray.x[0], PointsArray.y[0] );

        for( mov( 1, ebx ); ebx < PointCnt; inc( ebx )) do

            LineTo( PointsArray.x[ebx*8], PointsArray.y[ebx*8] );

        endfor;

        w.ShowCursor( false );

        w.SetCursor( w.LoadCursor( NULL, @string(w.IDC_ARROW )));

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end MousePoints;
Listing 8-6: The MousePts Program

Figure 8-1: MousePts Output, Before Releasing Button

Figure 8-2: MousePts Output, After Releasing Button

The MousePts program continually selects points whenever a mouse movement occurs. In most applications you will want to choose the pixels to draw based on some even such as a mouse click. The MousePts2 program is a slight modification of the MousePts program that chooses pixels to plot based on mouse clicks. Another difference between the MousePts and MousePts2 programs is that MousePts2 draws straight lines between each point and every other point, rather than simply between adjacent points. The listing follows and some sample output appears in Figure 8-3.

// MousePts2.hla-

//

//  Another program that demonstrates the use of the mouse.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit MousePoints2;

// Set the following to true to display interesting information

// about the bitmap file this program opens. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "hll.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

const

    maxPoints   := 128;

static

    PointCnt:       uns32 := 0;

    PointsToPlot:   uns32 := 0;

    PointsArray:    w.POINT[ maxPoints ];

readonly

    ClassName   :string := "MousePts2WinClass";     // Window Class Name

    AppCaption  :string := "MousePts2 Program";     // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY,        &QuitApplication    ],

            MsgProcPtr_t:[ w.WM_PAINT,          &Paint              ],

            MsgProcPtr_t:[ w.WM_LBUTTONDOWN,    &LButtonDown        ],

            MsgProcPtr_t:[ w.WM_LBUTTONDBLCLK,  &LButtonDblClk      ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We have to activate the double-click feature in the window style

procedure initWC; @noframe;

begin initWC;

    or( w.CS_DBLCLKS, wc.style );   // Activate double-clicks

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Tell the application to quit:

    w.PostQuitMessage( 0 );

end QuitApplication;

// LButtonDown:

//

//  This procedure handles the w.WM_LBUTTONDOWN message.

procedure LButtonDown( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc:    dword;

begin LButtonDown;

    GetDC( hwnd, hdc );

        mov( PointCnt, ecx );

        movzx( (type word lParam), eax );

        mov( eax, PointsArray.x[ ecx*8 ] );

        movzx( (type word lParam[2]), edx );

        mov( edx, PointsArray.y[ ecx*8 ] );

        SetPixel( eax, edx, 0 );

        inc( PointCnt );

    ReleaseDC;

    xor( eax, eax ); // Return zero to indicate success.

end LButtonDown;

// LButtonDblClk:

//

//  This procedure handles the w.WM_LBUTTONSBLCLK message.

procedure LButtonDblClk( hwnd: dword; wParam:dword; lParam:dword );

begin LButtonDblClk;

    mov( PointCnt, PointsToPlot );

    mov( 0, PointCnt );

    w.InvalidateRect( hwnd, NULL, true );

    xor( eax, eax ); // Return zero to indicate success.

end LButtonDblClk;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        w.SetCursor( w.LoadCursor( NULL, @string( w.IDC_WAIT ) ));

        w.ShowCursor( true );

        // Draw a line between each point and every other point

        // in our points array.

        for( mov( 0, ebx ); ebx < PointsToPlot; inc( ebx )) do

            for( lea( esi, [ebx+1]); esi < PointsToPlot; inc( esi )) do

                MoveTo( PointsArray.x[ebx*8], PointsArray.y[ebx*8] );

                LineTo( PointsArray.x[esi*8], PointsArray.y[esi*8] );

            endfor;

        endfor;

        mov( 0, PointsToPlot );

        w.ShowCursor( false );

        w.SetCursor( w.LoadCursor( NULL, @string(w.IDC_ARROW )));

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end MousePoints2;
Listing 8-7: MousePts2 Program

Figure 8-3: MousePts2 Sample Output

Windows only sends the messages we've discussed thus far when the mouse cursor is within the client area of your application's window. If the mouse cursor is in one of the non-client areas of your application's window (e.g., in the title bar, on the boundary, or on the resize tab) then your application will not receive any messages from Windows. So, for example, if you want to trap a mouse-down event on the resize tab so you can do something just prior to the window being resized, you will not be able to do this using the mouse event messages we've discussed thus far. To detect such events, your application needs to handle Windows' non-client mouse event messages (see Table 8-5).

Windows sends a non-client mouse event message whenever there is a mouse event (movement, click, double-click) within the window, but outside the client area. It is important to realize that Windows does not usually send your application any mouse messages when the mouse cursor is outside the application's window. Windows send non-client mouse event messages when the mouse cursor is in the non-client area of your application's window (e.g., the title bar and borders). The non-client mouse messages mirror the client area messages (see Table 8-5). By handling non-client area mouse messages, you application can check for events such as clicking the mouse on the resize section of the window's frame.

Table 8-5:
Message
Description
lParam
wParam
w.WM_NCLBUTTONDBLCLK
Sent after the second click of a double-click sequence by the left mouse button.
L.O. word holds the mouse cursor's x-coordinate, H.O. word holds the cursor's y-coordinate.

Note that these coordinates are screen coordinates, not client area coordinates.
Flags (bits) containing modifier key and mouse button status.
w.WM_NCLBUTTONDOWN
Sent when the user presses the left mouse button.
w.WM_NCLBUTTONUP
Sent when the user releases the left mouse button.
w.WM_NCMBUTTONDBLCLK
Sent after the second click of a double-click sequence by the middle mouse button.
w.WM_NCMBUTTONDOWN
Sent when the user presses the middle mouse button
w.WM_NCMBUTTONUP
Sent when the user releases the middle mouse button.
w.WM_NCRBUTTONDBLCLK
Sent after the second click of a double-click sequence by the right mouse button.
w.WM_NCRBUTTONDOWN
Sent when the user presses the right mouse button
w.WM_NCRBUTTONUP
Sent when the user releases the right mouse button.
w.WM_NCMOUSEMOVE
Sent whenever the user moves the mouse in the client area.
Non-Client Area Mouse Message

An important difference between client area mouse messages and non-client area mouse messages is the value of the (x,y) coordinate that Windows passes in the lParam parameter. With normal client window mouse messages, these coordinates are always client-area coordinates. Coordinate (0,0) is the upper-left hand corner of the client area of the window. Obviously, this coordinate system does not apply to the non-client portion of the application's window. Therefore, Windows returns screen coordinates in the lParam parameter when it sends your application a non-client area mouse message. Fortunately, Windows provides a pair of API functions that let you easily convert between screen and client area coordinate systems:

    w.ScreenToClient( hwnd, point );

    w.ClientToScreen( hwnd, point );

As their names suggest, these functions take a point and translate its coordinates between the two different coordinate systems. The hwnd parameter is the handle of the window whose client area coordinate system you want to use, the point parameter is an object of type w.POINT (having a pair of int32 x and y fields). These calls replace the input values of the point parameter with the new, converted, value. It is very important that you realize that the screen-to-client conversion can produce coordinates whose values are negative. If a point is above or to the left of the client area of a window, then the w.ScreenToClient API will will produce negative values for the x and/or y coordinate values. This is why it is always important to treat screen coordinate values as signed integers.

Although the non-client mouse messages are useful for detecting events outside the client area, but still inside the application's window, there are times when an application needs to track the mouse outside the window area. For example, many drawing programs allow you to move objects beyond the current limits of the client area by clicking on an object, holding the mouse button down, dragging the object (beyond the bounds of the window), and then releasing the mouse button. By scrolling the window (or resizing the window) you can see where you've moved the object.

Normally, this activity is not possible with Windows because Windows will stop sending your application mouse events when the mouse leaves the application's window. To overcome this limitation, Windows provides a facility known as "mouse capture". Mouse capture tells Windows, under certain circumstances, to continue sending mouse messages to an application even if the mouse cursor leaves the window's boundaries. You have to explicitly request a mouse capture with the following API call:

w.SetCapture( hwnd );

The hwnd parameter is the handle of the window that is capturing the mouse. Windows will send all future mouse messages to the window procedure associated with this handle until the application calls the release API function:

w.ReleaseCapture;

For very practical reasons, you must only call w.SetCapture while the mouse button is depressed and you must call w.ReleaseCapture whenever the user releases the mouse button. The following application is a quick "rectangle drawing" application that demonstrates the use of w.SetCapture and w.ReleaseCapture. The MouseCapture application allows users to draw a rectangle in the window by pressing the mouse button, holding the button down, and then dragging the mouse cursor to some other point and releasing the button. Upon releasing the button, the application fills in the rectangle described by the two points. Figure 8-4 shows what the window looks like while dragging the mouse cursor across the window. Figure 8-5 shows the output when the user releases the mouse button. Figure 8-6 shows what the window looks like while the user is dragging the mouse cursor from the client area to a spot outside the window (when the user releases the button, the original rectangle will disappear, replaced by one that goes beyond the edge of the window). Note that by resizing the window, you can see the full rectangle that was originally drawn beyond the end of the window's boundaries.

Windows always returns client-window relative coordinates when you've got the mouse captured. This means that the coordinates could very well be negative values (if you've move the cursor to the left or above the client window). Once again, don't forget to treat the values that Windows returns in lParam as signed 16-bit integers or you may get incorrect results.

Figure 8-4: MouseCapture - Creating a Rectangle's Outline

Figure 8-5: MouseCapture - Releasing the Mouse Button

Figure 8-6: MouseCapture - Extending Beyond the End of the Window

Here's the source code for the MouseCapture program:

// MouseCapture.hla-

//

//  A program that demonstrates "capturing the mouse to track mouse positions

// outside the client area.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit MouseCapture;

// Set the following to true to display interesting information

// about the bitmap file this program opens. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "hll.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    ptBegin:    w.POINT;

    ptEnd:      w.POINT;

    ptBoxBegin: w.POINT;

    ptBoxEnd:   w.POINT;

    fBlocking:  boolean;

    fValidBox:  boolean;

readonly

    ClassName   :string := "MouseCaptureWinClass";      // Window Class Name

    AppCaption  :string := "MouseCapture Program";      // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY,        &QuitApplication    ],

            MsgProcPtr_t:[ w.WM_PAINT,          &Paint              ],

            MsgProcPtr_t:[ w.WM_MOUSEMOVE,      &MouseMove          ],

            MsgProcPtr_t:[ w.WM_LBUTTONDOWN,    &LButtonDown        ],

            MsgProcPtr_t:[ w.WM_LBUTTONUP,      &LButtonUp          ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Tell the application to quit:

    w.PostQuitMessage( 0 );

end QuitApplication;

procedure drawBoxOutline( hwnd:dword; ptBegin:w.POINT; ptEnd:w.POINT ); @nodisplay;

var

    hdc:    dword;

begin drawBoxOutline;

    GetDC( hwnd, hdc );

        SetROP2( w.R2_NOT );

        SelectObject( w.GetStockObject( w.NULL_BRUSH ));

        Rectangle( ptBegin.x, ptBegin.y, ptEnd.x, ptEnd.y );

    ReleaseDC;

end drawBoxOutline;

// MouseMove:

//

//  This procedure handles the w.WM_MOUSEMOVE message (mouse movement within the client
window).

procedure MouseMove( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc: dword;

begin MouseMove;

    if( fBlocking ) then

        w.SetCursor( w.LoadCursor( NULL, @string(w.IDC_CROSS) ));

        drawBoxOutline( hwnd, ptBegin, ptEnd );

        movsx( (type word lParam), eax );

        mov( eax, ptEnd.x );

        movsx( (type word lParam[2]), eax );

        mov( eax, ptEnd.y );

        drawBoxOutline( hwnd, ptBegin, ptEnd );

    endif;

    xor( eax, eax );

end MouseMove;

// LButtonDown:

//

//  This procedure handles the w.WM_LBUTTONDOWN message, which this

// program uses to capture the mouse.

procedure LButtonDown( hwnd: dword; wParam:dword; lParam:dword );

begin LButtonDown;

    movsx( (type word lParam), eax );

    mov( eax, ptBegin.x );

    mov( eax, ptEnd.x );

    movsx( (type word lParam[2]), eax );

    mov( eax, ptBegin.y );

    mov( eax, ptEnd.y );

    drawBoxOutline( hwnd, ptBegin, ptEnd );

    w.SetCapture( hwnd );

    w.SetCursor( w.LoadCursor( NULL, @string( w.IDC_CROSS)));

    mov( true, fBlocking );

    xor( eax, eax ); // Return zero to indicate success.

end LButtonDown;

// LButtonUp:

//

//  This procedure handles the w.WM_LBUTTONUP message which

// this program uses to release the mouse.

procedure LButtonUp( hwnd: dword; wParam:dword; lParam:dword );

begin LButtonUp;

    if( fBlocking ) then

        drawBoxOutline( hwnd, ptBegin, ptEnd );

        mov( ptBegin.x, eax );

        mov( eax, ptBoxBegin.x );

        mov( ptBegin.y, eax );

        mov( eax, ptBoxBegin.y );

        movsx( (type word lParam), eax );

        mov( eax, ptBoxEnd.x );

        movsx( (type word lParam[2]), eax );

        mov( eax, ptBoxEnd.y );

        w.ReleaseCapture();

        w.SetCursor( w.LoadCursor( NULL, @string(w.IDC_ARROW)));

        mov( false, fBlocking );

        mov( true, fValidBox );

        w.InvalidateRect( hwnd, NULL, true );

    endif;

    xor( eax, eax ); // Return zero to indicate success.

end LButtonUp;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        if( fValidBox ) then

            SelectObject( w.GetStockObject( w.BLACK_BRUSH ));

            Rectangle( ptBoxBegin.x, ptBoxBegin.y, ptBoxEnd.x, ptBoxEnd.y );

            if( fBlocking ) then

                SetROP2( w.R2_NOT );

                SelectObject( w.GetStockObject( w.NULL_BRUSH ));

                Rectangle( ptBoxBegin.x, ptBoxBegin.y, ptBoxEnd.x, ptBoxEnd.y );

            endif;

        endif;

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end MouseCapture;
Listing 8-8: MouseCapture Source Code

There are a couple of things worth nothing in the MouseCapture source code. First of all, whenever you press a mouse button or move the mouse, this application draws an outline of the rectangle by first erasing the existing rectangle (if present) and then reframing that rectangle in the window. Erasure is accomplished by first noting whether the application has previously draw the rectangle's outline, and if so, then changing the pen copy mode to w.R2_NOT via the SetROP2 call. "NOT drawing" the outline over the old one erases the outline. Once the outline is erased, the code draws a new outline using the new rectangle coordinates. When you first press the mouse button, the application captures the mouse cursor using the w.SetCapture API call. When you release the mouse button, the application releases the mouse capture using the w.ReleaseCapture API call and then fills in the rectangle by setting the device context's brush to w.BLACK_BRUSH.

"MouseEyes" is a second program that uses the w.SetCapture and w.ReleaseCapture to allow mouse activity outside the application's window. The idea here is somewhat whimsical - to use a pair of "eyeballs" in a window that help you pinpoint the mouse cursor anywhere on the screen. This first version of MouseEyes will not be very practical, it's intent is to demonstrate mouse capture and non-client area messages. However, we'll fix the practicality problems in a little bit.

The basic operation of MouseEyes is shown in Figure 8-7. Two round circles (the "eyeballs") inside a pair of larger circles determine the position of the mouse cursor on the screen by "looking" at the cursor. With such a program running on your display, you can easily locate a small mouse cursor, even on a crowded or dimmed display, by simply looking in the direction where the mouse eyes are pointing.

Figure 8-7: MouseEyes Functionality

If MouseEyes were a real application, we'd want the eyeballs to look in the direction of the mouse cursor, even when the current application doesn't have the input focus (meaning that the current application is not receiving mouse event messages). Indeed, in a little bit, we'll correct this oversight. For the time being, however, we're going to use the MouseEyes program to demonstrate non-client area mouse messages, so we don't want this additional functionality.

Most of the programs appearing in this chapter (indeed, in this book up to this point) have been fairly straight-forward. Most of the complex or new material has been learning the Win32 API. The MouseEyes application, however, requires a bit of geometry and trigonometry to pull off, so we need to spend a few moments discussing exactly how to draw the eyeballs.

The first step is to draw the outer circles that represent the eyes. For simplicity, we will always draw circles rather than ellipses. The radius of the two circles representing the eyes will be the minimum of the height of the client area or the width of the client area, divided by two (divided by two because we will need to draw two of the eyes, side-by-side, in the window). The first circle will be draw using a bounding box of [(0,0), (radius*2,radius*2)] and the second circle will have the bounding box [(radius*2,0), (radius*4, radius*2)], see Figure 8-8.

Figure 8-8: Drawing the Two Eyes

Calculating the bounding box for the outer circles is relatively easy. The inner circles (the "eyeballs") are much more difficult to deal with. We want to draw the inner circle "eyeballs" so that they just touch the inside of the outer circle. To see how to do this, consider Figure 8-9. We are given (X1, Y1), the current mouse cursor position, and (X0, Y0), the center of one of the outer circles. We need to compute two points, (X2,Y2), the point where the line described by ((X0, Y0), (X1, Y1)) intersects the outer circle, and (X3, Y3), the centerpoint of the inner circle (not explicitly labelled in Figure 8-9).

Figure 8-9: Computing the Coordinates of the "Eyeball"

From geometry, we know that the ratio between R1 (the distance from (X0, Y0) to (X1, Y1)) and R2 (the distance from (X0, Y0) to (X2, Y2)) is the same as the ratio between (X1-X0) and (X2-X0). The same fact holds for the ratio between R1/R2 and (Y1-Y0)/(Y2-Y0). We know X0, Y0, X1, Y1, and R2 (the radius of the outer circle). We can easily compute R1 using the formula:

Given Y1 (the y-coordinate of the mouse cursor), we can then compute Y2 as follows:

Likewise, we can compute X2 as follows:

However, if we draw the circle centered around this point, our eyeball will overlap the outer circle; we want it just inside the circle. This correction is easily achieved by adjusting the bounding box inward by the radius of the smaller circle (that radius, by the way, is simply computed as one-eighth of the outer circle's radius in this program). The actual output of the MouseEyes program appears in Figure 8-10.

Figure 8-10: MouseEyes Output

The MouseEyes program updates the position of the "eyeballs" whenever it receives a mouse movement message. Normally, mouse movement messages are only sent to the application when a mouse event occurs within the client area - not very interesting for this application. Were you to move the mouse cursor into the non-client area (e.g., the title bar) the eyeballs would stop tracking the mouse. To correct this problem, and demonstrate non-client mouse messages (really, the whole purpose of this program), the MouseEyes application also handles non-client area mouse messages so that the eyeballs still track the mouse cursor when it enters the non-client area of the window. Of course, once the mouse cursor leaves the application's window entirely, the application no longer receives mouse messages, so the eyeballs stop tracking. As yet another demonstration of mouse capture, the MouseEyes program will capture the mouse when you hold the mouse button down and it will track the mouse as long as you hold the button down (even if it leaves the application's window). This isn't great behavior for a true "MouseEyes" program, but it will suit our purposes for the time being.

Here's the full code to the MouseEyes application:

// MouseEyes.hla-

//

//  A program that demonstrates the use of the mouse and capturing the mouse,

// even in non-client areas.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit MouseEyes;

// Set the following to true to display interesting information

// about the bitmap file this program opens. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "hll.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

#includeonce( "math.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    MousePosn   :w.POINT;

readonly

    ClassName   :string := "MouseEyesWinClass";     // Window Class Name

    AppCaption  :string := "MouseEyes Program";     // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY,        &QuitApplication    ],

            MsgProcPtr_t:[ w.WM_PAINT,          &Paint              ],

            MsgProcPtr_t:[ w.WM_MOUSEMOVE,      &MouseMove          ],

            MsgProcPtr_t:[ w.WM_NCMOUSEMOVE,    &NCMouseMove        ],

            MsgProcPtr_t:[ w.WM_LBUTTONDOWN,    &LButtonDown        ],

            MsgProcPtr_t:[ w.WM_LBUTTONUP,      &LButtonUp          ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Tell the application to quit:

    w.PostQuitMessage( 0 );

end QuitApplication;

// MouseMove:

//

//  This procedure handles the w.WM_MOUSEMOVE message (mouse movement within the client
window).

procedure MouseMove( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc: dword;

begin MouseMove;

    movsx( (type word lParam), eax );

    mov( eax, MousePosn.x );

    movsx( (type word lParam[2]), eax );

    mov( eax, MousePosn.y );

    // Force a redraw of the window.

    w.InvalidateRect( hwnd, NULL, true );

    xor( eax, eax );

end MouseMove;

// NCMouseMove:

//

//  This procedure handles the w.WM_NCMOUSEMOVE message (mouse movement outside the client
window).

procedure NCMouseMove( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc: dword;

begin NCMouseMove;

    // The coordinates we've received are screen coordinates:

    movsx( (type word lParam), eax );

    mov( eax, MousePosn.x );

    movsx( (type word lParam[2]), eax );

    mov( eax, MousePosn.y );

    // The coordinates we've received are screen window coordinates.

    // We need to convert them to client coordinates.

    w.ScreenToClient( hwnd, MousePosn );

    // Force a redraw of the window:

    w.InvalidateRect( hwnd, NULL, true );

    xor( eax, eax );

end NCMouseMove;

// LButtonDown:

//

//  This procedure handles the w.WM_LBUTTONDOWN message, which this

// program uses to capture the mouse.

procedure LButtonDown( hwnd: dword; wParam:dword; lParam:dword );

begin LButtonDown;

    w.SetCapture( hwnd );

    xor( eax, eax ); // Return zero to indicate success.

end LButtonDown;

// LButtonUp:

//

//  This procedure handles the w.WM_LBUTTONUP message which

// this program uses to release the mouse.

procedure LButtonUp( hwnd: dword; wParam:dword; lParam:dword );

begin LButtonUp;

    w.ReleaseCapture();

    xor( eax, eax ); // Return zero to indicate success.

end LButtonUp;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

    clientRect  :w.RECT;

    Diameter    :dword;

    smallRadius :int32;

    newRadius   :int32;

    eyeR        :int32;

    OuterX      :int32;

    OuterY      :int32;

    x           :int32;

    y           :int32;

    r           :real64;

    xSave       :real64;

    ySave       :real64;

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

    // Get the coordinates of the client rectangle area so we know

    // where to draw our "eyes":

    w.GetClientRect( hwnd, clientRect );

        // Draw two circles within the window.

        // Draw the largest pair of circles that will

        // fit side-by-side in the window.

        // The largest diameter for the circles will be

        // min( clientRect.right/2, clientRect.bottom)

        // because the two circles are side-by-side.

        mov( clientRect.right, eax );

        shr( 1, eax );

        mov( clientRect.bottom, ebx );

        if( eax > ebx ) then

            mov( ebx, eax );

        endif;

        // EAX contains the diameter. Compute the center point (i.e., radius) of

        // the (outer) circle by dividing this value in half. For the first circle,

        // the x and y coordinates are both the same.

        mov( eax, Diameter );

        shr( 1, eax );

        mov( eax, OuterX );

        mov( eax, OuterY );

        // Compute the radius of the smaller (eyeball) circle as 1/4th the larger Diameter.

        shr( 2, eax );

        mov( eax, smallRadius );

        neg( eax );

        add( OuterX, eax );

        mov( eax, eyeR );

        // Draw the two circles:

        Ellipse( 0, 0, Diameter, Diameter );

        mov( Diameter, eax );

        shl( 1, eax );

        Ellipse( Diameter, 0, eax, Diameter );

        // Compute the coordinates for the "eyeball".

        // r = sqrt( (MousePosn.x - OuterX)**2 + (MousePosn.y-OuterY)**2 )

        fild( MousePosn.x );

        fild( OuterX );

        fsub;

        fst( xSave );

        fld( st0 );

        fmul;

        fild( MousePosn.y );

        fild( OuterY );

        fsub;

        fst( ySave );

        fld( st0 );

        fmul;

        fadd;

        fsqrt;

        fst( r );

        // sin(theta) = y/r

        fld( ySave );

        fdivr;

        // sin(theta) is on the stack now.

        // Compute y-coordinate of intersection as y=rnew*sin(theta)

        // where rnew is the radius of our circle and sin(theta) is from the above.

        fild( eyeR );

        fmul;

        fistp( y );

        // Compute the x-coordinate as x=rnew*cos(theta)

        // cos(theta) = x/r

        fld( xSave );

        fld( r );

        fdiv;

        fild( eyeR );

        fmul;

        fistp( x );

        // Draw a filled ellipse centered around this point:

        SelectObject( w.GetStockObject( w.BLACK_BRUSH) );

        mov( x, eax );          // The smaller circle's position

        add( OuterX, eax );     // was computed relative to (0,0),

        mov( eax, ebx );        // need to offset it to the (OuterX,OuterY)

        mov( y, ecx );

        add( OuterY, ecx );

        mov( ecx, edx );

        sub( smallRadius, eax );    // Move the smaller circle

        sub( smallRadius, ecx );    // to just inside the larger one.

        add( smallRadius, ebx );

        add( smallRadius, edx );

        Ellipse( eax, ecx, ebx, edx );

        // Do the computations over for the second eyeball.

        // Work is just about the same, the only difference

        // is the fact that we have a different point as the

        // center of the second eyeball:

        mov( Diameter, eax );   // Compute the x-coordinate of the center

        add( eax, OuterX );     // of the second eyeball.

        fild( MousePosn.x );    // Everything from here on is the same.

        fild( OuterX );

        fsub;

        fst( xSave );

        fld( st0 );

        fmul;

        fild( MousePosn.y );

        fild( OuterY );

        fsub;

        fst( ySave );

        fld( st0 );

        fmul;

        fadd;

        fsqrt;

        fst( r );

        // sin(theta) = y/r

        fld( ySave );

        fdivr;

        // sin(theta) is on the stack now.

        // Compute y-coordinate of intersection as y=rnew*sin(theta)

        // where rnew is the radius of our circle and sin(theta) is from the above.

        fild( eyeR );

        fmul;

        fistp( y );

        // Compute the x-coordinate as x=rnew*cos(theta)

        // cos(theta) = x/r

        fld( xSave );

        fld( r );

        fdiv;

        fild( eyeR );

        fmul;

        fistp( x );

        // Draw an ellipse centered around this point:

        SelectObject( w.GetStockObject( w.BLACK_BRUSH) );

        mov( x, eax );

        add( OuterX, eax );

        mov( eax, ebx );

        mov( y, ecx );

        add( OuterY, ecx );

        mov( ecx, edx );

        sub( smallRadius, eax );

        sub( smallRadius, ecx );

        add( smallRadius, ebx );

        add( smallRadius, edx );

        Ellipse( eax, ecx, ebx, edx );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end MouseEyes;
Listing 8-9: The MouseEyes Application

To see the effects of the non-client messages, try commenting out the following line in the MouseEyes source code:

MsgProcPtr_t:[ w.WM_NCMOUSEMOVE,    &NCMouseMove        ],

i.e.,

Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY,        &QuitApplication    ],

            MsgProcPtr_t:[ w.WM_PAINT,          &Paint              ],

            MsgProcPtr_t:[ w.WM_MOUSEMOVE,      &MouseMove          ],

            // MsgProcPtr_t:[ w.WM_NCMOUSEMOVE,    &NCMouseMove        ],

            MsgProcPtr_t:[ w.WM_LBUTTONDOWN,    &LButtonDown        ],

            MsgProcPtr_t:[ w.WM_LBUTTONUP,      &LButtonUp          ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

Recompile and run this program with the non-client mouse message handler disabled. Now you'll find that the eyeballs stop tracking the mouse cursor when it enters the title bar and other non-client areas of the window. This quickly demonstrates the purpose of the non-client mouse messages. Of course, most applications won't need to process non-client mouse messages at all, but should you need to know when a mouse event occurs in a non-client area of the window, these messages provide you with everything you need.

Note that if you capture the mouse, you will continue to receive mouse event messages as standard mouse messages, even when the cursor is over a non-client area. You don't have to process non-client mouse messages if the only time you're worried about the non-client area is when you've captured the mouse.

A very common problem in a modern Windows program is to determine if the mouse cursor is resting on a particular object whenever the user presses the mouse button. This is known as hit testing. Hit testing is actually a fairly simple process for regular objects (like rectangles) or objects whose hit test simply involves checking the cursor position to see if it lies within an object's bounding box (which is a rectangle, of course).

The HitTest application demonstrates this process. The HitTest program provides a palette of four colors on the left hand side of the window and a grid containing 16 rectangles in the main area of the window (see Figure 8-11). By clicking on one of the four color rectangles you can select a "current color" and then by clicking on one of the 16 rectangles in the main section of the window you can set that rectangle to the current color. This application uses the Windows API function w.PtInRect to do the actual hit testing. This function has the following prototype:

procedure w.PtInRect( rect:w.RECT; p:w.POINT );

This function returns true in EAX if the point is within the bounds of the specified rectangle, it returns false otherwise. The "left button down" message handling procedure first checks to see if the mouse button was pressed when the mouse was on one of the four "set color" rectangles in the HitTest application. If so, the code sets the current color and returns. If not, the code tests to see if the mouse is on top of one of the other rectangles in the display; if so, the code redraws that rectangle with the new color, if not, the button down message handler does nothing.

Figure 8-11: HitTest Output

// HitTest.hla-

//

//  A program that demonstrates testing a mouse press to see if it occurs within some
region.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit HitTest;

// Set the following to true to display interesting information

// about the bitmap file this program opens. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "hll.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

const

    numColorBoxes := 4;

    numFillRects := 16;

type

    cSrc_t  :record

                r:  w.RECT;

                c:  w.COLORREF;

    endrecord;

static

    curColor    :w.COLORREF := RGB( $ff, $ff, $ff );

    ColorSrcs   :cSrc_t [numColorBoxes] :=

                [

                    cSrc_t:[ w.RECT:[10, 10,40, 40], RGB(255,0,0)],

                    cSrc_t:[ w.RECT:[10, 60,40, 90], RGB(0,255,0)],

                    cSrc_t:[ w.RECT:[10,110,40,140], RGB(0,0,255)],

                    cSrc_t:[ w.RECT:[10,160,40,190], RGB(255,255,255)]

                ];

    RectsToFill :cSrc_t [numFillRects] :=

                [

                    cSrc_t:[ w.RECT:[75,  25, 125, 100], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[150, 25, 200, 100], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[225, 25, 275, 100], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[300, 25, 355, 100], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[75,  125, 125, 200], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[150, 125, 200, 200], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[225, 125, 275, 200], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[300, 125, 355, 200], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[75,  225, 125, 300], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[150, 225, 200, 300], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[225, 225, 275, 300], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[300, 225, 355, 300], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[75,  325, 125, 400], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[150, 325, 200, 400], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[225, 325, 275, 400], RGB(0,0,0)],

                    cSrc_t:[ w.RECT:[300, 325, 355, 400], RGB(0,0,0)]

                ];

readonly

    ClassName   :string := "HitTestWinClass";       // Window Class Name

    AppCaption  :string := "HitTest Program";       // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_DESTROY,        &QuitApplication    ],

            MsgProcPtr_t:[ w.WM_PAINT,          &Paint              ],

            MsgProcPtr_t:[ w.WM_LBUTTONDOWN,    &LButtonDown        ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Tell the application to quit:

    w.PostQuitMessage( 0 );

end QuitApplication;

// LButtonDown:

//

//  This procedure handles the w.WM_LBUTTONDOWN message, which this

// program uses to capture the mouse.

procedure LButtonDown( hwnd: dword; wParam:dword; lParam:dword );

var

    p   :w.POINT;

begin LButtonDown;

    push( ebx );

    push( esi );

    movsx( (type word lParam), eax );

    mov( eax, p.x );

    movsx( (type word lParam[2]), eax );

    mov( eax, p.y );

    for( mov( 0, ebx ); ebx < numColorBoxes; inc( ebx )) do

        intmul( @size(cSrc_t), ebx, esi );

        w.PtInRect( ColorSrcs.r[ esi ], p );

        if( eax ) then

            mov( ColorSrcs.c[esi], eax );

            mov( eax, curColor );

        endif;

    endfor;

    for( mov( 0, ebx ); ebx < numFillRects; inc( ebx )) do

        intmul( @size(cSrc_t), ebx, esi );

        w.PtInRect( RectsToFill.r[ esi ], p );

        if( eax ) then

            mov( curColor, eax );

            mov( eax, RectsToFill.c[esi] );

        endif;

    endfor;

    w.InvalidateRect( hwnd, NULL, true );

    xor( eax, eax );

    pop( esi );

    pop( ebx );

end LButtonDown;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hbrush      :dword;

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

    r           :w.RECT;

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        for( mov( 0, ebx ); ebx < numFillRects; inc( ebx )) do

            intmul( @size( cSrc_t ), ebx, esi );

            w.CreateSolidBrush( RectsToFill.c[ esi ] );

            mov( eax, hbrush );

            SelectObject( hbrush );

            FillRect( RectsToFill.r[esi], hbrush );

            SelectObject( w.GetStockObject( w.NULL_BRUSH ));

            w.DeleteObject( hbrush );

            Rectangle

            (

                RectsToFill.r.left[ esi ],

                RectsToFill.r.top[ esi ],

                RectsToFill.r.right[ esi ],

                RectsToFill.r.bottom[ esi ]

            );

        endfor;

        for( mov( 0, ebx ); ebx < numColorBoxes; inc( ebx )) do

            intmul( @size( cSrc_t ), ebx, esi );

            w.CreateSolidBrush( ColorSrcs.c[ esi ] );

            mov( eax, hbrush );

            SelectObject( hbrush );

            FillRect( ColorSrcs.r[esi], hbrush );

            SelectObject( w.GetStockObject( w.NULL_BRUSH ));

            w.DeleteObject( hbrush );

            Rectangle

            (

                ColorSrcs.r.left[ esi ],

                ColorSrcs.r.top[ esi ],

                ColorSrcs.r.right[ esi ],

                ColorSrcs.r.bottom[ esi ]

            );

        endfor;

        MoveTo( 50, 0 );

        LineTo( 50, 200 );

        LineTo( 0, 200 );

        // Display the currently selected color:

        w.SetRect( r, 0, 210, 50, 240 );

        w.CreateSolidBrush( curColor );

        mov( eax, hbrush );

        SelectObject( hbrush );

        FillRect( r, hbrush );

        SelectObject( w.GetStockObject( w.NULL_BRUSH ));

        w.DeleteObject( hbrush );

        Rectangle( 0, 210, 50, 240 );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end HitTest;
Listing 8-10: HitTest Application

Limiting hit testing to rectangular regions isn't always appropriate. If you need to see if the user has pressed a mouse button within the confines of a complex object (only only within that object), you can always create a region from that object and then use the w.PtInRegion API function to check for a hit:

procedure w.PtInRegion( hrgn:dword; x:int32; y:int32 );

This procedure returns true if the point sepcified by (x,y) is within the region whose handle you pass as the first parameter.

Newer versions of Windows (2000, XP, and later) provide additional mouse events for newer-style Microsoft mice as well as support for "tool-tip" messages. Specfically, there are three new messages that support "XBUTTON" down, up, and double-clicks (X-buttons are additional buttons on newer mice). There is also a w.TrackMouseEvent function that activates two other messages: w.WM_MOUSEHOVER and w.WM_MOUSELEAVE, that Windows sends when the mouse cursor hovers above a certain object for some length of time (e.g., applications use this to bring up little "tool tip" windows). Check out the Microsoft MSDN documentation (on-line) for more details on these messages.

8.11: Timer Messages

The MouseEyes application suffers from a very major problem: it only tracks the mouse cursor while the mouse cursor is actually within that application's window (or while you're holding down the mouse button if you pressed the mouse button while inside the window). For this application to be practical, it needs to track the mouse cursor position even when it is not receiving mouse event message (i.e., when some other window has the input focus and is receiving the mouse messages). Sometimes an application (like MouseEyes) needs some periodic messages to come along so that it can do some work (such as track the current cursor position) even though the window doesn't have the focus and isn't receiving any other messages. Windows' timer messages can solve this problem.

A timer message is a message that Windows can send to your application on a periodic basis. Therefore, even if no other events occur (because the application is in the background and never gets the input focus), the application can receive the attention of the CPU so it can take care of some processing needs. The MouseEyes application is a classic example, it needs to periodically check the cursor position so it can update its eyeballs, even though it's not receiving a stream of mouse messages (because those messages are headed to another window). You can also use timers to implement various "timeout" or "timing" features in an application such as periodically updating status information, autosaving files every few minutes, or controlling how long an application runs before it terminates (e.g., for demo versions of an application).

To receive timer messages, you must explicitly request that Windows do this. You accomplish this with the w.SetTimer API call:

    procedure w.SetTimer

    (

        hwnd:dword;

        timerID:dword;

        timeout:uns32;

        timerproc: w.TIMERPROC

    );

type

    TIMERPROC:

        procedure( hwnd:dword; uMsg:dword; idEvent:dword; dwTime:dword );

The first parameter to w.SetTimer is the handle of the window procedure that is going to receive the w.WM_TIMER messages that Windows will generate in response to this request. In this situation, the last parameter (timerproc) should be NULL. The second parameter specifies a non-zero timer ID. An application can activate multiple timers; it differentiates those timers by specifying differing numeric values for the timer ID. Whenever an application receives a w.WM_TIMER message, the wParam parameter contains the timer ID value for the timer that triggered the message. The third parameter is the timeout period, in milliseconds. The last parameter is the address of a callback routine; this parameter should be NULL if you want Windows to pass the message through to the window procedure you specify by the hwnd handle. If you prefer, you can tell Windows to call a procedure of your choosing (compatible with the w.TIMERPROC type). The advantage of the callback procedure is that you get more information in the parameter list that Windows passes to the callback procedure.

You can kill a timer by calling the w.KillTimer API function:

procedure w.KillTimer( hwnd:dword; timerID:dword );

The hwnd and timerID values must match the values you passed to the corresponding w.SetTimer call. As timers are system-wide resources, you must ensure that you kill all timers you start in your application. In general, it's a good idea to start all timers in your Create procedure and kill them in your Destroy procedure, assuming your application allows this. This avoids paths through the application that start timers without ever killing them.

To conclude this chapter, we'll fix the aforementioned problems with the MouseEyes application so that it runs off of a timer event rather than mouse events. The MouseEyes2 application arms a timer to send it a message every 150ms. When a timer message arrives, the MouseEyes2 application gets the current mouse cursor position via the w.GetCursorPos API call:

procedure w.GetCursorPos( var point:w.POINT );

This function stores the current mouse position into the point parameter you pass by reference. Note that this function returns screen coordinates, not MouseEyes2 client area coordinates. Therefore, the application will have to convert those coordinates to client area coordinates before using them. After getting these coordinates from Windows, MouseEyes2 draws the eyeballs exactly as in the MouseEyes program. Here's the code:

// MouseEyes.hla-

//

//  A program that demonstrates the use of the mouse and capturing the mouse,

// even in non-client areas.

//

// Note: this is a unit because it uses the WinMail library module that

//       provides a win32 main program for us.

unit MouseEyes2;

// Set the following to true to display interesting information

// about the bitmap file this program opens. You must be running

// the "DebugWindow" application for this output to appear.

?debug := false;

#includeonce( "hll.hhf" )

#includeonce( "w.hhf" )

#includeonce( "wpa.hhf" )

#includeonce( "winmain.hhf" )

#includeonce( "math.hhf" )

?@NoDisplay := true;

?@NoStackAlign := true;

static

    MousePosn   :w.POINT;

readonly

    ClassName   :string := "MouseEyesWinClass";     // Window Class Name

    AppCaption  :string := "MouseEyes Program";     // Caption for Window

    // The dispatch table:

    //

    //  This table is where you add new messages and message handlers

    //  to the program.  Each entry in the table must be a MsgProcPtr_t

    //  record containing two entries: the message value (a constant,

    //  typically one of the w.WM_***** constants found in windows.hhf)

    //  and a pointer to a "MsgProcPtr_t" procedure that will handle the

    //  message.

    Dispatch    :MsgProcPtr_t; @nostorage;

        MsgProcPtr_t

            MsgProcPtr_t:[ w.WM_CREATE,         &Create             ],

            MsgProcPtr_t:[ w.WM_DESTROY,        &QuitApplication    ],

            MsgProcPtr_t:[ w.WM_PAINT,          &Paint              ],

            MsgProcPtr_t:[ w.WM_TIMER,          &TimerMsg           ],

            // Insert new message handler records here.

            MsgProcPtr_t:[ 0, NULL ];   // This marks the end of the list.

/**************************************************************************/

/*          W I N M A I N   S U P P O R T   C O D E                       */

/**************************************************************************/

// initWC - We don't have any initialization to do, so just return:

procedure initWC; @noframe;

begin initWC;

    ret();

end initWC;

// appCreateWindow- the default window creation code is fine, so just

//                  call defaultCreateWindow.

procedure appCreateWindow; @noframe;

begin appCreateWindow;

    jmp defaultCreateWindow;

end appCreateWindow;

// appException-

//

// Gives the application the opportunity to clean up before

// aborting when an unhandled exception comes along:

procedure appException( theException:dword in eax );

begin appException;

    raise( eax );

end appException;

// This is the custom message translation procedure.

// We're not doing any custom translation, so just return EAX=0

// to tell the caller to go ahead and call the default translation

// code.

procedure LocalProcessMsg( var lpmsg:w.MSG );

begin LocalProcessMsg;

    xor( eax, eax );

end LocalProcessMsg;

/**************************************************************************/

/*          A P P L I C A T I O N   S P E C I F I C   C O D E             */

/**************************************************************************/

// Create:

//

//  Called when we first create the window. Initializes the timer.

procedure Create( hwnd: dword; wParam:dword; lParam:dword );

begin Create;

    // Turn off the timer:

    w.SetTimer( hwnd, 1, 150, NULL );

    xor( eax, eax );

end Create;

// QuitApplication:

//

//  This procedure handles the w.WM_DESTROY message.

//  It tells the application to terminate.  This code sends

//  the appropriate message to the main program's message loop

//  that will cause the application to terminate.

procedure QuitApplication( hwnd: dword; wParam:dword; lParam:dword );

begin QuitApplication;

    // Turn off the timer:

    w.KillTimer( hwnd, 1 );

    // Tell the application to quit:

    w.PostQuitMessage( 0 );

end QuitApplication;

// MouseMove:

//

//  This procedure handles the w.WM_MOUSEMOVE message (mouse movement within the client
window).

procedure TimerMsg( hwnd: dword; wParam:dword; lParam:dword );

begin TimerMsg;

    // Get the current cursor position:

    w.GetCursorPos( MousePosn );

    w.ScreenToClient( hwnd, MousePosn );

    // Force a redraw of the window.

    w.InvalidateRect( hwnd, NULL, true );

    xor( eax, eax );

end TimerMsg;

// Paint:

//

//  This procedure handles the w.WM_PAINT message.

procedure Paint( hwnd: dword; wParam:dword; lParam:dword );

var

    hdc         :dword;         // Handle to video display device context.

    ps          :w.PAINTSTRUCT; // Used while painting text.

    clientRect  :w.RECT;

    Diameter    :dword;

    smallRadius :int32;

    newRadius   :int32;

    eyeR        :int32;

    OuterX      :int32;

    OuterY      :int32;

    x           :int32;

    y           :int32;

    r           :real64;

    xSave       :real64;

    ySave       :real64;

begin Paint;

    // Message handlers must preserve EBX, ESI, and EDI.

    // (They've also got to preserve EBP, but HLA's procedure

    // entry code already does that.)

    push( ebx );

    push( esi );

    push( edi );

    // Note that all GDI calls must appear within a

    // BeginPaint..EndPaint pair.

    BeginPaint( hwnd, ps, hdc );

        // Get the coordinates of the client rectangle area so we know

        // where to draw our "eyes":

        w.GetClientRect( hwnd, clientRect );

        // Draw two circles within the window.

        // Draw the largest pair of circles that will

        // fit side-by-side in the window.

        // The largest diameter for the circles will be

        // min( clientRect.right/2, clientRect.bottom)

        // because the two circles are side-by-side.

        mov( clientRect.right, eax );

        shr( 1, eax );

        mov( clientRect.bottom, ebx );

        if( eax > ebx ) then

            mov( ebx, eax );

        endif;

        // EAX contains the diameter. Compute the center point (i.e., radius) of

        // the (outer) circle by dividing this value in half. For the first circle,

        // the x and y coordinates are both the same.

        mov( eax, Diameter );

        shr( 1, eax );

        mov( eax, OuterX );

        mov( eax, OuterY );

        // Compute the radius of the smaller (eyeball) circle as 1/4th the larger Diameter.

        shr( 2, eax );

        mov( eax, smallRadius );

        neg( eax );

        add( OuterX, eax );

        mov( eax, eyeR );

        // Draw the two circles:

        Ellipse( 0, 0, Diameter, Diameter );

        mov( Diameter, eax );

        shl( 1, eax );

        Ellipse( Diameter, 0, eax, Diameter );

        // Compute the coordinates for the "eyeball".

        // r = sqrt( (MousePosn.x - OuterX)**2 + (MousePosn.y-OuterY)**2 )

        fild( MousePosn.x );

        fild( OuterX );

        fsub;

        fst( xSave );

        fld( st0 );

        fmul;

        fild( MousePosn.y );

        fild( OuterY );

        fsub;

        fst( ySave );

        fld( st0 );

        fmul;

        fadd;

        fsqrt;

        fst( r );

        // sin(theta) = y/r

        fld( ySave );

        fdivr;

        // sin(theta) is on the stack now.

        // Compute y-coordinate of intersection as y=rnew*sin(theta)

        // where rnew is the radius of our circle and sin(theta) is from the above.

        fild( eyeR );

        fmul;

        fistp( y );

        // Compute the x-coordinate as x=rnew*cos(theta)

        // cos(theta) = x/r

        fld( xSave );

        fld( r );

        fdiv;

        fild( eyeR );

        fmul;

        fistp( x );

        // Draw a filled ellipse centered around this point:

        SelectObject( w.GetStockObject( w.BLACK_BRUSH) );

        mov( x, eax );          // The smaller circle's position

        add( OuterX, eax );     // was computed relative to (0,0),

        mov( eax, ebx );        // need to offset it to the (OuterX,OuterY)

        mov( y, ecx );

        add( OuterY, ecx );

        mov( ecx, edx );

        sub( smallRadius, eax );    // Move the smaller circle

        sub( smallRadius, ecx );    // to just inside the larger one.

        add( smallRadius, ebx );

        add( smallRadius, edx );

        Ellipse( eax, ecx, ebx, edx );

        // Do the computations over for the second eyeball.

        // Work is just about the same, the only difference

        // is the fact that we have a different point as the

        // center of the second eyeball:

        mov( Diameter, eax );   // Compute the x-coordinate of the center

        add( eax, OuterX );     // of the second eyeball.

        fild( MousePosn.x );    // Everything from here on is the same.

        fild( OuterX );

        fsub;

        fst( xSave );

        fld( st0 );

        fmul;

        fild( MousePosn.y );

        fild( OuterY );

        fsub;

        fst( ySave );

        fld( st0 );

        fmul;

        fadd;

        fsqrt;

        fst( r );

        // sin(theta) = y/r

        fld( ySave );

        fdivr;

        // sin(theta) is on the stack now.

        // Compute y-coordinate of intersection as y=rnew*sin(theta)

        // where rnew is the radius of our circle and sin(theta) is from the above.

        fild( eyeR );

        fmul;

        fistp( y );

        // Compute the x-coordinate as x=rnew*cos(theta)

        // cos(theta) = x/r

        fld( xSave );

        fld( r );

        fdiv;

        fild( eyeR );

        fmul;

        fistp( x );

        // Draw an ellipse centered around this point:

        SelectObject( w.GetStockObject( w.BLACK_BRUSH) );

        mov( x, eax );

        add( OuterX, eax );

        mov( eax, ebx );

        mov( y, ecx );

        add( OuterY, ecx );

        mov( ecx, edx );

        sub( smallRadius, eax );

        sub( smallRadius, ecx );

        add( smallRadius, ebx );

        add( smallRadius, edx );

        Ellipse( eax, ecx, ebx, edx );

    EndPaint;

    pop( edi );

    pop( esi );

    pop( ebx );

end Paint;

end MouseEyes2;
Listing 8-12: MouseEyes2 Application

8.13: But Wait! There's More!

As long as this chapter is, it isn't the final word on Windows input by any stretch of the imagination. Rather, it's just an introduction to the subject. Of course, completely missing from the discussion in this chapter are input issues such as reading text from textedit boxes and input from other controls. Fear not, we're getting to that before too much longer.

1In Windows' terminology, "cursor" describes the screen object the denotes the position of the mouse pointer.
TOC PREV NEXT INDEX


Index: Windows Programming in Assembly Language

Symbols

!( boolean_expression ) 1
#(...)# in macro parameters 1
#(...)# macro quoting symbols 1
#{ ... }# sequence for manually passing parameters 1
#{...}# parameter quoting mechanism 1
#{...}#" code brackets in boolean expressions 1
#closeread compile-time statement 1
#closewrite compile-time statement 1
#else clause 1
#elseif clause in #if statement 1
#endif clause 1
#endmacro 1
#ERROR directive 1
#for..#endfor Compile-Time Loop 1
#KEYWORD reserved word 1
#macro 1
#openread compile-time statement 1
#openwrite compile-time statement 1
#PRINT directive 1
#system compile-time statement 1
#TERMINATOR keyword 1
#text..#endtext statement 1
#while..#endwhile Compile-Time Loop 1
#write compile-time statement 1
&& operator in boolean expressions 1
@a 1
@abs function 1
@ae 1
@align 1
@Align procedure option 1
@alignstack 1
@alignstack procedure option 1, 2
@arity compile-time function 1
@arity function 1
@b 1
@be 1
@byte compile-time function 1
@byte function 1
@c 1, 2
@cdecl procedure option 1, 2
@ceil function 1
@char compile-time function 1
@cos function 1
@cset compile-time function 1
@date function 1
@defined function 1
@delete function 1
@dim compile-time function 1
@dim function 1
@display 1
@display procedure option 1
@e 1
@elements compile-time function 1
@elements function 1
@elementsize compile-time function 1
@elementsize function 1
@enter 1
@enter procedure option 1
@eval function 1
@exp function 1
@External procedures 1
@extract function 1
@floor function 1
@frame 1
@g 1
@ge 1
@index function 1
@insert function 1
@int64 compile-time function 1
@int8 compile-time function 1
@isalpha function 1
@isalphanum function 1
@isconst function 1
@isdigit function 1
@IsExternal function 1
@isfreg function 1
@islower function 1
@ismem function 1
@isreg function 1
@isreg16 function 1
@isreg32 function 1
@isreg8 function 1
@isspace function 1
@istype function 1
@isupper function 1
@isxdigit function 1
@l 1
@le 1
@leave 1
@leave procedure option 1
@length function 1
@linenumber function 1, 2
@log function 1
@log10 function 1
@lowercase function 1
@max function 1
@min function 1
@na 1
@nae 1
@name function 1
@nb 1
@nbe 1
@nc 1, 2
@ne 1
@nge 1
@nl 1
@nle 1
@no 1, 2
@noalignstack 1
@noalignstack procedure option 1, 2
@nodisplay 1
@nodisplay procedure option 1
@noenter 1
@noenter procedure option 1
@noframe 1
@noframe procedure option 1
@noleave 1
@noleave procedure option 1
@ns 1, 2
@nz 1, 2
@o 1
@o, 1
@odd function 1
@offset function 1
@pascal procedure option 1, 2
@qword compile-time function 1
@random function 1
@randomize function 1
@read compile-time function 1
@real32 compile-time function 1
@Returns procedure option 1
@returns procedure option 1
@rindex function 1
@s 1, 2
@sin function 1
@size compile-time function 1
@size function 1
@sqrt function 1
@stdcall procedure option 1, 2
@string compile-time function 1
@substr function 1
@tan function 1
@text operator 1
@time function 1
@tostring operator 1
@typename function 1
@uns64 compile-time function 1
@uns8 compile-time function 1
@uppercase function 1
@use procedure option 1
@use reg32 procedure option 1
@z 1, 2
_display_ variable 1
_parms_ constant 1
_vars_ constant 1
|| operator in boolean expressions 1

A

ABI (Application Binary Interface) 1
Accessing the fields of a record 1
Accessing the fields of a union 1
Align directives in a record 1
align procedure option 1
AllocConsole 1, 2
AND operator in boolean expressions 1
Anonymous records 1
Anonymous unions 1, 2
Anonymous Unions and Records 1
ANYEXCEPTION clause in the TRY..ENDTRY statement 1
API Functions 1
Arithmetic expressions (run-time) 1
Array Types 1
Array types 1
array.index array indexing function 1
Assignments at compile-time 1
Associativity 1, 2
Automatic code generation in procedures 1

B

BeginPaint 1, 2
Boolean expressions 1, 2
BREAK 1
Break statement 1
breakif 1

C

C calling convention 1
C programming language 1
C scalar data types 1
C Types 1
C/C++ Naming conventions 1
Call instruction 1
Callback functions 1
Calling conventions 1
Calling HLA Procedures 1
Cascading exceptions 1
case neutrality 1
Character data types 1
Character string types 1
Class types 1
Client regions 1
Command Line Development 1
Compile-time arithmetic and relational operators 1
Compile-Time Assignment Statements 1
Compile-time functions 1
Compile-Time I/O 1
Compile-Time Language 1
Compile-time language performance 1
Compile-time variable declarations 1
Composite Data Types 1
Composite data types 1
Conditional assembly 1
Conjunction in boolean expressions 1
Console API 1
Console applications 1
Console I/O 1
Constant declarations 1
Constant declarations (compile-time language) 1
Context-Free macros 1
Context-free macros 1
Context-free macros (HLA) 1
CONTINUE 1
Continue statement 1
continueif 1
Converting C/C++ boolean expressions to HLA boolean expressions 1
Converting expressions into assembly language 1
CreateConsoleScreenBuffer 1
CreateFontIndirect 1
CreateWindowEx 1
Creating a New Project in RadASM 1
Creating a Window 1
Creating HLA Procedure Prototypes for Win32 API Functions 1

D

Data alignment and padding 1
Data Types 1
DEFAULT section of a SWITCH statement 1
Deferred macro parameter expansion 1
Device capabilities 1
Device context 1
Device context attributes 1
Directly compatible types 1
Discriminant union type 1
Disjunction in boolean expressions 1
Displaying a window 1
Displaying messages during compilation 1
Dithering 1
DO..WHILE Loops 1
DrawText 1
DUP operator (array constants) 1

E

Eager evaluation of macro parameters 1
Editing HLA source files within RadASM 1
ELSE 1
else 1
ELSEIF 1
elseif 1
ENDFOR 1
ENDIF 1
EndPaint 1, 2
ENDWHILE 1
EnumFontFamilies 1
Errors 1
Escape character sequence 1
ESP 1
Event-oriented programming 1
Exception handling 1
Exception numbers 1
EXIT 1
exitif 1
Expressions and temporary values 1
ExtTextOut 1

F

Fields of a union 1
FillConsoleOutputAttribute 1
FillConsoleOutputCharacter 1
FindWindow 1
Floating point (real) data types 1
FlushConsoleInputBuffer 1
Fonts 1
FOR loops 1
FOR loops (for..endfor, forever..endfor, foreach..endfor) 1
forever..endfor 1
frame procedure option 1
FreeConsole 1
Function Calls 1
Function return results 1

G

GetBkColor 1
GetBkMode 1
GetConsoleCursorInfo 1
GetConsoleScreenBufferInfo 1
GetConsoleTitle 1
GetConsoleWindow 1
GetDC 1
GetDeviceCaps 1
GetNumberOfConsoleInputEvents 1
GetScrollPos 1
GetScrollRange 1
GetStdHandle 1
GetSystemMetrics 1
GetTextAlign 1
GetTextColor 1
GetTextMetrics 1
GetWindowDC 1, 2
GOTO statement 1

H

Handles 1
High Level Assembler (HLA) 1
HLA Naming Conventions 1
HLA Standard Library 1
HLA.INI initialization file 1
Hungarian Notation 1
Hybrid high level boolean expressions 1
Hybrid languages 1
Hybrid parameter passing in HLA 1

I

IDE 1
IF Statement 1
If statements and time-critical code 1
IF..ENDIF 1
Implicit rules in a make file 1
IN operator 1
IN reg parameter specification 1
Inherited fields in records 1
inherits keyword 1
Integer data types 1
Integrated Development Environments 1

L

Lazy (pass by lazy evaluation) parameter option 1
Leading (spacing in a font) 1
Local symbols in macros 1
Local symbols in multi-part macros 1
Logical inch 1

M

Macro parameters containing commas or parentheses 1
Macros 1
Make 1
Make files 1
Make menu in RadASM 1
Make program (make.exe) 1
Memory protection 1
Message passing 1, 2
MessageBox 1
Monospaced fonts 1, 2
multi-part macros 1

N

Name (pass by name) parameter option 1
Naming Conventions 1, 2
Nested records and unions 1
NMake 1
Non-client regions 1
Non-proportional fonts 1
NULL 1

O

OllyDbg Debugger 1
Operator precedence in run-time expressions 1
OR operator in boolean expressions 1

P

PAINTSTRUCT 1
Parenthesis in macro parameters 1
Pascal calling convention 1
Pass by Reference 1
Pass by Value 1
Pass by value 1
Passing parameters in registers 1, 2
Path specifications in RadASM 1
PeekConsoleInput 1
Pointer constants 1
Pointer types 1, 2
Points (font sizes) 1
PostMessage 1
Procedural programming languages 1
Procedure declarations 1
Procedure Invocations 1
Project organization 1
Project types in RadASM 1
Prototypes for Win32 API Functions 1

R

RadASM Integrated Development Environment (IDE) 1
RadASM project management 1
RadASM templates 1
RADASM.INI file 1
RAISE statement 1
Raster fonts 1
ReadConsole 1
ReadConsoleInput 1
ReadConsoleOutput 1
ReadConsoleOutputAttribute 1
ReadConsoleOutputCharacter 1
Real data types 1
Record (Structure) Types 1
Record/structure types 1
Reference parameters 1
Regions 1
Register parameters 1
Register Preservation and Scratch Registers in Win32 Calls 1
Registering a window class 1
RegisterWindowMessage 1
Relational operators 1
ReleaseDC 1, 2
REPEAT..UNTIL 1
repeat..until 1
Reraising an exception 1
Result (pass by result) parameter option 1
RET with NOFRAME option 1
RGB value 1
row-major order 1

S

Scratch Registers in Win32 Calls 1
Scroll bars 1
ScrollConsoleScreenBuffer 1
SelectObject 1
SendMessage 1
SendMessageCallback 1
SendMessageTimeout 1, 2
Sequence points 1
SetBkColor 1
SetBkMode 1
SetConsoleActiveScreenBuffer 1
SetConsoleCursorInfo 1
SetConsoleCursorPosition 1
SetConsoleScreenBufferSize 1
SetConsoleTextAttribute 1
SetConsoleTitle 1
SetConsoleWindowInfo 1
SetScrollPos 1
SetScrollRange 1
SetStdHandle 1
SetTextAlign 1
SetTextColor 1
Setting Up RadASM 1
Side effects 1
Signed vs. unsigned comparisons in boolean expressions 1
Small projects and RadASM 1
Sniplets 1
Stack Pointer 1
Stdcall calling convention 1
stdout.put macro implementation 1
str.init 1
str.put 1
stralloc 1
Structure Types 1
Structured constants 1
SWITCH/CASE statement 1

T

Templates in RadASM 1
Temporary values in an expression 1
TextOut 1, 2
Thunk types 1
Translating C/C++ expressions to assembly language 1
True Type fonts 1
TRY..EXCEPTION..ENDTRY statement 1
tstralloc 1
Type Coercion 1
Typefaces 1
Typefaces and fonts 1

U

Unicode 1
Union data types 1
Union Types 1
UNPROTECTED clause in the TRY..ENDTRY statement 1
Untyped reference parameters 1, 2
User-defined compilation errors 1
User-defined exceptions 1

V

Val (pass by value) parameter option 1
Valres (pass by value/result) parameter option 1
Value parameters 1
Var (pass by reference) parameter option 1
VAR (untyped reference parameters) 1
Var type (untyped reference parameters) 1
Variable parameter lists in a macro 1
Variable parameter lists in macros 1
Variant types 1
Viewport origin 1

W

w.AllocConsole 1, 2
w.BeginPaint 1
w.CreateConsoleScreenBuffer 1
w.CreateFontIndirect 1
w.CreateWindowEx 1
w.DrawText 1
w.EndPaint 1
w.EnumFontFamilies 1
w.ExtTextOut 1
w.FillConsoleOutputAttribute 1
w.FillConsoleOutputCharacter 1
w.FindWindow 1
w.FlushConsoleInputBuffer 1
w.FreeConsole 1
w.GetBkColor 1
w.GetConsoleCursorInfo 1
w.GetConsoleScreenBufferInfo 1
w.GetConsoleTitle 1
w.GetConsoleWindow 1
w.GetDC 1
w.GetDeviceCaps 1
w.GetNumberOfConsoleInputEvents 1
w.GetScrollPos 1
w.GetScrollRange 1
w.GetStdHandle 1
w.GetSystemMetrics 1
w.GetTextAlign 1
w.GetTextColor 1
w.GetTextMetrics 1
w.GetWindowDC 1
w.MessageBox 1
w.PeekConsoleInput 1
w.PostMessage 1
w.ReadConsole 1
w.ReadConsoleInput 1
w.ReadConsoleOutput 1
w.ReadConsoleOutputAttribute 1
w.ReadConsoleOutputCharacter 1
w.RegisterWindowMessage 1
w.ReleaseDC 1
w.ScrollConsoleScreenBuffer 1
w.SelectObject 1
w.SendMessage 1
w.SendMessageCallback 1
w.SendMessageTimeout 1, 2
w.SetBkColor 1
w.SetConsoleActiveScreenBuffer 1
w.SetConsoleCursorInfo 1
w.SetConsoleCursorPosition 1
w.SetConsoleScreenBufferSize 1
w.SetConsoleTextAttribute 1
w.SetConsoleTitle 1
w.SetConsoleWindowInfo 1
w.SetScrollPos 1
w.SetScrollRange 1
w.SetStdHandle 1
w.SetTextAlign 1
w.SetTextColor 1
w.TextOut 1, 2
w.WriteConsole 1
w.WriteConsoleAttribute 1
w.WriteConsoleInput 1
w.WriteConsoleOutput 1
w.WriteConsoleOutputCharacter 1
welse clause in a while statement 1
WHILE loops 1
WHILE statement 1
WHILE..ENDWHILE 1
Win32 API Functions 1
Window classes 1, 2
Window origin 1
Window procedures 1
Windows coordinate systems 1
Windows Structured Exception Handler 1
WM_COPYDATA 1
WM_PAINT message 1
WndProc 1
WriteConsole 1
WriteConsoleAttribute 1
WriteConsoleInput 1
WriteConsoleOutput 1
WriteConsoleOutputCharacter 1
TOC PREV NEXT INDEX


Untitled Document

GIFLIB
v1.0
Introduction
Tutorial
Functions
Structures
Constants
About


Untitled Document

About

Introduction

GIFLIB is an easy to use library that enables you to use the GIF file format in your programs. It comes as a static library which can be linked to your program. GIFLIB is very small. This documentation is focussed on win32asm programming, if you want to use it with other languages, you'll have to convert the include files. GIFLIB is focussed on MASM users.

Specifications

Supported

  • 2, 4, 8, 16, 32, 64, 128 and 256 color gifs
  • Outputting data as 1-bit, 4-bit and 8-bit data
  • Global or local color tables
  • Exporting the color table to RGBQUAD format
  • Useful functions for implementing with the windows GDI

Not (yet) supported

  • Interlaced gifs
  • Non-image drawing extensions (plain text extension)
  • Graphic control extensions (transparency, etc)
  • All other things not mentioned under the supported section ;-)

More

  • GIFLIB is entirely written in assembly language (win32asm)
  • GIFLIB is very small (around 5kb)
  • Your programs will grow just about 2kb when using the library
  • GIFLIB is free!

Licensing

GIFLIB is © copyrighted by Exagone as freeware. This library is entirely free to use, you may use it in your programs and distribute your programs freely. Furthermore, the general public license apllies to this software. By using this software (documentation, library or source code), you agree to the terms in this license.

Additionally, the official GIF89a specification which applies for this library declares:

The Graphics Interchange Format(c) is the copyright property of CompuServe
Incorporated. Only CompuServe Incorporated is authorized to define, redefine,
enhance, alter, modify or change in any way the definition of the format.

CompuServe Incorporated hereby grants a limited, non-exclusive, royalty-free
license for the use of the Graphics Interchange Format(sm) in computer
software; computer software utilizing GIF(sm) must acknowledge ownership of the
Graphics Interchange Format and its Service Mark by CompuServe Incorporated, in
User and Technical Documentation. Computer software utilizing GIF, which is
distributed or may be distributed without User or Technical Documentation must
display to the screen or printer a message acknowledging ownership of the
Graphics Interchange Format and the Service Mark by CompuServe Incorporated; in
this case, the acknowledgement may be displayed in an opening screen or leading
banner, or a closing screen or trailing banner. A message such as the following
may be used:

"The Graphics Interchange Format(c) is the Copyright property of
CompuServe Incorporated. GIF(sm) is a Service Mark property of
CompuServe Incorporated."

For further information, please contact :

CompuServe Incorporated
Graphics Technology Department
5000 Arlington Center Boulevard
Columbus, Ohio 43220
U. S. A.


Untitled Document

Constants

Currently, there are only three constants:

Image output formats

IMAGEFORMAT_1BIT | 1-bit, monochrome image
IMAGEFORMAT_4BIT | 4-bit, 16 color image
IMAGEFORMAT_8BIT | 8-bit, 256 color image


Untitled Document

Functions

GIFLoadHeader

GIFLoadHeader lpGifInfo:DWORD

function

Initializes the GIF decoder. This function should be called once for each gif that is decoded, The GIF_INFO structure is prepared for using GIFDecompress.

parameters

lpGifInfo Pointer to a GIF_INFO Structure that defines the GIF to use. The lpGIFData member of the structure should point to the raw GIF image data, the dwGIFDataSize member is the size of the data pointed to by lpGIFData

return value

The function returns TRUE if successful, FALSE if it failed. The GIFVersion, dwLSWidth and dwLSHeight members of the GIF_INFO are filled by the structure.

GIFInitalizeDecoder

GIFInitalizeDecoder lpGifInfo:DWORD

function

Initializes the GIF decoder. This function should be called once for each gif that is decoded, The GIF_INFO structure is prepared for using GIFDecompress. Always use GIFLoadHeader before GIFInitializeDecoder

parameters

lpGifInfo Pointer to a GIF_INFO Structure that defines the GIF to use. The structure should be initialized by GIFLoadHeader

return values

Returns TRUE if successful, FALSE if it failed. The following members of the GIF_INFO structure are initialized by the function:

lpColorTable | Pointer to the GIF's color table (RR GG BB format)
ColorCount | Number of colors in the GIF image
dwImageWidth | Width of the GIF Image
dwImageHeight | Height of the GIF Image

GIFGetColorTable

GIFGetColorTable lpGifInfo:DWORD, lpBuffer:DWORD

function

Retrieves the colortable to a given buffer as an array of RGBQUAD structures. Use this function for compatibility with the windows GDI functions.

parameters

lpGifInfo | Pointer to a GIF_INFO Structure that defines the GIF to use. The structure should be initialized by GIFLoadHeader and GIFInitializeDecoder.
lpBuffer | Points to a buffer that receives the color table as an array of RGBQUADs

remarks

The size of lpBuffer can be found by multiplying the number of colors in the color table by the size of a RGBQUAD structure (=4 bytes):
SizeOf(lpBuffer) = GifInfo.ColorCount * 4

return value

This function does not return a value.

GIFGetMinimumColorType

GIFGetMinimumColorType lpGifInfo:DWORD

function

Retrieves the output color type that ensures the smallest possible output format.

parameters

lpGifInfo Pointer to a GIF_INFO Structure that defines the GIF to use. The structure should be initialized by GIFLoadHeader and GIFInitializeDecoder.

return value

This function returns IMAGEFORMAT_1BIT, IMAGEFORMAT_4BIT or IMAGEFORMAT_8BIT. You can use the return value as output format when decoding the GIF. GIFGetMinimumColorType ensures that this output format gives the smallest output data. You can always use a higher output format (i.e. higher nr of bits), lower is not supported.

GIFGetOutputSize

GIFGetOutputSize lpGifInfo:DWORD

function

Retrieves the number of bytes to reserve for the output data.

parameters

lpGifInfo Pointer to a GIF_INFO Structure that defines the GIF to use. The structure should be initialized by GIFLoadHeader and GIFInitializeDecoder.

return value

The return value indicates the number of bytes that should be allocated to fit the output data size.

GIFDecompress

GIFDecompress lpGifInfo:DWORD

function

This is the main decompression function. It decompresses the GIF Image to the buffer pointed to by the lpImageData member of the GIF_INFO structure, in the format defined by the dwImageFormat member.

parameters

lpGifInfo Pointer to a GIF_INFO Structure that defines the GIF to use. The structure should be initialized by GIFLoadHeader and GIFInitializeDecoder.

return value

The function returns TRUE if successful.

GIFCleanup

GIFCleanup lpGifInfo:DWORD

function

Cleans the memory that is used internally by GIFLIB. This function should be called after using the decoder.

parameters

lpGifInfo Pointer to a GIF_INFO Structure.

return value

This function does not return a value.

GIFFillBitmapInfoStruct

GIFFillBitmapInfoStruct lpGifInfo:DWORD, lpBitmapInfoHeader:DWORD

function

GIFFillBitmapInfoStruct fills a BITMAPINFOHEADER with the right values from a GIF_INFO Structure.

parameters

lpGifInfo | Pointer to a GIF_INFO Structure. The dwImageWidth, dwImageHeight and dwImageFormat of this structure should have the right values. These values are set by GIFInitializeDecoder.
lpBitmapInfoHeader | Pointer to a BITMAPINFOHEADER structure to be filled in.

return value

This function does not return a value.

GIFLoadResource

GIFLoadResource lpGifInfo:DWORD, hInstance:DWORD, lpName:DWORD

function

Loads a GIF in the form of a resource of the RT_RCDATA type, and sets the lpGIFData and dwGIFDataSize members of the GIF_INFO structure.

parameters

lpGifInfo | Pointer to a GIF_INFO Structure. The lpGIFData and dwGIFDataSize members of the structure are set by GIFLoadResource
lpBitmapInfoHeader | Identifies the module whose executable file contains the resource.
lpName | Specifies the name of the resource or, if the high word of the parameter is NULL, a resource ID.

return value

This function does not return a value.

GIFLoadFile

GIFLoadFile lpGifInfo:DWORD, lpFile:DWORD

function

Loads a GIF-File from harddisk for use with the library.
NOTE: The lpGIFData member of the GIF_INFO structure is set to a pointer to the data in the file. You should free this memory using GlobalFree if the data is not needed anymore.

parameters

lpGifInfo | Pointer to a GIF_INFO Structure. The lpGIFData and dwGIFDataSize members of the structure are set by GIFLoadFile.
lpFile | Points to a string that specifies the filename of the GIF to be loaded

return value

This function does not return a value.


Untitled Document

GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.

1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.

You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.

2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:

a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.

b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.

c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.

In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.

3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:

a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,

b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,

c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.

If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.

4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.

6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.

7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.

It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.

This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.

8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.

9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.

Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.

10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.

NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

END OF TERMS AND CONDITIONS


Untitled Document

GIFLIB v1.0

This is the documentation of GIFLIB, version 1.0. The menu on the left contains documentation for various parts of the library.
See about for more information about distributing and using the library.

(C) Copyright 2000 by Exagone. http://exagone.cjb.net.


Untitled Document

Structures

In version 1.0, there's only one structure: GIF_INFO. It is used by every function in the GIFLIB. You should have one structure for each GIF you decode.

GIF_INFO STRUCT
GIFVersion db 3 dup(?)
dwLSWidth dd ?
dwLSHeight dd ?
lpColorTable dd ?
ColorCount dd ?
dwImageWidth dd ?
dwImageHeight dd ?
lpImageData dd ?
dwImageFormat dd ?
lpGIFData dd ?
dwGIFDataSize dd ?
InternalData db 44 dup (?)
GIF_INFO ENDS

GIFVersion | These are the second 3 bytes of the file, that indicate the version number of the GIF File, like "87a" or "89a".
dwLSWidth | Logical screen width (see GIF format)
dwLSHeight | Logical screen height (see GIF format)
lpColorTable | Pointer to the GIF color table (RR GG BB RR GG BB format)
ColorCount | Number of colors in the GIF
dwImageWidth | Actual image width (use this one to get the image dimensions)
dwImageHeight | Actual image height (use this one to get the image dimensions)
lpImageData | Output buffer for decoded data (in the format specified by dwImageFormat). Data is outputted as windows bitmap data, in one of the following three formats: 1-bit monochrome, 4-bit 16-color or 8-bit 256 color. Scanlines are aligned to dwords (see the bitmap documentation in the win32 programmer's reference)
dwImageFormat | Specifies the image format (IMAGEFORMAT_XXXX constants). You can use a format that has a higher maximum number of colors than the original GIF, but lower is not supported (256 color output from a 16 color gif is allowed, 16 color output from a 256 color gif is NOT).
lpGIFData | Points to the actual GIF data (in the GIF file format).
dwGIFDataSize | Size of the data pointed to by lpGIFData
InternalData | Data that is used internally by GIFLIB.


Untitled Document

GIFLIB v1.0 Documentation


Untitled Document

Tutorial

This is a quick start guide to the library.

Step 1 - GIF Data

First of all, you will need to specify what GIF data to use. This is defined by the lpGIFData and dwGIFDataSize members of the GIF_INFO structure. Create a new GIF_INFO structure:

.data?
TestGif GIF_INFO <>

Now you will have to fill the lpGIFData and dwGIFDataSize members of this structure. Set the values manually to some pointer, use GIFLoadResource or GIFLoadFile:

;manually
push somepointertogifdata
pop TestGif.lpGIFData
push sizeofsomegifdata
pop TestGif.dwDataSize

;or with GIFLoadResource (loads GIF data from a resource)
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke GIFLoadResource, ADDR TestGif, hInstance, ID_GIFRESOURCE

;or width GIFLoadFile (loads GIF data from a file)
.data
GifFilename db "testimage.gif",0
.code
invoke GIFLoadFile, ADDR TestGif, ADDR GifFilename

Step 2 - Load header

Now load the GIF header width GIFLoadHeader

invoke GIFLoadHeader, ADDR TestGif

The GIFVersion, dwLSWidth and dwLSHeight members of TestGif are now set. This function should be called before using GIFInitializeDecoder or GIFDecompress.

Step 3 - Initialize decoder

Before using the decoder, you should initialize it:

invoke GIFInitializeDecoder, ADDR TestGif

The dwImageWidth, dwImageHeight, lpColorTable and ColorCount members of TestGif are set. This function should be called before using GIFDecompress.

Step 4 - Set image format etc.

You will now need to set an image format and an output buffer. The image format (dwImageFormat) can be IMAGEFORMAT_1BIT, IMAGEFORMAT_4BIT or IMAGEFORMAT_8BIT. A higher color format than the original gif may be used, lower is not allowed (see functions for more information). You can set the color format manually or use the minimum (i.e. the one that creates the smallest output) color format by using the return value of GIFGetMinimumColorType:

;manually
mov TestGif.dwImageFormat, IMAGEFORMAT_4BIT

;or automatically
invoke GIFGetMinimumColorType, ADDR TestGif
mov TestGif.dwImageFormat, eax

If you want to create a bitmap from it, use GIFFillBitmapInfoStruct to get a valid BITMAPINFOHEADER structure. Note that if you use a function like CreateDIBitmap, the color table should immediatly follow the BITMAPINFOHEADER structure (BITMAPINFO format).

.data?
BitmapInfoHeader BITMAPINFOHEADER <>
.code
invoke GIFFillBitmapInfoStruct, ADDR TestGif, ADDR BitmapInfoHeader

Now set the lpImageData member to a valid pointer to a buffer that's big enough to store the bitmap image (If you are using CreateDIBitmap, this buffer is created by windows for you). You can get the nr of bytes needed with GIFGetOutputSize:

invoke GIFGetOutputSize, ADDR TestGif
; ......
; create a buffer with size eax
; ......
mov TestGif.lpImageData, {pointer to buffer}

Step 5 - Decompress

GIFDecompress will decode the GIF with all the given parameters in the GIF_INFO structure:

invoke GIFDecompress, ADDR TestGif

Step 6- Cleanup

As the final step, cleanup the allocated memory:

invoke GIFCleanup, ADDR TestGif


ASM Mode Reel

    Cover Sheet for the GIF89a Specification

    DEFERRED CLEAR CODE IN LZW COMPRESSION

    There has been confusion about where clear codes can be found in the
    data stream.  As the specification says, they may appear at anytime.  There
    is not a requirement to send a clear code when the string table is full.

    It is the encoder's decision as to when the table should be cleared.  When
    the table is full, the encoder can chose to use the table as is, making no
    changes to it until the encoder chooses to clear it.  The encoder during
    this time sends out codes that are of the maximum Code Size.

    As we can see from the above, when the decoder's table is full, it must
    not change the table until a clear code is received.  The Code Size is that
    of the maximum Code Size.  Processing other than this is done normally.

    Because of a large base of decoders that do not handle the decompression in
    this manner, we ask developers of GIF encoding software to NOT implement
    this feature until at least January 1991 and later if they see that their
    particular market is not ready for it.  This will give developers of GIF
    decoding software time to implement this feature and to get it into the
    hands of their clients before the decoders start "breaking" on the new
    GIF's.  It is not required that encoders change their software to take
    advantage of the deferred clear code, but it is for decoders.

    APPLICATION EXTENSION BLOCK - APPLICATION IDENTIFIER

    There will be a Courtesy Directory file located on CompuServe in the PICS
    forum.  This directory will contain Application Identifiers for Application
    Extension Blocks that have been used by developers of GIF applications.
    This file is intended to help keep developers that wish to create
    Application Extension Blocks from using the same Application Identifiers.
    This is not an official directory; it is for voluntary participation only
    and does not guarantee that someone will not use the same identifier.

    E-Mail can be sent to Larry Wood (forum manager of PICS) indicating the
    request for inclusion in this file with an identifier.

                        GRAPHICS INTERCHANGE FORMAT(sm)

                                  Version 89a

                            (c)1987,1988,1989,1990

                                   Copyright
                            CompuServe Incorporated
                                Columbus, Ohio

CompuServe Incorporated                           Graphics Interchange Format
Document Date : 31 July 1990                            Programming Reference

                               Table of Contents

Disclaimer.................................................................  1

Foreword...................................................................  1

Licensing..................................................................  1

About the Document.........................................................  2

General Description........................................................  2

Version Numbers............................................................  2

The Encoder................................................................  3

The Decoder................................................................  3

Compliance.................................................................  3

About Recommendations......................................................  4

About Color Tables.........................................................  4

Blocks, Extensions and Scope...............................................  4

Block Sizes................................................................  5

Using GIF as an embedded protocol..........................................  5

Data Sub-blocks............................................................  5

Block Terminator...........................................................  6

Header.....................................................................  7

Logical Screen Descriptor..................................................  8

Global Color Table......................................................... 10

Image Descriptor........................................................... 11

Local Color Table.......................................................... 13

Table Based Image Data..................................................... 14

Graphic Control Extension.................................................. 15

Comment Extension.......................................................... 17

Plain Text Extension....................................................... 18

Application Extension...................................................... 21

Trailer.................................................................... 23

Quick Reference Table...................................................... 24

GIF Grammar................................................................ 25

Glossary................................................................... 27

Conventions................................................................ 28

Interlaced Images.......................................................... 29

Variable-Length-Code LZW Compression....................................... 30

On-line Capabilities Dialogue.............................................. 33
                                                                                1

1. Disclaimer.

The information provided herein is subject to change without notice. In no
event will CompuServe Incorporated be liable for damages, including any loss of
revenue, loss of profits or other incidental or consequential damages arising
out of the use or inability to use the information; CompuServe Incorporated
makes no claim as to the suitability of the information.

2. Foreword.

This document defines the Graphics Interchange Format(sm). The specification
given here defines version 89a, which is an extension of version 87a.

The Graphics Interchange Format(sm) as specified here should be considered
complete; any deviation from it should be considered invalid, including but not
limited to, the use of reserved or undefined fields within control or data
blocks, the inclusion of extraneous data within or between blocks, the use of
methods or algorithms not specifically listed as part of the format, etc. In
general, any and all deviations, extensions or modifications not specified in
this document should be considered to be in violation of the format and should
be avoided.

3. Licensing.

The Graphics Interchange Format(c) is the copyright property of CompuServe
Incorporated. Only CompuServe Incorporated is authorized to define, redefine,
enhance, alter, modify or change in any way the definition of the format.

CompuServe Incorporated hereby grants a limited, non-exclusive, royalty-free
license for the use of the Graphics Interchange Format(sm) in computer
software; computer software utilizing GIF(sm) must acknowledge ownership of the
Graphics Interchange Format and its Service Mark by CompuServe Incorporated, in
User and Technical Documentation. Computer software utilizing GIF, which is
distributed or may be distributed without User or Technical Documentation must
display to the screen or printer a message acknowledging ownership of the
Graphics Interchange Format and the Service Mark by CompuServe Incorporated; in
this case, the acknowledgement may be displayed in an opening screen or leading
banner, or a closing screen or trailing banner. A message such as the following
may be used:

      "The Graphics Interchange Format(c) is the Copyright property of
      CompuServe Incorporated. GIF(sm) is a Service Mark property of
      CompuServe Incorporated."

For further information, please contact :

      CompuServe Incorporated
      Graphics Technology Department
      5000 Arlington Center Boulevard
      Columbus, Ohio  43220
      U. S. A.

CompuServe Incorporated maintains a mailing list with all those individuals and
organizations who wish to receive copies of this document when it is corrected
                                                                         2
or revised. This service is offered free of charge; please provide us with your
mailing address.

4. About the Document.

This document describes in detail the definition of the Graphics Interchange
Format.  This document is intended as a programming reference; it is
recommended that the entire document be read carefully before programming,
because of the interdependence of the various parts. There is an individual
section for each of the Format blocks. Within each section, the sub-section
labeled Required Version refers to the version number that an encoder will have
to use if the corresponding block is used in the Data Stream. Within each
section, a diagram describes the individual fields in the block; the diagrams
are drawn vertically; top bytes in the diagram appear first in the Data Stream.
Bits within a byte are drawn most significant on the left end.  Multi-byte
numeric fields are ordered Least Significant Byte first. Numeric constants are
represented as Hexadecimal numbers, preceded by "0x".  Bit fields within a byte
are described in order from most significant bits to least significant bits.

5. General Description.

The Graphics Interchange Format(sm) defines a protocol intended for the on-line
transmission and interchange of raster graphic data in a way that is
independent of the hardware used in their creation or display.

The Graphics Interchange Format is defined in terms of blocks and sub-blocks
which contain relevant parameters and data used in the reproduction of a
graphic. A GIF Data Stream is a sequence of protocol blocks and sub-blocks
representing a collection of graphics. In general, the graphics in a Data
Stream are assumed to be related to some degree, and to share some control
information; it is recommended that encoders attempt to group together related
graphics in order to minimize hardware changes during processing and to
minimize control information overhead. For the same reason, unrelated graphics
or graphics which require resetting hardware parameters should be encoded
separately to the extent possible.

A Data Stream may originate locally, as when read from a file, or it may
originate remotely, as when transmitted over a data communications line. The
Format is defined with the assumption that an error-free Transport Level
Protocol is used for communications; the Format makes no provisions for
error-detection and error-correction.

The GIF Data Stream must be interpreted in context, that is, the application
program must rely on information external to the Data Stream to invoke the
decoder process.

6. Version Numbers.

The version number in the Header of a Data Stream is intended to identify the
minimum set of capabilities required of a decoder in order to fully process the
Data Stream.  An encoder should use the earliest possible version number that
includes all the blocks used in the Data Stream. Within each block section in
this document, there is an entry labeled Required Version which specifies the
                                                                                3
earliest version number that includes the corresponding block.  The encoder
should make every attempt to use the earliest version number covering all the
blocks in the Data Stream; the unnecessary use of later version numbers will
hinder processing by some decoders.

7. The Encoder.

The Encoder is the program used to create a GIF Data Stream. From raster data
and other information, the encoder produces the necessary control and data
blocks needed for reproducing the original graphics.

The encoder has the following primary responsibilities.

            - Include in the Data Stream all the necessary information to
            reproduce  the graphics.

            - Insure that a Data Stream is labeled with the earliest possible
            Version Number that will cover the definition of all the blocks in
            it; this is to ensure that the largest number of decoders can
            process the Data Stream.

            - Ensure encoding of the graphics in such a way that the decoding
            process is optimized. Avoid redundant information as much as
            possible.

            - To the extent possible, avoid grouping graphics which might
            require resetting hardware parameters during the decoding process.

            - Set to zero (off) each of the bits of each and every field
            designated as reserved. Note that some fields in the Logical Screen
            Descriptor and the Image Descriptor were reserved under Version
            87a, but are used under version 89a.

8. The Decoder.

The Decoder is the program used to process a GIF Data Stream. It processes the
Data Stream sequentially, parsing the various blocks and sub-blocks, using the
control information to set hardware and process parameters and interpreting the
data to render the graphics.

The decoder has the following primary responsibilities.

            - Process each graphic in the Data Stream in sequence, without
            delays other than those specified in the control information.

            - Set its hardware parameters to fit, as closely as possible, the
            control information contained in the Data Stream.

9. Compliance.

An encoder or a decoder is said to comply with a given version of the Graphics
Interchange Format if and only if it fully conforms with and correctly
implements the definition of the standard associated with that version.  An
                                                                         4

encoder or a decoder may be compliant with a given version number and not
compliant with some subsequent version.

10. About Recommendations.

Each block section in this document contains an entry labeled Recommendation;
this section lists a set of recommendations intended to guide and organize the
use of the particular blocks. Such recommendations are geared towards making
the functions of encoders and decoders more efficient, as well as making
optimal use of the communications bandwidth.  It is advised that these
recommendations be followed.

11. About Color Tables.

The GIF format utilizes color tables to render raster-based graphics. A color
table can have one of two different scopes: global or local. A Global Color
Table is used by all those graphics in the Data Stream which do not have a
Local Color Table associated with them. The scope of the Global Color Table is
the entire Data Stream. A Local Color Table is always associated with the
graphic that immediately follows it; the scope of a Local Color Table is
limited to that single graphic. A Local Color Table supersedes a Global Color
Table, that is, if a Data Stream contains a Global Color Table, and an image
has a Local Color Table associated with it, the decoder must save the Global
Color Table, use the Local Color Table to render the image, and then restore
the Global Color Table. Both types of color tables are optional, making it
possible for a Data Stream to contain numerous graphics without a color table
at all. For this reason, it is recommended that the decoder save the last
Global Color Table used until another Global Color Table is encountered. In
this way, a Data Stream which does not contain either a Global Color Table or
a Local Color Table may be processed using the last Global Color Table saved.
If a Global Color Table from a previous Stream is used, that table becomes the
Global Color Table of the present Stream. This is intended to reduce the
overhead incurred by color tables. In particular, it is recommended that an
encoder use only one Global Color Table if all the images in related Data
Streams can be rendered with the same table.  If no color table is available at
all, the decoder is free to use a system color table or a table of its own. In
that case, the decoder may use a color table with as many colors as its
hardware is able to support; it is recommended that such a table have black and
white as its first two entries, so that monochrome images can be rendered
adequately.

The Definition of the GIF Format allows for a Data Stream to contain only the
Header, the Logical Screen Descriptor, a Global Color Table and the GIF
Trailer. Such a Data Stream would be used to load a decoder with a Global Color
Table, in preparation for subsequent Data Streams without a color table at all.

12. Blocks, Extensions and Scope.

Blocks can be classified into three groups : Control, Graphic-Rendering and
Special Purpose.  Control blocks, such as the Header, the Logical Screen
Descriptor, the Graphic Control Extension and the Trailer, contain information
used to control the process of the Data Stream or information  used in setting
hardware parameters.  Graphic-Rendering blocks such as the Image Descriptor and
                                                                                5
the Plain Text Extension contain information and data used to render a graphic
on the display device. Special Purpose blocks such as the Comment Extension and
the Application Extension are neither used to control the process of the Data
Stream nor do they contain information or data used to render a graphic on the
display device. With the exception of the Logical Screen Descriptor and the
Global Color Table, whose scope is the entire Data Stream, all other Control
blocks have a limited scope, restricted to the Graphic-Rendering block that
follows them.  Special Purpose blocks do not delimit the scope of any Control
blocks; Special Purpose blocks are transparent to the decoding process.
Graphic-Rendering blocks and extensions are used as scope delimiters for
Control blocks and extensions. The labels used to identify labeled blocks fall
into three ranges : 0x00-0x7F (0-127) are the Graphic Rendering blocks,
excluding the Trailer (0x3B); 0x80-0xF9 (128-249) are the Control blocks;
0xFA-0xFF (250-255) are the Special Purpose blocks. These ranges are defined so
that decoders can handle block scope by appropriately identifying block labels,
even when the block itself cannot be processed.

13. Block Sizes.

The Block Size field in a block, counts the number of bytes remaining in the
block, not counting the Block Size field itself, and not counting the Block
Terminator, if one is to follow.  Blocks other than Data Blocks are intended to
be of fixed length; the Block Size field is provided in order to facilitate
skipping them, not to allow their size to change in the future.  Data blocks
and sub-blocks are of variable length to accommodate the amount of data.

14. Using GIF as an embedded protocol.

As an embedded protocol, GIF may be part of larger application protocols,
within which GIF is used to render graphics.  In such a case, the application
protocol could define a block within which the GIF Data Stream would be
contained. The application program would then invoke a GIF decoder upon
encountering a block of type GIF.  This approach is recommended in favor of
using Application Extensions, which become overhead for all other applications
that do not process them. Because a GIF Data Stream must be processed in
context, the application must rely on some means of identifying the GIF Data
Stream outside of the Stream itself.

15. Data Sub-blocks.

      a. Description. Data Sub-blocks are units containing data. They do not
      have a label, these blocks are processed in the context of control
      blocks, wherever data blocks are specified in the format. The first byte
      of the Data sub-block indicates the number of data bytes to follow. A
      data sub-block may contain from 0 to 255 data bytes. The size of the
      block does not account for the size byte itself, therefore, the empty
      sub-block is one whose size field contains 0x00.

      b. Required Version.  87a.
                                                                         6
      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       Block Size                    Byte
     +---------------+
  1  |               |
     +-             -+
  2  |               |
     +-             -+
  3  |               |
     +-             -+
     |               |       Data Values                   Byte
     +-             -+
 up  |               |
     +-   . . . .   -+
 to  |               |
     +-             -+
     |               |
     +-             -+
255  |               |
     +---------------+

            i) Block Size - Number of bytes in the Data Sub-block; the size
            must be within 0 and 255 bytes, inclusive.

            ii) Data Values - Any 8-bit value. There must be exactly as many
            Data Values as specified by the Block Size field.

      d. Extensions and Scope. This type of block always occurs as part of a
      larger unit. It does not have a scope of itself.

      e. Recommendation. None.

16. Block Terminator.

      a. Description. This zero-length Data Sub-block is used to terminate a
      sequence of Data Sub-blocks. It contains a single byte in the position of
      the Block Size field and does not contain data.

      b. Required Version.  87a.

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       Block Size                    Byte
     +---------------+

            i) Block Size - Number of bytes in the Data Sub-block; this field
            contains the fixed value 0x00.

            ii) Data Values - This block does not contain any data.
                                                                                7
      d. Extensions and Scope. This block terminates the immediately preceding
      sequence of Data Sub-blocks. This block cannot be modified by any
      extension.

      e. Recommendation. None.

17. Header.

      a. Description. The Header identifies the GIF Data Stream in context. The
      Signature field marks the beginning of the Data Stream, and the Version
      field identifies the set of capabilities required of a decoder to fully
      process the Data Stream.  This block is REQUIRED; exactly one Header must
      be present per Data Stream.

      b. Required Version.  Not applicable. This block is not subject to a
      version number. This block must appear at the beginning of every Data
      Stream.

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
   0 |               |       Signature                     3 Bytes
     +-             -+
   1 |               |
     +-             -+
   2 |               |
     +---------------+
   3 |               |       Version                       3 Bytes
     +-             -+
   4 |               |
     +-             -+
   5 |               |
     +---------------+

            i) Signature - Identifies the GIF Data Stream. This field contains
            the fixed value 'GIF'.

            ii) Version - Version number used to format the data stream.
            Identifies the minimum set of capabilities necessary to a decoder
            to fully process the contents of the Data Stream.

            Version Numbers as of 10 July 1990 :       "87a" - May 1987
                                                       "89a" - July 1989

            Version numbers are ordered numerically increasing on the first two
            digits starting with 87 (87,88,...,99,00,...,85,86) and
            alphabetically increasing on the third character (a,...,z).

            iii) Extensions and Scope. The scope of this block is the entire
            Data Stream. This block cannot be modified by any extension.
                                                                         8
        d. Recommendations.

            i) Signature - This field identifies the beginning of the GIF Data
            Stream; it is not intended to provide a unique signature for the
            identification of the data. It is recommended that the GIF Data
            Stream be identified externally by the application. (Refer to
            Appendix G for on-line identification of the GIF Data Stream.)

            ii) Version - ENCODER : An encoder should use the earliest possible
            version number that defines all the blocks used in the Data Stream.
            When two or more Data Streams are combined, the latest of the
            individual version numbers should be used for the resulting Data
            Stream. DECODER : A decoder should attempt to process the data
            stream to the best of its ability; if it encounters a version
            number which it is not capable of processing fully, it should
            nevertheless, attempt to process the data stream to the best of its
            ability, perhaps after warning the user that the data may be
            incomplete.

18. Logical Screen Descriptor.

      a. Description.  The Logical Screen Descriptor contains the parameters
      necessary to define the area of the display device within which the
      images will be rendered.  The coordinates in this block are given with
      respect to the top-left corner of the virtual screen; they do not
      necessarily refer to absolute coordinates on the display device.  This
      implies that they could refer to window coordinates in a window-based
      environment or printer coordinates when a printer is used.

      This block is REQUIRED; exactly one Logical Screen Descriptor must be
      present per Data Stream.

      b. Required Version.  Not applicable. This block is not subject to a
      version number. This block must appear immediately after the Header.

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       Logical Screen Width          Unsigned
     +-             -+
  1  |               |
     +---------------+
  2  |               |       Logical Screen Height         Unsigned
     +-             -+
  3  |               |
     +---------------+
  4  | |     | |     |                      See below
     +---------------+
  5  |               |       Background Color Index        Byte
     +---------------+
  6  |               |       Pixel Aspect Ratio            Byte
     +---------------+

                                                                         9

       =      Global Color Table Flag       1 Bit
                             Color Resolution              3 Bits
                             Sort Flag                     1 Bit
                             Size of Global Color Table    3 Bits

            i) Logical Screen Width - Width, in pixels, of the Logical Screen
            where the images will be rendered in the displaying device.

            ii) Logical Screen Height - Height, in pixels, of the Logical
            Screen where the images will be rendered in the displaying device.

            iii) Global Color Table Flag - Flag indicating the presence of a
            Global Color Table; if the flag is set, the Global Color Table will
            immediately follow the Logical Screen Descriptor. This flag also
            selects the interpretation of the Background Color Index; if the
            flag is set, the value of the Background Color Index field should
            be used as the table index of the background color. (This field is
            the most significant bit of the byte.)

            Values :    0 -   No Global Color Table follows, the Background
                              Color Index field is meaningless.
                        1 -   A Global Color Table will immediately follow, the
                              Background Color Index field is meaningful.

            iv) Color Resolution - Number of bits per primary color available
            to the original image, minus 1. This value represents the size of
            the entire palette from which the colors in the graphic were
            selected, not the number of colors actually used in the graphic.
            For example, if the value in this field is 3, then the palette of
            the original image had 4 bits per primary color available to create
            the image.  This value should be set to indicate the richness of
            the original palette, even if not every color from the whole
            palette is available on the source machine.

            v) Sort Flag - Indicates whether the Global Color Table is sorted.
            If the flag is set, the Global Color Table is sorted, in order of
            decreasing importance. Typically, the order would be decreasing
            frequency, with most frequent color first. This assists a decoder,
            with fewer available colors, in choosing the best subset of colors;
            the decoder may use an initial segment of the table to render the
            graphic.

            Values :    0 -   Not ordered.
                        1 -   Ordered by decreasing importance, most
                              important color first.

            vi) Size of Global Color Table - If the Global Color Table Flag is
            set to 1, the value in this field is used to calculate the number
            of bytes contained in the Global Color Table. To determine that
            actual size of the color table, raise 2 to [the value of the field
            + 1].  Even if there is no Global Color Table specified, set this
            field according to the above formula so that decoders can choose
            the best graphics mode to display the stream in.  (This field is
            made up of the 3 least significant bits of the byte.)

            vii) Background Color Index - Index into the Global Color Table for

                                                                        10

            the Background Color. The Background Color is the color used for
            those pixels on the screen that are not covered by an image. If the
            Global Color Table Flag is set to (zero), this field should be zero
            and should be ignored.

            viii) Pixel Aspect Ratio - Factor used to compute an approximation
            of the aspect ratio of the pixel in the original image.  If the
            value of the field is not 0, this approximation of the aspect ratio
            is computed based on the formula:

            Aspect Ratio = (Pixel Aspect Ratio + 15) / 64

            The Pixel Aspect Ratio is defined to be the quotient of the pixel's
            width over its height.  The value range in this field allows
            specification of the widest pixel of 4:1 to the tallest pixel of
            1:4 in increments of 1/64th.

            Values :        0 -   No aspect ratio information is given.
                       1..255 -   Value used in the computation.

      d. Extensions and Scope. The scope of this block is the entire Data
      Stream. This block cannot be modified by any extension.

      e. Recommendations. None.

19. Global Color Table.

      a. Description. This block contains a color table, which is a sequence of
      bytes representing red-green-blue color triplets. The Global Color Table
      is used by images without a Local Color Table and by Plain Text
      Extensions. Its presence is marked by the Global Color Table Flag being
      set to 1 in the Logical Screen Descriptor; if present, it immediately
      follows the Logical Screen Descriptor and contains a number of bytes
      equal to
                    3 x 2^(Size of Global Color Table+1).

      This block is OPTIONAL; at most one Global Color Table may be present
      per Data Stream.

      b. Required Version.  87a

                                                                        11

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +===============+
  0  |               |       Red 0                         Byte
     +-             -+
  1  |               |       Green 0                       Byte
     +-             -+
  2  |               |       Blue 0                        Byte
     +-             -+
  3  |               |       Red 1                         Byte
     +-             -+
     |               |       Green 1                       Byte
     +-             -+
 up  |               |
     +-   . . . .   -+       ...
 to  |               |
     +-             -+
     |               |       Green 255                     Byte
     +-             -+
767  |               |       Blue 255                      Byte
     +===============+

      d. Extensions and Scope. The scope of this block is the entire Data
      Stream. This block cannot be modified by any extension.

      e. Recommendation. None.

20. Image Descriptor.

      a. Description. Each image in the Data Stream is composed of an Image
      Descriptor, an optional Local Color Table, and the image data.  Each
      image must fit within the boundaries of the Logical Screen, as defined
      in the Logical Screen Descriptor.

      The Image Descriptor contains the parameters necessary to process a table
      based image. The coordinates given in this block refer to coordinates
      within the Logical Screen, and are given in pixels. This block is a
      Graphic-Rendering Block, optionally preceded by one or more Control
      blocks such as the Graphic Control Extension, and may be optionally
      followed by a Local Color Table; the Image Descriptor is always followed
      by the image data.

      This block is REQUIRED for an image.  Exactly one Image Descriptor must
      be present per image in the Data Stream.  An unlimited number of images
      may be present per Data Stream.

      b. Required Version.  87a.

                                                                        12

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       Image Separator               Byte
     +---------------+
  1  |               |       Image Left Position           Unsigned
     +-             -+
  2  |               |
     +---------------+
  3  |               |       Image Top Position            Unsigned
     +-             -+
  4  |               |
     +---------------+
  5  |               |       Image Width                   Unsigned
     +-             -+
  6  |               |
     +---------------+
  7  |               |       Image Height                  Unsigned
     +-             -+
  8  |               |
     +---------------+
  9  | | | |   |     |                      See below
     +---------------+

       =      Local Color Table Flag        1 Bit
                             Interlace Flag                1 Bit
                             Sort Flag                     1 Bit
                             Reserved                      2 Bits
                             Size of Local Color Table     3 Bits

           i) Image Separator - Identifies the beginning of an Image
           Descriptor. This field contains the fixed value 0x2C.

           ii) Image Left Position - Column number, in pixels, of the left edge
           of the image, with respect to the left edge of the Logical Screen.
           Leftmost column of the Logical Screen is 0.

           iii) Image Top Position - Row number, in pixels, of the top edge of
           the image with respect to the top edge of the Logical Screen. Top
           row of the Logical Screen is 0.

           iv) Image Width - Width of the image in pixels.

           v) Image Height - Height of the image in pixels.

           vi) Local Color Table Flag - Indicates the presence of a Local Color
           Table immediately following this Image Descriptor. (This field is
           the most significant bit of the byte.)

           Values :    0 -   Local Color Table is not present. Use
                             Global Color Table if available.
                       1 -   Local Color Table present, and to follow
                             immediately after this Image Descriptor.

                                                                        13

           vii) Interlace Flag - Indicates if the image is interlaced. An image
           is interlaced in a four-pass interlace pattern; see Appendix E for
           details.

           Values :    0 - Image is not interlaced.
                       1 - Image is interlaced.

            viii) Sort Flag - Indicates whether the Local Color Table is
            sorted.  If the flag is set, the Local Color Table is sorted, in
            order of decreasing importance. Typically, the order would be
            decreasing frequency, with most frequent color first. This assists
            a decoder, with fewer available colors, in choosing the best subset
            of colors; the decoder may use an initial segment of the table to
            render the graphic.

            Values :    0 -   Not ordered.
                        1 -   Ordered by decreasing importance, most
                              important color first.

            ix) Size of Local Color Table - If the Local Color Table Flag is
            set to 1, the value in this field is used to calculate the number
            of bytes contained in the Local Color Table. To determine that
            actual size of the color table, raise 2 to the value of the field
            + 1. This value should be 0 if there is no Local Color Table
            specified. (This field is made up of the 3 least significant bits
            of the byte.)

     d. Extensions and Scope. The scope of this block is the Table-based Image
     Data Block that follows it. This block may be modified by the Graphic
     Control Extension.

     e. Recommendation. None.

21. Local Color Table.

     a. Description. This block contains a color table, which is a sequence of
     bytes representing red-green-blue color triplets. The Local Color Table
     is used by the image that immediately follows. Its presence is marked by
     the Local Color Table Flag being set to 1 in the Image Descriptor; if
     present, the Local Color Table immediately follows the Image Descriptor
     and contains a number of bytes equal to
                          3x2^(Size of Local Color Table+1).
     If present, this color table temporarily becomes the active color table
     and the following image should be processed using it. This block is
     OPTIONAL; at most one Local Color Table may be present per Image
     Descriptor and its scope is the single image associated with the Image
     Descriptor that precedes it.

     b. Required Version.  87a.

                                                                        14

     c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +===============+
  0  |               |       Red 0                         Byte
     +-             -+
  1  |               |       Green 0                       Byte
     +-             -+
  2  |               |       Blue 0                        Byte
     +-             -+
  3  |               |       Red 1                         Byte
     +-             -+
     |               |       Green 1                       Byte
     +-             -+
 up  |               |
     +-   . . . .   -+       ...
 to  |               |
     +-             -+
     |               |       Green 255                     Byte
     +-             -+
767  |               |       Blue 255                      Byte
     +===============+

     d. Extensions and Scope. The scope of this block is the Table-based Image
     Data Block that immediately follows it. This block cannot be modified by
     any extension.

     e. Recommendations. None.

22. Table Based Image Data.

     a. Description. The image data for a table based image consists of a
     sequence of sub-blocks, of size at most 255 bytes each, containing an
     index into the active color table, for each pixel in the image.  Pixel
     indices are in order of left to right and from top to bottom.  Each index
     must be within the range of the size of the active color table, starting
     at 0. The sequence of indices is encoded using the LZW Algorithm with
     variable-length code, as described in Appendix F

     b. Required Version.  87a.

     c. Syntax. The image data format is as follows:

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
     |               |       LZW Minimum Code Size         Byte
     +---------------+

     +===============+
     |               |
     /               /       Image Data                    Data Sub-blocks
     |               |
     +===============+

                                                                        15

            i) LZW Minimum Code Size.  This byte determines the initial number
            of bits used for LZW codes in the image data, as described in
            Appendix F.

     d. Extensions and Scope. This block has no scope, it contains raster
     data. Extensions intended to modify a Table-based image must appear
     before the corresponding Image Descriptor.

     e. Recommendations. None.

23. Graphic Control Extension.

      a. Description. The Graphic Control Extension contains parameters used
      when processing a graphic rendering block. The scope of this extension is
      the first graphic rendering block to follow. The extension contains only
      one data sub-block.

      This block is OPTIONAL; at most one Graphic Control Extension may precede
      a graphic rendering block. This is the only limit to the number of
      Graphic Control Extensions that may be contained in a Data Stream.

      b. Required Version.  89a.

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       Extension Introducer          Byte
     +---------------+
  1  |               |       Graphic Control Label         Byte
     +---------------+

     +---------------+
  0  |               |       Block Size                    Byte
     +---------------+
  1  |     |     | | |                      See below
     +---------------+
  2  |               |       Delay Time                    Unsigned
     +-             -+
  3  |               |
     +---------------+
  4  |               |       Transparent Color Index       Byte
     +---------------+

     +---------------+
  0  |               |       Block Terminator              Byte
     +---------------+

        =     Reserved                      3 Bits
                             Disposal Method               3 Bits
                             User Input Flag               1 Bit
                             Transparent Color Flag        1 Bit

            i) Extension Introducer - Identifies the beginning of an extension

                                                                        16

            block. This field contains the fixed value 0x21.

            ii) Graphic Control Label - Identifies the current block as a
            Graphic Control Extension. This field contains the fixed value
            0xF9.

            iii) Block Size - Number of bytes in the block, after the Block
            Size field and up to but not including the Block Terminator.  This
            field contains the fixed value 4.

            iv) Disposal Method - Indicates the way in which the graphic is to
            be treated after being displayed.

            Values :    0 -   No disposal specified. The decoder is
                              not required to take any action.
                        1 -   Do not dispose. The graphic is to be left
                              in place.
                        2 -   Restore to background color. The area used by the
                              graphic must be restored to the background color.
                        3 -   Restore to previous. The decoder is required to
                              restore the area overwritten by the graphic with
                              what was there prior to rendering the graphic.
                     4-7 -    To be defined.

            v) User Input Flag - Indicates whether or not user input is
            expected before continuing. If the flag is set, processing will
            continue when user input is entered. The nature of the User input
            is determined by the application (Carriage Return, Mouse Button
            Click, etc.).

            Values :    0 -   User input is not expected.
                        1 -   User input is expected.

            When a Delay Time is used and the User Input Flag is set,
            processing will continue when user input is received or when the
            delay time expires, whichever occurs first.

            vi) Transparency Flag - Indicates whether a transparency index is
            given in the Transparent Index field. (This field is the least
            significant bit of the byte.)

            Values :    0 -   Transparent Index is not given.
                        1 -   Transparent Index is given.

            vii) Delay Time - If not 0, this field specifies the number of
            hundredths (1/100) of a second to wait before continuing with the
            processing of the Data Stream. The clock starts ticking immediately
            after the graphic is rendered. This field may be used in
            conjunction with the User Input Flag field.

            viii) Transparency Index - The Transparency Index is such that when
            encountered, the corresponding pixel of the display device is not
            modified and processing goes on to the next pixel. The index is
            present if and only if the Transparency Flag is set to 1.

            ix) Block Terminator - This zero-length data block marks the end of

                                                                        17

            the Graphic Control Extension.

      d. Extensions and Scope. The scope of this Extension is the graphic
      rendering block that follows it; it is possible for other extensions to
      be present between this block and its target. This block can modify the
      Image Descriptor Block and the Plain Text Extension.

      e. Recommendations.

            i) Disposal Method - The mode Restore To Previous is intended to be
            used in small sections of the graphic; the use of this mode imposes
            severe demands on the decoder to store the section of the graphic
            that needs to be saved. For this reason, this mode should be used
            sparingly.  This mode is not intended to save an entire graphic or
            large areas of a graphic; when this is the case, the encoder should
            make every attempt to make the sections of the graphic to be
            restored be separate graphics in the data stream. In the case where
            a decoder is not capable of saving an area of a graphic marked as
            Restore To Previous, it is recommended that a decoder restore to
            the background color.

            ii) User Input Flag - When the flag is set, indicating that user
            input is expected, the decoder may sound the bell (0x07) to alert
            the user that input is being expected.  In the absence of a
            specified Delay Time, the decoder should wait for user input
            indefinitely.  It is recommended that the encoder not set the User
            Input Flag without a Delay Time specified.

24. Comment Extension.

      a. Description. The Comment Extension contains textual information which
      is not part of the actual graphics in the GIF Data Stream. It is suitable
      for including comments about the graphics, credits, descriptions or any
      other type of non-control and non-graphic data.  The Comment Extension
      may be ignored by the decoder, or it may be saved for later processing;
      under no circumstances should a Comment Extension disrupt or interfere
      with the processing of the Data Stream.

      This block is OPTIONAL; any number of them may appear in the Data Stream.

      b. Required Version.  89a.

                                                                        18

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       Extension Introducer          Byte
     +---------------+
  1  |               |       Comment Label                 Byte
     +---------------+

     +===============+
     |               |
  N  |               |       Comment Data                  Data Sub-blocks
     |               |
     +===============+

     +---------------+
  0  |               |       Block Terminator              Byte
     +---------------+

            i) Extension Introducer - Identifies the beginning of an extension
            block. This field contains the fixed value 0x21.

            ii) Comment Label - Identifies the block as a Comment Extension.
            This field contains the fixed value 0xFE.

            iii) Comment Data - Sequence of sub-blocks, each of size at most
            255 bytes and at least 1 byte, with the size in a byte preceding
            the data.  The end of the sequence is marked by the Block
            Terminator.

            iv) Block Terminator - This zero-length data block marks the end of
            the Comment Extension.

      d. Extensions and Scope. This block does not have scope. This block
      cannot be modified by any extension.

      e. Recommendations.

            i) Data - This block is intended for humans.  It should contain
            text using the 7-bit ASCII character set. This block should
            not be used to store control information for custom processing.

            ii) Position - This block may appear at any point in the Data
            Stream at which a block can begin; however, it is recommended that
            Comment Extensions do not interfere with Control or Data blocks;
            they should be located at the beginning or at the end of the Data
            Stream to the extent possible.

25. Plain Text Extension.

      a. Description. The Plain Text Extension contains textual data and the
      parameters necessary to render that data as a graphic, in a simple form.
      The textual data will be encoded with the 7-bit printable ASCII
      characters.  Text data are rendered using a grid of character cells

                                                                        19

      defined by the parameters in the block fields. Each character is rendered
      in an individual cell. The textual data in this block is to be rendered
      as mono-spaced characters, one character per cell, with a best fitting
      font and size. For further information, see the section on
      Recommendations below. The data characters are taken sequentially from
      the data portion of the block and rendered within a cell, starting with
      the upper left cell in the grid and proceeding from left to right and
      from top to bottom. Text data is rendered until the end of data is
      reached or the character grid is filled.  The Character Grid contains an
      integral number of cells; in the case that the cell dimensions do not
      allow for an integral number, fractional cells must be discarded; an
      encoder must be careful to specify the grid dimensions accurately so that
      this does not happen. This block requires a Global Color Table to be
      available; the colors used by this block reference the Global Color Table
      in the Stream if there is one, or the Global Color Table from a previous
      Stream, if one was saved. This block is a graphic rendering block,
      therefore it may be modified by a Graphic Control Extension.  This block
      is OPTIONAL; any number of them may appear in the Data Stream.

      b. Required Version.  89a.

                                                                        20

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       Extension Introducer          Byte
     +---------------+
  1  |               |       Plain Text Label              Byte
     +---------------+

     +---------------+
  0  |               |       Block Size                    Byte
     +---------------+
  1  |               |       Text Grid Left Position       Unsigned
     +-             -+
  2  |               |
     +---------------+
  3  |               |       Text Grid Top Position        Unsigned
     +-             -+
  4  |               |
     +---------------+
  5  |               |       Text Grid Width               Unsigned
     +-             -+
  6  |               |
     +---------------+
  7  |               |       Text Grid Height              Unsigned
     +-             -+
  8  |               |
     +---------------+
  9  |               |       Character Cell Width          Byte
     +---------------+
 10  |               |       Character Cell Height         Byte
     +---------------+
 11  |               |       Text Foreground Color Index   Byte
     +---------------+
 12  |               |       Text Background Color Index   Byte
     +---------------+

     +===============+
     |               |
  N  |               |       Plain Text Data               Data Sub-blocks
     |               |
     +===============+

     +---------------+
  0  |               |       Block Terminator              Byte
     +---------------+

            i) Extension Introducer - Identifies the beginning of an extension
            block. This field contains the fixed value 0x21.

            ii) Plain Text Label - Identifies the current block as a Plain Text
            Extension. This field contains the fixed value 0x01.

            iii) Block Size - Number of bytes in the extension, after the Block
            Size field and up to but not including the beginning of the data
            portion. This field contains the fixed value 12.

                                                                        21

            iv) Text Grid Left Position - Column number, in pixels, of the left
            edge of the text grid, with respect to the left edge of the Logical
            Screen.

            v) Text Grid Top Position - Row number, in pixels, of the top edge
            of the text grid, with respect to the top edge of the Logical
            Screen.

            vi) Image Grid Width - Width of the text grid in pixels.

            vii) Image Grid Height - Height of the text grid in pixels.

            viii) Character Cell Width - Width, in pixels, of each cell in the
            grid.

            ix) Character Cell Height - Height, in pixels, of each cell in the
            grid.

            x) Text Foreground Color Index - Index into the Global Color Table
            to be used to render the text foreground.

            xi) Text Background Color Index - Index into the Global Color Table
            to be used to render the text background.

            xii) Plain Text Data - Sequence of sub-blocks, each of size at most
            255 bytes and at least 1 byte, with the size in a byte preceding
            the data.  The end of the sequence is marked by the Block
            Terminator.

            xiii) Block Terminator - This zero-length data block marks the end
            of the Plain Text Data Blocks.

      d. Extensions and Scope. The scope of this block is the Plain Text Data
      Block contained in it. This block may be modified by the Graphic Control
      Extension.

      e. Recommendations. The data in the Plain Text Extension is assumed to be
      preformatted. The selection of font and size is left to the discretion of
      the decoder.  If characters less than 0x20 or greater than 0xf7 are
      encountered, it is recommended that the decoder display a Space character
      (0x20). The encoder should use grid and cell dimensions such that an
      integral number of cells fit in the grid both horizontally as well as
      vertically.  For broadest compatibility, character cell dimensions should
      be around 8x8 or 8x16 (width x height); consider an image for unusual
      sized text.

26. Application Extension.

      a. Description. The Application Extension contains application-specific
      information; it conforms with the extension block syntax, as described
      below, and its block label is 0xFF.

      b. Required Version.  89a.

                                                                        22

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       Extension Introducer          Byte
     +---------------+
  1  |               |       Extension Label               Byte
     +---------------+

     +---------------+
  0  |               |       Block Size                    Byte
     +---------------+
  1  |               |
     +-             -+
  2  |               |
     +-             -+
  3  |               |       Application Identifier        8 Bytes
     +-             -+
  4  |               |
     +-             -+
  5  |               |
     +-             -+
  6  |               |
     +-             -+
  7  |               |
     +-             -+
  8  |               |
     +---------------+
  9  |               |
     +-             -+
 10  |               |       Appl. Authentication Code     3 Bytes
     +-             -+
 11  |               |
     +---------------+

     +===============+
     |               |
     |               |       Application Data              Data Sub-blocks
     |               |
     |               |
     +===============+

     +---------------+
  0  |               |       Block Terminator              Byte
     +---------------+

            i) Extension Introducer - Defines this block as an extension. This
            field contains the fixed value 0x21.

            ii) Application Extension Label - Identifies the block as an
            Application Extension. This field contains the fixed value 0xFF.

            iii) Block Size - Number of bytes in this extension block,
            following the Block Size field, up to but not including the
            beginning of the Application Data. This field contains the fixed
            value 11.

                                                                        23

            iv) Application Identifier - Sequence of eight printable ASCII
            characters used to identify the application owning the Application
            Extension.

            v) Application Authentication Code - Sequence of three bytes used
            to authenticate the Application Identifier. An Application program
            may use an algorithm to compute a binary code that uniquely
            identifies it as the application owning the Application Extension.

      d. Extensions and Scope. This block does not have scope. This block
      cannot be modified by any extension.

      e. Recommendation. None.

27. Trailer.

      a. Description. This block is a single-field block indicating the end of
      the GIF Data Stream.  It contains the fixed value 0x3B.

      b. Required Version.  87a.

      c. Syntax.

      7 6 5 4 3 2 1 0        Field Name                    Type
     +---------------+
  0  |               |       GIF Trailer                   Byte
     +---------------+

      d. Extensions and Scope. This block does not have scope, it terminates
      the GIF Data Stream. This block may not be modified by any extension.

      e. Recommendations. None.

                                                                        24

Appendix
A. Quick Reference Table.

Block Name                  Required   Label       Ext.   Vers.
Application Extension       Opt. (*)   0xFF (255)  yes    89a
Comment Extension           Opt. (*)   0xFE (254)  yes    89a
Global Color Table          Opt. (1)   none        no     87a
Graphic Control Extension   Opt. (*)   0xF9 (249)  yes    89a
Header                      Req. (1)   none        no     N/A
Image Descriptor            Opt. (*)   0x2C (044)  no     87a (89a)
Local Color Table           Opt. (*)   none        no     87a
Logical Screen Descriptor   Req. (1)   none        no     87a (89a)
Plain Text Extension        Opt. (*)   0x01 (001)  yes    89a
Trailer                     Req. (1)   0x3B (059)  no     87a

Unlabeled Blocks
Header                      Req. (1)   none        no     N/A
Logical Screen Descriptor   Req. (1)   none        no     87a (89a)
Global Color Table          Opt. (1)   none        no     87a
Local Color Table           Opt. (*)   none        no     87a

Graphic-Rendering Blocks
Plain Text Extension        Opt. (*)   0x01 (001)  yes    89a
Image Descriptor            Opt. (*)   0x2C (044)  no     87a (89a)

Control Blocks
Graphic Control Extension   Opt. (*)   0xF9 (249)  yes    89a

Special Purpose Blocks
Trailer                     Req. (1)   0x3B (059)  no     87a
Comment Extension           Opt. (*)   0xFE (254)  yes    89a
Application Extension       Opt. (*)   0xFF (255)  yes    89a

legend:           (1)   if present, at most one occurrence
                  (*)   zero or more occurrences
                  (+)   one or more occurrences

Notes : The Header is not subject to Version Numbers.
(89a) The Logical Screen Descriptor and the Image Descriptor retained their
syntax from version 87a to version 89a, but some fields reserved under version
87a are used under version 89a.

                                                                        25

Appendix
B. GIF Grammar.

A Grammar is a form of notation to represent the sequence in which certain
objects form larger objects.  A grammar is also used to represent the number of
objects that can occur at a given position.  The grammar given here represents
the sequence of blocks that form the GIF Data Stream. A grammar is given by
listing its rules.  Each rule consists of the left-hand side, followed by some
form of equals sign, followed by the right-hand side.  In a rule, the
right-hand side describes how the left-hand side is defined. The right-hand
side consists of a sequence of entities, with the possible presence of special
symbols. The following legend defines the symbols used in this grammar for GIF.

Legend:           <>    grammar word
                  ::=   defines symbol
                  *     zero or more occurrences
                  +     one or more occurrences
                  |     alternate element
                  []    optional element

Example:

 ::= Header  * Trailer

This rule defines the entity  as follows. It must begin with a
Header. The Header is followed by an entity called Logical Screen, which is
defined below by another rule. The Logical Screen is followed by the entity
Data, which is also defined below by another rule. Finally, the entity Data is
followed by the Trailer.  Since there is no rule defining the Header or the
Trailer, this means that these blocks are defined in the document.  The entity
Data has a special symbol (*) following it which means that, at this position,
the entity Data may be repeated any number of times, including 0 times. For
further reading on this subject, refer to a standard text on Programming
Languages.

The Grammar.

 ::=     Header  * Trailer

 ::=      Logical Screen Descriptor [Global Color Table]

 ::=                  |

 ::=       [Graphic Control Extension]

 ::=    |
                               Plain Text Extension

 ::=   Image Descriptor [Local Color Table] Image Data

 ::=    Application Extension  |
                               Comment Extension

                                                                        26

NOTE : The grammar indicates that it is possible for a GIF Data Stream to
contain the Header, the Logical Screen Descriptor, a Global Color Table and the
GIF Trailer. This special case is used to load a GIF decoder with a Global
Color Table, in preparation for subsequent Data Streams without color tables at
all.

                                                                        27

Appendix
C. Glossary.

Active Color Table - Color table used to render the next graphic. If the next
graphic is an image which has a Local Color Table associated with it, the
active color table becomes the Local Color Table associated with that image.
If the next graphic is an image without a Local Color Table, or a Plain Text
Extension, the active color table is the Global Color Table associated with the
Data Stream, if there is one; if there is no Global Color Table in the Data
Stream, the active color table is a color table saved from a previous Data
Stream, or one supplied by the decoder.

Block - Collection of bytes forming a protocol unit. In general, the term
includes labeled and unlabeled blocks, as well as Extensions.

Data Stream - The GIF Data Stream is composed of blocks and sub-blocks
representing images and graphics, together with control information to render
them on a display device. All control and data blocks in the Data Stream must
follow the Header and must precede the Trailer.

Decoder - A program capable of processing a GIF Data Stream to render the
images and graphics contained in it.

Encoder - A program capable of capturing and formatting image and graphic
raster data, following the definitions of the Graphics Interchange Format.

Extension - A protocol block labeled by the Extension Introducer 0x21.

Extension Introducer - Label (0x21) defining an Extension.

Graphic - Data which can be rendered on the screen by virtue of some algorithm.
The term graphic is more general than the term image; in addition to images,
the term graphic also includes data such as text, which is rendered using
character bit-maps.

Image - Data representing a picture or a drawing; an image is represented by an
array of pixels called the raster of the image.

Raster - Array of pixel values representing an image.

                                                                        28

Appendix
D. Conventions.

Animation - The Graphics Interchange Format is not intended as a platform for
animation, even though it can be done in a limited way.

Byte Ordering - Unless otherwise stated, multi-byte numeric fields are ordered
with the Least Significant Byte first.

Color Indices - Color indices always refer to the active color table, either
the Global Color Table or the Local Color Table.

Color Order - Unless otherwise stated, all triple-component RGB color values
are specified in Red-Green-Blue order.

Color Tables - Both color tables, the Global and the Local, are optional; if
present, the Global Color Table is to be used with every image in the Data
Stream for which a Local Color Table is not given; if present, a Local Color
Table overrides the Global Color Table.  However, if neither color table is
present, the application program is free to use an arbitrary color table. If
the graphics in several Data Streams are related and all use the same color
table, an encoder could place the color table as the Global Color Table in the
first Data Stream and leave subsequent Data Streams without a Global Color
Table or any Local Color Tables; in this way, the overhead for the table is
eliminated.  It is recommended that the decoder save the previous Global Color
Table to be used with the Data Stream that follows, in case it does not contain
either a Global Color Table or any Local Color Tables. In general, this allows
the application program to use past color tables, significantly reducing
transmission overhead.

Extension Blocks - Extensions are defined using the Extension Introducer code
to mark the beginning of the block, followed by a block label, identifying the
type of extension.  Extension Codes are numbers in the range from 0x00 to 0xFF,
inclusive. Special purpose extensions are transparent to the decoder and may be
omitted when transmitting the Data Stream on-line. The GIF capabilities
dialogue makes the provision for the receiver to request the transmission of
all blocks; the default state in this regard is no transmission of Special
purpose blocks.

Reserved Fields - All Reserved Fields are expected to have each bit set to zero
(off).

                                                                        29

Appendix
E. Interlaced Images.

The rows of an Interlaced images are arranged in the following order:

      Group 1 : Every 8th. row, starting with row 0.              (Pass 1)
      Group 2 : Every 8th. row, starting with row 4.              (Pass 2)
      Group 3 : Every 4th. row, starting with row 2.              (Pass 3)
      Group 4 : Every 2nd. row, starting with row 1.              (Pass 4)

The Following example illustrates how the rows of an interlaced image are
ordered.

      Row Number                                        Interlace Pass

 0    -----------------------------------------       1
 1    -----------------------------------------                         4
 2    -----------------------------------------                   3
 3    -----------------------------------------                         4
 4    -----------------------------------------             2
 5    -----------------------------------------                         4
 6    -----------------------------------------                   3
 7    -----------------------------------------                         4
 8    -----------------------------------------       1
 9    -----------------------------------------                         4
 10   -----------------------------------------                   3
 11   -----------------------------------------                         4
 12   -----------------------------------------             2
 13   -----------------------------------------                         4
 14   -----------------------------------------                   3
 15   -----------------------------------------                         4
 16   -----------------------------------------       1
 17   -----------------------------------------                         4
 18   -----------------------------------------                   3
 19   -----------------------------------------                         4

                                                                        30

Appendix
F. Variable-Length-Code LZW Compression.

The Variable-Length-Code LZW Compression is a variation of the Lempel-Ziv
Compression algorithm in which variable-length codes are used to replace
patterns detected in the original data. The algorithm uses a code or
translation table constructed from the patterns encountered in the original
data; each new pattern is entered into the table and its index is used to
replace it in the compressed stream.

The compressor takes the data from the input stream and builds a code or
translation table with the patterns as it encounters them; each new pattern is
entered into the code table and its index is added to the output stream; when a
pattern is encountered which had been detected since the last code table
refresh, its index from the code table is put on the output stream, thus
achieving the data compression.  The expander takes input from the compressed
data stream and builds the code or translation table from it; as the compressed
data stream is processed, codes are used to index into the code table and the
corresponding data is put on the decompressed output stream, thus achieving
data decompression.  The details of the algorithm are explained below.  The
Variable-Length-Code aspect of the algorithm is based on an initial code size
(LZW-initial code size), which specifies the initial number of bits used for
the compression codes.  When the number of patterns detected by the compressor
in the input stream exceeds the number of patterns encodable with the current
number of bits, the number of bits per LZW code is increased by one.

The Raster Data stream that represents the actual output image can be
represented as:

         7 6 5 4 3 2 1 0
        +---------------+
        | LZW code size |
        +---------------+

        +---------------+ ----+
        |  block size   |     |
        +---------------+     |
        |               |     +-- Repeated as many
        |  data bytes   |     |   times as necessary.
        |               |     |
        +---------------+ ----+

        . . .       . . . ------- The code that terminates the LZW
                                  compressed data must appear before
                                  Block Terminator.
        +---------------+
        |0 0 0 0 0 0 0 0|  Block Terminator
        +---------------+

The conversion of the image from a series of pixel values to a transmitted or
stored character stream involves several steps. In brief these steps are:

1. Establish the Code Size - Define the number of bits needed to represent the
actual data.

2. Compress the Data - Compress the series of image pixels to a series of

                                                                        31

compression codes.

3. Build a Series of Bytes - Take the set of compression codes and convert to a
string of 8-bit bytes.

4. Package the Bytes - Package sets of bytes into blocks preceded by character
counts and output.

ESTABLISH CODE SIZE

The first byte of the Compressed Data stream is a value indicating the minimum
number of bits required to represent the set of actual pixel values. Normally
this will be the same as the number of color bits. Because of some algorithmic
constraints however, black & white images which have one color bit must be
indicated as having a code size of 2.
This code size value also implies that the compression codes must start out one
bit longer.

COMPRESSION

The LZW algorithm converts a series of data values into a series of codes which
may be raw values or a code designating a series of values. Using text
characters as an analogy, the output code consists of a character or a code
representing a string of characters.

The LZW algorithm used in GIF matches algorithmically with the standard LZW
algorithm with the following differences:

1.  A special Clear code is defined which resets all compression/decompression
parameters and tables to a start-up state. The value of this code is 2**. For example if the code size indicated was 4 (image was 4 bits/pixel)
the Clear code value would be 16 (10000 binary). The Clear code can appear at
any point in the image data stream and therefore requires the LZW algorithm to
process succeeding codes as if a new data stream was starting. Encoders should
output a Clear code as the first code of each image data stream.

2. An End of Information code is defined that explicitly indicates the end of
the image data stream. LZW processing terminates when this code is encountered.
It must be the last code output by the encoder for an image. The value of this
code is +1.

3. The first available compression code value is +2.

4. The output codes are of variable length, starting at +1 bits per
code, up to 12 bits per code. This defines a maximum code value of 4095
(0xFFF). Whenever the LZW code value would exceed the current code length, the
code length is increased by one. The packing/unpacking of these codes must then
be altered to reflect the new code length.

BUILD 8-BIT BYTES

Because the LZW compression used for GIF creates a series of variable length
codes, of between 3 and 12 bits each, these codes must be reformed into a
series of 8-bit bytes that will be the characters actually stored or
transmitted. This provides additional compression of the image. The codes are
formed into a stream of bits as if they were packed right to left and then

                                                                        32

picked off 8 bits at a time to be output.

Assuming a character array of 8 bits per character and using 5 bit codes to be
packed, an example layout would be similar to:

     +---------------+
  0  |               |    bbbaaaaa
     +---------------+
  1  |               |    dcccccbb
     +---------------+
  2  |               |    eeeedddd
     +---------------+
  3  |               |    ggfffffe
     +---------------+
  4  |               |    hhhhhggg
     +---------------+
           . . .
     +---------------+
  N  |               |
     +---------------+

Note that the physical packing arrangement will change as the number of bits
per compression code change but the concept remains the same.

PACKAGE THE BYTES

Once the bytes have been created, they are grouped into blocks for output by
preceding each block of 0 to 255 bytes with a character count byte. A block
with a zero byte count terminates the Raster Data stream for a given image.
These blocks are what are actually output for the GIF image. This block format
has the side effect of allowing a decoding program the ability to read past the
actual image data if necessary by reading block counts and then skipping over
the data.

FURTHER READING

[1] Ziv, J. and Lempel, A. : "A Universal Algorithm for Sequential Data
Compression", IEEE Transactions on Information Theory, May 1977.
[2] Welch, T. : "A Technique for High-Performance Data Compression", Computer,
June 1984.
[3] Nelson, M.R. : "LZW Data Compression", Dr. Dobb's Journal, October 1989.

                                                                        33

Appendix
G. On-line Capabilities Dialogue.

NOTE : This section is currently (10 July 1990) under revision; the information
provided here should be used as general guidelines. Code written based on this
information should be designed in a flexible way to accommodate any changes
resulting from the revisions.

The following sequences are defined for use in mediating control between a GIF
sender and GIF receiver over an interactive communications line. These
sequences do not apply to applications that involve downloading of static GIF
files and are not considered part of a GIF file.

GIF CAPABILITIES ENQUIRY

The GIF Capabilities Enquiry sequence is issued from a host and requests an
interactive GIF decoder to return a response message that defines the graphics
parameters for the decoder. This involves returning information about available
screen sizes, number of bits/color supported and the amount of color detail
supported. The escape sequence for the GIF Capabilities Enquiry is defined as:

ESC[>0g           0x1B 0x5B 0x3E 0x30 0x67

GIF CAPABILITIES RESPONSE

The GIF Capabilities Response message is returned by an interactive GIF decoder
and defines the decoder's display capabilities for all graphics modes that are
supported by the software. Note that this can also include graphics printers as
well as a monitor screen. The general format of this message is:

#version;protocol{;dev, width, height, color-bits, color-res}...

'#'            GIF Capabilities Response identifier character.
version        GIF format version number;  initially '87a'.
protocol='0'   No end-to-end protocol supported by decoder Transfer as direct
               8-bit data stream.
protocol='1'   Can use CIS B+ error correction protocol to transfer GIF data
               interactively from the host directly to the display.
dev = '0'      Screen parameter set follows.
dev = '1'      Printer parameter set follows.
width          Maximum supported display width in pixels.
height         Maximum supported display height in pixels.
color-bits     Number of bits per pixel supported. The number of supported
               colors is therefore 2**color-bits.
color-res      Number of bits per color component supported in the hardware
               color palette. If color-res is '0' then no hardware palette
               table is available.

Note that all values in the GIF Capabilities Response are returned as ASCII
decimal numbers and the message is terminated by a Carriage Return character.

The following GIF Capabilities Response message describes three standard IBM PC
Enhanced Graphics Adapter configurations with no printer; the GIF data stream
                                                                                34
can be processed within an error correcting protocol:

#87a;1;0,320,200,4,0;0,640,200,2,2;0,640,350,4,2

ENTER GIF GRAPHICS MODE

Two sequences are currently defined to invoke an interactive GIF decoder into
action. The only difference between them is that different output media are
selected. These sequences are:

ESC[>1g     Display GIF image on screen

                  0x1B 0x5B 0x3E 0x31 0x67

ESC[>2g   Display image directly to an attached graphics printer. The image may
optionally be displayed on the screen as well.

                  0x1B 0x5B 0x3E 0x32 0x67

Note that the 'g' character terminating each sequence is in lowercase.

INTERACTIVE ENVIRONMENT

The assumed environment for the transmission of GIF image data from an
interactive application is a full 8-bit data stream from host to micro.  All
256 character codes must be transferrable. The establishing of an 8-bit data
path for communications will normally be taken care of by the host application
programs. It is however up to the receiving communications programs supporting
GIF to be able to receive and pass on all 256 8-bit codes to the GIF decoder
software.


ASM Mode Reel

                   LZW and GIF explained----Steve Blackstock

      I hope this little document will help enlighten those of you out there
who want to know more about the Lempel-Ziv Welch compression algorithm, and,
specifically, the implementation that GIF uses.
     Before we start, here's a little terminology, for the purposes of this
document:

      "character": a fundamental data element. In normal text files, this is
just a single byte. In raster images, which is what we're interested in, it's
an index that specifies the color of a given pixel. I'll refer to an arbitray
character as "K".
      "charstream": a stream of characters, as in a data file.
      "string": a number of continuous characters, anywhere from one to very
many characters in length. I can specify an arbitrary string as "[...]K".
      "prefix": almost the same as a string, but with the implication that a
prefix immediately precedes a character, and a prefix can have a length of
zero. So, a prefix and a character make up a string. I will refer to an
arbitrary prefix as "[...]".
      "root": a single-character string. For most purposes, this is a
character, but we may occasionally make a distinction. It is [...]K, where
[...] is empty.
      "code": a number, specified by a known number of bits, which maps to a
string.
      "codestream": the output stream of codes, as in the "raster data"
      "entry": a code and its string.
      "string table": a list of entries; usually, but not necessarily, unique.
      That should be enough of that.

     LZW is a way of compressing data that takes advantage of repetition of
strings in the data. Since raster data usually contains a lot of this
repetition, LZW is a good way of compressing and decompressing it.
     For the moment, lets consider normal LZW encoding and decoding. GIF's
variation on the concept is just an extension from there.
     LZW manipulates three objects in both compression and decompression: the
charstream, the codestream, and the string table. In compression, the
charstream is the input and the codestream is the output. In decompression,
the codestream is the input and the charstream is the output. The string table
is a product of both compression and decompression, but is never passed from
one to the other.
     The first thing we do in LZW compression is initialize our string table.
To do this, we need to choose a code size (how many bits) and know how many
values our characters can possibly take. Let's say our code size is 12 bits,
meaning we can store 0->FFF, or 4096 entries in our string table. Lets also
say that we have 32 possible different characters. (This corresponds to, say,
a picture in which there are 32 different colors possible for each pixel.) To
initialize the table, we set code#0 to character#0, code #1 to character#1,
and so on, until code#31 to character#31. Actually, we are specifying that
each code from 0 to 31 maps to a root. There will be no more entries in the
table that have this property.
     Now we start compressing data. Let's first define something called the
"current prefix". It's just a prefix that we'll store things in and compare
things to now and then. I will refer to it as "[.c.]". Initially, the current
prefix has nothing in it. Let's also define a "current string", which will be
the current prefix plus the next character in the charstream. I will refer to
the current string as "[.c.]K", where K is some character. OK, look at the
first character in the charstream. Call it P. Make [.c.]P the current string.
(At this point, of course, it's just the root P.) Now search through the
string table to see if [.c.]P appears in it. Of course, it does now, because
our string table is initialized to have all roots. So we don't do anything.
Now make [.c.]P the current prefix. Look at the next character in the
charstream. Call it Q. Add it to the current prefix to form [.c.]Q, the
current string. Now search through the string table to see if [.c.]Q appears
in it. In this case, of course, it doesn't. Aha! Now we get to do something.
Add [.c.]Q (which is PQ in this case) to the string table for code#32, and
output the code for [.c.] to the codestream. Now start over again with the
current prefix being just the root P. Keep adding characters to [.c.] to form
[.c.]K, until you can't find [.c.]K in the string table. Then output the code
for [.c.] and add [.c.]K to the string table. In pseudo-code, the algorithm
goes something like this:

     [1] Initialize string table;
     [2] [.c.] <- empty;
     [3] K <- next character in charstream;
     [4] Is [.c.]K in string table?
      (yes: [.c.] <- [.c.]K;
            go to [3];
      )
      (no: add [.c.]K to the string table;
           output the code for [.c.] to the codestream;
           [.c.] <- K;
           go to [3];
      )

       It's as simple as that! Of course, when you get to step [3] and there
aren't any more characters left, you just output the code for [.c.] and throw
the table away. You're done.
      Wanna do an example? Let's pretend we have a four-character alphabet:
A,B,C,D. The charstream looks like ABACABA. Let's compress it. First, we
initialize our string table to: #0=A, #1=B, #2=C, #3=D. The first character is
A, which is in the string table, so [.c.] becomes A. Next we get AB, which is
not in the table, so we output code #0 (for [.c.]),
     and add AB to the string table as code #4. [.c.] becomes B. Next we get
[.c.]A = BA, which is not in the string table, so output code #1, and add BA
to the string table as code #5. [.c.] becomes A. Next we get AC, which is not
in the string table. Output code #0, and add AC to the string table as code
#6. Now [.c.] becomes C. Next we get [.c.]A = CA, which is not in the table.
Output #2 for C, and add CA to table as code#7. Now [.c.] becomes A. Next we
get AB, which IS in the string table, so [.c.] gets AB, and we look at ABA,
which is not in the string table, so output the code for AB, which is #4, and
add ABA to the string table as code #8. [.c.] becomes A. We can't get any more
characters, so we just output #0 for the code for A, and we're done. So, the
codestream is #0#1#0#2#4#0.
      A few words (four) should be said here about efficiency: use a hashing
strategy. The search through the string table can be computationally
intensive, and some hashing is well worth the effort. Also, note that
"straight LZW" compression runs the risk of overflowing the string table -
getting to a code which can't be represented in the number of bits you've set
aside for codes. There are several ways of dealing with this problem, and GIF
implements a very clever one, but we'll get to that.
      An important thing to notice is that, at any point during the
compression, if [...]K is in the string table, [...] is there also. This fact
suggests an efficient method for storing strings in the table. Rather than
store the entire string of K's in the table, realize that any string can be
expressed as a prefix plus a character: [...]K. If we're about to store [...]K
in the table, we know that [...] is already there, so we can just store the
code for [...] plus the final character K.
      Ok, that takes care of compression. Decompression is perhaps more
difficult conceptually, but it is really easier to program.
      Here's how it goes: We again have to start with an initialized string
table. This table comes from what knowledge we have about the charstream that
we will eventually get, like what possible values the characters can take. In
GIF files, this information is in the header as the number of possible pixel
values. The beauty of LZW, though, is that this is all we need to know. We
will build the rest of the string table as we decompress the codestream. The
compression is done in such a way that we will never encounter a code in the
codestream that we can't translate into a string.
      We need to define something called a "current code", which I will refer
to as "", and an "old-code", which I will refer to as "". To start
things off, look at the first code. This is now . This code will be in
the intialized string table as the code for a root. Output the root to the
charstream. Make this code the old-code . *Now look at the next code, and
make it . It is possible that this code will not be in the string table,
but let's assume for now that it is. Output the string corresponding to
to the codestream. Now find the first character in the string you just
translated. Call this K. Add this to the prefix [...] generated by  to
form a new string [...]K. Add this string [...]K to the string table, and set
the old-code  to the current code . Repeat from where I typed the
asterisk, and you're all set. Read this paragraph again if you just skimmed
it!!!  Now let's consider the possibility that  is not in the string
table. Think back to compression, and try to understand what happens when you
have a string like P[...]P[...]PQ appear in the charstream. Suppose P[...] is
already in the string table, but P[...]P is not. The compressor will parse out
P[...], and find that P[...]P is not in the string table. It will output the
code for P[...], and add P[...]P to the string table. Then it will get up to
P[...]P for the next string, and find that P[...]P is in the table, as
     the code just added. So it will output the code for P[...]P if it finds
that P[...]PQ is not in the table. The decompressor is always "one step
behind" the compressor. When the decompressor sees the code for P[...]P, it
will not have added that code to it's string table yet because it needed the
beginning character of P[...]P to add to the string for the last code, P[...],
to form the code for P[...]P. However, when a decompressor finds a code that
it doesn't know yet, it will always be the very next one to be added to the
string table. So it can guess at what the string for the code should be, and,
in fact, it will always be correct. If I am a decompressor, and I see
code#124, and yet my string table has entries only up to code#123, I can
figure out what code#124 must be, add it to my string table, and output the
string. If code#123 generated the string, which I will refer to here as a
prefix, [...], then code#124, in this special case, will be [...] plus the
first character of [...]. So just add the first character of [...] to the end
of itself. Not too bad.  As an example (and a very common one) of this special
case, let's assume we have a raster image in which the first three pixels have
the same color value. That is, my charstream looks like: QQQ.... For the sake
of argument, let's say we have 32 colors, and Q is the color#12. The
compressor will generate the code sequence 12,32,.... (if you don't know why,
take a minute to understand it.) Remember that #32 is not in the initial
table, which goes from #0 to #31. The decompressor will see #12 and translate
it just fine as color Q. Then it will see #32 and not yet know what that
means. But if it thinks about it long enough, it can figure out that QQ should
be entry#32 in the table and QQ should be the next string output.  So the
decompression pseudo-code goes something like:

      [1] Initialize string table;
     [2] get first code: ;
     [3] output the string for  to the charstream;
     [4]  = ;
     [5]  <- next code in codestream;
     [6] does  exist in the string table?
      (yes: output the string for  to the charstream;
            [...] <- translation for ;
            K <- first character of translation for ;
            add [...]K to the string table;         <- ;  )
      (no: [...] <- translation for ;
           K <- first character of [...];
           output [...]K to charstream and add it to string table;
            <-
      )
     [7] go to [5];

      Again, when you get to step [5] and there are no more codes, you're
finished.  Outputting of strings, and finding of initial characters in strings
are efficiency problems all to themselves, but I'm not going to suggest ways
to do them here. Half the fun of programming is figuring these things out!
      ---
      Now for the GIF variations on the theme. In part of the header of a GIF
file, there is a field, in the Raster Data stream, called "code size". This is
a very misleading name for the field, but we have to live with it. What it is
really is the "root size". The actual size, in bits, of the compression codes
actually changes during compression/decompression, and I will refer to that
size here as the "compression size". The initial table is just the codes for
all the roots, as usual, but two special codes are added on top of those.
Suppose you have a "code size", which is usually the number of bits per pixel
in the image, of N. If the number of bits/pixel is one, then N must be 2: the
roots take up slots #0 and #1 in the initial table, and the two special codes
will take up slots #4 and #5. In any other case, N is the number of bits per
pixel, and the roots take up slots #0 through #(2**N-1), and the special codes
are (2**N) and (2**N + 1). The initial compression size will be N+1 bits per
code. If you're encoding, you output the codes (N+1) bits at a time to start
with, and if you're decoding, you grab (N+1) bits from the codestream at a
time.  As for the special codes:  or the clear code, is (2**N), and ,
or end-of-information, is (2**N + 1).  tells the compressor to re-
initialize the string table, and to reset the compression size to (N+1).
means there's no more in the codestream.  If you're encoding or decoding, you
should start adding things to the string table at  + 2. If you're
encoding, you should output  as the very first code, and then whenever
after that you reach code #4095 (hex FFF), because GIF does not allow
compression sizes to be greater than 12 bits. If you're decoding, you should
reinitialize your string table when you observe .  The variable
compression sizes are really no big deal. If you're encoding, you start with a
compression size of (N+1) bits, and, whenever you output the code
(2**(compression size)-1), you bump the compression size up one bit. So the
next code you output will be one bit longer. Remember that the largest
compression size is 12 bits, corresponding to a code of 4095. If you get that
far, you must output  as the next code, and start over.  If you're
decoding, you must increase your compression size AS SOON AS YOU write entry
#(2**(compression size) - 1) to the string table. The next code you READ will
be one bit longer. Don't make the mistake of waiting until you need to add the
code (2**compression size) to the table. You'll have already missed a bit from
the last code.  The packaging of codes into a bitsream for the raster data is
also a potential stumbling block for the novice encoder or decoder. The lowest
order bit in the code should coincide with the lowest available bit in the
first available byte in the codestream. For example, if you're starting with
5-bit compression codes, and your first three codes are, say, ,
, , where e, j, and o are bit#0, then your codestream will start
off like:

       byte#0: hijabcde
       byte#1: .klmnofg

      So the differences between straight LZW and GIF LZW are: two additional
special codes and variable compression sizes. If you understand LZW, and you
understand those variations, you understand it all!
      Just as sort of a P.S., you may have noticed that a compressor has a
little bit of flexibility at compression time. I specified a "greedy" approach
to the compression, grabbing as many characters as possible before outputting
codes. This is, in fact, the standard LZW way of doing things, and it will
yield the best compression ratio. But there's no rule saying you can't stop
anywhere along the line and just output the code for the current prefix,
whether it's already in the table or not, and add that string plus the next
character to the string table. There are various reasons for wanting to do
this, especially if the strings get extremely long and make hashing difficult.
If you need to, do it.
      Hope this helps out.----steve blackstock


ASM Mode Reel

;===============================================================================
; GIFLIB v1.0 by Exagone - Include file
;===============================================================================
; GIFLIB is (C) Copyright by Exagone. You may freely distribute this library
; according to the licensing defined in the manual.
; [best viewed width tab stop = 4 spaces and syntax highlighting]

;-------------------------------------------------------------------------------
; Function prototypes
;-------------------------------------------------------------------------------

GIFLoadHeader		     	   PROTO	STDCALL lpGifInfo:DWORD
GIFInitalizeDecoder		   PROTO	STDCALL lpGifInfo:DWORD
GIFDecompress		        	PROTO	STDCALL lpGifInfo:DWORD
GIFGetMinimumColorType 	PROTO	STDCALL lpGifInfo:DWORD
GIFGetOutputSize		      PROTO	STDCALL lpGifInfo:DWORD
GIFGetColorTable	      	PROTO	STDCALL lpGifInfo:DWORD, lpBuffer:DWORD
GIFCleanup	          			PROTO	STDCALL lpGifInfo:DWORD
GIFFillBitmapInfoStruct	PROTO	STDCALL lpGifInfo:DWORD, lpBitmapInfoHeader:DWORD
GIFLoadResource	      		PROTO	STDCALL lpGifInfo:DWORD, hInstance:DWORD, lpName:DWORD
GIFLoadFile			         	PROTO	STDCALL lpGifInfo:DWORD, lpFile:DWORD

;-------------------------------------------------------------------------------
; Structures
;-------------------------------------------------------------------------------
GIF_INFO		STRUCT
    ; --- Header ---
    GIFVersion		db	3 dup(?)		; 3 byte string (not 0-terminated) that
                                        ; identifies the GIF Version (like 97a
                                        ; or 98a)
    ; --- Logical screen descriptor ---
    dwLSWidth		dd	?				; Logical screen width
    dwLSHeight		dd	?				; Logical screen height
                                        ; NOTE: Use ImageWidth/Height to find
                                        ; the dimensions, image dimensions can
                                        ; be different from the logical screen
                                        ; width and height.

    ; --- Color info ---
    lpColorTable	dd	?				; Pointer to the color table
    ColorCount		dd	?				; Number of colors in image (and color
                                        ; table)

    ; --- Image info ---
    dwImageWidth	dd	?				; Width of image
    dwImageHeight	dd	?				; Height of image

    ; --- Image data ---
    lpImageData		dd	?				; Pointer to the actual image data
    dwImageFormat	dd	?				; Image format (IMAGEFORMAT_XXXXX)

    ; --- Input data ---
    lpGIFData		dd	?				; Pointer to the GIF File data
    dwGIFDataSize	dd	?				; Size of GIF Data

    ; --- Internal data reserved for giflib ---
    InternalData	db	44 dup (?)
GIF_INFO		ENDS
;-------------------------------------------------------------------------------
; Constants
;-------------------------------------------------------------------------------
IMAGEFORMAT_1BIT	equ 2		 ; 1-bit, monochrome
IMAGEFORMAT_4BIT	equ 16		 ; 4-bit, 16-color
IMAGEFORMAT_8BIT	equ 256		 ; 8-bit, 256-color


ASM Mode Reel

;----------------------------------------------------------------------- BATH
@echo off
ml /c /coff giflib.asm
lib /out:GIFLIB.LIB /LIBPATH:\masm32\lib giflib.obj
pause >nul
@echo off
;----------------------------------------------------------------------- giflib_internal.INC
GIF_INFO		STRUCT
    ; --- Header ---
    GIFVersion		db	3 dup(?)		; 3 byte string (not 0-terminated) that identifies
                                        ; the GIF Version (like 97a or 98a)
    ; --- Logical screen descriptor ---
    dwLSWidth		dd	?				; Logical screen width
    dwLSHeight		dd	?				; Logical screen height
                                        ; NOTE: Use ImageWidth/Height to find the dimensions,
                                        ; This one can be different from the logical screen
                                        ; width and height.

    ; --- Color info ---
    lpColorTable	dd	?				; Pointer to the color table
    ColorCount		dd	?				; Number of colors in image (and color table)

    ; --- Image info ---
    dwImageWidth	dd	?				; Width of image
    dwImageHeight	dd	?				; Height of image

    ; --- Image data ---
    lpImageData		dd	?				; Pointer to the actual image data
    dwImageFormat	dd	?				; Image format (IMAGEFORMAT_XXXXX)

    ; --- Input data ---
    lpGIFData		dd	?				; Pointer to the GIF File data
    dwGIFDataSize	dd	?				; Size of GIF Data

    ; --- Internal data ---
    ; Don't touch this data below, it is used internally by GIFLIB
    lpLZWTable		dd	?				; Pointer to LZW Table
    dwLZWTableSize	dd	?				; Current maximum size of table
                                        ; This size is dynamic and changes
                                        ; if the codesize exceeds 9 bits
    dwLZWTableCount	dd	?				; Current nr of entries in the LZW table
    fGlobalCTable	db	?				; If set, the GIF file has a global color table
    dbGCTableSize	db	?				; Size of the global color table, if available
    fLocalCTable	db	?				; If set, the first image has a local color table
    dbLCTableSize	db	?				; Size of the local color table, if available
    lpGIFImage		dd	?				; Pointer to the first real image data(Table based image data)
    lpCurrentByte	dd	?				; Pointer to the current byte (used with decompression)
                                        ; note: if this value is -1, end of the file is reached and
                                        ; the code-supplier returns 0 as padding
    dbBitsAvailable	db	?				; Nr of codebits currently available
    dbCurBitSize	db	?				; Current LZW code size
    dbInitBitSize	db	?				; Initial LZW code size
    lpLastEOSB		dd	?				; Pointer to end of current subblock (used with decompression)
    dwLZWClear		dd	?				; Value of LZW Clear code (CC)
    CurrentBits		dd	?				; Holds the current bits loaded
    IsFirstCode		db	?				; Indicates if the first code is being read now
    PixelCounter	dd	?				; Counts nr of PIXELS outputted
GIF_INFO		ENDS

IMAGEFORMAT_1BIT	equ 2		 ; 1-bit, monochrome
IMAGEFORMAT_4BIT	equ 16		 ; 4-bit, 16-color
IMAGEFORMAT_8BIT	equ 256		 ; 8-bit, 256-color

EXTENSIONBLOCK				equ		021h
IMAGEDESCRIPTOR				equ		02Ch
GRAPHICCONTROLEXTENSTION	equ		0F9h
COMMENTEXTENSION			equ		0FEh
PLAINTEXTEXTENSION			equ		001h
APPLICATIONEXTENSION		equ		0FFh

;-----------------------------------------------------------------------  GIFLIB.ASM
;===============================================================================
;	GIFLIB v1.0 - (C) Copyright by Exagone
;===============================================================================
; The included general public license (see license.txt) applies to this source
; code. By using this code, you agree to the terms of the license.
;
; Original source code by Exagone
; http://exagone.cjb.net | http://www.exagone.org
; _thomas_@mailroom.com
;
; Libraries and include files from the Win32Asm package by Iczelion:
; http://win32asm.cjb.net/
.486
.model flat, stdcall
option casemap:none

includelib 	kernel32.lib
includelib  user32.lib
includelib 	gdi32.lib

include		\masm32\include\kernel32.inc
include		\masm32\include\user32.inc
include		\masm32\include\gdi32.inc
include		\masm32\include\windows.inc
include		giflib_internal.inc

GIFLoadHeader			PROTO	STDCALL :DWORD
GIFInitalizeDecoder		PROTO	STDCALL :DWORD
GIFDecompress			PROTO	STDCALL :DWORD
SkipExtension			PROTO	STDCALL :DWORD, :DWORD
SkipSubBlocks			PROTO	STDCALL :DWORD
ProcessImageDescriptor	PROTO	STDCALL :DWORD, :DWORD, :DWORD
DECInitialize			PROTO	STDCALL :DWORD
DECResetLZW				PROTO	STDCALL :DWORD
DECDecodeGIF			PROTO	STDCALL :DWORD
DECIsCodeInTable		PROTO	STDCALL :DWORD, :DWORD
DECOutputString			PROTO	STDCALL :DWORD, :DWORD
GetNextCode				PROTO	STDCALL :DWORD
LoadNextByte			PROTO	STDCALL :DWORD
DECAddToTable			PROTO	STDCALL :DWORD, :DWORD, :DWORD
GIFGetMinimumColorType	PROTO	STDCALL :DWORD
GIFGetOutputSize		PROTO	STDCALL :DWORD
GIFGetColorTable		PROTO	STDCALL :DWORD, :DWORD
GIFCleanup				PROTO	STDCALL :DWORD
GIFFillBitmapInfoStruct	PROTO	STDCALL :DWORD, :DWORD
GIFLoadResource			PROTO	STDCALL :DWORD, :DWORD, :DWORD
GIFLoadFile				PROTO	STDCALL :DWORD, :DWORD
ClearMemory				PROTO	STDCALL :DWORD, :DWORD

.code
;===============================================================================
;	ClearMemory
;===============================================================================
; Zeroes memory
ClearMemory		proc	lpMem:DWORD, dwSize:DWORD
    cld
    mov edi, lpMem
    mov ecx, dwSize
    xor	eax, eax
    shr ecx, 2
    rep stosd

    mov ecx, dwSize
    and ecx, 3
    xor	al, al
    rep stosb
    ret
ClearMemory		endp
;===============================================================================
;	GIFLoadResource
;===============================================================================
; Loads gif data from a resource (of type RT_RCDATA)
; lpName:pointer to a resource name OR resource ID
GIFLoadResource proc	lpGifInfo:DWORD, hInst:DWORD, lpName:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO

    invoke	FindResource, hInst, lpName, RT_RCDATA
    .IF		eax==0
        ret
    .ENDIF
    push	eax
    invoke	LoadResource, hInst, eax
    .IF		eax==0
        ret
    .ENDIF
    mov		[esi].lpGIFData, eax
    pop		eax
    invoke	SizeofResource, hInst, eax
    .IF		eax==0
        ret
    .ENDIF
    mov		[esi].dwGIFDataSize, eax
    assume	esi:nothing
xor		eax, eax
inc		eax
ret
GIFLoadResource	endp

;===============================================================================
;	GIFLoadFile
;===============================================================================
; Loads gif data from a file
; SHOULD CLEANUP POINTER AFTERWARDS (globalalloc)
GIFLoadFile		proc lpGifInfo:DWORD, lpFile:DWORD
LOCAL	hFile:DWORD
LOCAL	pMem:DWORD
LOCAL	bRead:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO
    invoke	CreateFile, lpFile, GENERIC_READ, FILE_SHARE_READ, NULL,\
                OPEN_EXISTING, NULL, NULL
    .IF		eax==INVALID_HANDLE_VALUE
        xor		eax, eax
        ret
    .ENDIF
    mov		hFile, eax
    invoke	GetFileSize, eax, NULL
    .IF		eax==-1
        invoke	CloseHandle, hFile
        xor		eax, eax
        ret
    .ENDIF
    mov		[esi].dwGIFDataSize, eax
    invoke	GlobalAlloc, GMEM_FIXED, eax
    .IF		eax==0
        invoke	CloseHandle, hFile
        xor		eax, eax
        ret
    .ENDIF
    mov		pMem, eax
    invoke	ReadFile, hFile, pMem, [esi].dwGIFDataSize, ADDR bRead, NULL
    .IF		eax==NULL
        invoke	CloseHandle, hFile
        xor		eax, eax
        ret
    .ENDIF
    push	pMem
    pop		[esi].lpGIFData
    invoke	CloseHandle, hFile
    xor		eax, eax
    inc		eax
    assume	esi:nothing
ret
GIFLoadFile		endp
;===============================================================================
;	GIFCleanup
;===============================================================================
; Cleanup
;
GIFCleanup		proc	uses esi lpGifInfo:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO
    invoke	GlobalFree, [esi].lpLZWTable
    assume	esi:nothing
ret
GIFCleanup		endp

;===============================================================================
;	GIFGetColorTable
;===============================================================================
; Retrieves the color table to a given buffer, in the RGBQUAD format
;
GIFGetColorTable	proc	uses esi edi ebx lpGifInfo:DWORD, lpBuffer:DWORD

    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO

    mov		edi, lpBuffer
    mov		ebx, [esi].lpColorTable
    xor		ecx, ecx
    .WHILE	ecx<[esi].ColorCount
        mov		eax, [ebx]	; RR GG BB 00 -> eax: 00 BB GG RR
        bswap 	eax			; eax: RR GG BB 00
        shr		eax, 8		; eax: 00 RR GG BB (RGBQUAD)
        stosd
        add		ebx, 3
    inc	ecx
    .ENDW
    assume	esi:nothing
ret
GIFGetColorTable	endp
;===============================================================================
;	GIFGetOutputSize
;===============================================================================
; Gets the number of bytes to reserve for the output data
GIFGetOutputSize	proc	uses esi ebx 	lpGifInfo:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO

    mov		eax, [esi].dwImageWidth
    mov		ecx, eax

    ; ---Get aligned width ---
    .IF [esi].dwImageFormat==IMAGEFORMAT_1BIT
        and		ecx, 11111b
        .IF		ecx!=0		;
            shr		eax,5
            inc		eax
            shl		eax, 5	;get nr of bytes
        .ENDIF
    .ELSEIF	[esi].dwImageFormat==IMAGEFORMAT_4BIT

        and		ecx, 111b
        .IF		ecx!=0		;
            shr		eax,3
            inc		eax
            shl		eax,3	;get nr of bytes
        .ENDIF
    .ELSEIF [esi].dwImageFormat==IMAGEFORMAT_8BIT
        and		ecx, 11b
        .IF		ecx!=0
            shr		eax, 2
            inc		eax
            shl		eax, 2
        .ENDIF
    .ENDIF

    ; --- multiply output width by nr of scanlines ---
    mul		[esi].dwImageHeight

    assume	esi:nothing
ret
GIFGetOutputSize	endp

;===============================================================================
;	GIFFillBitmapInfoStruct
;===============================================================================
;  Fills a bitmap info structure width the given properties or automatically
GIFFillBitmapInfoStruct	proc	uses esi edi	lpGifInfo:DWORD, lpBIH:DWORD
    mov		esi, lpGifInfo
    mov		edi, lpBIH
    assume	esi:PTR GIF_INFO
    assume	edi:PTR BITMAPINFOHEADER
    mov		[edi].biSize, SIZEOF BITMAPINFOHEADER
    mov		eax,[esi].dwImageHeight
    neg		eax	;negative because it outputs a top-down bitmap
    mov		[edi].biHeight, eax
    push	[esi].dwImageWidth
    pop		[edi].biWidth

    mov		[edi].biPlanes, 1
    mov		eax, [esi].dwImageFormat
    .IF		eax==IMAGEFORMAT_1BIT
        mov		ax, 1
    .ELSEIF eax==IMAGEFORMAT_4BIT
        mov		ax, 4
    .ELSE
        mov		ax, 8
    .ENDIF
    mov		[edi].biBitCount, ax
    xor		eax, eax
    mov		[edi].biCompression, BI_RGB
    mov		[edi].biSizeImage, eax
    mov		[edi].biXPelsPerMeter, eax
    mov		[edi].biYPelsPerMeter, eax
    push	[esi].ColorCount
    pop		[edi].biClrUsed
    mov		[edi].biClrImportant, eax

    assume	esi:nothing
    assume	edi:nothing
ret
GIFFillBitmapInfoStruct	endp
;===============================================================================
;	GIFGetMinimumColorType
;===============================================================================
; Gets the minimum color type (IMAGEFORMAT_XXX) to use for outputting the
; image.
GIFGetMinimumColorType	proc	uses esi	lpGifInfo:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO
    mov		eax, [esi].ColorCount
    .IF		eax==2
        mov		eax, IMAGEFORMAT_1BIT
    .ELSEIF	eax<=16
        mov		eax, IMAGEFORMAT_4BIT
    .ELSEIF eax<=256
        mov		eax, IMAGEFORMAT_8BIT
    .ENDIF
    assume	esi:nothing
ret
GIFGetMinimumColorType	endp
;===============================================================================
;	GIFDecompress
;===============================================================================
; Decompresses the gif and outputs
; returns:
; 0  - color format not allowed
; 1  - success
; -1 - failed
GIFDecompress	proc	uses esi edi ebx lpGifInfo:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO
    mov		edi, [esi].lpImageData
    ; --- Check if color format is allowed ---
    mov		eax, [esi].dwImageFormat
    .IF		eax<[esi].ColorCount
        xor		eax, eax
        ret
    .ENDIF

    ; --- Initially, load 4 bytes ---
    mov		[esi].CurrentBits, 0
    mov		[esi].dbBitsAvailable, 0
    mov		ebx,4
    .WHILE	ebx>0
        invoke	LoadNextByte, esi
        dec 	ebx
    .ENDW

    invoke	DECInitialize, esi
    invoke	DECDecodeGIF, lpGifInfo

    assume	esi:nothing
xor	eax, eax
inc eax
ret
GIFDecompress	endp
;===============================================================================
;	DECDecodeGIF
;===============================================================================
; The actual decoder
;
DECDecodeGIF	proc	uses esi edi ebx lpGifInfo:DWORD
LOCAL	OldCode:DWORD
LOCAL	FirstCharOfOldCode:BYTE
LOCAL	FirstCharOfCode:BYTE
LOCAL	NrCodesToOutput:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO

    ; --- set counters ---
    mov		[esi].PixelCounter, 0	;0 codes outputted
    mov		eax, [esi].dwImageWidth
    mul		[esi].dwImageHeight
    mov		NrCodesToOutput, eax	;this is an OUTPUT CODE counter, not nr of compressed codes
    ; --- clear output mem ---
    invoke	GIFGetOutputSize, lpGifInfo
    mov		ecx, eax
    invoke	ClearMemory, [esi].lpImageData, ecx

;	 LZW Decompression
;     [1] Initialize string table;
;     [2] get first code: ;
;     [3] output the string for  to the charstream;
;     [4]  = ;
;     [5]  <- next code in codestream;
;     [6] does  exist in the string table?
;      (yes: output the string for  to the charstream; (A)
;            [...] <- translation for ;				 (B)
;            K <- first character of translation for ; (C)
;            add [...]K to the string table;    			 (D)
;			  <- ;  )							 (E)
;      (no: [...] <- translation for ;					 (F)
;           K <- first character of [...];					 (G)
;           output [...]K to charstream and add it to string table; (H)
;            <- 									 (I)
;      )
;     [7] go to [5];

    .WHILE	TRUE
        mov		eax, NrCodesToOutput
        .IF		[esi].PixelCounter==eax
            .BREAK	;done
        .ENDIF
        invoke	GetNextCode, esi			; [1] and [5]
        mov		ebx, eax
        .IF		ebx==[esi].dwLZWClear		; Check for clearcode
            invoke	DECResetLZW, esi		; Reset LZW
            .CONTINUE
        .ENDIF
        .IF		[esi].IsFirstCode==1		; Checks if steps [1]-[4] have to be done
            dec		[esi].IsFirstCode		  ;[2]
            invoke	DECOutputString, esi, ebx ;[3]
            mov		FirstCharOfOldCode, al	  ; saved for (G)
            mov		OldCode, ebx			  ;[4]
            .CONTINUE						  ;[5]
        .ENDIF
        invoke	DECIsCodeInTable, esi, ebx	  ;[6]
        .IF		eax==TRUE					  ;(A)-(E)
            invoke	DECOutputString, esi, ebx 	;(B)
            mov		FirstCharOfCode, al			;(C)
            mov		FirstCharOfOldCode, al		; saved for (G)
            invoke	DECAddToTable, esi, OldCode, eax ;(D)
            mov		OldCode, ebx
        .ELSE								  ;(F)-(I)
            mov		al, FirstCharOfOldCode		; (G)
            invoke	DECAddToTable, esi, OldCode, eax ;(H)
            invoke	DECOutputString, esi, ebx   ;(H)
            mov		FirstCharOfOldCode, al		; saved for (G)
            mov		OldCode, ebx
        .ENDIF
    .ENDW									  ;[7]

    assume	esi:nothing
xor	eax, eax
inc eax
ret
DECDecodeGIF	endp

;===============================================================================
; DECAddToTable
;===============================================================================
; Adds an entry to the LZW Table
; The enry consists of a prefix and a suffix (size is calculated automatically)
; returns the code for the new entry
DECAddToTable proc uses esi edi ebx lpGifInfo:DWORD, dwPrefix:DWORD, dbSuffix:DWORD
LOCAL	NewMem:DWORD
LOCAL	OldSize:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO

    ; --- Check if enough space in the table left
    mov		ecx, [esi].dwLZWTableSize
    mov		eax, [esi].dwLZWTableCount
    inc		eax
    shl		eax, 2
    ; --- If no space left, make the table bigger:
    .IF		eax==ecx
        mov		OldSize, ecx
        shl		ecx, 1		;Table size doubles
        mov		[esi].dwLZWTableSize, ecx
        ; --- Reallocate memory ---
        mov		eax, [esi].lpLZWTable
        invoke	GlobalAlloc, GMEM_FIXED, ecx
        mov		NewMem, eax
        ; --- Copy old contents to new mem ---
        push	esi
        push	edi
        mov		esi, [esi].lpLZWTable
        mov		edi, NewMem
        mov		ecx, OldSize
        shr		ecx, 2
        rep		movsd
        pop		edi
        pop		esi
        invoke	GlobalFree, [esi].lpLZWTable
        push	NewMem
        pop		[esi].lpLZWTable
    .ENDIF

    ; --- Create pointer to first free entry ---
    mov		ebx, [esi].dwLZWTableCount
    push	ebx
    shl		ebx, 2
    add		ebx, [esi].lpLZWTable

    ; --- Find out the size of the string ---
    mov		eax, dwPrefix
    shl		eax, 2
    add		eax, [esi].lpLZWTable
    mov		eax, [eax]
    shr 	eax, 8
    and		eax, 0FFFh	;eax now holds the size of the prefix string
    inc		eax

    ; --- Create the entry DWORD ---
    ; (see DECResetLZW for the format)
    mov		ecx, dwPrefix
    shl		ecx, 20
    shl		eax, 8
    or		ecx, eax
    mov		eax, dbSuffix
    and		eax, 0ffh
    or		ecx, eax

    ; --- Store entry ---
    mov		[ebx], ecx

    inc		[esi].dwLZWTableCount
    pop		ebx	;get saved code of new entry
    mov		cl, [esi].dbCurBitSize
    xor		eax, eax
    inc		eax
    shl		eax, cl
    dec		eax
    .IF		ebx==eax	;bitsize should be increased
        inc		[esi].dbCurBitSize	;increase bitsize
    .ENDIF

    mov		eax, ebx
ret
DECAddToTable endp

;===============================================================================
; DECOutputString
;===============================================================================
; Writes the string of dwCode to the output stream
; returns the first character of the string in al
DECOutputString		proc uses esi edi  ebx lpGifInfo:DWORD, dwCode:DWORD
LOCAL	LastChar:BYTE
LOCAL	AlignedWidth:DWORD		;image width DWORD  ALIGNED (scanline aligned width)
LOCAL	ImageWidth:DWORD		;actual image width (scanline width)
LOCAL	BitOffset:BYTE
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO

    ; --- Set widths ---
    mov		eax, [esi].dwImageWidth
    mov		ImageWidth, eax
    mov		ecx, eax

    ; --- Set aligned width
    .IF [esi].dwImageFormat==IMAGEFORMAT_1BIT
        and		ecx, 11111b
        .IF		ecx!=0		;
            shr		eax,5
            inc		eax
            shl		eax, 5	;get nr of bytes
        .ENDIF
    .ELSEIF	[esi].dwImageFormat==IMAGEFORMAT_4BIT

        and		ecx, 111b
        .IF		ecx!=0		;
            shr		eax,3
            inc		eax
            shl		eax,3	;get nr of bytes
        .ENDIF
    .ELSEIF [esi].dwImageFormat==IMAGEFORMAT_8BIT
        and		ecx, 11b
        .IF		ecx!=0
            shr		eax, 2
            inc		eax
            shl		eax, 2
        .ENDIF
    .ENDIF
    mov		AlignedWidth, eax

    ; --- Set basepointer ---
    mov		edi, [esi].lpLZWTable

    ; --- Get entry ---
    mov		eax, dwCode
    mov		ebx, [edi+4*eax]

    ; --- Get size of string ---
    shr		ebx, 8
    and		ebx, 0fffh
    ; --- 8-BIT output ---
    .IF		[esi].dwImageFormat==IMAGEFORMAT_8BIT
        ; --- 1-BIT output ---
        mov		edx, [esi].lpImageData
        push	ebx

        ; --- Output all codes backwards ---
        mov		eax, dwCode
        dec		ebx				;use ebx as counter

        .WHILE	TRUE
            mov		eax, [edi+4*eax]
            mov		LastChar, al
            push	eax
            push	edx
            ;--- PixelOffset = (PixelCounter/width)*alignedwidth + (PixelCounter % width) ---
            mov		eax, ebx
            add		eax, [esi].PixelCounter

            cdq
            div		ImageWidth
            mov		ecx, edx
            mul		AlignedWidth
            add		ecx, eax

            mov		al, LastChar
            ;RealByteOffset = PixelOffset
            ;RealBitOffset = 0
            pop		edx
            or		[edx+ecx], al

            pop		eax
            shr		eax, 20
            and		eax, 0fffh
            .BREAK .IF ebx==0
            dec		ebx
        .ENDW
        pop		ebx
        add		[esi].PixelCounter, ebx
        mov		al, LastChar

    .ELSEIF [esi].dwImageFormat==IMAGEFORMAT_4BIT
    ; --- 4-BIT output ---
        mov		edx, [esi].lpImageData
        push	ebx

        ; --- Output all codes backwards ---
        mov		eax, dwCode
        dec		ebx				;use ebx as counter

        .WHILE	TRUE
            mov		eax, [edi+4*eax]
            mov		LastChar, al
            push	eax
            push	edx
            ;--- PixelOffset = (PixelCounter/width)*alignedwidth + (PixelCounter % width) ---
            mov		eax, ebx
            add		eax, [esi].PixelCounter

            cdq
            div		ImageWidth
            mov		ecx, edx
            mul		AlignedWidth
            add		ecx, eax
            mov		edx, ecx

            pop     edx
            ;====
            mov		al, LastChar
            shr		ecx, 1

            .IF		!CARRY?		;use high bits
                shl		al, 4
                or		[edx+ecx], al
            .ELSE
                or		[edx+ecx], al
            .ENDIF

            pop		eax
            shr		eax, 20
            and		eax, 0fffh
            .BREAK .IF ebx==0
            dec		ebx
        .ENDW
        pop		ebx
        add		[esi].PixelCounter, ebx
        mov		al, LastChar
    .ELSEIF [esi].dwImageFormat==IMAGEFORMAT_1BIT
    ; --- 1-BIT output ---
        mov		edx, [esi].lpImageData
        push	ebx

        ; --- Output all codes backwards ---
        mov		eax, dwCode
        dec		ebx				;use ebx as counter

        .WHILE	TRUE
            mov		eax, [edi+4*eax]
            mov		LastChar, al
            push	eax
            push	edx
            ;--- PixelOffset = (PixelCounter/width)*alignedwidth + (PixelCounter % width) ---
            mov		eax, ebx
            add		eax, [esi].PixelCounter

            cdq
            div		ImageWidth
            mov		ecx, edx
            mul		AlignedWidth
            add		ecx, eax
            mov		edx, ecx

            and		cl, 111b	;RealBitOffset  = PixelOffset & 111b;
            mov		al, LastChar
            ror		al, 1
            ror		al, cl

            shr		edx, 3		;RealByteOffset = PixelOffset % 8;
            mov		ecx, edx
            pop		edx
            or		[edx+ecx], al

            pop		eax
            shr		eax, 20
            and		eax, 0fffh
            .BREAK .IF ebx==0
            dec		ebx
        .ENDW
        pop		ebx
        add		[esi].PixelCounter, ebx
        mov		al, LastChar

    .ENDIF
ret
DECOutputString	endp
;===============================================================================
; DECIsCodeInTable
;===============================================================================
; Returns TRUE if the given code is in the string table
;
DECIsCodeInTable	proc	uses esi  ebx lpGifInfo:DWORD, dwCode:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO
    mov		ebx, dwCode
    xor		eax, eax
    mov		ecx, [esi].dwLZWTableCount
    .IF		ecx>ebx
        inc		eax
    .ENDIF
    assume	esi:nothing
ret
DECIsCodeInTable	endp
;===============================================================================
; GetNextCode
;===============================================================================
; retrieves the next code from the compressed data stream and loads new bits
; if needed.
GetNextCode		proc	uses esi edi ebx lpGifInfo:DWORD

    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO
    ;mov		edi, [esi].lpGIFImage

    mov		ebx, [esi].CurrentBits

    ; --- Create bitmask for current nr of bits ---
    mov		cl,	[esi].dbCurBitSize
    xor		eax, eax
    inc		eax
    shl		eax, cl
    dec		eax

    ; --- AND ebx with bitmask ---
    and		ebx, eax

    ; --- Shift used bits ---
    mov		cl, [esi].dbCurBitSize
    shr		[esi].CurrentBits, cl

    ; --- Decrease # of bits available ---
    sub		[esi].dbBitsAvailable, cl

    ; --- Load as many bits as possible ---
    .WHILE	[esi].dbBitsAvailable<=24
        invoke	LoadNextByte, esi
    .ENDW

    ; --- Return bits ---
    mov		eax, ebx

assume	esi:nothing
ret
GetNextCode		endp

;===============================================================================
; LoadNextByte
;===============================================================================
; Loads the currentbits entry with new bits
;
LoadNextByte	proc	uses esi edi ebx lpGifInfo:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO

    ; --- Get next byte from datastream ---
    mov		edi, [esi].lpCurrentByte
    .IF		edi==-1		;end of data already reached
        xor		al, al	;return 0 as padding
        jmp		@gotbyte
    .ENDIF
    ;End of subblock reached?
    .IF		edi==[esi].lpLastEOSB
        xor		eax, eax
        mov		al, [edi]	; new subblock size
        .IF		al==0		; if size=0, end of data
            mov		[esi].lpCurrentByte, -1	;EOF indicator
            xor		al, al
            jmp		@gotbyte
        .ELSE 				; set new end of subblock
            inc		edi		; new subblock with size al
            add		eax, edi
            mov		[esi].lpLastEOSB, eax ;set new end of subblock
            add		[esi].lpCurrentByte, 2
            mov		al, [edi]
        .ENDIF
    .ELSE
        mov		al, [edi]			;get next byte
        inc		[esi].lpCurrentByte	;increase for next read
    .ENDIF

    ; --- Got the new byte from the datasteam ---
@gotbyte:
    mov		cl, [esi].dbBitsAvailable	;Get nr of bits available
    mov		edx, [esi].CurrentBits		;Get current bits
    and		eax, 0ffh					;eax = al
    shl		eax, cl						;Shift bits to right position
    or		edx, eax					;OR new bits with current bits
    mov		[esi].CurrentBits, edx		;Save bits
    add		[esi].dbBitsAvailable, 8	;Set new bits avaialble
    assume	esi:nothing
ret
LoadNextByte	endp
;===============================================================================
;	DECInitialize
;===============================================================================
; Initializes the decoder (table for LZW etc)
;
DECInitialize	proc	uses esi edi lpGifInfo:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO
    mov		edi, [esi].lpGIFImage
    ; --- Get initial LZW code size ---
    mov		cl, [edi]
    inc 	cl
    mov		[esi].dbInitBitSize, cl

    ; --- Set current bit size to initial bitsize ---
    mov		[esi].dbCurBitSize, cl

    ; --- Set size of LZW table ---
    .IF		cl<8		;if initial code size is less than 8-bits,
        mov 	cl, 8	;set to 8-bits, if higher, don't change it
    .ENDIF
    ; Calculate size of codetable:
    xor		eax, eax
    inc		eax
    add		cl, 2		; bitsize + 2
    shl		eax, cl		; eax = 2 ^ (bitsize + 2) = 4 * (2^bitsize)
    mov		[esi].dwLZWTableSize, eax

    ; --- Allocate memory for LZW Table and store pointer ---
    invoke	GlobalAlloc, GMEM_FIXED, eax
    mov		[esi].lpLZWTable, eax

    ; --- Reset LZW ---
    invoke	DECResetLZW, esi

    assume	esi:nothing
xor	eax, eax
inc eax
ret
DECInitialize	endp
;===============================================================================
; DECResetLZW
;===============================================================================
; Resets the LZW Table and variables (this is the proper action for a clear
; code (CC) in data
DECResetLZW		proc	uses esi edi ebx lpGifInfo:DWORD
    mov		esi, lpGifInfo
    assume	esi:PTR GIF_INFO
    ; The LZW table has the following format:
    ; An array of DWORDS represents an array of string entries
    ; One entry consists of a prefix code (which is an index to
    ; another entry that should prefix the entry), a string length,
    ; and the suffix byte (this byte should be added to the prefix
    ; to get the full string).
    ; DWORD:
    ; | AAAA AAAA | AAAA BBBB | BBBB BBBB | CCCC CCCC |
    ; ^												^
    ; MSB                                         LSB
    ; AAAAAAAAAAAA:  12-bit index in the LZW Table that indicates the prefix
    ; BBBBBBBBBBBB:  12-bit length of the string
    ; CCCCCCCC	  :   8-bit suffix byte

;int 3
    ; --- reset LZW Table ---
    ; Reset bit size:
    mov		cl, [esi].dbInitBitSize

    mov		[esi].dbCurBitSize, cl
    ; Get initial nr of entries
    ;mov		eax, [esi].ColorCount
    ;NOTE: THE LINE ABOVE WAS PREVIOUSLY USED TO GET THE NR OF ROOT ENTRIES
    ; THIS DIDN'T WORK BECAUSE 1-BIT GIF'S ARE FOR AN UNKNOWN REASON CODED
    ; AS 2-BIT GIFS, SO THE NR OF ROOT ENTRIES HAS TO BE DETERMINED FROM THE
    ; INITIAL BITSIZE
    dec		cl
    xor		eax, eax
    inc		eax
    shl		eax, cl		; 2**codesize
    add		eax, 2
    mov		[esi].dwLZWTableCount, eax
    mov		edx, [esi].lpLZWTable
    ; Fill first {2**codcesize+2} entries
    shl		eax, 2
    add		eax, edx
    xor		ecx, ecx
    inc		ch			;size of sting is set to 1
    .WHILE	edx invalid GIF
        jmp		@failed
    .ENDIF

    ; --- Check first 3 bytes ("GIF" signature) ---
    mov		eax, [edi]
    and		eax, 0ffffffh
    .IF		eax!="FIG"			; GIF Magic signature
        jmp		@failed
    .ENDIF

    ; --- Copy version number ---
    lea		ecx, [esi].GIFVersion
    mov		ax, [edi+3]
    mov		[ecx], ax
    mov		al, [edi+5]
    mov		[ecx+2], al

    ; --- Copy logical screen sizes ---
    xor		eax, eax
    mov		ax, [edi+6]			;Get Logical screen width
    mov		[esi].dwLSWidth, eax
    mov		ax, [edi+8]			;Get Logical screen width
    mov		[esi].dwLSHeight, eax

    ; --- Set global color table flag and size ---
    mov		cl, [edi+10]
    mov		al, cl
    rol		al, 1
    and		al, 1
    mov		[esi].fGlobalCTable, al
    and		cl, 111b
    mov		[esi].dbGCTableSize, cl

    assume 	esi:nothing
xor		eax, eax
inc		eax
ret
@failed:
xor		eax, eax
ret
GIFLoadHeader	endp
;===============================================================================
;	GIFInitalizeDecoder
;===============================================================================
; Initializes decoder
;
GIFInitalizeDecoder	proc	uses esi edi ebx lpGifInfo:DWORD
LOCAL	GCTSize:DWORD
    mov		esi, lpGifInfo
    assume	esi:ptr GIF_INFO

    ; --- EDI points to GIF Data, EBX to end of data ---
    mov		edi, [esi].lpGIFData
    mov		ebx, [esi].dwGIFDataSize
    add		ebx, edi

    add		edi, 13		; skip header and local screen descriptor

    ; --- Get global color tablesize in bytes ---
    mov		cl, [esi].dbGCTableSize
    inc		cl
    xor		eax, eax
    inc		eax
    shl		eax, cl
    mov		ecx, eax
    shl		ecx, 1	;--+--> ecx * 3
    add		ecx, eax;--+
    mov		GCTSize, ecx

    ; --- Skip global color table if available ---
    .IF		[esi].fGlobalCTable!=0
        add		edi, ecx
    .ENDIF
    .IF		edi>=ebx
        jmp		@failed
    .ENDIF

    ; --- Search through the GIF blocks for the first graphic block ---
    .WHILE	edi < ebx
        mov		al,	[edi]					; Get block type
        .IF		al==EXTENSIONBLOCK
            invoke	SkipExtension, edi, ebx
            mov		edi, eax
        .ELSEIF	al==IMAGEDESCRIPTOR
            invoke	ProcessImageDescriptor, lpGifInfo, edi, ebx
            add		edi, 11
            invoke	SkipSubBlocks, edi
            .BREAK
        .ENDIF
    .ENDW

    assume 	esi:nothing
xor		eax, eax
inc		eax
ret
@failed:
xor		eax, eax
ret
GIFInitalizeDecoder	endp

;===============================================================================
; ProcessImageDescriptor
;===============================================================================
; Processes an image desriptor block
ProcessImageDescriptor	proc uses esi edi lpGIFInfo:DWORD, lpStart:DWORD, lpEOF:DWORD
    mov		esi, lpGIFInfo
    assume	esi:ptr GIF_INFO
    mov		edx, lpStart
    xor		eax, eax

    ; --- copy image width & height ---
    mov		ax, [edx+5]
    mov		[esi].dwImageWidth, eax
    mov		ax, [edx+7]
    mov		[esi].dwImageHeight, eax

    ; --- look for local color table ---
    mov		cl, [edx+9]
    mov		al, cl
    rol		cl, 1
    and		cl, 1
    mov		[esi].fLocalCTable, cl
    and		al, 111b
    mov		[esi].dbLCTableSize, al

    ; --- Find out which color table to use ---
    .IF		cl==1		;local color table available
        ; --- Set color count ---
        mov		cl, [esi].dbLCTableSize
        inc		cl
        xor		eax, eax
        inc		eax
        shl		eax, cl
        mov		[esi].ColorCount, eax
        ; --- Set color table pointer ---
        mov		eax, edx
        add		eax, 10	;move to the local color table
        mov		[esi].lpColorTable, eax
    .ELSE				;no local color table, use global color table
        ; --- Set color count ---
        mov		cl, [esi].dbGCTableSize
        inc		cl
        xor		eax, eax
        inc		eax
        shl		eax, cl
        mov		[esi].ColorCount, eax
        ; --- Set color table pointer ---
        mov		eax, [esi].lpGIFData
        add		eax, 13 ;move to the global color table (at offset 13 in file)
        mov		[esi].lpColorTable, eax
    .ENDIF

    ; --- Set pointer to real image data (table based image data)
    mov		eax, edx
    add		eax, 10 				;Skip image descriptor
    .IF		[esi].fLocalCTable==1
        mov		ecx, [esi].ColorCount	;-+
        add		eax, ecx				; + add size of colortable
        shl		ecx, 1					; + (colorcount * 3)
        add		eax, ecx				;-+
    .ENDIF
    mov		[esi].lpGIFImage, eax
    ; --- Set some pointers to initialize the decompressor
    inc		eax
    mov		[esi].lpLastEOSB, eax
    mov		[esi].lpCurrentByte, eax
    assume	esi:nothing
xor eax, eax
inc eax
ret
ProcessImageDescriptor	endp

;
; Skips an extension block that starts at lpStart. EOF is at lpEnd.
; returns pointer to the end of block + 1
; or 0 if failed
SkipExtension proc	lpStart:DWORD, lpEnd:DWORD

    ; --- Get type of extension ---
    mov		edx, lpStart
    mov		al, [edx+1]

    ; --- Skip each type seperately ---
    .IF		al==GRAPHICCONTROLEXTENSTION
        ; Graphic control extension, has a fixed size of 8 bytes
        add		edx, 8
        mov		eax, edx
    .ELSEIF	al==COMMENTEXTENSION
        ; Comment extension, consists of subblocks of text
        add		edx, 2	;move to first subblock
        invoke	SkipSubBlocks, edx

    .ELSEIF	al==PLAINTEXTEXTENSION
        ; Plain text extension, 15 header bytes and subblocks follow
        add		edx, 15
        invoke	SkipSubBlocks, edx

    .ELSEIF al==APPLICATIONEXTENSION
        ; Application extension, 14 header bytes and subblocks follow
        add		edx, 14
        invoke	SkipSubBlocks, edx

    .ENDIF
ret
SkipExtension endp

;===============================================================================
;	SkipSubBlocks
;===============================================================================
; Skips a sequence of subblocks until a block-terminator is found.
; returns a pointer to the end of the block + 1.
; lpStart points to the start of the subblocks
;
SkipSubBlocks	proc lpStart:DWORD
    mov		eax, lpStart
    xor		ecx, ecx
    .WHILE	TRUE
        mov		cl, [eax]	; get size of subblock
        .BREAK .IF cl==0	; size=0 means terminator
        inc		eax
        add		eax, ecx	; add size to pointer
    .ENDW
    inc 	eax
ret
SkipSubBlocks	endp

end


Iczelion's PE Tutorial 1: Overview of PE File Format

Tutorial 1: Overview of PE file format

This is the complete rewrite of the old PE tutorial no1 which I considered the worst tutorial I have ever written. So I decided to replace it with this new one.

PE stands for Portable Executable. It's the native file format of Win32. Its specification is derived somewhat from the Unix Coff (common object file format). The meaning of "portable executable" is that the file format is universal across win32 platform: the PE loader of every win32 platform recognizes and uses this file format even when Windows is running on CPU platforms other than Intel. It doesn't mean your PE executables would be able to port to other CPU platforms without change. Every win32 executable (except VxDs and 16-bit Dlls) uses PE file format. Even NT's kernel mode drivers use PE file format. Thus studying the PE file format gives you valuable insights into the structure of Windows.

Let's jump into the general outline of PE file format without further ado.

DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2
Section ...
Section n

The above picture is the general layout of a PE file. All PE files (even 32-bit DLLs) must start with a simple DOS MZ header. We usually aren't interested in this structure much. It's provided in the case when the program is run from DOS, so DOS can recognize it as a valid executable and can thus run the DOS stub which is stored next to the MZ header. The DOS stub is actually a valid EXE that is executed in case the operating system doesn't know about PE file format. It can simply display a string like "This program requires Windows" or it can be a full-blown DOS program depending on the intent of the programmer. We are also not very interested in DOS stub: it's usually provided by the assembler/compiler. In most case, it simply uses int 21h, service 9 to print a string saying "This program cannot run in DOS mode".

After the DOS stub comes the PE header. The PE header is a general term for the PE-related structure named IMAGE_NT_HEADERS. This structure contains many essential fields that are used by the PE loader. We will be quite familiar with it as you know more about PE file format. In the case the program is executed in the operating system that knows about PE file format, the PE loader can find the starting offset of the PE header from the DOS MZ header. Thus it can skip the DOS stub and go directly to the PE header which is the real file header.

The real content of the PE file is divided into blocks called sections. A section is nothing more than a block of data with common attributes such as code/data, read/write etc. You can think of a PE file as a logical disk. The PE header is the boot sector and the sections are files in the disk. The files can have different attributes such as read-only, system, hidden, archive and so on. I want to make it clear from this point onwards that the grouping of data into a section is done on the common attribute basis: not on logical basis. It doesn't matter how the code/data are used , if the data/code in the PE file have the same attribute, they can be lumped together in a section. You should not think of a section as "data", "code" or some other logical concepts: sections can contain both code and data provided that they have the same attribute. If you have a block of data that you want to be read-only, you can put that data in the section that is marked as read-only. When the PE loader maps the sections into memory, it examines the attributes of the sections and gives the memory block occupied by the sections the indicated attributes.

If we view the PE file format as a logical disk, the PE header as the boot sector and the sections as files, we still don't have enough information to find out where the files reside on the disk, ie. we haven't discussed the directory equivalent of the PE file format. Immediately following the PE header is the section table which is an array of structures. Each structure contains the information about each section in the PE file such as its attribute, the file offset, virtual offset. If there are 5 sections in the PE file, there will be exactly 5 members in this structure array. We can then view the section table as the root directory of the logical disk. Each member of the array is equvalent to the each directory entry in the root directory.

That's all about the physical layout of the PE file format. I'll summarize the major steps in loading a PE file into memory below:

  1. When the PE file is run, the PE loader examines the DOS MZ header for the offset of the PE header. If found, it skips to the PE header.
  2. The PE loader checks if the PE header is valid. If so, it goes to the end of the PE header.
  3. Immediately following the PE header is the section table. The PE header reads information about the sections and maps those sections into memory using file mapping. It also gives each section the attributes as specified in the section table.
  4. After the PE file is mapped into memory, the PE loader concerns itself with the logical parts of the PE file, such as the import table.

The above steps are oversimplification and are based on my own observation. There may be some inaccuracies but it should give you the clear picture of the process.

You should download LUEVELSMEYER's description about PE file format. It's very detailed and you should keep it as a reference.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 2: Detecting a Valid PE File

Tutorial 2: Detecting a Valid PE File

In this tutorial, we will learn how to check if a given file is a valid PE file.
Download the example.

Theory:

How can you verify if a given file is a PE file? That question is difficult to answer. That depends on the length that you want to go to do that. You can verify every data structure defined in the PE file format or you are satisfied with verifying only the crucial ones. Most of the time, it's pretty pointless to verify every single structure in the files. If the crucial structures are valid, we can assume that the file is a valid PE. And we will use that assumption.

The essential structure we will verify is the PE header itself. So we need to know a little about it, programmatically. The PE header is actually a structure called IMAGE_NT_HEADERS. It has the following definition:

IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS

Signature is a dword that contains the value 50h, 45h, 00h, 00h. In more human term, it contains the text "PE" followed by two terminating zeroes. This member is the PE signature so we will use it in verifying if a given file is a valid PE one.
FileHeader is a structure that contains information about the physical layout of the PE file such as the number of sections, the machine the file is targeted and so on.
OptionalHeader is a structure that contains information about the logical layout of the PE file. Despite the "Optional" in its name, it's always present.

Our goal is now clear. If value of the signature member of the IMAGE_NT_HEADERS is equal to "PE" followed by two zeroes, then the file is a valid PE. In fact, for comparison purpose, Microsoft has defined a constant named IMAGE_NT_SIGNATURE which we can readily use.

IMAGE_DOS_SIGNATURE equ 5A4Dh
IMAGE_OS2_SIGNATURE equ 454Eh
IMAGE_OS2_SIGNATURE_LE equ 454Ch
IMAGE_VXD_SIGNATURE equ 454Ch
IMAGE_NT_SIGNATURE equ 4550h

The next question: how can we know where the PE header is? The answer is simple: the DOS MZ header contains the file offset of the PE header. The DOS MZ header is defined as IMAGE_DOS_HEADER structure. You can check it out in windows.inc. The e_lfanew member of the IMAGE_DOS_HEADER structure contains the file offset of the PE header.

The steps are now as follows:

  1. Verify if the given file has a valid DOS MZ header by comparing the first word of the file with the value IMAGE_DOS_SIGNATURE.
  2. If the file has a valid DOS header, use the value in e_lfanew member to find the PE header
  3. Comparing the first word of the PE header with the value IMAGE_NT_HEADER. If both values match, then we can assume that the file is a valid PE.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

SEH struct
PrevLink dd ?    ; the address of the previous seh structure
CurrentHandler dd ?    ; the address of the exception handler
SafeOffset dd ?    ; The offset where it's safe to continue execution
PrevEsp dd ?      ; the old value in esp
PrevEbp dd ?     ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.2",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                 db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileValidPE db "This file is a valid PE",0
FileInValidPE db "This file is not a valid PE",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start proc
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
       mov hFile, eax
       invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
       .if eax!=NULL
          mov hMapping, eax
          invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
          .if eax!=NULL
             mov pMapping,eax
             assume fs:nothing
             push fs:[0]
             pop seh.PrevLink
             mov seh.CurrentHandler,offset SEHHandler
             mov seh.SafeOffset,offset FinalExit
             lea eax,seh
             mov fs:[0], eax
             mov seh.PrevEsp,esp
             mov seh.PrevEbp,ebp
             mov edi, pMapping
             assume edi:ptr IMAGE_DOS_HEADER
             .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                add edi, [edi].e_lfanew
                assume edi:ptr IMAGE_NT_HEADERS
                .if [edi].Signature==IMAGE_NT_SIGNATURE
                   mov ValidPE, TRUE
                .else
                   mov ValidPE, FALSE
                .endif
             .else
                 mov ValidPE,FALSE
             .endif
FinalExit:
             .if ValidPE==TRUE
                 invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .else
                invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .endif
             push seh.PrevLink
             pop fs:[0]
             invoke UnmapViewOfFile, pMapping
          .else
             invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
       .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
    .else
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
    .endif
.endif
invoke ExitProcess, 0
start endp

SEHHandler proc C uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
    mov edx,pFrame
    assume edx:ptr SEH
    mov eax,pContext
    assume eax:ptr CONTEXT
    push [edx].SafeOffset
    pop [eax].regEip
    push [edx].PrevEsp
    pop [eax].regEsp
    push [edx].PrevEbp
    pop [eax].regEbp
    mov ValidPE, FALSE
    mov eax,ExceptionContinueExecution
    ret
SEHHandler endp
end start

Analysis:

The program opens a file and checks if the DOS header is valid, if it is, it checks the PE header if it's valid. If it is, then it assumes the file is a valid PE. In this example, I use structured exception handling (SEH) so that we don't have to check for every possible error: if a fault occurs, we assume that it's because the file is not a valid PE thus giving our program wrong information. Windows itself uses SEH heavily in its parameter validation routines. If you're interested in SEH, read the article by Jeremy Gordon.

The program displays an open file common dialog to the user and when the user chooses an executable file, it opens the file and maps it into memory. Before it goes on with the verification, it sets up a SEH:

   assume fs:nothing
   push fs:[0]
   pop seh.PrevLink
   mov seh.CurrentHandler,offset SEHHandler
   mov seh.SafeOffset,offset FinalExit
   lea eax,seh
   mov fs:[0], eax
   mov seh.PrevEsp,esp
   mov seh.PrevEbp,ebp

We start by assuming the use of fs register as nothing. This must be done because MASM assumes the use of fs register to ERROR. Next we store the address of the previous SEH handler in our structure for use by Windows. We store the address of our SEH handler, the address where the execution can safely resume if a fault occurs, the current values of esp and ebp so that our SEH handler can get the state of the stack back to normal before it resumes the execution of our program.

   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   .if [edi].e_magic==IMAGE_DOS_SIGNATURE

After we are done with setting up SEH, we continue with the verification. We put the address of the first byte of the target file in edi, which is the first byte of the DOS header. For ease of comparison, we tell the assembler that it can assume edi as pointing to the IMAGE_DOS_HEADER structure (which is the truth). We then compare the first word of the DOS header with the string "MZ" which is defined as a constant in windows.inc named IMAGE_DOS_SIGNATURE. If the comparison is ok, we continue to the PE header. If not, we set the value in ValidPE to FALSE, meaning that the file is not a valid PE.

      add edi, [edi].e_lfanew
      assume edi:ptr IMAGE_NT_HEADERS
      .if [edi].Signature==IMAGE_NT_SIGNATURE
         mov ValidPE, TRUE
      .else
         mov ValidPE, FALSE
      .endif

To get to the PE header, we need the value in e_lfanew of the DOS header. This field contains the file offset of the PE header, relative to the file beginning. Thus we add this value to edi and we get to the first byte of the PE header. It's this place that a fault may occur. If the file is really not a PE file, the value in e_lfanew will be incorrect and thus using it amounts to using a wild pointer. If we don't use SEH, we must check the value of the e_lfanew against the file size which is ugly. If all goes well, we compare the first dword of the PE header with the string "PE". Again there is a handy constant named IMAGE_NT_SIGNATURE which we can use. If the result of comparison is true, we assume the file is a valid PE.
If the value in e_lfanew is incorrect, a fault may occur and our SEH handler will get control. It simply restores the stack pointer, bsae pointer and resumes the execution at the safe offset which is at the FinalExit label.

FinalExit:
   .if ValidPE==TRUE
      invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .else
      invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif

The above code is simplicity itself. It checks the value in ValidPE and displays a message to the user accordingly.

   push seh.PrevLink
   pop fs:[0]

When the SEH is no longer used, we dissociate it from the SEH chain.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE tutorial 3: File Header

Tutorial 3: File Header

In this tutorial, we will study the file header portion of the PE header.

Let's summarize what we have learned so far:

  • DOS MZ header is called IMAGE_DOS_HEADER. Only two of its members are important to us: e_magic which contains the string "MZ" and e_lfanew which contains the file offset of the PE header.
  • We use the value in e_magic to check if the file has a valid DOS header by comparing it to the value IMAGE_DOS_SIGNATURE. If both values match, we can assume that the file has a valid DOS header.
  • In order to go to the PE header, we must move the file pointer to the offset specified by the value in e_lfanew.
  • The first dword of the PE header should contain the string "PE" followed by two zeroes. We compare the value in this dword to the value IMAGE_NT_SIGNATURE. If they match, then we can assume that the PE header is valid.

We will learn more about the PE header in this tutorial. The official name of the PE header is IMAGE_NT_HEADERS. To refresh your memory, I show it below.

IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS

Signature is the PE signature, "PE" followed by two zeroes. You already know and use this member.
FileHeader is a structure that contains the information about the physical layout/properies of the PE file in general.
OptionalHeader is also a structure that contains the information about the logical layout inside the PE file.

The most interesting information is in OptionalHeader. However, some fields in FileHeader are also important. We will learn about FileHeader in this tutorial so we can move to study OptionalHeader in the next tutorials.

IMAGE_FILE_HEADER STRUCT
Machine WORD ?
NumberOfSections WORD ?
TimeDateStamp dd ?
PointerToSymbolTable dd ?
NumberOfSymbols dd ?
SizeOfOptionalHeader WORD ?
Characteristics WORD ?
IMAGE_FILE_HEADER ENDS

Field name | Meanings
Machine | The CPU platform the file is intended for. For Intel platform, the value is IMAGE_FILE_MACHINE_I386 (14Ch). I tried to use 14Dh and 14Eh as stated in the pe.txt by LUEVELSMEYER but Windows refused to run it. This field is rarely of interest to us except as a quick way of preventing a program to be executed.
NumberOfSections | The number of sections in the file. We will need to modify the value in this member if we add or delete a section from the file.
TimeDateStamp | The date and time the file is created. Not useful to us.
PointerToSymbolTable | used for debugging.
NumberOfSymbols | used for debugging.
SizeOfOptionalHeader | The size of the OptionalHeader member that immediately follows this structure. Must be set to a valid value.
Characteristics | Contains flags for the file, such as whether this file is an exe or a dll.

In summary, only three members are somewhat useful to us: Machine, NumberOfSections and Characteristics. You would normally not change the values of Machine and Characteristics but you must use the value in NumberOfSections when you're walking the section table.
I'm jumping the gun here but in order to illustrate the use of NumberOfSections, I need to digress briefly to the section table.

The section table is an array of structures. Each structure contains the information of a section. Thus if there are 3 sections, there will be 3 members in this array. You need the value in NumberOfSections so you know how many members there are in the array. You would think that checking for the structure with all zeroes in its members would help. Windows does use this approach. You can verify this fact by setting the value in NumberOfSections to a value higher than the real value and Windows still runs the file without problem. From my observation, I think Windows reads the value in NumberOfSections and examines each structure in the section table. If it finds a structure that contains all zeroes, it terminates the search. Else it would process until the number of structures specified in NumberOfSections is met. Why can't we ignore the value in NumberOfSections? Several reasons. The PE specification doesn't specify that the section table array must end with an all-zero structure. Thus there may be a situation where the last array member is contiguous to the first section, without empty space at all. Another reason has to do with bound imports. The new-style binding puts the information immediately following the section table's last structure array member. Thus you still need NumberOfSections.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 4: Optional Header

Tutorial 4: Optional Header

We have learned about the DOS header and some members of the PE header. Here's the last, the biggest and probably the most important member of the PE header, the optional header.

To refresh your memory, the optional header is a structure that is the last member of IMAGE_NT_HEADERS. It contains information about the logical layout in the PE file. There are 31 fields in this structure. Some of them are crucial and some are not useful. I'll explain only those fields that are really useful.

There is a word that's used frequently in relation to PE file format: RVA
RVA stands for relative virtual address. You know what virtual address is. RVA is a daunting term for such a simple concept. Simply put, an RVA is a distance from a reference point in the virtual address space. I bet you're familiar with file offset: an RVA is exactly the same thing as file offset. However, it's relative to a point in virtual address space, not a file. I'll show you an example. If a PE file loads at 400000h in the virtual address (VA) space and the program starts execution at the virtual address 401000h, we can say that the program starts execution at RVA 1000h. An RVA is relative to the starting VA of the module.
Why does the PE file format use RVA? It's to help reduce the load of the PE loader. Since a module can be relocated anywhere in the virtual address space, it would be a hell for the PE loader to fix every relocatable items in the module. In contrast, if all relocatable items in the file use RVA, there is no need for the PE loader to fix anything: it simply relocates the whole module to a new starting VA. It's like the concept of relative path and absolute path: RVA is akin to relative path, VA is like absolute path.

Field | Meanings
AddressOfEntryPoint | It's the RVA of the first instruction that will be executed when the PE loader is ready to run the PE file. If you want to divert the flow of execution right from the start, you need to change the value in this field to a new RVA and the instruction at the new RVA will be executed first.
ImageBase | It's the preferred load address for the PE file. For example, if the value in this field is 400000h, the PE loader will try to load the file into the virtual address space starting at 400000h. The word "preferred" means that the PE loader may not load the file at that address if some other module already occupied that address range.
SectionAlignment | The granularity of the alignment of the sections in memory. For example, if the value in this field is 4096 (1000h), each section must start at multiples of 4096 bytes. If the first section is at 401000h and its size is 10 bytes, the next section must be at 402000h even if the address space between 401000h and 402000h will be mostly unused.
FileAlignment | The granularity of the alignment of the sections in the file. For example, if the value in this field is 512 (200h), each section must start at multiples of 512 bytes. If the first section is at file offset 200h and the size is 10 bytes, the next section must be located at file offset 400h: the space between file offsets 522 and 1024 is unused/undefined.
MajorSubsystemVersion MinorSubsystemVersion | The win32 subsystem version. If the PE file is designed for Win32, the subsystem version must be 4.0 else the dialog won't have 3-D look.
SizeOfImage | The overall size of the PE image in memory. It's the sum of all headers and sections aligned to SectionAlignment.
SizeOfHeaders | The size of all headers+section table. In short, this value is equal to the file size minus the combined size of all sections in the file. You can also use this value as the file offset of the first section in the PE file.
Subsystem | Tell in which of the NT subsystem the PE file is intended for. For most win32 progs, only two values are used: Windows GUI and Windows CUI (console).
DataDirectory | An array of IMAGE_DATA_DIRECTORY structures. Each structure gives the RVA of an important data structure in the PE file such as the import address table.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 5: Section Table

Tutorial 5: Section Table

Download the example.

Theory:

Up to this tutorial, we learned about the DOS header, the PE header. What remains is the section table. A section table is actually an array of structure immediately following the PE header. The number of the array members is determined by NumberOfSections field in the file header (IMAGE_FILE_HEADER) structure. The structure is called IMAGE_SECTION_HEADER.

IMAGE_SIZEOF_SHORT_NAME equ 8

IMAGE_SECTION_HEADER STRUCT
Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?)
union Misc
PhysicalAddress dd ?
VirtualSize dd ?
ends
VirtualAddress dd ?
SizeOfRawData dd ?
PointerToRawData dd ?
PointerToRelocations dd ?
PointerToLinenumbers dd ?
NumberOfRelocations dw ?
NumberOfLinenumbers dw ?
Characteristics dd ?
IMAGE_SECTION_HEADER ENDS

Again, not all members are useful. I'll describe only the ones that are really important.

Field | Meanings
Name1 | Actually the name of this field is "name" but the word "name" is an MASM keyword so we have to use "Name1" instead. This member contains the name of the section. Note that the maximum length is 8 bytes. The name is just a label, nothing more. You can use any name or even leave this field blank. Note that there is no mention of the terminating null. The name is not an ASCIIZ string so don't expect it to be terminated with a null.
VirtualAddress | The RVA of the section. The PE loader examines and uses the value in this field when it's mapping the section into memory. Thus if the value in this field is 1000h and the PE file is loaded at 400000h, the section will be loaded at 401000h.
SizeOfRawData | The size of the section's data rounded up to the next multiple of file alignment. The PE loader examines the value in this field so it knows how many bytes in the section it should map into memory.
PointerToRawData | The file offset of the beginning of the section. The PE loader uses the value in this field to find where the data in the section is in the file.
Characteristics | Contains flags such as whether this section contains executable code, initialized data, uninitialized data, can it be written to or read from.

Now that we know about IMAGE_SECTION_HEADER structure, let's see how we can emulate the PE loader's job:

  1. Read NumberOfSections in IMAGE_FILE_HEADER so we know how many sections there are in the file.
  2. Use the value in SizeOfHeaders as the file offset of the section table and moves the file pointer to that offset.
  3. Walk the structure array, examining each member.
  4. For each structure, we obtain the value in PointerToRawData and move the file pointer to that offset. Then we read the value in SizeOfRawData so we know how many bytes we should map into memory. Read the value in VirtualAddress and add the value in ImageBase to it to get the virtual address the section should start from. And then we are ready to map the section into memory and mark the attribute of the memory according to the flags in Characteristics.
  5. Walk the array until all the sections are processed.

Note that we didn't make use the the name of the section: it's not really necessary.

Example:

This example opens a PE file and walks the section table, showing the information about the sections in a listview control.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_SECTIONTABLE equ 104
IDC_SECTIONLIST equ 1001

SEH struct
PrevLink dd ? ; the address of the previous seh structure
CurrentHandler dd ? ; the address of the new exception handler
SafeOffset dd ? ; The offset where it's safe to continue execution
PrevEsp dd ? ; the old value in esp
PrevEbp dd ? ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.5",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileInValidPE db "This file is not a valid PE",0
template db "%08lx",0
SectionName db "Section",0
VirtualSize db "V.Size",0
VirtualAddress db "V.Address",0
SizeOfRawData db "Raw Size",0
RawOffset db "Raw Offset",0
Characteristics db "Characteristics",0

.data?
hInstance dd ?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?
NumberOfSections dd ?

.code
start proc
LOCAL seh:SEH
   invoke GetModuleHandle,NULL
   mov hInstance,eax
   mov ofn.lStructSize,SIZEOF ofn
   mov ofn.lpstrFilter, OFFSET FilterString
   mov ofn.lpstrFile, OFFSET buffer
   mov ofn.nMaxFile,512
   mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
   invoke GetOpenFileName, ADDR ofn
   .if eax==TRUE
      invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
      .if eax!=INVALID_HANDLE_VALUE
         mov hFile, eax
         invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
         .if eax!=NULL
            mov hMapping, eax
            invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
            .if eax!=NULL
               mov pMapping,eax
               assume fs:nothing
               push fs:[0]
               pop seh.PrevLink
               mov seh.CurrentHandler,offset SEHHandler
               mov seh.SafeOffset,offset FinalExit
               lea eax,seh
               mov fs:[0], eax
               mov seh.PrevEsp,esp
               mov seh.PrevEbp,ebp
               mov edi, pMapping
               assume edi:ptr IMAGE_DOS_HEADER
               .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                  add edi, [edi].e_lfanew
                  assume edi:ptr IMAGE_NT_HEADERS
                  .if [edi].Signature==IMAGE_NT_SIGNATURE
                     mov ValidPE, TRUE
                  .else
                     mov ValidPE, FALSE
                  .endif
               .else
                  mov ValidPE,FALSE
               .endif
FinalExit:
               push seh.PrevLink
               pop fs:[0]
               .if ValidPE==TRUE
                  call ShowSectionInfo
               .else
                  invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
               .endif
               invoke UnmapViewOfFile, pMapping
           .else
               invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
       .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
    .else
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
    .endif
  .endif
  invoke ExitProcess, 0
  invoke InitCommonControls
start endp

SEHHandler proc C uses pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
   mov edx,pFrame
   assume edx:ptr SEH
   mov eax,pContext
   assume eax:ptr CONTEXT
   push [edx].SafeOffset
   pop [eax].regEip
   push [edx].PrevEsp
   pop [eax].regEsp
   push [edx].PrevEbp
   pop [eax].regEbp
   mov ValidPE, FALSE
   mov eax,ExceptionContinueExecution
   ret
SEHHandler endp

DlgProc proc uses edi esi hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
   LOCAL lvc:LV_COLUMN
   LOCAL lvi:LV_ITEM
   .if uMsg==WM_INITDIALOG
      mov esi, lParam
      mov lvc.imask,LVCF_FMT or LVCF_TEXT or LVCF_WIDTH or LVCF_SUBITEM
      mov lvc.fmt,LVCFMT_LEFT
      mov lvc.lx,80
      mov lvc.iSubItem,0
      mov lvc.pszText,offset SectionName
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,0,addr lvc inc lvc.iSubItem
      mov lvc.fmt,LVCFMT_RIGHT
      mov lvc.pszText,offset VirtualSize
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,1,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset VirtualAddress
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,2,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset SizeOfRawData
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,3,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset RawOffset
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,4,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset Characteristics
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,5,addr lvc
      mov ax, NumberOfSections
      movzx eax,ax
      mov edi,eax
      mov lvi.imask,LVIF_TEXT
      mov lvi.iItem,0
      assume esi:ptr IMAGE_SECTION_HEADER
      .while edi>0
         mov lvi.iSubItem,0
         invoke RtlZeroMemory,addr buffer,9
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8
         lea eax,buffer
         mov lvi.pszText,eax
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].Misc.VirtualSize
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].VirtualAddress
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].SizeOfRawData
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].PointerToRawData
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].Characteristics
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         inc lvi.iItem
         dec edi
         add esi, sizeof IMAGE_SECTION_HEADER
      .endw
   .elseif
      uMsg==WM_CLOSE
         invoke EndDialog,hDlg,NULL
   .else
      mov eax,FALSE
      ret
   .endif
   mov eax,TRUE
   ret
DlgProc endp

ShowSectionInfo proc uses edi
   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS
   mov ax,[edi].FileHeader.NumberOfSections
   movzx eax,ax
   mov NumberOfSections,eax
   add edi,sizeof IMAGE_NT_HEADERS
   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi
   ret
ShowSectionInfo endp
end start

Analysis:

This example reuses the code of the example in PE tutorial 2. After it verifies that the file is a valid PE, it calls a function, ShowSectionInfo.

ShowSectionInfo proc uses edi
   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS

We use edi as the pointer to the data in the PE file. At first, we initialize it to the value of pMapping which is the address of the DOS header. Then we add the value in e_lfanew to it so it now contains the address of the PE header.

   mov ax,[edi].FileHeader.NumberOfSections
   mov NumberOfSections,ax

Since we need to walk the section table, we must obtain the number of sections in this file. That's the value in NumberOfSections member of the file header. Don't forget that this member is of word size.

   add edi,sizeof IMAGE_NT_HEADERS

Edi currently contains the address of the PE header. Adding the size of the PE header to it will make it point at the section table.

   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi

Call DialogBoxParam to show the dialog box containing the listview control. Note that we pass the address of the section table as its last parameter. This value will be available in lParam during WM_INITDIALOG message.

In the dialog box procedure, in response to WM_INITDIALOG message, we store the value of lParam (address of the section table) in esi, the number of sections in edi and then dress up the listview control. When everything is ready, we enter a loop which will insert the info about each section into the listview control. This part is very simple.

      .while edi>0
         mov lvi.iSubItem,0

Put this string in the first column.

         invoke RtlZeroMemory,addr buffer,9
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8
         lea eax,buffer
         mov lvi.pszText,eax

We will display the name of the section but we must convert it to an ASCIIZ string first.

         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi

Then we display it in the first column.
We continue with this scheme until the last value we want to display for this section is displayed. Then we must move to the next structure.

         dec edi
         add esi, sizeof IMAGE_SECTION_HEADER
      .endw

We decrement the value in edi for each section processed. And we add the size of IMAGE_SECTION_HEADER to esi so it contains the address of the next IMAGE_SECTION_HEADER structure.

The steps in walking the section table are:

  1. Verify that the file is a valid PE
  2. Go to the beginning of the PE header
  3. Obtain the number of sections from NumberOfSections field in the file header.
  4. Go to the section table either by adding ImageBase to SizeOfHeaders or by adding the address of the PE header to the size of the PE header. (The section table immediately follows the PE header). If you don't use file mapping, you need to move the file pointer to the section table using SetFilePointer. The file offset of the section table is in SizeOfHeaders.(SizeOfHeaders is a member of IMAGE_OPTIONAL_HEADER)
  5. Process each IMAGE_SECTION_HEADER structure.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 6: Import Table

Tutorial 6: Import Table

We will learn about import table in this tutorial. Let me warn you first. This tutorial is a long and difficult one for those who aren't familiar with the import table. You may need to read this tutorial several times and may even have to examine the related structures under a debugger.

Download the example.

Theory:

First of all, you should know what an import function is. An import function is a function that is not in the caller's module but is called by the module, thus the name "import". The import functions actually reside in one or more DLLs. Only the information about the functions is kept in the caller's module. That information includes the function names and the names of the DLLs in which they reside.
Now how can we find out where in the PE file the information is kept? We must turn to the data directory for the answer. I'll refresh your memory a bit. Below is the PE header:

IMAGE_NT_HEADERS STRUCT
   Signature dd ?
   FileHeader IMAGE_FILE_HEADER <>
   OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS

The last member of the optional header is the data directory:

IMAGE_OPTIONAL_HEADER32 STRUCT
   ....
   LoaderFlags dd ?
   NumberOfRvaAndSizes dd ?
   DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>)
IMAGE_OPTIONAL_HEADER32 ENDS

The data directory is an array of IMAGE_DATA_DIRECTORY structure. A total of 16 members. If you remember the section table as the root directory of the sections in a PE file, you should also think of the data directory as the root directory of the logical components stored inside those sections. To be precise, the data directory contains the locations and sizes of the important data structures in the PE file. Each member contains information about an important data structure.

Member | Info inside
0 | Export symbols
1 | Import symbols
2 | Resources
3 | Exception
4 | Security
5 | Base relocation
6 | Debug
7 | Copyright string
8 | Unknown
9 | Thread local storage (TLS)
10 | Load configuration
11 | Bound Import
12 | Import Address Table
13 | Delay Import
14 | COM descriptor

Only the members painted in gold are known to me. Now that you know what each member of the data directory contains, we can learn about the member in detail. Each member of the data directory is a structure called IMAGE_DATA_DIRECTORY which has the following definition:

IMAGE_DATA_DIRECTORY STRUCT
  VirtualAddress dd ?
  isize dd ?
IMAGE_DATA_DIRECTORY ENDS

VirtualAddress is actually the relative virtual address (RVA) of the data structure. For example, if this structure is for import symbols, this field contains the RVA of the IMAGE_IMPORT_DESCRIPTOR array.
isize contains the size in bytes of the data structure referred to by VirtualAddress.

Here's the general scheme on finding important data structures in a PE file:

  1. From the DOS header, you go to the PE header
  2. Obtain the address of the data directory in the optional header.
  3. Multiply the size of IMAGE_DATA_DIRECTORY with the member index you want: for example if you want to know where the import symbols are, you must multiply the size of IMAGE_DATA_DIRECTORY (8 bytes) with 1.
  4. Add the result to the address of the data directory and you have the address of the IMAGE_DATA_DIRECTORY structure that contains the info about the desired data structure.

Now we will enter into the real discussion about the import table. The address of the import table is contained in the VirtualAddress field of the second member of the data directory. The import table is actually an array of IMAGE_IMPORT_DESCRIPTOR structures. Each structure contains information about a DLL the PE file imports symbols from. For example, if the PE file imports functions from 10 different DLLs, there will be 10 members in this array. The array is terminated by the member which contain all zeroes. Now we can examine the structure in detail:

IMAGE_IMPORT_DESCRIPTOR STRUCT
  union
    Characteristics dd ?
    OriginalFirstThunk dd ?
  ends
  TimeDateStamp dd ?
  ForwarderChain dd ?
  Name1 dd ?
  FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS

The first member of this structure is a union. Actually, the union only provides the alias for OriginalFirstThunk, so you can call it "Characteristics". This member contains the the RVA of an array of IMAGE_THUNK_DATA structures.
What is IMAGE_THUNK_DATA? It's a union of dword size. Usually, we interpret it as the pointer to an IMAGE_IMPORT_BY_NAME structure. Note that IMAGE_THUNK_DATA contains the pointer to an IMAGE_IMPORT_BY_NAME structure: not the structure itself.
Look at it this way: There are several IMAGE_IMPORT_BY_NAME structures. We collect the RVA of those structures (IMAGE_THUNK_DATAs) into an array, terminate it with 0. Then we put the RVA of the array into OriginalFirstThunk.
The IMAGE_IMPORT_BY_NAME structure contains information about an import function. Now let's see what IMAGE_IMPORT_BY_NAME structure looks like:

IMAGE_IMPORT_BY_NAME STRUCT
  Hint dw ?
  Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS

Hint contains the index into the export table of the DLL the function resides in. This field is for use by the PE loader so it can look up the function in the DLL's export table quickly.This value is not essential and some linkers may set the value in this field to 0.
Name1 contains the name of the import function. The name is an ASCIIZ string. Note that Name1's size is defined as byte but it's really a variable-sized field. It's just that there is no way to represent a variable-sized field in a structure. The structure is provided so that you can refer to the data structure with descriptive names.

TimeDateStamp and ForwarderChain are advanced stuff: We will talk about them after you have firm grasp of the other members.

Name1 contains the RVA to the name of the DLL, in short, the pointer to the name of the DLL. The string is an ASCIIZ one.

FirstThunk is very similar to OriginalFirstThunk, ie. it contains an RVA of an array of IMAGE_THUNK_DATA structures(a different array though).
Ok, if you're still confused, look at it this way: There are several IMAGE_IMPORT_BY_NAME structures. You create two arrays, then fill them with the RVAs of those IMAGE_IMPORT_BY_NAME structures, so both arrays contain exactly the same values (i.e. exact duplicate). Now you assign the RVA of the first array to OriginalFirstThunk and the RVA of the second array to FirstThunk.

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
<---
<---
<---
<---
<---
<---
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA

Now you should be able to understand what I mean. Don't be confused by the name IMAGE_THUNK_DATA: it's only an RVA into IMAGE_IMPORT_BY_NAME structure. If you replace the word IMAGE_THUNK_DATA with RVA in your mind, you'll perhaps see it more clearly. The number of array elements in OriginalFirstThunk and FirstThunk array depends on the functions the PE file imports from the DLL. For example, if the PE file imports 10 functions from kernel32.dll, Name1 in the IMAGE_IMPORT_DESCRIPTOR structure will contain the RVA of the string "kernel32.dll" and there will be 10 IMAGE_THUNK_DATAs in each array.
The next question is: why do we need two arrays that are exactly the same? To answer that question, we need to know that when the PE file is loaded into memory, the PE loader will look at the IMAGE_THUNK_DATAs and IMAGE_IMPORT_BY_NAMEs and determine the addresses of the import functions. Then it replaces the IMAGE_THUNK_DATAs in the array pointed to by FirstThunk with the real addresses of the functions. Thus when the PE file is ready to run, the above picture is changed to:

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Function 1
Function 2
Function 3
Function 4
...
Function n
Address of Function 1
Address of Function 2
Address of Function 3
Address of Function 4
...
Address of Function n

The array of RVAs pointed to by OriginalFirstThunk remains unchanged so that if the need arises to find the names of import functions, the PE loader can still find them.
There is a little twist on this *straightforward* scheme. Some functions are exported by ordinal only. It means you don't call the functions by their names: you call them by their positions. In this case, there will be no IMAGE_IMPORT_BY_NAME structure for that function in the caller's module. Instead, the IMAGE_THUNK_DATA for that function will contain the ordinal of the function in the low word and the most significant bit (MSB) of IMAGE_THUNK_DATA set to 1. For example, if a function is exported by ordinal only and its ordinal is 1234h, the IMAGE_THUNK_DATA for that function will be 80001234h. Microsoft provides a handy constant for testing the MSB of a dword, IMAGE_ORDINAL_FLAG32. It has the value of 80000000h.
Suppose that we want to list the names of ALL import functions of a PE file, we need to follow the steps below:

  1. Verify that the file is a valid PE
  2. From the DOS header, go to the PE header
  3. Obtain the address of the data directory in OptionalHeader
  4. Go to the 2nd member of the data directory. Extract the value of VirtualAddress
  5. Use that value to go to the first IMAGE_IMPORT_DESCRIPTOR structure
  6. Check the value of OriginalFirstThunk. If it's not zero, follow the RVA in OriginalFirstThunk to the RVA array. If OriginalFirstThunk is zero, use the value in FirstThunk instead. Some linkers generate PE files with 0 in OriginalFirstThunk. This is considered a bug. Just to be on the safe side, we check the value in OriginalFirstThunk first.
  7. For each member in the array, we check the value of the member against IMAGE_ORDINAL_FLAG32. If the most significant bit of the member is 1, then the function is exported by ordinal and we can extract the ordinal number from the low word of the member.
  8. If the most significant bit of the member is 0, use the value in the member as the RVA into the IMAGE_IMPORT_BY_NAME, skip Hint, and you're at the name of the function.
  9. Skip to the next array member, and retrieve the names until the end of the array is reached (it's null -terminated). Now we are done extracting the names of the functions imported from a DLL. We go to the next DLL.
  10. Skip to the next IMAGE_IMPORT_DESCRIPTOR and process it. Do that until the end of the array is reached (IMAGE_IMPORT_DESCRIPTOR array is terminated by a member with all zeroes in its fields).

Example:

This example opens a PE file and reads the names of all import functions of that file into an edit control. It also shows the values in the IMAGE_IMPORT_DESCRIPTOR structures.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowImportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
PrevLink dd ? ; the address of the previous seh structure
CurrentHandler dd ? ; the address of the new exception handler
SafeOffset dd ? ; The offset where it's safe to continue execution
PrevEsp dd ? ; the old value in esp
PrevEbp dd ? ; The old value in ebp
SEH ends

.data
AppName db "PE tutorial no.6",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
CRLF db 0Dh,0Ah,0
ImportDescriptor db 0Dh,0Ah,"================[ IMAGE_IMPORT_DESCRIPTOR ]=============",0
IDTemplate db "OriginalFirstThunk = %lX",0Dh,0Ah
           db "TimeDateStamp = %lX",0Dh,0Ah
           db "ForwarderChain = %lX",0Dh,0Ah
           db "Name = %s",0Dh,0Ah
           db "FirstThunk = %lX",0
NameHeader db 0Dh,0Ah,"Hint Function",0Dh,0Ah
           db "-----------------------------------------",0
NameTemplate db "%u %s",0
OrdinalTemplate db "%u (ord.)",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
  invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
  invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
  .if lParam==0
    mov eax,wParam
    .if ax==IDM_OPEN
      invoke ShowImportFunctions,hDlg
    .else ; IDM_EXIT
      invoke SendMessage,hDlg,WM_CLOSE,0,0
    .endif
  .endif
.else
  mov eax,FALSE
  ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc C pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
  mov edx,pFrame
  assume edx:ptr SEH
  mov eax,pContext
  assume eax:ptr CONTEXT
  push [edx].SafeOffset
  pop [eax].regEip
  push [edx].PrevEsp
  pop [eax].regEsp
  push [edx].PrevEbp
  pop [eax].regEbp
  mov ValidPE, FALSE
  mov eax,ExceptionContinueExecution
  ret
SEHHandler endp

ShowImportFunctions proc uses edi hDlg:DWORD
  LOCAL seh:SEH
  mov ofn.lStructSize,SIZEOF
  ofn mov ofn.lpstrFilter, OFFSET FilterString
  mov ofn.lpstrFile, OFFSET buffer
  mov ofn.nMaxFile,512
  mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
  invoke GetOpenFileName, ADDR ofn
  .if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
      mov hFile, eax
      invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
      .if eax!=NULL
        mov hMapping, eax
        invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
        .if eax!=NULL
          mov pMapping,eax
          assume fs:nothing
          push fs:[0]
          pop seh.PrevLink
          mov seh.CurrentHandler,offset SEHHandler
          mov seh.SafeOffset,offset FinalExit
          lea eax,seh
          mov fs:[0], eax
          mov seh.PrevEsp,esp
          mov seh.PrevEbp,ebp
          mov edi, pMapping
          assume edi:ptr IMAGE_DOS_HEADER
          .if [edi].e_magic==IMAGE_DOS_SIGNATURE
            add edi, [edi].e_lfanew
            assume edi:ptr IMAGE_NT_HEADERS
            .if [edi].Signature==IMAGE_NT_SIGNATURE
              mov ValidPE, TRUE
            .else
              mov ValidPE, FALSE
            .endif
          .else
            mov ValidPE,FALSE
          .endif
FinalExit:
          push seh.PrevLink
          pop fs:[0]
          .if ValidPE==TRUE
            invoke ShowTheFunctions, hDlg, edi
          .else
            invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke UnmapViewOfFile, pMapping
      .else
          invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
      .endif
      invoke CloseHandle,hMapping
    .else
      invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
    .endif
    invoke CloseHandle, hFile
   .else
   invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
 .endif
 ret
ShowImportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
   ret
AppendText endp

RVAToOffset PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
   mov esi,pFileMap
   assume esi:ptr IMAGE_DOS_HEADER
   add esi,[esi].e_lfanew
   assume esi:ptr IMAGE_NT_HEADERS
   mov edi,RVA ; edi == RVA
   mov edx,esi
   add edx,sizeof IMAGE_NT_HEADERS
   mov cx,[esi].FileHeader.NumberOfSections
   movzx ecx,cx
   assume edx:ptr IMAGE_SECTION_HEADER
   .while ecx>0 ; check all sections
     .if edi>=[edx].VirtualAddress
       mov eax,[edx].VirtualAddress
       add eax,[edx].SizeOfRawData
       .if edi<eax ; The address is in this section
         mov eax,[edx].VirtualAddress
         sub edi,eax
         mov eax,[edx].PointerToRawData
         add eax,edi ; eax == file offset
         ret
       .endif
     .endif
     add edx,sizeof IMAGE_SECTION_HEADER
     dec ecx
   .endw
   assume edx:nothing
   assume esi:nothing
   mov eax,edi
   ret
RVAToOffset endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE
   invoke SetDlgItemText,hDlg,IDC_EDIT,0
   invoke AppendText,hDlg,addr buffer
   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress
   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping
   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)
     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping
     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp
     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif
     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax
     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0
       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal
       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME
       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText
ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx
ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4
    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR
  .endw
  ret
ShowTheFunctions endp
end start

Analysis:

The program shows an open file dialog box when the user clicks Open in the menu. It verifies that the file is a valid PE and then calls ShowTheFunctions.

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE

Reserve 512 bytes of stack space for string operation.

   invoke SetDlgItemText,hDlg,IDC_EDIT,0

Clear the text in the edit control

   invoke AppendText,hDlg,addr buffer

Insert the name of the PE file into the edit control. AppendText just sends EM_REPLACESEL messages to append the text to the edit control. Note that it sends EM_SETSEL with wParam=-1 and lParam=0 to the edit control to move the cursor to the end of the text.

   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress

Obtain the RVA of the import symbols. edi at first points to the PE header. We use it to go to the 2nd member of the data directory array and obtain the value of VirtualAddress member.

   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping

Here comes one of the pitfalls for newcomers to PE programming. Most of the addresses in the PE file are RVAs and RVAs are meaningful only when the PE file is loaded into memory by the PE loader. In our case, we do map the file into memory but not the way the PE loader does. Thus we cannot use those RVAs directly. Somehow we have to convert those RVAs into file offsets. I write RVAToOffset function just for this purpose. I won't analyze it in detail here. Suffice to say that it checks the submitted RVA against the starting-ending RVAs of all sections in the PE file and use the value in PointerToRawData field in the IMAGE_SECTION_HEADER structure to convert the RVA to file offset.
To use this function, you pass it two parameters: the pointer to the memory mapped file and the RVA you want to convert. It returns the file offset in eax. In the above snippet, we must add the pointer to the memory mapped file to the file offset to convert it to virtual address. Seems complicated, huh? :)

    assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
.while !([edi].OriginalFirstThunk==0 &&
[edi].TimeDateStamp==0   && [edi].ForwarderChain==0 &&
[edi].Name1==0 && [edi].FirstThunk==0)

edi now points to the first IMAGE_IMPORT_DESCRIPTOR structure. We will walk the array until we find the structure with zeroes in all members which denotes the end of the array.

     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping

We want to display the values of the current IMAGE_IMPORT_DESCRIPTOR structure in the edit control. Name1 is different from the other members since it contains the RVA to the name of the dll. Thus we must convert it to a virtual address first.

     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp

Display the values of the current IMAGE_IMPORT_DESCRIPTOR.

     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif

Next we prepare to walk the IMAGE_THUNK_DATA array. Normally we would choose to use the array pointed to by OriginalFirstThunk. However, some linkers errornously put 0 in OriginalFirstThunk thus we must check first if the value of OriginalFirstThunk is zero. If it is, we use the array pointed to by FirstThunk instead.

     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax

Again, the value in OriginalFirstThunk/FirstThunk is an RVA. We must convert it to virtual address.

     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0

Now we are ready to walk the array of IMAGE_THUNK_DATAs to look for the names of the functions imported from this DLL. We will walk the array until we find an entry which contains 0.

       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal

The first thing we do with the IMAGE_THUNK_DATA is to test it against IMAGE_ORDINAL_FLAG32. This test checks if the most significant bit of the IMAGE_THUNK_DATA is 1. If it is, the function is exported by ordinal so we have no need to process it further. We can extract its ordinal from the low word of the IMAGE_THUNK_DATA and go on with the next IMAGE_THUNK_DATA dword.

       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME

If the MSB of the IAMGE_THUNK_DATA is 0, it contains the RVA of IMAGE_IMPORT_BY_NAME structure. We need to convert it to virtual address first.

       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText

Hint is a word-sized field. We must convert it to a dword-sized value before submitting it to wsprintf. And we print both the hint and the function name in the edit control

ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx

In the case the function is exported by ordinal only, we zero out the high word and display the ordinal.

ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4

After inserting the function name/ordinal into the edit control, we skip to the next IMAGE_THUNK_DATA.

    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR

When all IMAGE_THUNK_DATA dwords in the array are processed, we skip to the next IMAGE_IMPORT_DESCRIPTOR to process the import functions from other DLLs.

Appendix:

It would be incomplete if I don't mention something about bound import. In order to explain what it is, I need to digress a bit. When the PE loader loads a PE file into memory, it examines the import table and loads the required DLLs into the process address space. Then it walks the IMAGE_THUNK_DATA array much like we did and replaces the IMAGE_THUNK_DATAs with the real addresses of the import functions. This step takes time. If somehow the programmer can predict the addresses of the functions correctly, the PE loader doesn't have to fix the IMAGE_THUNK_DATAs each time the PE file is run. Bound import is the product of that idea.
To put it in simple terms, there is a utility named bind.exe that comes with Microsoft compilers such as Visual Studio that examines the import table of a PE file and replaces the IMAGE_THUNK_DATA dwords with the addresses of the import functions.When the file is loaded, the PE loader must check if the addresses are valid. If the DLL versions do not match the ones in the PE files or if the DLLs need to be relocated, the PE loader knows that the precomputed addresses are not valid thus it must walk the array pointed to by OriginalFirstThunk to calculate the new addresses of import functions.
Bound import doesn't have much significance in our example because we use OriginalFirstThunk by default. For more information about the bound import, I recommmend LUEVELSMEYER's pe.txt.

[Iczelion's Win32 Assembly Homepage]


Iczelion's PE Tutorial 7: Export Table

Tutorial 7: Export Table

We have learned about one part of the dynamic linking, namely the import table, in the previous tutorial. Now we will learn about the other side of the coin, the export table.

Download the example.

Theory:

When the PE loader runs a program, it loads the associated DLLs into the process address space. It then extracts information about the import functions from the main program. It uses the information to search the DLLs for the addresses of the functions to be patched into the main program. The place in the DLLs where the PE loader looks for the addresses of the functions is the export table.

When a DLL/EXE exports a function to be used by other DLL/EXE, it can do so in two ways: it can export the function by name or by ordinal only. Say if there is a function named "GetSysConfig" in a DLL, it can choose to tell the other DLLs/EXEs that if they want to call the function, they must specify it by its name, ie. GetSysConfig. The other way is to export by ordinal. What's an ordinal? An ordinal is a 16-bit number that uniquely identifies a function in a particular DLL. This number is unique only within the DLL it refers to. For example, in the above example, the DLL can choose to export the function by ordinal, say, 16. Then the other DLLs/EXEs which want to call this function must specify this number in GetProcAddress. This is called export by ordinal only.

Export by ordinal only is strongly discouraged because it can cause a maintenance problem for the DLL. If the DLL is upgraded/updated, the programmer of that DLL cannot alter the ordinals of the functions else other programs that depend on the DLL will break.

Now we can examine the export structure. As with import table, you can find where the export table is from looking at the data directory. In this case, the export table is the first member of the data directory. The export structure is called IMAGE_EXPORT_DIRECTORY. There are 11 members in the structure but only some of them are really used.

Field Name | Meaning
nName | The actual name of the module. This field is necessary because the name of the file can be changed. If it's the case, the PE loader will use this internal name.
nBase | A number that you must bias against the ordinals to get the indexes into the address-of-function array.
NumberOfFunctions | Total number of functions/symbols that are exported by this module.
NumberOfNames | Number of functions/symbols that are exported by name. This value is not the number of ALL functions/symbols in the module. For that number, you need to check NumberOfFunctions. This value can be 0. In that case, the module may export by ordinal only. If there is no function/symbol to be exported in the first case, the RVA of the export table in the data directory will be 0.
AddressOfFunctions | An RVA that points to an array of RVAs of the functions/symbols in the module. In short, RVAs to all functions in the module are kept in an array and this field points to the head of that array.
AddressOfNames | An RVA that points to an array of RVAs of the names of functions in the module.
AddressOfNameOrdinals | An RVA that points to a 16-bit array that contains the ordinals associated with the function names in the AddressOfNames array above.

Just reading the above table may not give you the real picture of the export table. The simplified explanation below will clarify the concept.

The export table exists for use by the PE loader. First of all, the module must keep the addresses of all exported functions somewhere so the PE loader can look them up. It keeps them in an array that is pointed to by the field AddressOfFunctions. The number of elements in the array is kept in NumberOfFunctions. Thus if the module exports 40 functions, it must have 40 members in the array pointed to by AddressOfFunctions and NumberOfFunctions must contain a value 40. Now if some functions are exported by names, the module must keep the names in the file. It keeps the RVAs to the names in an array so the PE loader can look them up. That array is pointed to by AddressOfNames and the number of names in NumberOfNames. Think about the job of the PE loader, it knows the names of the functions, it must somehow obtain the addresses of those functions. Up to now, the module has two arrays: the names and the addresses but there is no linkage between them. Thus we need something that relates the names of the functions to their addresses. The PE specification uses indexes into the address array as that essential linkage. Thus if the PE loader finds the name it looks for in the name array, it can obtain the index into the address table for that name too. The indexes are kept in another array (the last one) pointed to by the field AddressOfNameOrdinals. Since this array exists as the linkage between the names and the addresses, it must have exactly the same number of elements as the name array, ie. each name can have one and only one associated address. The reverse is not true: an address may have several names associated with it. Thus we can have "aliases" that refer to the same address. To make the linkage works, both name and index arrays must run in parallel, ie. the first element in the index array must hold the index for the first name and so on.

AddressOfNames AddressOfNameOrdinals
|
|
RVA of Name 1
RVA of Name 2
RVA of Name 3
RVA of Name 4
...
RVA of Name N
<-->
<-->
<-->
<-->
...
<-->
Index of Name 1
Index of Name 2
Index of Name 3
Index of Name 4
...
Index of Name N

An example or two is in order. If we have the name of an export function and we need to get its address in the module, we can do like this:

  1. Go to the PE header
  2. Read the virtual address of the export table in the data directory
  3. Go to the export table and obtain the number of names (NumberOfNames)
  4. Walk the arrays pointed to by AddressOfNames and AddressOfNameOrdinals in parallel, searching for the matching name. If the name is found in the AddressOfNames array, you must extract the value in the associated element in the AddressOfNameOrdinals array. For example, if you find the RVA of the matching name in 77th element of the AddressOfNames array, you must extract the value stored in the 77th element of the AddressOfNameOrdinals array. If you walk the array until NumberOfNames elements are examined, you know that the name is not in this module.
  5. Use the value from the AddressOfNameOrdinals array as the index into the AddressOfFunctions array. Say, if the value is 5, you must extract the value in the 5th element of the AddressOfFunctions array. That value is the RVA of the function.

Now we can turn our attention to the nBase member of the IMAGE_EXPORT_DIRECTORY structure. You already know that the AddressOfFunctions array contains the addresses of all export symbols in a module. And the PE loader uses the indexes into this array to find the addresses of the functions. Let's imagine the scenario where we use the indexes into this array as the ordinals. Since the programmers can specify the starting ordinal number in .def file, like 200, it means that there must be at least 200 elements in the AddressOfFunctions array. Furthermore the first 200 elements are not used but they must exist so that the PE loader can use the indexes to find the correct addresses. This is not good at all. The nBase member exists to solve this problem. If the programmer specifies the starting ordinal of 200, the value in nBase would be 200. When the PE loader reads the value in nBase, it knows that the first 200 elements do not exist and that it should subtract the ordinal by the value in nBase to obtain the true index into the AddressOfFunctions array. With the use of nBase, there is no need to provide 200 empty elements.

Note that nBase doesn't affect the values in the AddressOfNameOrdinals array. Despite the name "AddressOfNameOrdinals", this array contains the true indexes into the AddressOfFunctions array, not the ordinals.

With the discussion of nBase out of the way, we can continue to the next example.
Suppose that we have an ordinal of a function and we need to obtain the address of that function, we can do it like this:

  1. Go to the PE header
  2. Obtain the RVA of the export table from the data directory
  3. Go to the export table and obtain the value of nBase.
  4. Subtract the ordinal by the value in nBase and you have the index into the AddressOfFunctions array.
  5. Compare the index with the value in NumberOfFunctions. If the index is larger or equal to the value in NumberOfFunctions, the ordinal is invalid.
  6. Use the index to obtain the RVA of the function in the AddressOfFunctions array.

Note that obtaining the address of a function from an ordinal is much easier and faster than using the name of the function. There is no need to walk the AddressOfNames and AddressOfNameOrdinals arrays. The performance gain, however, must be balanced against the difficulty in the maintaining the module.

In conclusion, if you want to obtain the address of a function from its name, you need to walk both AddressOfNames and AddressOfNameOrdinals arrays to obtain the index into the AddressOfFunctions array. If you have the ordinal of the function, you can go directly to the AddressOfFunctions array after the ordinal is biased by nBase.

If a function is exported by name, you can use either its name or its ordinal in GetProcAddress. But what if the function is exported by ordinal only? We come to that now.
"A function is exported by ordinal only" means the function doesn't have entries in both AddressOfNames and AddressOfNameOrdinals arrays. Remember the two fields, NumberOfFunctions and NumberOfNames. The existence of these two fields is the evidence that some functions may not have names. The number of functions must be at least equal to the number of names. The functions that don't have names are exported by their ordinals only. For example, if there are 70 functions but only 40 entries in the AddressOfNames array, it means there are 30 functions in the module that are exported by their ordinals only. Now how can we find out which functions are exported by ordinals only? It's not easy. You must find that out by exclusion, ie. the entries in the AddressOfFunctions array that are not referenced by the AddressOfNameOrdinals array contain the RVAs of the functions that are exported by ordinals only.

Example:

This example is similar to the one in the previous tutorial. However, it displays the values of some members of IMAGE_EXPORT_DIRECTORY structure and also lists the RVAs, ordinals, and names of the exported functions. Note that this example doesn't list the functions that are exported by ordinals only.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowExportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
   PrevLink dd ?
   CurrentHandler dd ?
   SafeOffset dd ?
   PrevEsp dd ?
   PrevEbp dd ?
SEH ends

.data
AppName db "PE tutorial no.7",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
NoExportTable db "No export information in this file",0
CRLF db 0Dh,0Ah,0
ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah
            db "Name of the module: %s",0Dh,0Ah
            db "nBase: %lu",0Dh,0Ah
            db "NumberOfFunctions: %lu",0Dh,0Ah
            db "NumberOfNames: %lu",0Dh,0Ah
            db "AddressOfFunctions: %lX",0Dh,0Ah
            db "AddressOfNames: %lX",0Dh,0Ah
            db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0
Header db "RVA Ord. Name",0Dh,0Ah
       db "----------------------------------------------",0
template db "%lX %u %s",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
   invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
   .if lParam==0
     mov eax,wParam
     .if ax==IDM_OPEN
       invoke ShowExportFunctions,hDlg
     .else ; IDM_EXIT
       invoke SendMessage,hDlg,WM_CLOSE,0,0
     .endif
   .endif
.else
   mov eax,FALSE
   ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc C pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
mov edx,pFrame
assume edx:ptr SEH
mov eax,pContext
assume eax:ptr CONTEXT
push [edx].SafeOffset
pop [eax].regEip
push [edx].PrevEsp
pop [eax].regEsp
push [edx].PrevEbp
pop [eax].regEbp
mov ValidPE, FALSE
mov eax,ExceptionContinueExecution
ret
SEHHandler endp

ShowExportFunctions proc uses edi hDlg:DWORD
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
   invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
   .if eax!=INVALID_HANDLE_VALUE
     mov hFile, eax
     invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
     .if eax!=NULL
       mov hMapping, eax
       invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
       .if eax!=NULL
         mov pMapping,eax
         assume fs:nothing
         push fs:[0]
         pop seh.PrevLink
         mov seh.CurrentHandler,offset SEHHandler
         mov seh.SafeOffset,offset FinalExit
         lea eax,seh
         mov fs:[0], eax
         mov seh.PrevEsp,esp
         mov seh.PrevEbp,ebp
         mov edi, pMapping
         assume edi:ptr IMAGE_DOS_HEADER
         .if [edi].e_magic==IMAGE_DOS_SIGNATURE
           add edi, [edi].e_lfanew
           assume edi:ptr IMAGE_NT_HEADERS
           .if [edi].Signature==IMAGE_NT_SIGNATURE
             mov ValidPE, TRUE
           .else
             mov ValidPE, FALSE
           .endif
         .else
           mov ValidPE,FALSE
         .endif
FinalExit:
         push seh.PrevLink
         pop fs:[0]
         .if ValidPE==TRUE
           invoke ShowTheFunctions, hDlg, edi
         .else
           invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
         .endif
         invoke UnmapViewOfFile, pMapping
       .else
         invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle,hMapping
     .else
       invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
     .endif
     invoke CloseHandle, hFile
   .else
     invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
.endif
ret
ShowExportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
ret
AppendText endp

RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
mov esi,pFileMap
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov edi,RVA ; edi == RVA
mov edx,esi
add edx,sizeof IMAGE_NT_HEADERS
mov cx,[esi].FileHeader.NumberOfSections
movzx ecx,cx
assume edx:ptr IMAGE_SECTION_HEADER
.while ecx>0
   .if edi>=[edx].VirtualAddress
     mov eax,[edx].VirtualAddress
     add eax,[edx].SizeOfRawData
     .if edi<eax
       mov eax,[edx].VirtualAddress
       sub edi,eax
       mov eax,[edx].PointerToRawData
       add eax,edi
       add eax,pFileMap
       ret
     .endif
   .endif
   add edx,sizeof IMAGE_SECTION_HEADER
   dec ecx
.endw
assume edx:nothing
assume esi:nothing
mov eax,edi
ret
RVAToFileMap endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
LOCAL temp[512]:BYTE
LOCAL NumberOfNames:DWORD
LOCAL Base:DWORD

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif
invoke SetDlgItemText,hDlg,IDC_EDIT,0
invoke AppendText,hDlg,addr buffer
invoke RVAToFileMap,pMapping,edi
mov edi,eax
assume edi:ptr IMAGE_EXPORT_DIRECTORY
mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp
invoke AppendText,hDlg,addr Header
push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base
invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax
.while NumberOfNames>0
   invoke RVAToFileMap,pMapping,dword ptr [esi]
   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   shl edx,2
   add edx,edi
   add ecx,Base
   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp
   dec NumberOfNames
   add esi,4
   add ebx,2
.endw
ret
ShowTheFunctions endp
end start

Analysis:

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif

After the program verifies that the file is a valid PE, it goes to the data directory and obtains the virtual address of the export table. If the virtual address is zero, the file doesn't have any exported symbol.

mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp

We display the important information in the IMAGE_EXPORT_DIRECTORY structure in the edit control.

push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base

Since we want to enumerate all function names, we need to know how many names there are in the export table. nBase is used when we want to convert the indexes into the AddressOfFunctions array into ordinals.

invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax

The addresses of the three arrays are stored in esi, ebx, and edi, ready to be accessed.

.while NumberOfNames>0

Continue until all names are processed.

   invoke RVAToFileMap,pMapping,dword ptr [esi]

Since esi points to an array of RVAs of the exported names, dereference it will give the RVA of the current name. We convert it to the virtual address, to be used in wsprintf later.

   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   add ecx,Base

ebx points to the array of ordinals. Its array elements are word-size. Thus we need to convert the value into a dword first. edx and ecx contain the index into the AddressOfFunctions array. We will use edx as the pointer into the AddressOfFunctions array. We add the value of nBase to ecx to obtain the ordinal number of the function.

   shl edx,2
   add edx,edi

We multiply the index by 4 (each element in the AddressOfFunctions array is 4 bytes in size) and then add the address of the AddressOfFunctions array to it. Thus edx points to the RVA of the function.

   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp

We display the RVA, ordinal, and the name of the function in the edit control.

   dec NumberOfNames
   add esi,4
   add ebx,2
.endw

Update the counter and the addresses of the current elements in AddressOfNames and AddressOfNameOrdinals arrays. Continue until all names are processed.

[Iczelion's Win32 Assembly Homepage]


Version 1.0
I N D E X

Tut1: Vue d'ensemble sur le format d'un PE File (Overview of PE File Format)
Tut2: Détecter un PE File Valide (Detecting a Valid PE File)
Tut3: Le File Header
Tut4: Les membres facultatifs du header (Optional Header)
Tut5: Table des Sections (Section Table)
Tut6: La Table des Importations (Import Table)
Tut7: La Table des Exportations (Export Table)

PE.txt de LUEVELSMEYER
PE.html de plusieurs auteurs regroupés par Christal
GlobalPE.html représentation graphique d'un PE File

V O C A B U L A I R E

PE File:
C'est un fichier de type PE, c'est à dire un exécutable Win32, un programme 32bits.

Format d'un PE File:
C'est le format d'un programme Win32, la manière dont il est intérieurement organisé. C'est donc le fait de savoir combien de sections il comprend et lesquelles (.data, .data?, .const, .code)... savoir à quelle adresse il commence (00400000) son RVA (00401000)....c'est son ossature en peu.

Offset(de fichier) contre RVA:
Vous trouverez parfois l'expression 'offset(de fichier)' c'est seulement un offset réel, c'est pour faire une différence avec le RVA.

RVA:
RVA (relative virtual address) est le diminutif de 'l'adresse virtuelle relative'. En général un programme Win32 est mappé en mémoire à partir de l'adresse 00400000h. De 00400000h à 004010000h se trouve l'entête du programme le Header global (le MZ header du Dos, le Dos Stub, le PE header, et la table des sections). Après l'adresse 00401000h on arrive dans le programme en lui même, dans ses sections (.data, .data?, .const, .code...)
Donc le vrai programme en lui-même commence en 00401000h alors qu'en réalité en globalité il commence en 00400000h. On se sert du point 00401000h comme référence pour nos adresses, le RVA=1000h (00401000h-00400000h).

Morgatte


PE File Format

Le PE Header

PE File Format

Avant propos

De plus en plus il arrive que des protections renforcées "s'amusent" avec le PE Header, les tables d'Import, etc...

Loin de vouloir faire un tour exhaustif de la question, le texte qui suit se veut juste une introduction à la comprehension de l'architecture du PE HEADER.

Le PE HEADER répond à une architecture de ce type:

Dos-Stub

File-Header

Optional Header

Data Directories

RVA's to directories in sections

Sections headers

RVA's to section borders

Section 1

...

Section n

L'exemple de Notepad.exe (version Win 95)

Le PE HEADER commence en 0x00 par la compatibilité MS-DOS: le MZ Header:

00000000 4D5A 9000 0300 0000 0400 0000 FFFF 0000 MZ..............
00000010 B800 0000 0000 0000 4000 0000 0000 0000 ........@.......
00000020 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030 0000 0000 0000 0000 0000 0000 8000 0000 ................
00000040 0E1F BA0E 00B4 09CD 21B8 014C CD21 5468 ........!..L.!Th
00000050 6973 2070 726F 6772 616D 2063 616E 6E6F is program canno
00000060 7420 6265 2072 756E 2069 6E20 444F 5320 t be run in DOS
00000070 6D6F 6465 2E0D 0D0A 2400 0000 0000 0000 mode....$.......

MZ Header

WORD e_magic;      > Nombre Magique ( 4D5A )
WORD e_cblp;       > Bytes on last page of file
WORD e_cp;         > Pages in file
WORD e_crlc;       > Relocations
WORD e_cparhdr;    > Size of header in paragraphs
WORD e_minalloc;   > Minimum extra paragraphs needed
WORD e_maxalloc;   > Maximum extra paragraphs needed
WORD e_ss;         > Initial (relative) SS value
WORD e_sp;         > Initial SP value
WORD e_csum;       > Checksum
WORD e_ip;         > Initial IP value
WORD e_cs;         > Initial (relative) CS value
WORD e_lfarlc;     > File address of relocation table
WORD e_ovno;       > Overlay number
WORD e_res[4];     > Reserved words
WORD e_oemid;      > OEM identifier (for e_oeminfo)
WORD e_oeminfo;    > OEM information; e_oemid specific
WORD e_res2[10];   > Reserved words
DWORD e_lfanew;    > File address of new exe header
Address     Values     Meaning

00000000     5A4D    Signature: MZ
00000002     0090    Extra Bytes
00000004     0003    Pages
00000006     0000    Reloc Items
00000008     0004    Header Size
0000000A     0000    Min Alloc
0000000C     FFFF    Max Alloc
0000000E     0000    Initial SS
00000010     00B8    Initial SP
00000012     0000    Check Sum
00000014     0000    Initial IP
00000016     0000    Initial CS
00000018     0040    Reloc Table
0000001A     0000    Overlay

Le premier champ, e_magic est aussi appelé magic number.Ce champ est utilisé
pour identifier un type de fichier compatible MS-Dos. Tous les exécutables compatibles MS-Dos placent cette
valeur à 0x54AD, qui représente le caractère ASCII "MZ". Le champ le plus important
pour Windows est le dernier: e_lfanew, indiquant à quel offset le PE Header est situé.

PE File Header:

00000080 5045 0000 4C01 0600 8D54 F32F 0000 0000 PE..L....T./....
00000090 0000 0000 E000 0E01                     ........

WORD   Machine Type                        ( 014C ) i386
WORD   Nombre de Sections                  ( 0006 )
DWORD  Time/Date                           ( 2FF3548D )
DWORD  Pointeur vers la table des Symboles ( 00000000 )
DWORD  Nombre de Symboles                  ( 00000000 )
WORD   Taille de l'Optional Header         ( 00E0 )
WORD   Caracteristiques                    ( 010E )

Address      Values        Meaning
00000080    00004550     Signature: PE
00000084        014C     Machine: 014C=I386
00000086        0005     Number of Sections
00000088    2FF3548D     Time/Date Stamp
0000008C    00000000     Pointer to Symbol Table
00000090    00000000     Number of Symbols
00000094        00E0     Optional Header Size
00000096        010E     Characteristics

PE Optional Header:

Informations données par un éditeur hexadécimal

00000090                     0B01 0232 003A 0000         ...2.:..
000000A0 004E 0000 0006 0000 0010 0000 0010 0000 .N..............
000000B0 0050 0000 0000 4000                     .P....@.

Informations données par ProcDump

Header lnfos
Entry Point :   00001000
Size of image : 0000D000
Image Base :    00400000

WORD    Magic                   ( 010B )
BYTE    MajorLinkerVersion      ( 02 )
BYTE    MinorLinkerVersion      ( 32 )
DWORD   SizeOfCode              ( 00003A00 )
DWORD   SizeOfInitializedData   ( 00004E00 )
DWORD   SizeOfUninitializedData ( 00000600 )
DWORD   AddressOfEntryPoint     ( 00001000 )
DWORD   BaseOfCode              ( 00001000 )
DWORD   BaseOfData              ( 00005000 )

DWORD   ImageBase               ( 00004000 )

000000B0                     0010 0000 0002 0000         ........
000000C0 0100 0000 0000 0000 0400 0000 0000 0000 ................
000000D0 00D0 0000 0004 0000 197B 0100 0200 0000 .........{......
000000E0 0000 1000 0010 0000 0000 1000 0010 0000 ................
000000F0 0000 0000 1000 0000                     ........

SectionAlignment            00 10 00 00 ; 16-bytes-alignment
FileAlignment               00 20 00 00 ; 32-bytes-alignment
MajorOperatingSystemVersion 01 00       ;
MinorOperatingSystemVersion 00 00       ;
MajorImageVersion           00 00       ; version 0.0
MinorImageVersion           00 00       ;
MajorSubsystemVersion       04 00       ; Win32 4.0
MinorSubsystemVersion       00 00       ;
Win32VersionValue           00 00 00 00 ;
SizeOfImage                 00 D0 00 00 ; checksum
SizeOfHeaders               00 04 00 00 ; offset de la première section
CheckSum                    19 7B 01 00 ; non utilisé pour les non-drivers
Subsystem                   02 00       ; Win32 console
DllCharacteristics          00 00       ; inutilisé (pas une DLL)
SizeOfStackReserve          00 00 10 00 ; 1 MB stack
SizeOfStackCommit           00 10 00 00 ; 4 KB pour démarrer
SizeOfHeapReserve           00 00 10 00 ; 1 MB heap
SizeOfHeapCommit            00 10 00 00 ; 4 KB to start with
LoaderFlags                 00 00 00 00 ;
NumberOfRvaAndSizes         10 00 00 00 ; constant

A noter que l'alignement des sections n'est pas identique en RAM et sur le DD

Informations données par un éditeur hexadécimal

000000F0 0000 0000 1000 0000 0000 0000 0000 0000 ................
00000100 0070 0000 9A0C 0000 0080 0000 C830 0000 .p...........0..
00000110 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000120 00C0 0000 F407 0000 0000 0000 0000 0000 ................
00000130 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000140 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000150 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000160 0000 0000 0000 0000 0000 0000 0000 0000 ................

Informations données par procdump:

PE directory informations (HEX)

Export Table
Import Table
Resource
Exception
Security
Relocations
Debug Datas
Description
GIobal PTR
TLS table
Load Config
Bound Import
Import Address Table

RVA
00000000
00007000
00008000
00000000
00000000
0000C000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

Size
00000000
00000C9A
000030C8
00000000
00000000
000007F4
00000000
00000000
00000000
00000000
00000000
00000000
00000000

Section Header

00000170 0000 0000 0000 0000 2E74 6578 7400 0000 .........text...
00000180 5339 0000 0010 0000 003A 0000 0004 0000 S9.......:......
00000190 0000 0000 0000 0000 0000 0000 2000 0060 ............ ..`
000001A0 2E62 7373 0000 0000 3A04 0000 0050 0000 .bss....:....P..
000001B0 0001 0000 0000 0000 0000 0000 0000 0000 ................
000001C0 0000 0000 8000 00C0 2E64 6174 6100 0000 .........data...
000001D0 1202 0000 0060 0000 0004 0000 003E 0000 .....`.......>..
000001E0 0000 0000 0000 0000 0000 0000 4000 00C0 ............@...
000001F0 2E69 6461 7461 0000 9A0C 0000 0070 0000 .idata.......p..
00000200 000E 0000 0042 0000 0000 0000 0000 0000 .....B..........
00000210 0000 0000 4000 0040 2E72 7372 6300 0000 ....@..@.rsrc...
00000220 0040 0000 0080 0000 0032 0000 0050 0000 .@.......2...P..
00000230 0000 0000 0000 0000 0000 0000 4000 0040 ............@..@
00000240 2E72 656C 6F63 0000 1E09 0000 00C0 0000 .reloc..........
00000250 000A 0000 0082 0000 0000 0000 0000 0000 ................
00000260 0000 0000 4000 0042 0000 0000 0000 0000 ....@..B........

BYTE    Name[IMAGE_SIZEOF_SHORT_NAME]  ( 2E74 6578 7400 )
DWORD   PhysicalAddress
DWORD   VirtualSize                    ( 5339 0000 )
DWORD   VirtualAddress                 ( 0010 0000 )
DWORD   SizeOfRawData                  ( 003A 0000 )
DWORD   PointerToRawData               ( 0004 0000 )
DWORD   PointerToRelocations
DWORD   PointerToLinenumbers
WORD    NumberOfRelocations
WORD    NumberOfLinenumbers
DWORD   Caracteristiques               ( 2000 0060 ) -> 60000020

Informations données par Procdump:

Sections lnformations :

 Name  Virtual Size  Virtual Off   Raw Size    Raw Offset Characterics
.text    00003953     00001000     00003A00     00000400    60000020
.bss     0000043A     00005000     00000100     00000000    C0000080
.data    00000212     00006000     00000400     00003E00    C0000040
.idata   00000C9A     00007000     00000E00     00004200    40000040
.rsrc    00004000     00008000     00003200     00005000    40000040
.reloc   0000091E     0000C000     00000A00     00008200    42000040

Les caractéristiques:

  Valeur              Definition

  0x00000020          Code section
  0x00000040          Initialized data section
  0x00000080          Uninitialized data section
  0x04000000          Section cannot be cached
  0x08000000          Section is not pageable
  0x10000000          Section is shared
  0x20000000          Executable section
  0x40000000          Readable section
  0x80000000          Writable section

(pour plus de détails voir  "caractéristiques du PE Header")

Notez qu'il y a une Section Header par section; "Thus". Pour le PE Header
de Notepad.exe, il y aura 06 Sections Headers. Chaque section contient son nom en ASCII (ex: ".text")
et un pointeur vers son emplacement; les headers ont une taille de 40 octets et il n'y a pas de "padding"
entre eux. Les sections qui sont habituellement présentes dans un exécutable sont (sans être
obligaoire):

Executable Code Section:   .text
Data Sections:             .data, .rdata, ou .bss
Resources Section:         .rsrc
Export Data Section:       .edata
Import Data Section:       .idata
Debug Information Section: .debug

Exécutable code section: .text

Cette section contient le code du programme

Data sections: .bss, .rdata, .data

Il y a trois types de sections data: .bss, qui contient des "uninitialized data" (incluant toutes les variables déclarées
et fixes), .rdata, qui contient
les "read-only datas" (comme les strings, et les constantes), et .data, qui contient les variables globales ou le programme. Ces sections n'ont pas de réelle
structure.

Ressources section: .rsrc

La section .rsrc contient toutes les ressources de l'application. Les 16 premiers octets de la section .rsrc contiennent
les Ressources du "Directory Header":

 Resource Directory
 DWORD    Caracteristiques
 DWORD    TimeDateStamp
 WORD     MajorVersion
 WORD     MinorVersion
 WORD     NumberOfNamedEntries
 WORD     NumberOfIdEntries

Qui est immédiatement suivie par le nombre d'entrées spécifiées
dans NumberOfNamedEntries + NumberOfIdEntries:

 Resource Directory Entry
 DWORD    Name;
 DWORD    OffsetToData;

Le Nom d'une entrée du Directory détermine le type de la ressource,
pendant que l'Offset pointe vers une autre "Ressource Directory Entry" (la structure habituelle est une
Ressource Directory contenant la Ressource type pointée par une Ressource Directory (ou subdirectory) contenant
la Resource ID # et pointant vers l'entrée des Ressources Datas), ou vers l'entrée d'une Ressource
Data:

 Resource Data Entry
 DWORD   OffsetToData;
 DWORD   Size;
 DWORD   CodePage;
 DWORD   Reserved;

L'entrée de "Resource Data" contient la taille de l'actuelle Ressource
data, qui sera à terme une "unicode string" pour la String Table, et l'image binaire pour une
bitmap, ou une liste de valeurs et de strings pour une boite de dialogue. L'information sur le format du contenu
de la ressource binaire peut être trouvée dans "Micro$oft's Win32 Binary Resource Formats.".

La section Export data: .edata

La section .edata contient les "exported data" et les fonctions pour une application ou une librairie
(.DLL). La section commence avec le "Export Directory":

 Export Directory
 DWORD   Characteristics;
 DWORD   TimeDateStamp;
 WORD    MajorVersion;
 WORD    MinorVersion;
 DWORD   Name;
 DWORD   Base;
 DWORD   NumberOfFunctions;
 DWORD   NumberOfNames;
 DWORD   *AddressOfFunctions;
 DWORD   *AddressOfNames;
 WORD    *AddressOfNameOrdinals;

Les trois derniers champs contiennent les pointeurs vers la liste des points d'entré
(Entry points) des fonctions exportées, la liste des noms des fonctions "null-seperated", et la
liste des valeurs ordinales des fonctions. Ces pointeurs sont opérationnels quand le programme est chargé
en mémoire.

Import data section: .idata

Cette section contient la liste des fonctions importées dans le programme. Elle commence avec l'Import Directory:

Import Directory
 DWORD    dwRVAFunctionNameList;
 DWORD    dwUseless1;
 DWORD    dwUseless2;
 DWORD    dwRVAModuleName;
 DWORD    dwRVAFunctionAddressList;

Les deux derniers champs sont répétés pour chaque application
ou librairie que le programme importe. Dans un éditeur hexadécimal vous pourrez voir en premier le
nom de la section d'un module donné, le nom du module, puis toutes les fonctions importées par ce
module.

La liste des imports est répétée jusqu'à une entrée nulle.

A lire:  Import Data-Import Table Tutorial  by el.CaRaCoL

Relative Virtual Addresses

La RVA (Relative Virtual Address) est utilisée pour décrire une adresse
mémoire si vous ne connaissez pas l'adresse base. C'est la valeur qui additionnée à l'adresse
base va vous donner l'adresse réelle telle que vous pourrez la lire dans un débuggeur ou un désassembleur.

Exemple: supposez qu'un fichier exécutable soit chargé à l'adresse 0x400000 et que son exécution
démarre à RVA 0x1560. L'adresse de l'Entry Point de l'exécutable sera en 0x401560. Si l'exécutable
était chargé en 0x100000, l'Entry Point se situerait en 0x101560.

Les choses commencent à se compliquer un peu si les sections ne sont pas alignées. Les sections d'un
fichier sont souvent alignées sur 512 octets, mais l'image chargée peut être alignées
sur 4096 octets...

Aussi, pour trouver une adresse RVA précise dans un fichier PE, vous devrez calculer l'offset d'où
le programme sera chargé.

La section Debug information: .debug

Cette section contient les informations de débuggage du programme, si le compilateur utilisé le permettait.
Elle commence par un "Debug Directory":

Debug Directory

DWORD   Characteristics
DWORD   TimeDateStamp
WORD    MajorVersion
WORD    MinorVersion
DWORD   Type
DWORD   SizeOfData
DWORD   AddressOfRawData
DWORD   PointerToRawData

Modification du PE Header:

Puisque le PE Header donne l'adresse de départ et la taille de chacune de ses différentes sections
(et c'est le cas de la section .rsrc, la taille des datas), il est facile de modifier le contenu du PE Header en
ajustant l'offset du champ de chaque section suivant ce qui doit être modifié. Pour ajouter du code
supplémentaire vous pouvez agrandir la section .text mais vous aurez à corriger chacune des sections
suivantes; pour ajouter ou changer des ressources, il faudra modifier chaque subdirectory de la section .rsrc et
corriger toutes les sections qui viennent après (c'est ainsi que Borland Resources Windows et les éditeurs
de ressources travaillent). Du coup il semble plus facile, plutôt que de modifier chacune des sections, de
ne s'occuper que de la dernière d'entre elles en l'agrandissant (ce qui au passage modifiera la taille de
l'application). Pourtant c'est la meilleurs et la plus rapide des méthodes.

A lire: Agrandir une section par Nody
           Agrandir une section par TeeJi

Caractéristiques des sections

Voici quelques explications rapides et très succinctes sur les attributs des
sections:

================

0x20...... : signifie eXecutable

0x40...... : signifie Readable

0x80...... : signifie Writeable

exemple 0x60.. -> eXecutable + Readable

0x60... -> R-X-.

0xC0... -> R-.-W

0xE0... -> R-X-W

=================

0x......20 : signifie contains Code

0x......40 : signifie Initialized data

0x......80 : signifie Unitialized data

0x......40 : la plupart des sections peuvent avoir cette valeur excepté les sections CODE et .BSS

=================

Pour en savoir plus il existe un bon article récupéré sur Microsoft.com appelé " PEERING INSIDE the PE " écrit en mars 94 par Matt Pietrek (qui travaille ou a travaillé chez..
chez... devinez: NU-MEGA TECHNOLOGIES INC.)

(contribution de Neutral AtomX)

Et pour ceux qui souhaitent plus de détails
(extrait de Advanced Registry Tracer de Psyché)

// Section characteristics.

#define IMAGE_SCN_TYPE_NO_PAD                0x00000008 // Reserved.
#define IMAGE_SCN_CNT_CODE                   0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080 // Section contains uninitialized data.
#define IMAGE_SCN_LNK_OTHER                  0x00000100 // Reserved.
#define IMAGE_SCN_LNK_INFO                   0x00000200 // Contains comments or some other  info
#define IMAGE_SCN_LNK_REMOVE                 0x00000800 // Contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT                 0x00001000 // Section contents comdat.
#define IMAGE_SCN_NO_DEFER_SPEC_EXC          0x00004000 // Reset speculative exceptions handling
#define IMAGE_SCN_GPREL                      0x00008000 // Section content can be accessed  to GP
#define IMAGE_SCN_MEM_FARDATA                0x00008000
#define IMAGE_SCN_MEM_PURGEABLE              0x00020000
#define IMAGE_SCN_MEM_16BIT                  0x00020000
#define IMAGE_SCN_MEM_LOCKED                 0x00040000
#define IMAGE_SCN_MEM_PRELOAD                0x00080000
#define IMAGE_SCN_ALIGN_1BYTES               0x00100000
#define IMAGE_SCN_ALIGN_2BYTES               0x00200000
#define IMAGE_SCN_ALIGN_4BYTES               0x00300000
#define IMAGE_SCN_ALIGN_8BYTES               0x00400000
#define IMAGE_SCN_ALIGN_16BYTES              0x00500000 // Default alignment if no others specified.
#define IMAGE_SCN_ALIGN_32BYTES              0x00600000
#define IMAGE_SCN_ALIGN_64BYTES              0x00700000
#define IMAGE_SCN_ALIGN_128BYTES             0x00800000
#define IMAGE_SCN_ALIGN_256BYTES             0x00900000
#define IMAGE_SCN_ALIGN_512BYTES             0x00A00000
#define IMAGE_SCN_ALIGN_1024BYTES            0x00B00000
#define IMAGE_SCN_ALIGN_2048BYTES            0x00C00000
#define IMAGE_SCN_ALIGN_4096BYTES            0x00D00000
#define IMAGE_SCN_ALIGN_8192BYTES            0x00E00000
#define IMAGE_SCN_LNK_NRELOC_OVFL            0x01000000 // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE            0x02000000 // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED             0x04000000 // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED              0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED                 0x10000000 // Section is shareable.
#define IMAGE_SCN_MEM_EXECUTE                0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ                   0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE                  0x80000000 // Section is writeable.

"Nous allons avoir besoin de ProcDump ici pour nous donner l'Entry Point en
virtual offset de ART.

Lancer Pdump => PE Editor => charger ART.EXE => Entry Point : 499B9 (ceci est le virtual offset, bref
c'est l'offset en mémoire et si vous voulez connaître l'adresse, vous ajoutez l'Image Base = 400000,
ce qui nous donne 4499B9 comme Entry Point en mémoire).

Pourquoi fait-on ça ? Pour savoir dans quelle section se trouve l'Entry Point.

Allez maintenant dans SECTION et regardez la colonne " virtual offset ".

499B9 se trouve dans .CODE car 499B9 est > 1000 (le début de la section .CODE) et est < 86000 (le
début de la section .DATA).

Voilà une bonne chose … maintenant regardons les attributs de la section .CODE

Cliquez avec le bouton de droite de la souris sur CODE (colonne NAME toujours dans ProcDump of course ) et choisissez
EDIT.

En bas, à droite se trouvent les " Section Characteristics " (= les attributs) qui valent : C00000040
qui, si on se réfèrent à notre tableau ci-dessus veut dire : readable (40000000 => accès
en lecture) + writable (80000000 => accès en écriture => c'est typique des exe compressés,
les programmes habituels protègent leur code en ne permettant pas l'écriture DANS le code, contrairement
à la section DATA où on peut lire et écrire) + Init Data (00000040) = C0000040.

Si vous regardez de plus près la liste, vous découvrirez deux choses intéressantes : 20000000
(exécutable) et 00000020 (contient du code).

Maintenant, si on disait que notre section doit être 20000000 + 40000000+ 80000000 + 00000020 = E00000020
?

Changeons C0000040 en E0000020 et quittons ProcDump.

Essayez maintenant de désassembler ART.EXE avec Wdasm … vi vi ça marche !

Cliquons sur l'icône PEP et on arrive à l'Entry Point (4499B9), allez sur cette ligne et vous verrez
tout en-bas @48DB9 (vous savez bien, le truc que vous regardez pour savoir où patcher …) et bien c'est le
" raw Offset ".

La même chose vue sous l'angle de TeeJi

Chaque caractéristique à une valeur. Si vous voulez avoir deux caractéristiques,
vous faites : caract1 OR caract2 ...

Donc, pour que Softice break à l'entrypoint, il faut que la section qui contient la première instruction
ait les caractéristiques suivantes :

   0x00000020 IMAGE_SCN_CNT_CODE
   0x20000000 IMAGE_SCN_MEM_EXECUTE
   0x40000000 IMAGE_SCN_MEM_READ
OR 0x80000000 IMAGE_SCN_MEM_WRITE
-----------------------------------
   0xE0000020

Et généralement, pour empêcher le break et le désassemblage de la section, on met les caractéristiques suivantes :

       0x00000040 IMAGE_SCN_CNT_INITIALIZED_DATA
   0x40000000 IMAGE_SCN_MEM_READ
OR 0x80000000 IMAGE_SCN_MEM_WRITE
--------------------------------------------
   0xC0000040

Maintenant que l'on connait les caractéristiques à changer, il nous
faut savoir quelle section contient l'entrypoint. Pour cela :

· Lancer ProcDump ( ou n'importe quel Editeur de PE )

· Clicker sur [PE Editor]

· Choisir le fichier compressé ( ou celui dont on veux modifier les caractéristiques )

· Regarder la valeur de l'entry point

· Cliquer sur [Section]

· Regarder la colonne des Virtual Offset

· Et la section qui contient l'Entry Point est celle qui commence avant l'entrypoint et qui est le plus
proche de celui-ci.

· Vous cliquez sur la section avec le bouton droit et vous sélectionné Edit Section

· et dans les caractéristiques vous mettez E0000020

· OK / OK / FIN

D’autres explications (extrait
de Awave 5.0 de CyberBobJr)

" Arrivé à ce niveau, il est nécessaire de savoir
comment fonctionne (vaguement ;) le format PE ...

Il faut savoir qu’un programme est divisé en sections, et chaque section possède une entrée
dans le PE Header, ca veut dire que le code de décryptage possède une section dans le PE Header,
ProcDump nous permet d’avoir les détails de cette section : "

 | Virtual size | Virtual Offset | Raw size | Raw Offset
.data | 00002000 | 000DF000 | 00001400 | 00050A00

" La taille virtuelle de notre code est 2000, ce qui veut dire qu'en RAM,
la mémoire allouée pour notre code est de taille 0x2000 octets, ce qui toujours un multiple de la
page (en particulier içi 0x1000), en effet, la mémoire allouée doit toujours être multiple
de l'offset virtuel de la section CODE.

L'offset virtuel de notre code est de 000DF000, additionné à l'image de base (00400000), ca nous
donne une adresse virtuelle de 004DF000 (et comme par hasard, cette adresse nous mène droit vers l'EIP du
programme ? en l'occurence Awave 5.0).

La taille sur disque (Raw Size) est de 0x1400, ce qui veut dire que le code n'utilise que 0x1400 octets dans le
fichier .exe.

L'offset sur disque (Raw Offset) est de 00050A00, autrement dit le code s'étale sur le disque de l'offset
0050A00 jusqu'a (0050A00+1400)=0051E00 ".

Et celles de Brénuche dans Adanced Zip Password revovery

Name    VirtualSize  VirtualOffset Raw Size Raw Offset Characteristics

.text     000AF000     00001000    0004AA00  00000600   C0000040
.data     0004B000     000B0000    00003E00  0004B000   C0000040
.tls      00001000     000FB000    00000200  0004EE00   C0000040
.rdata    00001000     000FC000    00000200  0004F000   C0000040
.idata    00003000     000FD000    00001200  0004F200   C0000040
.edata    00016000     00100000    00015800  00050400   C0000040
.rsrc     00044000     00116000    00020A00  00065C00   C0000040
.reloc    0000D000     0015A000    00000000  00086600   C0000040
.adata    0000F000     00167000    0000E200  00086600   C0000040
.udata    00001000     00176000    00000000  00094800   C0000040

" ProcDump32 affiche les sections
du fichier dans l'ordre (cad par rapport à leur offset). La taille virtuelle (virtual) des sections correspond
au code réel, celui qui sera en mémoire. Tandis que la taille brute (raw) correspond à l'espace
pris par cette section sur le disque dur (c'est ces données qui nous intéressent). Notez au passage
que la taille virtuelle est beaucoup plus grande que la taille brute, (compte tenu que le code décompressé
doit être écris dans la même section que celle qui contient le code compressé, cette
section doit être grande en mémoire mais petite sur disque ) Ca, c'est le propre des exe compressés. "


Iczelion's PE Tutorial 1: Overview of PE File Format

Tutorial 1 : Vue d'ensemble sur le
Format d'un PE File

C'est une complète réécriture d'un vieux Tutorial PE que je considérais comme le plus mauvais Tutorial que j'avais écrit. Donc j'ai décidé de le remplacer par ce nouveau.

PE veut dire 'Portable Executable'. C'est le format de base des fichiers Win32. Sa spécification provient en partie du format COFF d'Unix (format de fichier objet commun) ou (common object file format). La particularité du "Portable Executable" c'est que c'est un format de fichier universel à travers la plate-forme win32 : le PE Loader de chaque plate-forme win32 reconnaît et emploie ce format de fichier même lorsque Windows tourne sur des plates-formes CPU autres qu'Intel. Ça ne signifie pas que vos Programmes executables (au format PE) soient capables de supporter d'autres plates-formes CPU sans modification. Chaque Win32 exécutable (sauf les VxDs et les Dlls 16 bits) utilisent le format du PE File. Même les Drivers NT utilisent le format du PE File. C'est pourquoi le fait d'étudier le format du PE File vous fait bien comprendre la structure de Windows.

Plongeons dans la structure générale du format du PE File sans plus attendre.

DOS MZ header
DOS stub
PE header
Section table
Section 1
(ex: .data)
Section 2
Section ...
Section n
(ex: .code)

L'image ci-dessus représente la structure générale d'un PE File. Tous les PE File (même les DLLs 32 bits) doivent commencer par un simple entête DOS 'MZ'. Habituellement, nous ne sommes pas beaucoup intéressés par cette structure. Elle est fournit au cas où le programme tourne sous DOS, ainsi le DOS peut la reconnaître en tant qu'exécutable valide et peut ainsi lancer le 'DOS Stub' qui est stocké à la suite de l'entête MZ. Le 'DOS Stub' est en réalité un EXE valide qui est exécuté au cas où le système d'exploitation ne reconnaît pas le format du PE File. Ça sert simplement à afficher une chaîne de caractères comme " This program requires Windows" ou bien ça peut aussi faire apparaître un programme(fenêtre) DOS suivant l'intention du programmeur. Nous ne sommes pas non plus vraiment intéressés par le 'DOS Stub' : Habituellement, l'assembleur/compilateur le fournit de lui-même. Dans la plupart des cas, il emploie simplement l' int 21h, service 9 pour afficher une chaîne de caractères disant " This program cannot run in DOS mode".

Après le DOS stub, arrive le PE header (l'entête PE). Le PE header est un terme général pour la structure relative au PE nommée IMAGE_NT_HEADERS. Cette structure contient la plupart des paramètres essentiels pour le PE Loader. Par la suite, nous serons tout à fait familiers avec elle quand vous en saurez un peu plus à propos du format du PE File. Dans le cas où le programme est exécuté dans un système d'exploitation qui reconnaît le format du PE File, le PE Loader peut retrouver l'offset de départ du PE Header à partir du MZ header du DOS. De cette manière, il peut sauter le DOS Stub et aller directement au PE header qui est le véritable header du fichier. (son entête)

Le contenu réel du PE File est divisé en plusieurs blocs appelés sections. Une section n'est rien plus qu'un bloc de données avec des attributs communs comme le sont, le code en lecture et les données en écriture etc. Vous pouvez vous représenter le PE File comme un disque logique. Le PE header est le secteur de boot (la racine (comme C:)) et les sections sont les fichiers du disque. Les fichiers peuvent avoir des attributs différents comme lecture-seule, système, fichier caché, archives et cetera. Je souhaite éclaircir ce point dès à présent, toutes les données appartenant à un même groupe dans une section, ont pour base les mêmes attributs en communs : ce n'est pas le cas dans une base logique. Ça n'a pas d'importance la façon dont le code ou les données sont employés. Si les données et le code dans le PE File ont les mêmes attributs, on peut les regrouper dans une même section. Dans ce cas là, une section n'obéit plus au schéma classique des concepts logiques, comme le font les sections "data" et "code": Ici les sections peuvent contenir à la fois du code et des données à condition qu'ils aient les mêmes attributs. Si vous avez un bloc de données que vous souhaitez n'utiliser qu'en lecture-seule, vous pouvez placer ces données dans la section qui est affectée aux lectures-seules. Quand le PE Loader mappe des sections en mémoire, il examine les attributs des sections puis renvoie le bloc de mémoire qu'occupent maintenant ces sections.

Si nous nous représentons le format du PE File comme un disque logique, le PE header comme le secteur de boot et les sections comme des fichiers, parfois il nous manque des informations pour découvrir où les fichiers sont placés sur le disque. En fait, nous n'avons pas encore débattus sur ce qui serait l'équivalent des répertoires dans le cas du format des PE File. Immédiatement après le PE header vient La table des sections qui en fait est un tableau contenant plusieurs structures. Chaque structure contient les informations sur chaque section du PE File, comme par exemple ses attributs, l'offset du fichier, l'offset virtuel. S'il y a 5 sections dans le PE File, il y aura exactement 5 membres dans ce tableau de structures. Nous pouvons alors voir la table des sections un peu comme le 'root directory' (le répertoire principal) du disque logique. Chaque membre du tableau est l'équivalent d'un sous-répertoire du répertoire Principal.

C'est tout ce qu'il y a à savoir sur la disposition physique du format du PE File. Je récapitule les étapes principales pour charger un PE File en mémoire :

  1. Quand le PE File est lancé, le PE Loader examine le MZ header (du DOS) pour trouver l'offset du PE header. S'il le trouve, il saute au PE header.
  2. Le PE Loader vérifie si le PE header est valide. S'il l'est, il va à la fin de PE header.
  3. Immédiatement après le PE header on arrive à la table des sections. Le PE header lit les informations à propos des sections puis mappe ces sections en mémoire en employant le procédé de 'File Mapping' (on fait une copie en mémoire, d'un fichier qui lui est bien réel au départ). Il délègue également leurs attributs à chaque section comme c'est indiqué dans la table des sections.
  4. Après que le PE File ait été mappé en mémoire, le PE Loader se comporte lui-même comme une partie logique du PE File, en tant que table d'importation.

Les étapes ci-dessus sont d'une excessive simplification et sont basés sur mes propres observations. Il peut y avoir quelques inexactitudes mais ça doit vous éclairer le processus.

Vous devez télécharger La description du format du PE File de LUEVELSMEYER. Il est extrêmement détaillé et vous devez le considérer comme une véritable référence.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's PE Tutorial 2: Detecting a Valid PE File

Tutorial 2: Détecter un PE File Valide

Dans ce Tutorial, nous allons voir comment vérifier si un fichier donné est un PE File Valide.
Downloadez l'exemple.

(Un PE File est un ficher de type PE = un exécutable sous Win32. Un Programme Win32 en fait.)

Théorie:

Comment pouvez-vous vérifier si un fichier donné est PE file ? C'est difficile de répondre à cette question. Ça dépend jusqu'où vous voulez aller. Vous pouvez vérifier chaque structure de données définie dans le format du PE file ou bien vous pouvez vous satisfaire de vérifier seulement les éléments cruciaux. La plupart du temps, il est inutile de vérifier chaque structure appartenant aux fichiers. Si les structures cruciales sont valables, nous pouvons dire avec presque certitude que le fichier est un PE valide. Et nous nous contenterons de cette supposition.

La structure essentielle que nous vérifierons est le PE header lui-même. Donc nous avons besoin d'en savoir un peu plus sur sa programmation. Le PE header est en réalité une structure appelée IMAGE_NT_HEADERS. Sa définition est la suivante :

IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS

Signature est un dword qui contient la valeur 50h, 45h, 00h, 00h. En terme plus humain, ça représente le texte "PE" suivi par deux zéros de terminaison. Ce membre est la 'signature PE', donc nous nous en servirons pour vérifier si un fichier donné est ou n'est pas un PE valide.
FileHeader est une structure qui contient des informations sur la structure physique du PE file tels que son nombre de sections, la machine pour laquelle ce fichier est destinée et cetera.
OptionalHeader est une structure qui contient des informations sur la structure logique du PE file. Malgré qu'il soit "Optionel", il est toujours présent.

Notre but est maintenant clair. Si la valeur du membre 'signature' du IMAGE_NT_HEADERS est égale à "PE" suivi par deux zéros, alors le fichier est un fichier PE valide. En fait, justement dans le but de pouvoir comparer ça, Microsoft a défini une constante nommée IMAGE_NT_SIGNATURE que nous pouvons facilement employer.

IMAGE_DOS_SIGNATURE equ 5A4Dh
IMAGE_OS2_SIGNATURE equ 454Eh
IMAGE_OS2_SIGNATURE_LE equ 454Ch
IMAGE_VXD_SIGNATURE equ 454Ch
IMAGE_NT_SIGNATURE equ 4550h

La question est la suivante : comment pouvons-nous savoir où se situe le PE header ? La réponse est simple : le MZ header du DOS contient l'offset(de fichier) du PE header. Le MZ header est défini comme la structure IMAGE_DOS_HEADER. Vous pouvez le vérifier en regardant dans windows.inc. Le membre e_lfanew de la structure IMAGE_DOS_HEADER contient l'offset du PE header.

Les étapes sont les suivantes :

  1. Vérifiez si le fichier donné possède un MZ header (du DOS) valide en comparant le premier WORD du fichier avec la valeur IMAGE_DOS_SIGNATURE.
  2. Si le fichier possède bien un MZ Header valide, On utilise la valeur qui est dans le membre e_lfanew pour retrouver le PE header
  3. On compare le premier WORD du PE header avec la valeur IMAGE_NT_HEADER. Si les deux sont de même valeur, alors nous pouvons assurer que le fichier est un PE valide.

Exemple:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

SEH struct
PrevLink dd ?    ; adresse de la structure SEH ci-dessus
CurrentHandler dd ?    ; adresse du 'exception handler'
SafeOffset dd ?    ; endroit où est sauvegardé l'offset pour continuer l'exécution
PrevEsp dd ?      ; l'ancienne valeur dans 'esp'
PrevEbp dd ?     ; ancienne valeur dans 'ebp'
SEH ends

.data
AppName db "PE tutorial no.2",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
                 db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileValidPE db "This file is a valid PE",0
FileInValidPE db "This file is not a valid PE",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start proc
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
       mov hFile, eax
       invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
       .if eax!=NULL
          mov hMapping, eax
          invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
          .if eax!=NULL
             mov pMapping,eax
             assume fs:nothing
             push fs:[0]
             pop seh.PrevLink
             mov seh.CurrentHandler,offset SEHHandler
             mov seh.SafeOffset,offset FinalExit
             lea eax,seh
             mov fs:[0], eax
             mov seh.PrevEsp,esp
             mov seh.PrevEbp,ebp
             mov edi, pMapping
             assume edi:ptr IMAGE_DOS_HEADER
             .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                add edi, [edi].e_lfanew
                assume edi:ptr IMAGE_NT_HEADERS
                .if [edi].Signature==IMAGE_NT_SIGNATURE
                   mov ValidPE, TRUE
                .else
                   mov ValidPE, FALSE
                .endif
             .else
                 mov ValidPE,FALSE
             .endif
FinalExit:
             .if ValidPE==TRUE
                 invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .else
                invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
             .endif
             push seh.PrevLink
             pop fs:[0]
             invoke UnmapViewOfFile, pMapping
          .else
             invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
       .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
    .else
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
    .endif
.endif
invoke ExitProcess, 0
start endp

SEHHandler proc C uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
    mov edx,pFrame
    assume edx:ptr SEH
    mov eax,pContext
    assume eax:ptr CONTEXT
    push [edx].SafeOffset
    pop [eax].regEip
    push [edx].PrevEsp
    pop [eax].regEsp
    push [edx].PrevEbp
    pop [eax].regEbp
    mov ValidPE, FALSE
    mov eax,ExceptionContinueExecution
    ret
SEHHandler endp
end start

Analyse:

Le programme ouvre un fichier et vérifie si le DOS header (le MZ) est valide, s'il l'est, on vérifie alors si le PE header est valide à son tour. S'il l'est également, alors on peut certifier que le fichier est un PE File valide. Dans cet exemple, j'emploie la 'Structure Exception Handling' (SEH) pour ne pas avoir à faire de vérifier chaque erreur possible : si une erreur se produit, on est sûr que c'est parce que le fichier n'est pas un PE File valide. Windows lui-même utilise largement le SEH dans ses routines de validation de paramètres. Si le SEH vous intéresse, lisez l'article de Jeremy Gordon.

Son programme affiche une fenêtre OPENFILE (la boîte de Dialog qui sert à ouvrir d'autres fichiers) et quand l'utilisateur choisit un fichier exécutable, il ouvre ce fichier et les 'Mappe' en mémoire. Avant qu'il ne commence sa vérification, il installe un SEH :

   assume fs:nothing
   push fs:[0]
   pop seh.PrevLink
   mov seh.CurrentHandler,offset SEHHandler
   mov seh.SafeOffset,offset FinalExit
   lea eax,seh
   mov fs:[0], eax
   mov seh.PrevEsp,esp
   mov seh.PrevEbp,ebp

Nous commençons en assumant l'utilisation du register fs comme étant mis à zéro. On est obligé de faire comme ça parce que MASM assume l'utilisation du register fs à ERROR (par défaut). Ensuite nous stockons l'adresse du SEH Handler vu précédemment dans notre structure pour que Windows puisse l'utiliser. Nous stockons l'adresse de notre SEH Handler, l'adresse où l'exécution peut sans risque reprendre si une erreur arrive, les valeurs actuelles de esp et d'ebp pour que notre SEH Handler puisse récupérer l'état antérieur de la pile (comme en tant normale) avant que notre programme reprenne son exécution.

   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   .if [edi].e_magic==IMAGE_DOS_SIGNATURE

Après que nous en ayons finis avec l'installation du SEH, nous passons à la vérification. Nous mettons l'adresse du premier octet du fichier cible dans edi, qui est le premier octet du MZ header (du DOS). Pour le bien de la comparaison, nous disons à l'assembleur qu'il peut assumer edi en tant que pointeur sur la structure IMAGE_DOS_HEADER (Ce qui est la vérité). Nous comparons alors le premier WORD du MZ header avec la chaîne de caractères "MZ" qui est définie comme une constante dans Windows.inc sous le nom d'IMAGE_DOS_SIGNATURE. Si la comparaison est ok, nous passons au PE header. S'il n'est pas valide, nous mettons à FALSE la valeur dans ValidPE, signifiant ainsi que le fichier n'est pas PE File valide.

      add edi, [edi].e_lfanew
      assume edi:ptr IMAGE_NT_HEADERS
      .if [edi].Signature==IMAGE_NT_SIGNATURE
         mov ValidPE, TRUE
      .else
         mov ValidPE, FALSE
      .endif

Pour arriver jusqu'au PE header, nous avons besoin de la valeur dans e_lfanew du MZ header. Ce paramètre contient l'offset du PE header. Ainsi nous ajoutons cette valeur à edi et nous arrivons au premier octet du PE header. C'est à cet endroit qu'une erreur peut se produire. Si le fichier n'est pas un vrai PE file, la valeur dans e_lfanew sera incorrecte et ainsi en l'utilisant on pointe vers une zone sauvage. Si nous n'employons pas le SEH, nous devons vérifier la valeur de e_lfanew avec la taille de ce sale fichier. Si tout va bien, nous comparons le premier dword du PE header avec la chaîne de caratères "PE". De nouveau il y a une constante bien pratique du nom de IMAGE_NT_SIGNATURE que nous pouvons utiliser. Si le résultat de la comparaison est TRUE, nous assumons que notre fichier est un PE File valide.
Si la valeur dans e_lfanew est incorrecte, une erreur peut se produire et notre SEH Handler reprendra le contrôle. Il rétablit simplement le pointer de pile, et reprend l'exécution à l'offset sauveqardé précédemment à l'endroit du label FinalExit.

FinalExit:
   .if ValidPE==TRUE
      invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .else
      invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
   .endif

Le code ci-dessus est d'une simplicité en lui-même. Il vérifie la valeur dans ValidPE et affiche un message à l'utilisateur en conséquence.

   push seh.PrevLink
   pop fs:[0]

Quand le SEH n'est plus employé, nous le dissocions de la chaîne du SEH.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's PE tutorial 3: File Header

Tutorial 3: File Header

(Le File Header c'est l'entête d'un fichier (d'un programme), c'est sa carte d'identité. C'est ici qu'on trouve des informations sur lui. Sa date de création, la date de dernière exécution, le nombre de sections qu'il possède....)

Dans ce Tutorial, nous allons étudier la partie 'file header' qui appartient au PE header.

On va récapituler ce que nous avons appris jusqu'ici :

  • LE MZ header du DOS est appelé IMAGE_DOS_HEADER. Seuls deux de ses membres sont importants pour nous : e_magic lequel contient la chaîne de caractères "MZ" et e_lfanew qui contient l'offset du PE header.
  • Nous nous servons de la valeur dans e_magic pour vérifier si le fichier possède un MZ header (DOS) valide en le comparant à la valeur IMAGE_DOS_SIGNATURE. Si ces deux valeurs correspondent, nous pouvons assumer que le fichier est bien pourvu d'un MZ Header valide.
  • Pour arrivez jusqu'au PE header, nous devons déplacer le pointer sur l'offset indiqué par la valeur contenu dans e_lfanew.
  • Le premier DWORD du PE header doit contenir la chaine de caractères "PE" suivie par une paire de zéros. Le bon sens c'est de comparer la valeur dans ce dword à la valeur que représente le Flag IMAGE_NT_SIGNATURE. S'ils correspondent, alors nous pouvons assumer qu'on a à faire à un PE header est valide.

Dans ce Tutorial Nous allons encore en apprendre plus sur le PE header. Le nom officiel du PE header est IMAGE_NT_HEADERS. Pour vous rafraichir la mémoire, je vous le remontre ci-dessous.

IMAGE_NT_HEADERS STRUCT
Signature dd ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
IMAGE_NT_HEADERS ENDS

Signature est la signature 'PE', "PE" suivi de deux zéros. Vous la connaissez déjà et employez déjà ce membre.
FileHeader est une structure qui contient des informations sur la configuration/propriétés physique du PE file en général.
OptionalHeader est également une structure qui contient des informations sur la configuration logique de l'intérieur du PE file.

L'information la plus intéressante est dans OptionalHeader. Cependant, certains paramètres contenus dans FileHeader sont aussi importants. Nous allons en apprendre pas mal sur le FileHeader dans ce Tutorial donc nous nous occuperons plutôt de l'étude d'OptionalHeader dans les Tutorials suivants.

IMAGE_FILE_HEADER STRUCT
Machine WORD ?
NumberOfSections WORD ?
TimeDateStamp dd ?
PointerToSymbolTable dd ?
NumberOfSymbols dd ?
SizeOfOptionalHeader WORD ?
Characteristics WORD ?
IMAGE_FILE_HEADER ENDS

Noms des paramètres | Signification
Machine | est le type de CPU (la plate-forme) pour lequel est destiné le fichier. Pour les CPU de type Intel, la valeur est IMAGE_FILE_MACHINE_I386 (14Ch). J'ai essayé d'employer 14Dh et 14Eh comme c'est mis dans le PE.txt de LUEVELSMEYER mais Windows a refusé de le lancer. Ce paramètre a rarement d'intérêt pour nous sauf si on s'en sert comme une manière rapide d'empêcher un programme d'être exécuté.
NumberOfSections | est le nombre de sections dans le fichier. Nous aurons besoin de modifier la valeur dans ce membre si nous ajoutons ou supprimons une section du fichier.
TimeDateStamp | est la date à laquelle le fichier a été créé. C'est pas utile pour nous.
PointerToSymbolTable | est utilisé lorsqu'on débug le programme.
NumberOfSymbols | est utilisé lorsqu'on débug le programme.
SizeOfOptionalHeader | est la taille du membre OptionalHeader qui suit immédiatement cette structure. On doit lui donner une valeur valide.
Characteristics | contient les flags qui servent au fichier, comme : est-ce un fichier exe ou dll.

En résumé, seulement trois membres sont utiles pour nous : Machine, NumberOfSections et Characteristics. Normalement, vous ne devriez pas avoir à changer les valeurs de Machine et Characteristics mais vous devez utiliser la valeur qui est dans NumberOfSections lorsque vous vous baladez dans la table des sections.
Pour illustrer l'utilisation de NumberOfSections, j'ai besoin de faire une brève parenthèse sur la table des sections.

La table des sections est un tableau regroupant plusieurs structures. Chaque structure contient des informations sur une section particulière. Ainsi s'il y a 3 sections (ex : .data .data? .code), il y aura 3 membres dans ce tableau. Vous avez besoin de la valeur dans NumberOfSections ainsi vous connaîtrez combien de membres comporte ce tableau. On peut essayer de mettre la valeur dans NumberOfSections à une valeur supérieur à celle réelle, et bien Windows lance toujours le fichier sans problème. D'après ce que j'ai observé, je pense que Windows lit la valeur dans NumberOfSections et examine chaque structure dans la table des sections. Si Windows rencontre une structure qui ne contient que des zéros, il termine sa recherche. Sinon il continue jusqu'à ce que le nombre de structures (donc de sections) rencontrées soit le même que le nombre dans NumberOfSections. Pourquoi ne pouvons nous pas ignorer la valeur NumberOfSections ? Il y a plusieurs raisons à ça. En fait, le PE ne spécifie pas que le tableau qu'est la table des sections doit forcément finir par une structure remplie de zéros. C'est pourquoi il peut y avoir certaines situations dans lesquelles le dernier membre du tableau est directement accolé à la première section, sans espace vide du tout. Une autre raison a un rapport avec les limites des importations. Un nouveau type de limites c'est de placer les informations immédiatement à la suite de la dernière structure de la table des sections. C'est pourquoi vous aurez toujours besoin de NumberOfSections.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's PE Tutorial 4: Optional Header

Tutorial 4: Les membres facultatifs du header
(Optional Header)

 (Header = entête d'un fichier (du programme) dans lequel se trouvent les informations concernant ce fichier)

Nous avons vu le MZ header (du DOS) et quelques membres du PE header. Voici le dernier, le plus grand et probablement le membre le plus important du PE header, l'Optional header.

Pour vous rafraîchir la mémoire, l'Optional header est une des structures du IMAGE_NT_HEADERS c'est son dernier membre. Il contient des informations sur la disposition (constuction) logique du PE File. Cette structure possède 31 paramètres. Quelques-uns d'entre eux sont cruciaux, par contre d'autres ne sont pas utiles. J'expliquerai seulement les paramètres qui sont vraiment utiles pour nous.

Il y a un mot qui est fréquemment employé en rapport avec le format du PE file : c'est le RVA
RVA (relative virtual address) est le diminutif de 'l'adresse virtuelle relative'. Vous savez ce qu'est une adresse virtuelle. Le RVA est un terme qui sert à simplifier le concept. Simplement, le RVA est une distance par rapport à un point de référence dans l'espace d'adresses virtuelles. Je parie que vous êtes familiers avec les offsets d'un fichier: le RVA c'est exactement la même chose que pour les offsets (les offsets ont pour référence le segments où ils se trouvent). Cependant, le RVA est référencé par rapport à un point de l'espace d'adresses virtuelles, et non pas par rapport à un fichier. Je vais vous montrer un exemple. Si le PE file se charge à l'adresse 00400000h dans l'espace d'adresses virtuelles (VA) et que le programme commence son exécution à l'adresse virtuelle 00401000h, alors nous pouvons dire que le programme commence son exécution à un RVA de 1000h. UN RVA est pris par rapport au départ VA du programme.
L'espace d'adresses virtuelles (VA) (00400000h) est pris comme point d'origine (0) où est stocké votre programme, mais son code d'exécution ne commence qu'en 00401000h et le RVA est la distance entre les deux.
Pourquoi le PE file utilise-t-il le RVA ? C'est pour réduire le travail du PE Loader. Puisque un module (un programme ou une DLL) peut être transféré n'importe où dans l'espace d'adresses virtuelles, ce serait un véritable enfer pour le PE Loader de retrouver chaque variable réadressable dans le programme. Au contraire, si toutes les variables réadressables dans le fichier utilisent le RVA, le PE Loader n'a aucun besoin de convertir quoi que ce soit : il transfère simplement le programme en entier vers un nouveau VA de départ. C'est un peu comme le concept de chemin relatif et de chemin absolu : le RVA est apparenté au chemin relatif, et le VA représente le chemin absolu.

Paramètres | Signification
AddressOfEntryPoint | est l'adresse de la première instruction qui sera exécutée. Si vous voulez que votre programme commence à une autre adresse, vous avez besoin de changer la valeur de ce paramètre.
ImageBase | est l'adresse de chargement préférentielle pour charger le PE file. Par exemple, si la valeur dans ce paramètre est 400000h, le PE Loader essayera de charger le fichier dans l'espace d'adresses virtuelles commençant à 400000h. Le mot "préférentielle" signifie que le PE Loader ne peut pas charger le fichier à cette adresse si d'autres programmes occupent déjà cette zone d'adresses.
SectionAlignment | représente l'alignement des sections en mémoire. Par exemple, si la valeur de ce paramètre est 1000h (4096), chaque section doit commencer à un multiples de 4096 octets. Si la première section commence à l'adresse 401000h et qu'elle a un taille de 10 octets seulement, alors la section suivante doit quand même commencer à 402000h, même si l'espace d'adresse entre 401000ème et 402000ème n'est pas entièrement inutilisée.
FileAlignment | représente l'alignement des sections dans le fichier. Par exemple, si la valeur dans ce paramètre est 200h (512), chaque section doit commencer à un multiples de 512 octets. Si la première section est à l'offset(de fichier) 200h et que la taille est de 10 octets, alors la section suivante doit être placée à l'offset(de fichier) 400h : l'espace entre les offsets(de fichier) 512 et 1024 est inutilisé/ou non défini.
MajorSubsystemVersion MinorSubsystemVersion | est la version de votre sous-système win32. Si le PE file est conçu pour une plate-forme Win32, la version du sous-système doit être 4.0.
SizeOfImage | est la taille complète que le PE prend en mémoire. C'est la somme de tous les Headers et de toutes les sections qui sont regroupés dans SectionAlignment.
SizeOfHeaders | est la taille de toutes les headers tables + sections tables . Bref, cette valeur est égale à la taille du fichier moins la taille combinée de toutes les sections dans le fichier. Vous pouvez aussi employer cette valeur comme étant l'offset(de fichier) de la première section du PE file.
Subsystem | dit pour quel sous-système NT le PE file est destiné. Pour la plupart des progs win32, seuls deux valeurs sont utilisées : le GUI Windows et le CUI Windows.
DataDirectory | est un tableau de structures appartenant à IMAGE_DATA_DIRECTORY. Chaque structure donne le RVA d'une structure de données importante du PE file comme l'adresse de la table des importations.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's PE Tutorial 5: Section Table

Tutorial 5: Table des Sections
(Section Table)

Downloadez l'exemple.

Théorie:

Jusqu'à maintenant, nous avons vu le DOS header, et le PE header. Ce qui reste c'est la table des sections. Une table des sections est en réalité un tableau regroupant plusieurs structures, ce tableau se trouve immédiatement à la suite du PE header. Le nombre de membres du tableau est déterminé par le paramètre NumberOfSections du File Header (IMAGE_FILE_HEADER). Cette structure (la Table des Sections) est appelée IMAGE_SECTION_HEADER.

IMAGE_SIZEOF_SHORT_NAME equ 8

IMAGE_SECTION_HEADER STRUCT
Name1 db IMAGE_SIZEOF_SHORT_NAME dup(?)
union Misc
PhysicalAddress dd ?
VirtualSize dd ?
ends
VirtualAddress dd ?
SizeOfRawData dd ?
PointerToRawData dd ?
PointerToRelocations dd ?
PointerToLinenumbers dd ?
NumberOfRelocations dw ?
NumberOfLinenumbers dw ?
Characteristics dd ?
IMAGE_SECTION_HEADER ENDS

Une fois encore, tous les membres ne sont pas utiles. Je décrirai seulement ceux qui sont vraiment importants.

Paramètres | Signification
Name1 | En réalité le nom de ce paramètre est "name" mais le mot "name" est un mot-clé de MASM donc nous devons utiliser "Name1" à la place. Ce membre contient le nom de la section. Notez que la longueur maximale est de 8 octets. Le nom n'est qu'un label, rien plus. Vous pouvez employer n'importe quel nom ou laisser même ce paramètre vide. Notez qu'il n'y a aucun besoin d'un NULL de terminaison. Ce nom n'est pas une chaîne de caractères ASCIIZ donc ne vous attendez pas à ce qu'il se termine par un null.
VirtualAddress | est le RVA de la section. Le PE Loader examine et emploie la valeur de ce paramètre lorsqu'il Mappe la section en mémoire. Ainsi si la valeur dans ce paramètre est 1000h et que le PE file à été chargé à partir de l'adresse 400000h, alors la section en question sera chargée à partir de 401000h.
SizeOfRawData | représente la taille des données de la section arrondies au multiple suivant de l'alignement de fichiers. Le PE Loader examine la valeur dans ce paramètre, il sait ainsi combien d'octets de la section doivent êtres mappés en mémoire.
PointerToRawData | est l'offset(de fichier) où commence la section. Le PE Loader utilise la valeur de ce paramètre pour retrouver où les données de la section sont placées dans le fichier.
Characteristics | contient des flags de la même façon que si cette section contenait du code exécutable, des données initialisées, des données non initialisées, ils peuvent être accessible en écriture aussi bien qu'en lecture.

Maintenant que nous connaissons la structure IMAGE_SECTION_HEADER, on va voir comment nous pouvons simuler le travail du PE loader :

  1. On lit la valeur de NumberOfSections dans l'IMAGE_FILE_HEADER, ainsi on sait combien de sections sont présente dans le fichier.
  2. Utilisez la valeur de SizeOfHeaders en tant qu'offset(de fichier) de la table des sections et déplacez le pointer de fichier sur cet offset.
  3. Promenez-vous à l'intérieur du tableau des structures, en examinant chaque membre.
  4. Pour chaque structure, nous obtenons la valeur dans PointerToRawData et nous déplaçons le pointer de fichier sur cet offset. Ensuite nous lisons la valeur dans SizeOfRawData et comme ça on sait combien d'octets on doit mapper en mémoire. On lit la valeur dans VirtualAddress et on y ajoute la valeur de l'ImageBase pour obtenir l'adresse virtuelle à laquelle la section doit commencer. Maintenant nous sommes prêts à mapper la section en mémoire et à donner ses attributs (à cette section) en mémoire d'après les flags contenus dans Characteristics.

Notez qu'on ne s'est pas servit du nom de la section : ce n'est pas vraiment nécessaire.

Exemple:

Cet exemple ouvre un PE file (un fichier Win32 exécutable) et se balade dans sa table des sections, il affiche les informations à propos de ses sections dans un 'listview control' (un objet prédéterminé par Windows (ici une simple page de listes de noms, de tailles, caractéristiques...)).

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
include \masm32\include\comctl32.inc
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_SECTIONTABLE equ 104
IDC_SECTIONLIST equ 1001

SEH struct
PrevLink dd ? ; adresse de la structure SEH ci-dessus
CurrentHandler dd ? ; adresse du nouvel 'exception handler'
SafeOffset dd ? ; endroit où est sauvegardé l'offset pour continuer l'exécution
PrevEsp dd ? ; l'ancienne valeur dans 'esp'
PrevEbp dd ? ; ancienne valeur dans 'ebp'
SEH ends

.data
AppName db "PE tutorial no.5",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
FileInValidPE db "This file is not a valid PE",0
template db "%08lx",0
SectionName db "Section",0
VirtualSize db "V.Size",0
VirtualAddress db "V.Address",0
SizeOfRawData db "Raw Size",0
RawOffset db "Raw Offset",0
Characteristics db "Characteristics",0

.data?
hInstance dd ?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?
NumberOfSections dd ?

.code
start proc
LOCAL seh:SEH
   invoke GetModuleHandle,NULL
   mov hInstance,eax
   mov ofn.lStructSize,SIZEOF ofn
   mov ofn.lpstrFilter, OFFSET FilterString
   mov ofn.lpstrFile, OFFSET buffer
   mov ofn.nMaxFile,512
   mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
   invoke GetOpenFileName, ADDR ofn
   .if eax==TRUE
      invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
      .if eax!=INVALID_HANDLE_VALUE
         mov hFile, eax
         invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
         .if eax!=NULL
            mov hMapping, eax
            invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
            .if eax!=NULL
               mov pMapping,eax
               assume fs:nothing
               push fs:[0]
               pop seh.PrevLink
               mov seh.CurrentHandler,offset SEHHandler
               mov seh.SafeOffset,offset FinalExit
               lea eax,seh
               mov fs:[0], eax
               mov seh.PrevEsp,esp
               mov seh.PrevEbp,ebp
               mov edi, pMapping
               assume edi:ptr IMAGE_DOS_HEADER
               .if [edi].e_magic==IMAGE_DOS_SIGNATURE
                  add edi, [edi].e_lfanew
                  assume edi:ptr IMAGE_NT_HEADERS
                  .if [edi].Signature==IMAGE_NT_SIGNATURE
                     mov ValidPE, TRUE
                  .else
                     mov ValidPE, FALSE
                  .endif
               .else
                  mov ValidPE,FALSE
               .endif
FinalExit:
               push seh.PrevLink
               pop fs:[0]
               .if ValidPE==TRUE
                  call ShowSectionInfo
               .else
                  invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
               .endif
               invoke UnmapViewOfFile, pMapping
           .else
               invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke CloseHandle,hMapping
       .else
          invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle, hFile
    .else
       invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
    .endif
  .endif
  invoke ExitProcess, 0
  invoke InitCommonControls
start endp

SEHHandler proc C uses pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
   mov edx,pFrame
   assume edx:ptr SEH
   mov eax,pContext
   assume eax:ptr CONTEXT
   push [edx].SafeOffset
   pop [eax].regEip
   push [edx].PrevEsp
   pop [eax].regEsp
   push [edx].PrevEbp
   pop [eax].regEbp
   mov ValidPE, FALSE
   mov eax,ExceptionContinueExecution
   ret
SEHHandler endp

DlgProc proc uses edi esi hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
   LOCAL lvc:LV_COLUMN
   LOCAL lvi:LV_ITEM
   .if uMsg==WM_INITDIALOG
      mov esi, lParam
      mov lvc.imask,LVCF_FMT or LVCF_TEXT or LVCF_WIDTH or LVCF_SUBITEM
      mov lvc.fmt,LVCFMT_LEFT
      mov lvc.lx,80
      mov lvc.iSubItem,0
      mov lvc.pszText,offset SectionName
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,0,addr lvc inc lvc.iSubItem
      mov lvc.fmt,LVCFMT_RIGHT
      mov lvc.pszText,offset VirtualSize
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,1,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset VirtualAddress
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,2,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset SizeOfRawData
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,3,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset RawOffset
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,4,addr lvc
      inc lvc.iSubItem
      mov lvc.pszText,offset Characteristics
      invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTCOLUMN,5,addr lvc
      mov ax, NumberOfSections
      movzx eax,ax
      mov edi,eax
      mov lvi.imask,LVIF_TEXT
      mov lvi.iItem,0
      assume esi:ptr IMAGE_SECTION_HEADER
      .while edi>0
         mov lvi.iSubItem,0
         invoke RtlZeroMemory,addr buffer,9
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8
         lea eax,buffer
         mov lvi.pszText,eax
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].Misc.VirtualSize
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].VirtualAddress
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].SizeOfRawData
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].PointerToRawData
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         invoke wsprintf,addr buffer,addr template,[esi].Characteristics
         lea eax,buffer
         mov lvi.pszText,eax
         inc lvi.iSubItem
         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_SETITEM,0,addr lvi
         inc lvi.iItem
         dec edi
         add esi, sizeof IMAGE_SECTION_HEADER
      .endw
   .elseif
      uMsg==WM_CLOSE
         invoke EndDialog,hDlg,NULL
   .else
      mov eax,FALSE
      ret
   .endif
   mov eax,TRUE
   ret
DlgProc endp

ShowSectionInfo proc uses edi
   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS
   mov ax,[edi].FileHeader.NumberOfSections
   movzx eax,ax
   mov NumberOfSections,eax
   add edi,sizeof IMAGE_NT_HEADERS
   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi
   ret
ShowSectionInfo endp
end start

Analyse:

Cet exemple réutilise le code de l'exemple du Tutorial 2. Après qu'il vérifie que le fichier soit bien un PE valide, il appelle une fonction, ShowSectionInfo.

ShowSectionInfo proc uses edi
   mov edi, pMapping
   assume edi:ptr IMAGE_DOS_HEADER
   add edi, [edi].e_lfanew
   assume edi:ptr IMAGE_NT_HEADERS

Nous nous servons d'edi en tant que pointer de données dans le PE file. D'abord, nous l'initialisons à la valeur 'pMapping' lequel est l'adresse du DOS header. Ensuite nous lui ajoutons la valeur d'e_lfanew, de cette façon il contient maintenant l'adresse du PE header.

   mov ax,[edi].FileHeader.NumberOfSections
   mov NumberOfSections,ax

Puisque nous avons besoin de la table des sections, il nous faut obtenir le nombre de sections dans le fichier. C'est la valeur du membre NumberOfSections du File header. N'oubliez pas que ce membre est d'une taille d'un WORD.

   add edi,sizeof IMAGE_NT_HEADERS

edi contient actuellement l'adresse du PE header. L'addition de la taille du PE header à l'adresse de début du PE Header, fera qu'on pointera sur la table des sections.

   invoke DialogBoxParam, hInstance, IDD_SECTIONTABLE,NULL, addr DlgProc, edi

On appelle DialogBoxParam pour afficher la boîte de dialogue contenant le 'listview control'. Remarquez que nous passons l'adresse de la table des sections en tant que son dernier paramètre. Cette valeur sera disponible dans lParam pendant le message WM_INITDIALOG.

Dans la procédure servant à traiter les messages de la DialogBox, en réponse au message WM_INITDIALOG, nous stockons la valeur d'lParam (l'adresse de la table des sections) dans esi, le nombre de sections dans edi et ensuite on crée le 'listview control'. Quand tout est prêt, nous entrons dans une boucle qui insérera les renseignements de chaque section dans le 'listview control'. Cette partie est très simple.

      .while edi>0
         mov lvi.iSubItem,0

Met cette chaîne de caractères dans la première colonne.

         invoke RtlZeroMemory,addr buffer,9
         invoke lstrcpyn,addr buffer,addr [esi].Name1,8
         lea eax,buffer
         mov lvi.pszText,eax

Nous affichons le nom de la section, mais nous devons d'abord la convertir en une chaîne de caractères ASCIIZ.

         invoke SendDlgItemMessage,hDlg,IDC_SECTIONLIST,LVM_INSERTITEM,0,addr lvi

Ainsi nous l'affichons dans la première colonne.
Nous continuons avec ce schéma tant que la dernière valeur que nous souhaitons montrer concernant cette section soit affichée. Ensuite nous devons nous déplacer jusqu'à la structure suivante.

         dec edi
         add esi, sizeof IMAGE_SECTION_HEADER
      .endw

On fait décroître la valeur dans edi à chaque fois qu'une section à été traitée. Et nous ajoutons la taille d'IMAGE_SECTION_HEADER à esi, ainsi il contient l'adresse de la structure IMAGE_SECTION_HEADER suivante.

Les étapes de notre petite balade dans la table des sections sont les suivantes:

  1. On vérifie que le fichier est un PE valide
  2. On se place au début du PE header
  3. On récupère le nombre de sections grâce au paramètre NumberOfSections dans le File header.
  4. On arrive à la table des sections soit en ajoutant ImageBase et SizeOfHeaders soit en ajoutant l'adresse du PE header et la taille du PE header. (La table des sections suit immédiatement le PE header). Si vous n'utilisez pas le procédé de 'File Mapping', vous devez déplacer le pointer de fichier sur la table des sections en employant SetFilePointer. L'offset(de fichier) de la table des sections est dans SizeOfHeaders.(SizeOfHeaders est un membre d'IMAGE_OPTIONAL_HEADER)
  5. On épie chaque structure IMAGE_SECTION_HEADER.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's PE Tutorial 6: Import Table

Tutorial 6: La Table des Importations
(Import Table)

Dans ce Tutorial nous allons voir la table des importations. Laissez-moi vous avertir d'abord, ce Tutorial est long et difficile pour ceux qui ne connaissent rien à la table des importations. Il y a des chances que vous deviez relire ce Tutorial plusieurs fois et deviez même examiner les structures relatives aux debuggers.

Downloadez l'exemple.

Théorie:

Tout d'abord, vous devez savoir ce qu'est une fonction d'importation. Une fonction d'importation est une fonction qui n'est pas directement dans le programme, mais est appelée par le programme, d'où le nom "import". Les fonctions d'importation résident en réalité dans une ou plusieurs DLLs. Seules les informations à propos de ces fonctions sont contenue dans le programme qui fait appel à la DLL dans laquelle est contenue le fonction d'importation. Ces informations sont entre autres le nom de chaque fonction et le nom de la (ou les) DLL(s) dans lesquels elles ont été placées.
Maintenant comment pouvons-nous découvrir à quel endroit dans le PE file (le programme) se situent ces informations ? Nous devons nous tourner vers le Répertoire des Données pour trouver la réponse. Je vous rafraîchi un peu la mémoire. Ci-dessous le PE header :

IMAGE_NT_HEADERS STRUCT
   Signature dd ?
   FileHeader IMAGE_FILE_HEADER <>
   OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS

Le dernier membre de l'Optional header est le répertoire des données:

IMAGE_OPTIONAL_HEADER32 STRUCT
   ....
   LoaderFlags dd ?
   NumberOfRvaAndSizes dd ?
   DataDirectory IMAGE_DATA_DIRECTORY 16 dup(<>)
IMAGE_OPTIONAL_HEADER32 ENDS

Le 'répertoire des données' est un tableau, ce tableau est en faite une structure du nom d'IMAGE_DATA_DIRECTORY. Elle possède 16 membres (ou paramètres). Si vous vous rappelez de la table des sections qu'on avait comparés à un répertoire racine pour les sections du PE file, et bien vous pouvez de la même manière comparer le 'répertoire des données' à un répertoire racine des composants logiques stockés à l'intérieur de ces sections. Pour être plus précis, le 'répertoire des données' contient les emplacements et les tailles des structures de données importantes à l'intérieur du PE file. Chaque membre contient des informations sur une structure de données importante.

Membre | Info qu'il contient
0 | Export symbols
1 | Import symbols
2 | Resources
3 | Exception
4 | Security
5 | Base relocation
6 | Debug
7 | Copyright string
8 | Unknown
9 | Thread local storage (TLS)
10 | Load configuration
11 | Bound Import
12 | Import Address Table
13 | Delay Import
14 | COM descriptor

Je n'ai des connaissances qu'uniquement sur les membres surlignés. Maintenant que vous connaissez ce que contient chaque membre du 'répertoire des données', nous pouvons regarder en détail le membre. Chaque membre du 'répertoire des données' est une structure appelée IMAGE_DATA_DIRECTORY laquelle est défie comme suit :

IMAGE_DATA_DIRECTORY STRUCT
  VirtualAddress dd ?
  isize dd ?
IMAGE_DATA_DIRECTORY ENDS

VirtualAddress est en réalité l'adresse relative virtuelle (RVA) de la structure de données (Data Structure). Par exemple, si cette structure est destinée aux Symboles d'Importation, alors ce membre contient le RVA du tableau IMAGE_IMPORT_DESCRIPTOR.
isize contient la taille en octets de la structure de données mentionnée par VirtualAddress .

Voici l'arrangement général des différentes structures de données importantes dans un PE file:

  1. A partir du DOS header, vous allez au PE header
  2. Récupérez l'adresse du 'Répertoire des Données' dans l'Optional Header.
  3. Multipliez la taille d'IMAGE_DATA_DIRECTORY par l'index du membre que vous voulez : par exemple si vous voulez savoir où les symboles d'importation se situent, vous devez multiplier la taille d'IMAGE_DATA_DIRECTORY (8 octets) par 1.
  4. Ajoutez le résultat à l'adresse du 'Répertoire des données' et vous obtenez ainsi l'adresse de la structure IMAGE_DATA_DIRECTORY qui contient les renseignements sur la 'structure de données' désirée.

Maintenant on arrête de jouer, on va pouvoir vraiment discuter de la table des importations. L'adresse de la table des importations est contenue dans le membre (paramètre) VirtualAddress du deuxième membre du Répertoire des Données. La table des importations est en réalité un tableau contenant plusieures structures IMAGE_IMPORT_DESCRIPTOR. Chaque structure contient des informations sur une DLL dont le PE File importe ses symboles. Par exemple, si le PE File importe 10 fonctions appartenant à 10 DLLs différentes, alors nous retrouverons 10 membres dans ce tableau. Le dernier membre de ce tableau qui sert à terminé le tableau ne contient que des zéros. Maintenant nous pouvons examiner la structure en détail :

IMAGE_IMPORT_DESCRIPTOR STRUCT
  union
    Characteristics dd ?
    OriginalFirstThunk dd ?
  ends
  TimeDateStamp dd ?
  ForwarderChain dd ?
  Name1 dd ?
  FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS

Le premier membre de cette structure est une union. En réalité, l'union fournit seulement le pseudonyme d'OriginalFirstThunk, de cette façon vous pouvez appeler "Characteristics". Ce membre contient le RVA d'un tableau contenant des structures IMAGE_THUNK_DATA.
Qu'est-ce que IMAGE_THUNK_DATA ? C'est une union de taille dword. D'habitude, nous l'interprétons comme le pointer portant sur une structure IMAGE_IMPORT_BY_NAME. Remarquez qu'IMAGE_THUNK_DATA contient le pointer qui est sur une structure IMAGE_IMPORT_BY_NAME : et non pas sur la structure elle-même.
Regardez sa logique : Il y a plusieurs structures IMAGE_IMPORT_BY_NAME. Nous rassemblons dans un tableau le RVA de ces structures (IMAGE_THUNK_DATAs), et le terminons avec un 0. Ensuite nous mettons le RVA du tableau (lui-même) dans OriginalFirstThunk.
La structure IMAGE_IMPORT_BY_NAME contient les informations à propos d'une fonction d'importation. Maintenant regardons à quoi ressemble la structure IMAGE_IMPORT_BY_NAME:

IMAGE_IMPORT_BY_NAME STRUCT
  Hint dw ?
  Name1 db ?
IMAGE_IMPORT_BY_NAME ENDS

Hint contient l'index de la fonction, cet index réside dans 'la table d'exportations de la DLL'. Ce membre sert uniquement à l'utilisation du PE Loader. Ainsi le PE Loader peut rapidement retrouver la fonction à partir de la table d'exportation de la DLL. Ce n'est pas une valeur essentielle et il se peut même que certains linkers mette la valeur de ce paramètre à 0.
Name1 contient le nom de la fonction d'importation. Le nom est une chaîne de caractères ASCIIZ. Notez que la taille de Name1 est par défaut d'un octet mais en réalité c'est un paramètre de taille variable. C'est juste qu'il n'y a aucune façon de représenter un paramètre de taille variable dans une structure. Cette structure nous est gracieusement fournit, ainsi nous pouvons nous référer à la 'Structure de Données' avec des noms en guise de descriptifs.

TimeDateStamp et ForwarderChain sont des machins avancés : Nous parlerons d'eux une foie que vous aurez bien pris la main avec les autres membres.

Name1 contient le RVA du nom de la DLL, bref, le pointer qui est sur le nom de la DLL. C'est une chaîne de caractères ASCIIZ.

FirstThunk est très semblable à OriginalFirstThunk, c'est-à-dire. Il contient le RVA d'un tableau contenant des structures IMAGE_THUNK_DATA (bien qu'en fait c'est un tableau différent).
Ok, si vous êtes encore embrouillés, regardez ça : Il y a plusieurs structures IMAGE_IMPORT_BY_NAME. Vous créez deux tableaux, les remplissez ensuite avec les RVAs de ces structures IMAGE_IMPORT_BY_NAME, donc les deux tableaux contiennent exactement les mêmes valeurs (c'est-à-dire des duplicatas exactes). Maintenant vous assignez le RVA du premier tableau à OriginalFirstThunk et le RVA du deuxième tableau à FirstThunk .

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Fonction 1
Fonction 2
Fonction 3
Fonction 4
...
Fonction n
<---
<---
<---
<---
<---
<---
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA

Maintenant vous devez être capables de comprendre ce que je veux dire. Ne soyez pas intimidé par le nom IMAGE_THUNK_DATA : c'est seulement un RVA dans la structure IMAGE_IMPORT_BY_NAME. Si vous remplacez le mot IMAGE_THUNK_DATA par RVA dans votre esprit, ça vous semblera peut-être plus clairement. Le nombre d'éléments dans les tableaux OriginalFirstThunk et FirstThunk dépend des Fonctions que le PE file importe de la DLL. Par exemple, si le PE file importe 10 Fonctions de kernel32.DLL, alors Name1 appartenant à la structure IMAGE_IMPORT_DESCRIPTOR contiendra le RVA de la chaîne de caractères "kernel32.DLL" et il y aura 10 IMAGE_THUNK_DATAs dans chaque tableau.
La question suivante c'est : pourquoi avons-nous besoin de deux tableaux qui sont identiques trait pour trait ? En réponse à cette question, une fois que le PE file est chargé en mémoire nous avons besoin de savoir ce qu'il regardera dans les IMAGE_THUNK_DATAs et IMAGE_IMPORT_BY_NAMEs, il déterminera les adresses des Fonctions d'importation. Ainsi il remplace les IMAGE_THUNK_DATAs dans le tableau indiqué par FirstThunk avec les adresses réelles des Fonctions. Ainsi lorsque le PE file est prêt à être exécuté, la susdite image est déjà modifiée :

OriginalFirstThunk IMAGE_IMPORT_BY_NAME FirstThunk

|

|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
--->
--->
--->
--->
--->
--->
Fonction 1
Fonction 2
Fonction 3
Fonction 4
...
Fonction n
Adresse de la Fonction 1
Adresse de la Fonction 2
Adresse de la Fonction 3
Adresse de la Fonction 4
...
Adresse de la Fonction n

Le tableau des RVAs indiqué par OriginalFirstThunk reste inchangé pour que si le besoin s'en fasse sentir il soit possible de retrouver les noms des fonctions importées, le PE loader peut toujours les retrouver.
Il y a une petite entorse à ce schéma *simple et directe*. Certaines fonctions sont uniquement exportées suivant un ordre. Ça signifie que vous n'appelez pas les fonctions par leurs noms : vous les appelez par leurs positions. Dans ce cas, il n'y aura aucune structure IMAGE_IMPORT_BY_NAME pour ces fonctions dans le 'module Caller' (le programme principale qui fait appelle à une DLL pour se servir de ses fonctions (en les important)). Au lieu de ça, l'IMAGE_THUNK_DATA pour cette fonction contiendra le n° de position de la fonction dans le WORD de poids faible et le 'bit de poids fort' (MSB ou the most significant bit) d'IMAGE_THUNK_DATA sera mis à 1. Par exemple, si une fonction est exportée uniquement par ordre et que son n° est 1234h, l'IMAGE_THUNK_DATA pour cette fonction sera 80001234h. Microsoft fournit une constante très pratique pour tester l'état du MSB d'un dword, IMAGE_ORDINAL_FLAG32. Sa valeur est de 80000000h. (=poids fort à 1)
Supposons que nous voulons inscrire les noms de TOUTES les fonctions d'importation du PE file, nous avons besoin de suivre les étapes ci-dessous :

  1. On vérifie que le fichier est un PE File valide
  2. A partir du DOS header, on va au PE header
  3. On récupère l'adresse du Répertoire des Données présent dans l'OptionalHeader
  4. On passe au 2e member du Répertoire des Données. Et on extrait la valeur de VirtualAddress
  5. On se sert de cette valeur pour aller à la première structure IMAGE_IMPORT_DESCRIPTOR
  6. On vérifie la valeur d'OriginalFirstThunk. S'il n'est pas à zéro, on passe au RVA suivant d'OriginalFirstThunk dans le tableau de RVA. Si OriginalFirstThunk est nul (zéro), on utilise la valeur dans FirstThunk à sa place. Certains linkers génèrent des PE File avec 0 dans OriginalFirstThunk. C'est considéré comme un bogue(défaut). Juste pour rester sur le droit chemin, nous vérifions d'abord la valeur dans OriginalFirstThunk.
  7. Pour chaque membre du tableau, nous comparons la valeur de ce membre avec IMAGE_ORDINAL_FLAG32. Si le MSB de ce membre est a 1, alors la fonction est exportée par ordre et nous pouvons extraire le numéro d'ordre à partir du WORD de poids faible du membre.
  8. Si le bit le plus significatif (MSB) du membre est a 0, on utilise la valeur dans le membre en tant qu'RVA dans l'IMAGE_IMPORT_BY_NAME, on saute à Hint et on est au nom de la fonction.
  9. On saute au membre suivant du tableau et on retrouve les noms avant qu'on ai atteint la fin du tableau (qui est un null de terminaison). Maintenant nous avons réussi à extraire les noms des fonctions importées à partir d'une DLL, nous allons à la DLL suivante.
  10. On saute à l'IMAGE_IMPORT_DESCRIPTOR suivant et le traitons. Faites-le avant que la fin du tableau ne soit atteinte (le tableau IMAGE_IMPORT_DESCRIPTOR est terminé par un membre dont les paramètres sont tous nuls) .

Exemple:

Cet exemple ouvre un PE file et lit puis affiche les noms de toutes les fonctions d'importation de ce fichier dans un contrôle d'édition. Il affiche aussi les valeurs dans les structures IMAGE_IMPORT_DESCRIPTOR.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowImportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
PrevLink dd ? ; adresse de la structure SEH ci-dessus
CurrentHandler dd ? ; adresse du nouvel 'exception handler'
SafeOffset dd ? ; endroit où est sauvegardé l'offset pour continuer l'exécution
PrevEsp dd ? ; l'ancienne valeur dans 'esp'
PrevEbp dd ? ; l'ancienne valeur dans 'ebp'
SEH ends

.data
AppName db "PE tutorial no.6",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
CRLF db 0Dh,0Ah,0
ImportDescriptor db 0Dh,0Ah,"================[ IMAGE_IMPORT_DESCRIPTOR ]=============",0
IDTemplate db "OriginalFirstThunk = %lX",0Dh,0Ah
           db "TimeDateStamp = %lX",0Dh,0Ah
           db "ForwarderChain = %lX",0Dh,0Ah
           db "Name = %s",0Dh,0Ah
           db "FirstThunk = %lX",0
NameHeader db 0Dh,0Ah,"Hint Function",0Dh,0Ah
           db "-----------------------------------------",0
NameTemplate db "%u %s",0
OrdinalTemplate db "%u (ord.)",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
  invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
  invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
  .if lParam==0
    mov eax,wParam
    .if ax==IDM_OPEN
      invoke ShowImportFunctions,hDlg
    .else ; IDM_EXIT
      invoke SendMessage,hDlg,WM_CLOSE,0,0
    .endif
  .endif
.else
  mov eax,FALSE
  ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc C pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
  mov edx,pFrame
  assume edx:ptr SEH
  mov eax,pContext
  assume eax:ptr CONTEXT
  push [edx].SafeOffset
  pop [eax].regEip
  push [edx].PrevEsp
  pop [eax].regEsp
  push [edx].PrevEbp
  pop [eax].regEbp
  mov ValidPE, FALSE
  mov eax,ExceptionContinueExecution
  ret
SEHHandler endp

ShowImportFunctions proc uses edi hDlg:DWORD
  LOCAL seh:SEH
  mov ofn.lStructSize,SIZEOF
  ofn mov ofn.lpstrFilter, OFFSET FilterString
  mov ofn.lpstrFile, OFFSET buffer
  mov ofn.nMaxFile,512
  mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
  invoke GetOpenFileName, ADDR ofn
  .if eax==TRUE
    invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
    .if eax!=INVALID_HANDLE_VALUE
      mov hFile, eax
      invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
      .if eax!=NULL
        mov hMapping, eax
        invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
        .if eax!=NULL
          mov pMapping,eax
          assume fs:nothing
          push fs:[0]
          pop seh.PrevLink
          mov seh.CurrentHandler,offset SEHHandler
          mov seh.SafeOffset,offset FinalExit
          lea eax,seh
          mov fs:[0], eax
          mov seh.PrevEsp,esp
          mov seh.PrevEbp,ebp
          mov edi, pMapping
          assume edi:ptr IMAGE_DOS_HEADER
          .if [edi].e_magic==IMAGE_DOS_SIGNATURE
            add edi, [edi].e_lfanew
            assume edi:ptr IMAGE_NT_HEADERS
            .if [edi].Signature==IMAGE_NT_SIGNATURE
              mov ValidPE, TRUE
            .else
              mov ValidPE, FALSE
            .endif
          .else
            mov ValidPE,FALSE
          .endif
FinalExit:
          push seh.PrevLink
          pop fs:[0]
          .if ValidPE==TRUE
            invoke ShowTheFunctions, hDlg, edi
          .else
            invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
          .endif
          invoke UnmapViewOfFile, pMapping
      .else
          invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
      .endif
      invoke CloseHandle,hMapping
    .else
      invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
    .endif
    invoke CloseHandle, hFile
   .else
   invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
 .endif
 ret
ShowImportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
   ret
AppendText endp

RVAToOffset PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
   mov esi,pFileMap
   assume esi:ptr IMAGE_DOS_HEADER
   add esi,[esi].e_lfanew
   assume esi:ptr IMAGE_NT_HEADERS
   mov edi,RVA ; edi == RVA
   mov edx,esi
   add edx,sizeof IMAGE_NT_HEADERS
   mov cx,[esi].FileHeader.NumberOfSections
   movzx ecx,cx
   assume edx:ptr IMAGE_SECTION_HEADER
   .while ecx>0 ; check all sections
     .if edi>=[edx].VirtualAddress
       mov eax,[edx].VirtualAddress
       add eax,[edx].SizeOfRawData
       .if edi<eax ; The address is in this section
         mov eax,[edx].VirtualAddress
         sub edi,eax
         mov eax,[edx].PointerToRawData
         add eax,edi ; eax == file offset
         ret
       .endif
     .endif
     add edx,sizeof IMAGE_SECTION_HEADER
     dec ecx
   .endw
   assume edx:nothing
   assume esi:nothing
   mov eax,edi
   ret
RVAToOffset endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE
   invoke SetDlgItemText,hDlg,IDC_EDIT,0
   invoke AppendText,hDlg,addr buffer
   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress
   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping
   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)
     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping
     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp
     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif
     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax
     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0
       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal
       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME
       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText
ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx
ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4
    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR
  .endw
  ret
ShowTheFunctions endp
end start

Analyse:

Le programme affiche une DialogBox de type OPENFILE (laquelle sert à sélectionner et ouvrir un fichier quand l'utilisateur clique dessus). Il vérifie que le fichier est un PE valide et appelle ensuite ShowTheFunctions.

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
   LOCAL temp[512]:BYTE

Réservez un espace de 512 octets sur la pile pour des opérations sur des chaînes de caractères.

   invoke SetDlgItemText,hDlg,IDC_EDIT,0

Efface le texte dans le contrôle d'édition

   invoke AppendText,hDlg,addr buffer

Insère le nom du PE file dans le contrôle d'édition.AppendText envoie juste des messages EM_REPLACESEL pour ajouter du texte dans le contrôle d'édition. Notez qu'il envoie EM_SETSEL avec wParam=-1 et lParam=0 au contrôle d'édition pour déplacer le curseur en fin de texte.

   mov edi,pNTHdr
   assume edi:ptr IMAGE_NT_HEADERS
   mov edi, [edi].OptionalHeader.DataDirectory[sizeof IMAGE_DATA_DIRECTORY].VirtualAddress

Sert à récupérer le RVA des symboles d'importation. Au départ edi pointe sur le PE header. On s'en sert pour aller jusqu'au 2ème membre du tableau du Répertoire des Données et on récupère la valeur du membre VirtualAddress.

   invoke RVAToOffset,pMapping,edi
   mov edi,eax
   add edi,pMapping

Voici ici un des pièges pour les nouveaux venus à la programmation des PE. La plupart des adresses dans un PE file sont des RVAs et les RVAs n'ont de sens que lorsque le PE file est chargé en mémoire par le PE loader. Dans notre cas, nous mappons le fichier en mémoire, mais pas comme le fait le PE loader. Ainsi nous ne pouvons pas employer directement ces RVAs. D'une façon ou d'une autre nous devons convertir ces RVAs offsets pour le fichier. J'ai écris la fonction RVAToOffset dans ce but. Je ne l'analyserai pas en détail ici. Il est suffisant de dire qu'il compare le RVA qu'on lui a suggéré avec les début et fins des RVAs de toutes les sections du PE file, et utilise la valeur du paramètre PointerToRawData de la structure IMAGE_SECTION_HEADER pour convertir le RVA en un offset pour le fichier.
Pour utiliser cette fonction, vous devez lui passer deux paramètres : le pointer sur le fichier a Mapper en mémoire et le RVA que vous voulez convertir. Elle retourne l'offset(de fichier) dans eax. Dans ce susdit petit bout, nous devons additionner le 'pointer sur le fichier a Mapper en mémoire' avec l'offset (de fichier)' pour le convertir en une adresse virtuelle. Ça vous semble compliqué, huh ? :)

   assume edi:ptr IMAGE_IMPORT_DESCRIPTOR
   .while !([edi].OriginalFirstThunk==0 && [edi].TimeDateStamp==0 && [edi].ForwarderChain==0 && [edi].Name1==0 && [edi].FirstThunk==0)

edi pointe maintenant sur la première structure IMAGE_IMPORT_DESCRIPTOR. Nous lisons le tableau jusqu'à ce que nous trouvions la structure qui ne comporte que des membres nuls, elle sert à marquer la fin du tableau.

     invoke AppendText,hDlg,addr ImportDescriptor
     invoke RVAToOffset,pMapping, [edi].Name1
     mov edx,eax
     add edx,pMapping

Nous souhaitons afficher les valeurs de la structure IMAGE_IMPORT_DESCRIPTOR courante dans le contrôle d'édition. Name1 diffère des autres membres puisque il contient le RVA du nom de la dll. C'est pourquoi nous devons d'abord le convertir en une adresse virtuelle.

     invoke wsprintf, addr temp, addr IDTemplate, [edi].OriginalFirstThunk,[edi].TimeDateStamp,[edi].ForwarderChain,edx,[edi].FirstThunk      invoke AppendText,hDlg,addr temp

Affiche les valeurs de l'IMAGE_IMPORT_DESCRIPTOR courante.

     .if [edi].OriginalFirstThunk==0
        mov esi,[edi].FirstThunk
     .else
        mov esi,[edi].OriginalFirstThunk
     .endif

Ensuite nous nous préparons à parcourir le tableau IMAGE_THUNK_DATA. Normalement nous aimerions pouvoir utiliser le tableau pointé par OriginalFirstThunk. Cependant, certains mauvais linkers mettent 0 dans OriginalFirstThunk, c'est pourquoi nous devons d'abord vérifier si la valeur d'OriginalFirstThunk est zéro ou non. S'il est à 0, nous employons le tableau pointé par FirstThunk instead.

     invoke RVAToOffset,pMapping,esi
     add eax,pMapping
     mov esi,eax

De nouveau, la valeur dans OriginalFirstThunk/FirstThunk est un RVA. Nous devons la convertir en une adresse virtuelle.

     invoke AppendText,hDlg,addr NameHeader
     .while dword ptr [esi]!=0

Maintenant nous sommes prêts nous promener dans le tableau IMAGE_THUNK_DATAs pour chercher les noms des fonctions importées dans cette DLL. Nous lisons le tableau jusqu'à ce que nous trouvions une entrée qui contient 0.

       test dword ptr [esi],IMAGE_ORDINAL_FLAG32
       jnz ImportByOrdinal

La première chose que nous faisons avec l'IMAGE_THUNK_DATA c'est de le comparer à IMAGE_ORDINAL_FLAG32. Ce teste est validé si le bit le plus significatif (MSB) d'IMAGE_THUNK_DATA est 1. S'il l'est, la fonction est exportée par ordre donc nous n'avons aucun besoin de la traiter plus loin. Nous pouvons extraire son n° d'ordre à partir du WORD de poids faible d'IMAGE_THUNK_DATA puis passer au DWORD IMAGE_THUNK_DATA suivant.

       invoke RVAToOffset,pMapping,dword ptr [esi]
       mov edx,eax
       add edx,pMapping
       assume edx:ptr IMAGE_IMPORT_BY_NAME

Si le MSB d'IMAGE_THUNK_DATA est 0, il contient le RVA de la structure IMAGE_IMPORT_BY_NAME. Nous avons d'abord besoin de le convertir en une adresse virtuelle.

       mov cx, [edx].Hint
       movzx ecx,cx
       invoke wsprintf,addr temp,addr NameTemplate,ecx,addr [edx].Name1
       jmp ShowTheText

Hint est un paramètre de taille WORD. Nous devons le convertir en une valeur de taille DWORD avant de l'envoyer à l'API wsprintf. Et nous affichons ensemble dans le contrôle d'édition 'Hint' et le nom de la fonction

ImportByOrdinal:
       mov edx,dword ptr [esi]
       and edx,0FFFFh
       invoke wsprintf,addr temp,addr OrdinalTemplate,edx

Au cas où la fonction est uniquement exportée par ordre, nous mettons à zéro le WORD de poids fort et plaçons le n° d'ordre.

ShowTheText:
       invoke AppendText,hDlg,addr temp
       add esi,4

Après avoir inséré le nom/numéro de la fonction dans le contrôle d'édition, nous passons à l'IMAGE_THUNK_DATA suivante.

    .endw
    add edi,sizeof IMAGE_IMPORT_DESCRIPTOR

Quand tous les dwords IMAGE_THUNK_DATA du tableau ont été traités, nous sautons au IMAGE_IMPORT_DESCRIPTOR suivant pour traiter les fonctions d'importation des éventuelles autres DLLs.

Annexe:

Ce serait incomplet si je ne mentionnais rien sur les importations attachées (Bound Import). Pour expliquer ce que c'est, j'ai besoin de faire un petit aparté. Quand le PE loader charge le PE file (un programme) en mémoire, il examine sa table des importations et charge les DLLs qui lui serviront dans l'espace d'adresses qui lui est réservée. Ensuite il parcoure le tableau IMAGE_THUNK_DATA de long en large comme nous l'avons fait et remplace les IMAGE_THUNK_DATAs par les adresses réelles des fonctions d'importation, ça ne prend pas longtemps. Si d'une façon ou d'une autre le programmeur peut prévoir les adresses des fonctions correctement, alors le PE loader n'a pas besoin de fixer les IMAGE_THUNK_DATAs à chaque fois que le PE file est exécuté. L'importation attachée est le produit de cette idée.
Pour l'expliquée en termes simples, il existe un utilitaire nommé bind.exe qui est fournit avec les compilateurs de Microsoft tel que Visual Studio qui examine la table des importations du PE file et remplace les dwords IMAGE_THUNK_DATA par les adresses des fonctions d'importation. Lorsque le fichier est chargé, le PE loader doit vérifier si les adresses sont valides. Si les versions des DLL ne correspondent pas avec celles utilisées par le PE File ou bien si les DLLs ont besoin d'être transférées, alors le PE loader sait que les adresses de precomputed ne sont pas valides c'est pourquoi il doit parcourir le tableau indiqué par OriginalFirstThunk pour recalculer les nouvelles adresses des fonctions d'importation.
L'importation attachée n'a pas beaucoup de signification pour notre exemple parce que nous utilisons OriginalFirstThunk par défaut. Pour plus d'informations sur les importations attachées, je vous recommande de lire la doc LUEVELSMEYER's pe.txt.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


Iczelion's PE Tutorial 7: Export Table

Tutorial 7: La Table des Exportations
(Export Table)

Nous avons vus la première partie des liaisons dynamiques, à savoir la table des importations, ceci dans le Tutorial précédent. Maintenant nous allons nous attaquer à l'autre partie, la table des exportations.

Downloadez l'exemple.

Théorie:

Quand le PE loader lance un programme, il charge également les DLLs associées à ce process dans son espace d'adresses. Il extrait alors les informations sur les fonctions d'importation à partir du programme principal. Il se sert de ces informations pour rechercher dans les DLLs les adresses des fonctions que le programme principal va appeler. L'endroit à l'intérieur des DLLs où le PE loader recherche ces adresses (celles des fonctions) est la table des exportations.

Quand une DLL/EXE exporte une fonction pour qu'elle soit utilisée par d'autres DLLs/EXEs, elle peut le faire de deux façons : elle peut exporter la fonction à partir de son nom ou à partir de son classement (par ordre). Dire qu'il existe une fonction du nom de "GetSysConfig" dans une DLL, signifie que si d'autres DLLs/EXEs souhaitent appeler cette fonction, elles doivent le spécifier à partir de son nom, c'est-à-dire : GetSysConfig. L'autre méthode c'est d'exporter la fonction d'après sa position (son n° d'ordre dans la DLL). Qu'est ce qu'un ordre ? Un ordre est un numéro sur 16 bits qui identifie une unique fonction dans une DLL particulière. Ce numéro est unique seulement dans la DLL à laquelle le programme se réfère. Par exemple, dans le susdit exemple, la DLL peut vouloir exporter la fonction par ordre, disons le n°16 (la 16e fonction à l'intérieur de la DLL). Alors les autres DLLs/EXEs qui souhaitent appeler cette fonction doit spécifier ce numéro dans GetProcAddress. Ceci une exportation appelée par ordre (unique).

L'exportation par ordre est fortement déconseillée car elle peut provoquer des problèmes de maintenance pour la DLL. Si la DLL est mise à jour (nouvelle version), alors le programmeur de la DLL n'a pas d'autre choix que de garder l'ordre des fonctions sinon d'autres programmes qui dépendent eux aussi de cette DLL se planteront.

Maintenant nous pouvons examiner la structure d'exportation. De la même façon qu'avec la table des importations, vous pouvez trouver où est située la table des exportations en regardant le 'Répertoire de Données'. Dans ce cas, la table des exportations est le premier membre du Répertoire de Données. La structure d'exportation est appelée IMAGE_EXPORT_DIRECTORY. Elle possède 11 membres, mais seulement quelques-uns sont réellement utiles.

Nom des paramètres | Signification
nName | Le nom réel du module qui contient les fonctions (nom de la DLL). C'est un paramètre nécessaire parce que le nom du fichier peut être modifié. Si c'est le cas, le PE loader emploiera ce nom intérieurement.
nBase | Un nombre que vous devez appliquer sur le n° d'ordre des fonctions pour obtenir leurs index dans le tableau d'adresses des fonctions.
NumberOfFunctions | Le nombre total de fonctions/symboles qui sont exportés par ce module (par la DLL).
NumberOfNames | Le nombre de fonctions/symboles qui sont exportés grâce à leur nom. Cette valeur n'est pas le nombre TOTAL de fonctions/symboles dans le module. Pour le nombre total, vous avez besoin de regarder le paramètre NumberOfFunctions. Cette valeur peut être nulle. Dans ce cas, le module ne peut exporter que par ordre. S'il n'y a aucune fonction/symbole qui puissent être exportée par son nom, le RVA de la table des exportations à l'intérieur du Répertoire des Données sera 0.
AddressOfFunctions | Est un RVA qui pointe sur un tableau contenant les RVAs des fonctions/symboles du module. Bref, les RVAs de toutes les fonctions du module sont regroupés dans un tableau et ce paramètre pointe sur le début de ce tableau.
AddressOfNames | Un RVA qui pointe sur un tableau de RVAs contenant les noms des fonctions de la DLL (du module).
AddressOfNameOrdinals | Un RVA qui pointe sur un tableau (16 bits) qui contient les n° d'ordre associés aux noms des fonctions dans le tableau AddressOfNames ci-dessus.

Cette simple lecture de la table ne peut pas vous donner une image réelle de la table des exportations. Voici une explication simplifiée qui éclairera son concept.

La table des exportations existe pour que le PE loader puisse s'en servir. Tout d'abord, le module (la DLL) doit garder les adresses de chacune des fonctions exportées quelque part, ainsi le PE loader peut les retrouver. Il les référence dans un tableau qui est pointé par le paramètre AddressOfFunctions. Le nombre d'éléments dans le tableau est contenu dans NumberOfFunctions. Ainsi si le module (la DLL) exporte 40 fonctions, il doit y avoir 40 membres (paramètres) dans le tableau pointé par AddressOfFunctions, et NumberOfFunctions doit contenir la valeur 40. Maintenant si certaines fonctions sont exportées par leurs noms, le module doit contenir les noms. Il conserve les RVAs des noms dans un tableau, ainsi le PE loader peut les retrouver. Ce tableau est pointé par AddressOfNames et le nombre de noms est dans NumberOfNames. Pensez au travail du PE loader, il connaît les noms des fonctions, il doit d'une façon ou d'une autre obtenir les adresses de ces fonctions. Jusqu'ici, le module (la DLL) a deux tableaux à sa disposition : les noms et les adresses mais il n'y a aucun lien entre eux. C'est pourquoi nous avons besoin de quelque chose qui qui puisse relier ces noms de fonctions à leurs adresses. La spécification du PE c'est d'employer des index à l'intérieur d'un tableau d'adresses pour remplacer ce lien essentiel. Ainsi si le PE loader trouve le nom, alors il cherche dans le tableau des noms, il peut récupérer l'index dans la table d'adresses pour ce nom précis. Les index sont regroupés dans un autre tableau (le dernier) pointés par le paramètre AddressOfNameOrdinals. Puisque ce tableau existe en tant que lien entre les noms et les adresses, il doit posséder exactement le même nombre d'éléments que le tableau des noms. C'est-à-dire, chaque nom peut avoir une et uniquement une adresse associée. L'inverse n'est pas vrai : une seule adresse peut posséder plusieurs noms qui lui soient associés. Ainsi nous pouvons avoir des "alias" qui se réfèrent à une même adresse. Pour faire le travail qui consiste à élaborer une relation entre les noms et les index dans le tableau, on doit se déplacer en parallèle. C'est-à-dire, que le premier élément (1er index) du tableau doit contenir l'index associé au premier nom, et ainsi de suite...

AddressOfNames AddressOfNameOrdinals
|
|
RVA du Nom 1
RVA du Nom 2
RVA du Nom 3
RVA du Nom 4
...
RVA du Nom N
<-->
<-->
<-->
<-->
...
<-->
Index du Nom 1
Index du Nom 2
Index du Nom 3
Index du Nom 4
...
Index du Nom N

Un exemple ou deux suivant l'appel par ordre. Si nous avons le nom d'une fonction d'exportation et que nous avons besoin d'obtenir son adresse dans le module (la DLL), nous pouvons nous débrouiller comme ça:

  1. On va jusqu'au PE header
  2. On lit l'adresse virtuelle de la table des exportations dans le Répertoire des données
  3. On va à la table des exportations et on récupère le nombre de noms qui est dans (NumberOfNames)
  4. On parcours les tableaux pointés par AddressOfNames et AddressOfNameOrdinals en parallèle, en recherchant le nom (de la fonction) correspondant. Si le nom est trouvé dans le tableau AddressOfNames, vous devez extraire la valeur dans l'élément associé du tableau AddressOfNameOrdinals. Par exemple, si vous trouvez le RVA du nom correspondant, dans le 77h élément du tableau AddressOfNames, vous devez extraire la valeur stockée dans le 77h élément du tableau AddressOfNameOrdinals. Si vous parcourez le tableau jusqu'au dernier élément du tableau NumberOfNames sans succès, alors vous savez que le nom n'est pas dans ce module. (Votre programme principal fait appelle à une fonction dans une DLL mais cette fonction ne s'y trouve pas en fait.)
  5. On utilise la valeur du tableau AddressOfNameOrdinals en tant qu'index pour le tableau AddressOfFunctions. C'est à dire, si la valeur est 5, alors vous devez extraire la valeur contenue dans le 5ème élément du tableau AddressOfFunctions. Cette valeur est le RVA de la fonction.

Maintenant nous pouvons attirer notre attention sur le membre nBase de la structure IMAGE_EXPORT_DIRECTORY. Vous savez déjà que le tableau AddressOfFunctions contient les adresses de tous les symboles d'exportation d'une DLL. Et le PE loader utilise des index dans ce tableau pour trouver les adresses des fonctions. On va imaginer le scénario où nous nous servons des index de ce tableau en tant que n° d'ordre. Puisque les programmeurs peuvent spécifier le nombre initial de n° d'ordre dans le fichier .def, comme 200, ça signifie qu'il doit y avoir au moins 200 éléments dans le tableau AddressOfFunctions. Par ailleurs imaginons que les 200 premiers éléments ne sont pas utilisés, mais de toute façon ils doivent exister pour que le PE loader puisse se servir des index pour retrouver leurs adresses correctes. Ce n'est pas bon du tout. Le membre nBase existe pour résoudre ce problème. Si le programmeur indique que le n° d'ordre initial est de 200, alors la valeur dans nBase doit être de 200. Lorsque le PE loader lit la valeur dans nBase, il sait que les 200 premiers éléments n'existent pas et qu'il doit soustraire le n° d'ordre à la valeur de nBase pour obtenir le vrai index dans le tableau AddressOfFunctions. Index(Fonction)='n° d'ordre'-nBase. Grâce à l'utilisation du paramètre nBase, c'est pas la peine de créer 200 éléments vides.

Remarquez que nBase n'affecte pas les valeurs du tableau AddressOfNameOrdinals. Malgré le nom "AddressOfNameOrdinals", ce tableau contient les vrais index du tableau AddressOfFunctions, et non pas les n° de classement des fonctions (les n° d'ordre).

Continuons à parler de nBase.
Supposons que nous avons le n° de classement (n° d'ordre) d'une fonction et que nous avons besoin d'obtenir l'adresse de cette fonction, nous pouvons le faire comme ça :

  1. On va jusqu'au PE header
  2. On récupère le RVA de la table des exportations contenue dans le Répertoire des données
  3. On va à la table des exportations et on récupère la valeur de nBase .
  4. On soustrait la valeur dans nBase au n° d'ordre et ainsi on obtient l'index de la fonction qui se trouve dans le tableau AddressOfFunctions.
  5. On compare l'index avec la valeur dans NumberOfFunctions. Si l'index est plus grand ou égal à la valeur dans NumberOfFunctions, alors le n° d'ordre est invalide.
  6. On utilise l'index pour récupérer le RVA de la fonction à l'intérieur du tableau AddressOfFunctions

Remarquez que l'obtention de l'adresse d'une fonction (par ordre) est beaucoup plus facile et plus rapide que d'utiliser le nom de cette fonction. Il n'y a aucun besoin de parcourir les tableaux AddressOfNames et AddressOfNameOrdinals.

Pour conclure, si vous souhaitez obtenir l'adresse d'une fonction à partir de son nom, vous avez besoin de parcourir les tableaux AddressOfNames et AddressOfNameOrdinals pour récupérer son index qui se trouve dans le AddressOfFunctions. Si vous avez le n° d'ordre (de classement) de la fonction, vous pouvez directement fouiner dans le tableau AddressOfFunctions (après avoir soustrait nBase à son n° d'ordre, bien entendu).

Si une fonction est exportée à partir de son nom, vous pouvez employer soit son nom soit son n° d'ordre dans GetProcAddress. Mais si la fonction est uniquement exportée à partir de son n° d'ordre? Nous y voici maintenant.
"La fonction est exportée par n° d'ordre uniquement" signifie que la fonction n'a pas d'entrées dans les tableaux AddressOfNames et AddressOfNameOrdinals. Rappelez-vous des deux paramètres, NumberOfFunctions et NumberOfNames. L'existence de ces deux membres est évidemment que certaines fonctions ne peuvent pas avoir de noms. Le nombre de fonctions doit être au moins (au minimum) égal au nombre de noms. Les fonctions qui n'ont pas de noms sont exportées grâce à leur n° d'ordre (unique). Par exemple, s'il y a 70 fonctions, et qu'il n'y a que 40 entrées dans le tableau AddressOfNames, ça signifie qu'il y a 30 fonctions dans la DLL qui sont exportées d'après leur n° d'ordre. Maintenant comment pouvons-nous découvrir quelles fonctions sont exportées à partir de leur n° d'ordre ? Ce n'est pas facile. On ne peut les trouver que par exclusion. C'est-à-dire, les entrées dans le tableau AddressOfFunctions qui ne fait pas référence au tableau AddressOfNameOrdinals contiennent les RVAs des fonctions qui sont exportées par n° d'ordre.

Exemple:

Cet exemple est semblable à celui du Tutorial précédent. Cependant, il affiche les valeurs de certains membres de la structure IMAGE_EXPORT_DIRECTORY et inscrit aussi les RVAs, les n° d'ordre et les noms des fonctions exportées. Notez que cet exemple n'inscrit pas les fonctions qui sont exportées uniquement par n° d'ordre.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

IDD_MAINDLG equ 101
IDC_EDIT equ 1000
IDM_OPEN equ 40001
IDM_EXIT equ 40003

DlgProc proto :DWORD,:DWORD,:DWORD,:DWORD
ShowExportFunctions proto :DWORD
ShowTheFunctions proto :DWORD,:DWORD
AppendText proto :DWORD,:DWORD

SEH struct
   PrevLink dd ?
   CurrentHandler dd ?
   SafeOffset dd ?
   PrevEsp dd ?
   PrevEbp dd ?
SEH ends

.data
AppName db "PE tutorial no.7",0
ofn OPENFILENAME <>
FilterString db "Executable Files (*.exe, *.dll)",0,"*.exe;*.dll",0
             db "All Files",0,"*.*",0,0
FileOpenError db "Cannot open the file for reading",0
FileOpenMappingError db "Cannot open the file for memory mapping",0
FileMappingError db "Cannot map the file into memory",0
NotValidPE db "This file is not a valid PE",0
NoExportTable db "No export information in this file",0
CRLF db 0Dh,0Ah,0
ExportTable db 0Dh,0Ah,"======[ IMAGE_EXPORT_DIRECTORY ]======",0Dh,0Ah
            db "Name of the module: %s",0Dh,0Ah
            db "nBase: %lu",0Dh,0Ah
            db "NumberOfFunctions: %lu",0Dh,0Ah
            db "NumberOfNames: %lu",0Dh,0Ah
            db "AddressOfFunctions: %lX",0Dh,0Ah
            db "AddressOfNames: %lX",0Dh,0Ah
            db "AddressOfNameOrdinals: %lX",0Dh,0Ah,0
Header db "RVA Ord. Name",0Dh,0Ah
       db "----------------------------------------------",0
template db "%lX %u %s",0

.data?
buffer db 512 dup(?)
hFile dd ?
hMapping dd ?
pMapping dd ?
ValidPE dd ?

.code
start:
invoke GetModuleHandle,NULL
invoke DialogBoxParam, eax, IDD_MAINDLG,NULL,addr DlgProc, 0
invoke ExitProcess, 0

DlgProc proc hDlg:DWORD, uMsg:DWORD, wParam:DWORD, lParam:DWORD
.if uMsg==WM_INITDIALOG
   invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETLIMITTEXT,0,0
.elseif uMsg==WM_CLOSE
   invoke EndDialog,hDlg,0
.elseif uMsg==WM_COMMAND
   .if lParam==0
     mov eax,wParam
     .if ax==IDM_OPEN
       invoke ShowExportFunctions,hDlg
     .else ; IDM_EXIT
       invoke SendMessage,hDlg,WM_CLOSE,0,0
     .endif
   .endif
.else
   mov eax,FALSE
   ret
.endif
mov eax,TRUE
ret
DlgProc endp

SEHHandler proc C pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD
mov edx,pFrame
assume edx:ptr SEH
mov eax,pContext
assume eax:ptr CONTEXT
push [edx].SafeOffset
pop [eax].regEip
push [edx].PrevEsp
pop [eax].regEsp
push [edx].PrevEbp
pop [eax].regEbp
mov ValidPE, FALSE
mov eax,ExceptionContinueExecution
ret
SEHHandler endp

ShowExportFunctions proc uses edi hDlg:DWORD
LOCAL seh:SEH
mov ofn.lStructSize,SIZEOF ofn
mov ofn.lpstrFilter, OFFSET FilterString
mov ofn.lpstrFile, OFFSET buffer
mov ofn.nMaxFile,512
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY
invoke GetOpenFileName, ADDR ofn
.if eax==TRUE
   invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
   .if eax!=INVALID_HANDLE_VALUE
     mov hFile, eax
     invoke CreateFileMapping, hFile, NULL, PAGE_READONLY,0,0,0
     .if eax!=NULL
       mov hMapping, eax
       invoke MapViewOfFile,hMapping,FILE_MAP_READ,0,0,0
       .if eax!=NULL
         mov pMapping,eax
         assume fs:nothing
         push fs:[0]
         pop seh.PrevLink
         mov seh.CurrentHandler,offset SEHHandler
         mov seh.SafeOffset,offset FinalExit
         lea eax,seh
         mov fs:[0], eax
         mov seh.PrevEsp,esp
         mov seh.PrevEbp,ebp
         mov edi, pMapping
         assume edi:ptr IMAGE_DOS_HEADER
         .if [edi].e_magic==IMAGE_DOS_SIGNATURE
           add edi, [edi].e_lfanew
           assume edi:ptr IMAGE_NT_HEADERS
           .if [edi].Signature==IMAGE_NT_SIGNATURE
             mov ValidPE, TRUE
           .else
             mov ValidPE, FALSE
           .endif
         .else
           mov ValidPE,FALSE
         .endif
FinalExit:
         push seh.PrevLink
         pop fs:[0]
         .if ValidPE==TRUE
           invoke ShowTheFunctions, hDlg, edi
         .else
           invoke MessageBox,0, addr NotValidPE, addr AppName, MB_OK+MB_ICONERROR
         .endif
         invoke UnmapViewOfFile, pMapping
       .else
         invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR
       .endif
       invoke CloseHandle,hMapping
     .else
       invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR
     .endif
     invoke CloseHandle, hFile
   .else
     invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR
   .endif
.endif
ret
ShowExportFunctions endp

AppendText proc hDlg:DWORD,pText:DWORD
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,pText
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_REPLACESEL,0,addr CRLF
invoke SendDlgItemMessage,hDlg,IDC_EDIT,EM_SETSEL,-1,0
ret
AppendText endp

RVAToFileMap PROC uses edi esi edx ecx pFileMap:DWORD,RVA:DWORD
mov esi,pFileMap
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov edi,RVA ; edi == RVA
mov edx,esi
add edx,sizeof IMAGE_NT_HEADERS
mov cx,[esi].FileHeader.NumberOfSections
movzx ecx,cx
assume edx:ptr IMAGE_SECTION_HEADER
.while ecx>0
   .if edi>=[edx].VirtualAddress
     mov eax,[edx].VirtualAddress
     add eax,[edx].SizeOfRawData
     .if edi<eax ; The address is in this section mov eax,[edx].VirtualAddress sub edi,eax edi == difference between specified RVA and's eax,[edx].PointerToRawData add eax,edi eax == file offset eax,pFileMap ret .endif edx,sizeof IMAGE_SECTION_HEADER dec ecx .endw assume edx:nothing esi:nothing RVAToFileMap endp ShowTheFunctions proc uses esi ebx hDlg:DWORD, pNTHdr:DWORD LOCAL temp[512]:BYTE NumberOfNames:DWORD Base:DWORD AddressOfFunctions:DWORD AddressOfNameOrdinals:DWORD edi,pNTHdr edi:ptr IMAGE_NT_HEADERS edi, [edi].OptionalHeader.DataDirectory.VirtualAddress .if invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR SetDlgItemText,hDlg,IDC_EDIT,0 AppendText,hDlg,addr buffer RVAToFileMap,pMapping,edi IMAGE_EXPORT_DIRECTORY eax,[edi].NumberOfFunctions RVAToFileMap, pMapping,[edi].nName wsprintf, temp,addr ExportTable,eax,[edi].nBase,[edi].NumberOfFunctions,[edi].NumberOfNames,[edi].AddressOfFunctions,[edi].AddressOfNames,[edi].AddressOfNameOrdinals temp push [edi].NumberOfNames pop NumberOfNames [edi].nBase Base [edi].AddressOfFunctions AddressOfFunctions RVAToFileMap,pMapping,AddressOfFunctions AddressOfFunctions,eax esi,[edi].AddressOfNames RVAToFileMap,pMapping,esi esi,eax ebx,[edi].AddressOfNameOrdinals RVAToFileMap,pMapping,ebx ebx,eax AddressOfNameOrdinals,ebx Header edi,AddressOfFunctions .while><eax
       mov eax,[edx].VirtualAddress
       sub edi,eax
       mov eax,[edx].PointerToRawData
       add eax,edi
       add eax,pFileMap
       ret
     .endif
   .endif
   add edx,sizeof IMAGE_SECTION_HEADER
   dec ecx
.endw
assume edx:nothing
assume esi:nothing
mov eax,edi
ret
RVAToFileMap endp

ShowTheFunctions proc uses esi ecx ebx hDlg:DWORD, pNTHdr:DWORD
LOCAL temp[512]:BYTE
LOCAL NumberOfNames:DWORD
LOCAL Base:DWORD

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif
invoke SetDlgItemText,hDlg,IDC_EDIT,0
invoke AppendText,hDlg,addr buffer
invoke RVAToFileMap,pMapping,edi
mov edi,eax
assume edi:ptr IMAGE_EXPORT_DIRECTORY
mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp
invoke AppendText,hDlg,addr Header
push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base
invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax
.while NumberOfNames>0
   invoke RVAToFileMap,pMapping,dword ptr [esi]
   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   shl edx,2
   add edx,edi
   add ecx,Base
   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp
   dec NumberOfNames
   add esi,4
   add ebx,2
.endw
ret
ShowTheFunctions endp
end start

Analyse:

mov edi,pNTHdr
assume edi:ptr IMAGE_NT_HEADERS
mov edi, [edi].OptionalHeader.DataDirectory.VirtualAddress
.if edi==0
  invoke MessageBox,0, addr NoExportTable,addr AppName,MB_OK+MB_ICONERROR
  ret
.endif

Après que le programme ait vérifié que le fichier était un PE File valide, il va au répertoire des données et récupère l'adresse virtuelle de la table des exportations. Si l'adresse virtuelle est nulle, le fichier n'a aucun symbole exporté.

mov eax,[edi].NumberOfFunctions
invoke RVAToFileMap, pMapping,[edi].nName
invoke wsprintf, addr temp,addr ExportTable, eax, [edi].nBase, [edi].NumberOfFunctions, [edi].NumberOfNames, [edi].AddressOfFunctions, [edi].AddressOfNames, [edi].AddressOfNameOrdinals
invoke AppendText,hDlg,addr temp

Nous affichons les informations importantes de la structure IMAGE_EXPORT_DIRECTORY dans le contrôle d'édition.

push [edi].NumberOfNames
pop NumberOfNames
push [edi].nBase
pop Base

Puisque nous souhaitons énumérer tous les noms des fonctions, nous avons besoin de savoir combien de noms il y a dans la table des exportations. nBase est employé lorsque nous voulons convertir les index dans le tableau AddressOfFunctions en n° d'ordre.

invoke RVAToFileMap,pMapping,[edi].AddressOfNames
mov esi,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfNameOrdinals
mov ebx,eax
invoke RVAToFileMap,pMapping,[edi].AddressOfFunctions
mov edi,eax

Les adresses des trois tableaux sont stockées dans esi, ebx et edi, prêt pour qu'on puisse y accéder.

.while NumberOfNames>0

Continue tant que tous les noms n'aient pas été traités.

   invoke RVAToFileMap,pMapping,dword ptr [esi]

Puisque esi pointe sur un tableau contenant les RVAs des noms exportés, ça nous donne le RVA du nom actuel. On le convertit en une adresse virtuelle, pour pouvoir l'utiliser dans l'API wsprintf plus tard.

   mov dx,[ebx]
   movzx edx,dx
   mov ecx,edx
   add ecx,Base

ebx pointe sur le tableau des n° d'ordre. Les éléments de ce tableau sont de taille WORD. C'est pourquoi nous avons besoin de convertir ces valeur en dword d'abord. edx et ecx contiennent les index dans le tableau AddressOfFunctions. Nous emploierons edx en tant que pointer dans le tableau AddressOfFunctions. Et nous ajoutons la valeur de nBase à ecx pour obtenir le numéro d'ordre de la fonction.

   shl edx,2
   add edx,edi

On multiplie l'index par 4 (car chaque élément du tableau AddressOfFunctions est d'une taille de 4 octets) et on y ajoute ensuite l'adresse du tableau AddressOfFunctions. Comme ça edx représente le RVA de la fonction.

   invoke wsprintf, addr temp,addr template,dword ptr [edx],ecx,eax
   invoke AppendText,hDlg,addr temp

On affiche le RVA, le n° d'ordre et le nom de la fonction dans le contrôle d'édition.

   dec NumberOfNames
   add esi,4
   add ebx,2
.endw

On rafraîchit le compteur ainsi que les adresses des éléments actuels dans les tableaux AddressOfNames et AddressOfNameOrdinals. On continue tant que tous les noms n'aient été traités.

[Iczelion's Win32 Assembly Homepage]

Traduit par Morgatte


GlobalPEMap

DOS MZ HEADER

<IMAGE_DOS_HEADER>

IMAGE_DOS_HEADER <>
- e_magic : <-- ce sont les deux premiers octets du prog (la signature MZ)
- e_lflanew : <-- Offset du PE Header
IMAGE_DOS_HEADER ENDS
DOS STUB
Chaîne de caractères : "This Program Requires Windows"
ou : "This program cannot run in DOS mode"
PE HEADER <IMAGE_NT_HEADERS>
IMAGE_NT_HEADERS <>
  - Signature : <-- PE (ou 50h, 45h, 00h, 00h)
  - FileHeader IMAGE_FILE_HEADER <>
               - Machine *
               - NumberOfSections ***
               - TimeDateStamp
               - PointerToSymbolTable
               - NumberOfSymbols
               - SizeOfOptionalHeader
               IMAGE_FILE_HEADER ENDS
  - OptionalHeader IMAGE_OPTIONAL_HEADER32 <>
               - AddressOfEntryPoint ***
               - ImageBase ***
               - SectionAlignment
               - FileAlignment
               - MajorSubSystemVersion
               - SizeOfImage
               - SubSystem
               - DataDirectory IMAGE_DATA_DIRECTORY <>
                   - Export symbols (IMAGE_DATA_DIRECTORY <> * 0)<--  Vers Table des Exportations
                   - Import symbols (IMAGE_DATA_DIRECTORY <> * 1)<--  Vers Table des Importations
                   - Resources      (IMAGE_DATA_DIRECTORY <> * 2)
                   - Exception      (IMAGE_DATA_DIRECTORY <> * 3)
                   - Security       (IMAGE_DATA_DIRECTORY <> * 4)
                   - Base relocation (IMAGE_DATA_DIRECTORY <> * 5)
                   - Debug          (IMAGE_DATA_DIRECTORY <> * 6)
                   - Copyright string (IMAGE_DATA_DIRECTORY <> * 7)
                   - Unknown        (IMAGE_DATA_DIRECTORY <> * 8)
                   - Thread local storage (TLS) (IMAGE_DATA_DIRECTORY <> * 9)
                   - Load configuration (IMAGE_DATA_DIRECTORY <> * 10)
                   - Bound Import   (IMAGE_DATA_DIRECTORY <> * 11)
                   - Import Address Table (IMAGE_DATA_DIRECTORY <> * 12)
                   - Delay Import   (IMAGE_DATA_DIRECTORY <> * 13)
                   - COM descriptor (IMAGE_DATA_DIRECTORY <> * 14)
                   IMAGE_DATA_DIRECTORY ENDS
               IMAGE_OPTIONAL_HEADER32 ENDS
IMAGE_NT_HEADERS ENDS
TABLE DES SECTIONS
<IMAGE_SECTION_HEADER>

IMAGE_SECTION_HEADER <>
  - Name1 : <-- le nom de la section
  - Misc.VirtualSize
  - VirtualAddress
  - SizeOfRawData
  - PointerToRawData (aussi appelé Raw Offset ou Raw Data)
  - PointerToRelocation
  - PointerToLineNumbers
  - NumberOfRelocations
  - NumberOfLineNumbers
  - Characteristics
IMAGE_SECTION_HEADER ENDS

IMAGE_SECTION_HEADER <>
  - Name1 : <-- le nom de la section
  - Misc.VirtualSize
  - VirtualAddress
  - SizeOfRawData
  - PointerToRawData (aussi appelé Raw Offset ou Raw Data)
  - PointerToRelocation
  - PointerToLineNumbers
  - NumberOfRelocations
  - NumberOfLineNumbers
  - Characteristics
IMAGE_SECTION_HEADER ENDS

IMAGE_SECTION_HEADER <>
  - .... Il y aura autant de fois de répétition de
  - .... ces paramètres qu'il y a de sections dans notre programme.
  - .... Cette répétition est donc définit par
  - .... la valeur du membre 'NumberOfSections' se trouvant dans la
  - .... structure IMAGE_FILE_HEADER <> elle même
  - .... appartenant à la structure IMAGE_NT_HEADER <>
  - .... (le PE Header)
  - ....
IMAGE_SECTION_HEADER ENDS

IMAGE_SECTION_HEADER <>
  - Name1 : <-- le nom de la section
  - Misc.VirtualSize
  - VirtualAddress
  - SizeOfRawData
  - PointerToRawData (aussi appelé Raw Offset ou Raw Data)
  - PointerToRelocation
  - PointerToLineNumbers
  - NumberOfRelocations
  - NumberOfLineNumbers
  - Characteristics
IMAGE_SECTION_HEADER ENDS
SECTION 1
(EX: .DATA)
00401000h

Votre programme commence réellement à partir d'ici.

SECTION 2
(EX: .DATA?)
SECTION 3
....
SECTION 4
(EX: .CODE)

                                                    Synthétisé par Morgatte


 Nous en étions donc au membre 'Import Symbols' appartenant à la structure IMAGE_DATA_DIRECTORY

'Import symbols' est le deuxième membre d'IMAGE_DATA_DIRECTORY <>, l'index de 'Import symbols' est 1. Chaque membre de cette structure est définit ainsi:

IMAGE_DATA_DIRECTORY <>
VirtualAddress dd ? <-- et suivant le membre d'où on vient on est renvoyé vers le descripteur adéquoit.
isize dd ?
IMAGE_DATA_DIRECTORY ENDS

| IMAGE_DATA_DIRECTORY <>
1*| VirtualAddress dd ? -----------> adresse de la Table des Importations
| isize dd ?
| IMAGE_DATA_DIRECTORY ENDS

IMAGE_IMPORT_DESCRIPTOR <> <-- Table des Importations
  union
    - Characteristics dd ?
    - OriginalFirstThunk dd ?
  ends
  - TimeDateStamp dd ?
  - ForwarderChain dd ?
  - Name1 dd ?
  - FirstThunk dd ?
IMAGE_IMPORT_DESCRIPTOR ENDS

Retour


$Id: pe.txt,v 1.7 1998/07/29 17:55:13 LUEVELSMEYER Exp $

The PE file format
==================

Purpose
-------

The PE ("portable executable") file format is the format of executable
binaries (DLLs and programs) for MS windows NT, windows 95 and
win32s; in windows NT, the drivers are in this format, too.
It can also be used for object files and libraries.

The format is designed by Microsoft, apparently based on a good
knowledge of COFF, the "common object file format" used for object files
and executables on several UNIXes and on VMS.

The win32 SDK includes a header file <winnt.h> containing #defines and
typedefs for the PE-format. I will mention the struct-member-names and
#defines as we go.

You may also find the DLL "imagehelp.dll" to be helpful. It is part of
windows NT, but documentation is rare. Some of its functions are
described in the "Developer Network".

General Layout
--------------

At the start of a PE file we find an MS-DOS executable ("stub"); this
makes any PE file a valid MS-DOS executable.

After the DOS-stub there is a 32-bit-signature with the magic number
0x00004550 (IMAGE_NT_SIGNATURE).

Then there is a file header (in the COFF-format) that tells on which
machine the binary is supposed to run, how many sections are in it, the
time it was linked, whether it is an executable or a DLL and so on. (The
difference between executable and DLL in this context is: a DLL can not
be started but only be used by another binary, and a binary cannot link
to an executable).

After that, we have an optional header (it is always there but still
called "optional" - COFF uses an "optional header" for libraries but not
for objects, that's why it is called "optional"). This tells us more
about how the binary should be loaded: The starting address, the amount
of stack to reserve, the size of the data segment etc..

An interesting part of the optional header is the trailing array of
'data directories'; these directories contain pointers to data in the
'sections'. If, for example, the binary has an export directory, you
will find a pointer to that directory in the array member
IMAGE_DIRECTORY_ENTRY_EXPORT, and it will point into one of the
sections.

Following the headers we find the 'sections', introduced by the 'section
headers'. Essentially, the sections' contents is what you really need to
execute a program, and all the header and directory stuff is just there
to help you find it.
Each section has some flags about alignment, what kind of data it
contains ("initialized data" and so on), whether it can be shared etc.,
and the data itself. Most, but not all, sections contain one or more
directories referenced through the entries of the optional header's
"data directory" array, like the directory of exported functions or the
directory of base relocations. Directoryless types of contents are, for
example, "executable code" or "initialized data".

    +-------------------+
    | DOS-stub          |
    +-------------------+
    | file-header       |
    +-------------------+
    | optional header   |
    |- - - - - - - - - -|
    |                   |
    | data directories  |
    |                   |
    +-------------------+
    |                   |
    | section headers   |
    |                   |
    +-------------------+
    |                   |
    | section 1         |
    |                   |
    +-------------------+
    |                   |
    | section 2         |
    |                   |
    +-------------------+
    |                   |
    | ...               |
    |                   |
    +-------------------+
    |                   |
    | section n         |
    |                   |
    +-------------------+

DOS-stub and Signature
----------------------

The concept of a DOS-stub is well-known from the 16-bit-windows-
executables (which were in the "NE" format). The stub is used for
OS/2-executables, self-extracting archives and other applications, too.
For PE-files, almost always this MS-DOS-executable consists of about 100
bytes that output an error message such as "this program needs windows
NT".
You recognize a DOS-stub by the validating the DOS-header, being a
struct IMAGE_DOS_HEADER. The first 2 bytes should be the sequence "MZ"
(there is a #define IMAGE_DOS_SIGNATURE for this WORD).
You distinguish a PE binary from other stubbed binaries by the trailing
signature, which you find at the offset given by the header member
'e_lfanew' (which is 32 bits long beginning at byte offset 60). For OS/2
and windows binaries, the signature is a 16-bit-word; for PE files, it
is a 32-bit-longword with the value IMAGE_NT_SIGNATURE #defined to be
0x00004550.

File Header
-----------

To get to the IMAGE_FILE_HEADER, validate the "MZ" of the DOS-header
(1st 2 bytes), then find the 'e_lfanew' member of the DOS-stub's header
and skip that many bytes from the beginning of the file. Verify the
signature you will find there. The file header, a struct
IMAGE_FILE_HEADER, begins immediatly after it; the members are described
top to bottom.

The first member is the 'Machine', a 16-bit-value indicating the system
the binary is intended to run on. Known legal values are

    IMAGE_FILE_MACHINE_I386 (0x14c)
        for Intel 80386 processor or better

    0x014d
        for Intel 80486 processor or better

    0x014e
        for Intel Pentium processor or better

    0x0160
        for R3000 (MIPS) processor, big endian

    IMAGE_FILE_MACHINE_R3000 (0x162)
        for R3000 (MIPS) processor, little endian

    IMAGE_FILE_MACHINE_R4000 (0x166)
        for R4000 (MIPS) processor, little endian

    IMAGE_FILE_MACHINE_R10000 (0x168)
        for R10000 (MIPS) processor, little endian

    IMAGE_FILE_MACHINE_ALPHA (0x184)
        for DEC Alpha AXP processor

    IMAGE_FILE_MACHINE_POWERPC (0x1F0)
        for IBM Power PC, little endian

Then we have the 'NumberOfSections', a 16-bit-value. It is the number of
sections that follow the headers. We will discuss the sections later.

Next is a timestamp 'TimeDateStamp' (32 bit), giving the time the file
was created. You can distinguish several versions of the same file by
this value, even if the "official" version number was not altered. (The
format of the timestamp is not documented except that it should be
somewhat unique among versions of the same file, but apparently it is
'seconds since January 1 1970 00:00:00' in UTC - the format used by most
C compilers for the time_t.)
This timestamp is used for the binding of import directories, which will
be discussed later.
Warning: some compilers tend to set this timestamp to absurd values.

The members 'PointerToSymbolTable' and 'NumberOfSymbols' (both 32 bit)
are used for debugging information. I don't know how to decipher them,
and I've found the pointer to be always 0.

'SizeOfOptionalHeader' (16 bit) is simply sizeof(IMAGE_OPTIONAL_HEADER).
You can use it to validate the correctness of the PE file's structure.

'Characteristics' is 16 bits and consists of a collection of flags, most
of them being valid only for object files and libraries:

    Bit 0 (IMAGE_FILE_RELOCS_STRIPPED) is set if there is no relocation
    information in the file. This refers to relocation information per
    section in the sections themselves; it is not used for executables,
    which have relocation information in the 'base relocation' directory
    described below.

    Bit 1 (IMAGE_FILE_EXECUTABLE_IMAGE) is set if the file is executable
    (i.e. it is not an object file or a library).

    Bit 2 (IMAGE_FILE_LINE_NUMS_STRIPPED) is set if the line number
    information is stripped; this is not used for executable files.

    Bit 3 (IMAGE_FILE_LOCAL_SYMS_STRIPPED) is set if there is no
    information about local symbols in the file (this is not used
    for executable files).

    Bit 4 (IMAGE_FILE_AGGRESIVE_WS_TRIM) is set if the operating system
    is supposed to trim the working set of the running process (the
    amount of RAM the process uses) aggressivly by paging it out. This
    should be set if it is a demon-like application that waits most of
    the time and only wakes up once a day, or the like.

    Bits 7 (IMAGE_FILE_BYTES_REVERSED_LO) and 15
    (IMAGE_FILE_BYTES_REVERSED_HI) are set if the endianess of the file is
    not what the machine would expect, so it must swap bytes before
    reading. This is unreliable for executable files (the OS expects
    executables to be correctly byte-ordered).

    Bit 8 (IMAGE_FILE_32BIT_MACHINE) is set if the machine is expected
    to be a 32 bit machine. This is always set.

    Bit 9 (IMAGE_FILE_DEBUG_STRIPPED) is set if there is no debugging
    information in the file. This is unused for executable files.

    Bit 10 (IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP) is set if the application
    may not run from a removable medium such as a floppy or a CD-ROM. In
    this case, the operating system is advised to copy the file to the
    swapfile and execute it from there.

    Bit 11 (IMAGE_FILE_NET_RUN_FROM_SWAP) is set if the application may
    not run from the network. In this case, the operating system is
    advised to copy the file to the swapfile and execute it from there.

    Bit 12 (IMAGE_FILE_SYSTEM) is set if the file is a system file such
    as a driver. This is unused for executable files.

    Bit 13 (IMAGE_FILE_DLL) is set if the file is a DLL.

    Bit 14 (IMAGE_FILE_UP_SYSTEM_ONLY) is set if the file is not
    designed to run on multiprocessor systems (that is, it will crash
    there because it relies in some way on exactly one processor).

Relative Virtual Addresses
--------------------------

The PE format makes heavy use of so-called RVAs. An RVA, aka "relative
virtual address", is used to describe a memory address if you don't know
the base address. It is the value you need to add to the base address to
get the linear address.
The base address is the address the PE image is loaded to, and may vary
from one invocation to the next.

Example: suppose an executable file is loaded to address 0x400000 and
execution starts at RVA 0x1560. The effective execution start will then
be at the address 0x401560. If the executable were loaded to 0x100000,
the execution start would be 0x101560.

Things become complicated because the parts of the PE-file (the
sections) are not necessarily aligned the same way the loaded image is.
For example, the sections of the file are often aligned to
512-byte-borders, but the loaded image is perhaps aligned to
4096-byte-borders. See 'SectionAlignment' and 'FileAlignment' below.

So to find a piece of information in a PE-file for a specific RVA,
you must calculate the offsets as if the file were loaded, but skip
according to the file-offsets.
As an example, suppose you knew the execution starts at RVA 0x1560, and
want to diassemble the code starting there. To find the address in the
file, you will have to find out that sections in RAM are aligned to 4096
bytes and the ".code"-section starts at RVA 0x1000 in RAM and is 16384
bytes long; then you know that RVA 0x1560 is at offset 0x560 in that
section. Find out that the sections are aligned to 512-byte-borders in
the file and that ".code" begins at offset 0x800 in the file, and you
know that the code execution start is at byte 0x800+0x560=0xd60 in the
file.

Easy if you know how it works :-)

Optional Header
---------------

Immediatly following the file header is the IMAGE_OPTIONAL_HEADER
(which, in spite of the name, is always there). It contains
information about how to treat the PE-file exactly. We'll also have the
members from top to bottom.

The first 16-bit-word is 'Magic' and has, as far as I looked into
PE-files, always the value 0x010b.

The next 2 bytes are the version of the linker ('MajorLinkerVersion' and
'MinorLinkerVersion') that produced the file. These values, again, are
unreliable and do not always reflect the linker version properly.
(Several linkers simply don't set this field.)
And, coming to think about it, what good is the version if you have got
no idea *which* linker was used?

The next 3 longwords (32 bit each) are intended to be the size of the
executable code ('SizeOfCode'), the size of the initialized data
('SizeOfInitializedData', the so-called "data segment"), and the size of
the uninitialized data ('SizeOfUninitializedData', the so-called "bss
segment"). These values are, again, unreliable (e.g. the data segment
may actually be split into several segments by the compiler or linker),
and you get better sizes by inspecting the 'sections' that follow the
optional header.

Next is a 32-bit-value that is a RVA. This RVA is the offset to the
codes's entry point ('AddressOfEntryPoint'). Execution starts here; it
is e.g. the address of a DLL's LibMain() or a program's startup code
(which will in turn call main()) or a driver's DriverEntry().
If you dare to load the image "by hand", you call this address to start
the process after you have done all the fixups and relocations.

The next 2 32-bit-values are the offsets to the executable code
('BaseOfCode') and the initialized data ('BaseOfData'), both of them
RVAs again, and both of them being of little interest because you get
more reliable information by inspecting the 'sections' that follow the
headers.
There is no offset to the uninitialized data because, being
uninitialized, there is little point in providing this data in the
image.

The next entry is a 32-bit-value that is a RVA giving the preferred load
address ('ImageBase'). This is the address the file has been relocated
to by the linker; if the binary can in fact be loaded to that address,
the loader doesn't need to relocate the file again, which is a big win
in loading time.
The preferred load address can not be used if another image has already
been loaded to that address (an "address clash", which happens quite
often if you load several DLLs that are all relocated to the linker's
default), or the memory in question has been used for other purposes
(stack, malloc(), uninitialized data, whatever). In these cases, the
image must be loaded to some other address and it needs to be relocated
(see 'relocation directory' below). This has further consequences if the
image is a DLL, because then the "bound imports" are no longer valid,
and fixups have to be made to the binary that uses the DLL - see 'import
directory' below.

The next 2 32-bit-values are the alignments of the PE-file's sections in
RAM ('SectionAlignment', when the image has been loaded) and in the file
('FileAlignment'). Usually both values are 32, or FileAlignment is 512
and SectionAlignment is 4096. Sections will be discussed later.

The next 2 16-bit-words are the expected operating system version
('MajorOperatingSystemVersion' and 'MinorOperatingSystemVersion' [they
_do_ like self-documenting names at MS]). This version information is
intended to be the operating system's (e.g. NT or Win95) version, as
opposed to the subsystem's version (e.g. Win32); it is often not
supplied, or wrong supplied. The loader doesn't use it, apparently.

The next 2 16-bit-words are the binary's version, ('MajorImageVersion' and
'MinorImageVersion'). Many linkers don't set this information correctly
and many programmers don't bother to supply it, so it is better to rely
on the version-resource if one exists.

The next 2 16-bit-words are the expected subsystem version
('MajorSubsystemVersion' and 'MinorSubsystemVersion'). This should be
the Win32 version or the POSIX version, because 16-bit-programs or
OS/2-programs won't be in PE-format, obviously.
This subsystem version should be supplied correctly, because it *is*
checked and used:
If the application is a Win32-GUI-application and runs on NT, and the
subsystem version is *not* 4.0, the dialogs won't be 3D-style and
certain other features will also work "old-style" because the
application expects to run on NT 3.51, which had the program manager
instead of explorer and so on, and NT 4.0 will mimic that behaviour as
faithfully as possible.

Then we have a 'Win32VersionValue' of 32 bits. I don't know what it is
good for. It has been 0 in all the PE files that I inspected.

Next is a 32-bits-value giving the amount of memory the image will need,
in bytes ('SizeOfImage'). It is the sum of all sections' lengths. It is
a hint to the loader how many pages it will need in order to load the image.

The next thing is a 32-bit-value giving the total length of all headers
including the data directories and the section headers
('SizeOfHeaders'). It is at the same time the offset from the beginning
of the file to the first section's raw data.

Then we have got a 32-bit-checksum ('CheckSum'). This checksum is, for
current versions of NT, only checked if the image is a NT-driver (the
driver will fail to load if the checksum isn't correct).
The algorithm to compute the checksum is property of Microsoft, and they
won't tell you. However, several tools of the Win32 SDK will compute a
valid checksum, and the function CheckSumMappedFile() in the
imagehelp.dll will do so too.
The checksum is supposed to prevent loading of damaged binaries that
would crash anyway - and a crashing driver would result in a BSOD, so
it is better not to load it at all.

Then there is a 16-bit-word 'Subsystem' that tells in which of the
NT-subsystems the image runs:

    IMAGE_SUBSYSTEM_NATIVE (1)
        The binary doesn't need a subsystem. This is used for drivers.

    IMAGE_SUBSYSTEM_WINDOWS_GUI (2)
        The image is a Win32 graphical binary. (It can still open a
        console with AllocConsole() but won't get one automatically at
        startup.)

    IMAGE_SUBSYSTEM_WINDOWS_CUI (3)
        The binary is a Win32 console binary. (It will get a console
        per default at startup.)

    IMAGE_SUBSYSTEM_OS2_CUI (5)
        The binary is a OS/2 console binary. (OS/2 binaries will be in
        OS/2 format, so this value will seldom be used in a PE
        file.)

    IMAGE_SUBSYSTEM_POSIX_CUI (7)
        The binary uses the POSIX console subsystem.

Windows 95 binaries will always use the Win32 subsystem, so the only
legal values for these binaries are 2 and 3; I don't know if "native"
binaries on windows 95 are possible.

The next thing is a 16-bit-value that tells, if the image is a DLL, when
to call the DLL's entry point ('DllCharacteristics'). This seems not to
be used; apparently, the DLL is always notified about everything.
    If bit 0 is set, the DLL is notified about process attachment (i.e.
    DLL load).
    If bit 1 is set, the DLL is notified about thread detachments (i.e.
    thread terminations).
    If bit 2 is set, the DLL is notified about thread attachments (i.e.
    thread creations).
    If bit 3 is set, the DLL is notified about process detachment (i.e.
    DLL unload).

The next 4 32-bit-values are the size of reserved stack
('SizeOfStackReserve'), the size of initially committed stack
('SizeOfStackCommit'), the size of the reserved heap
('SizeOfHeapReserve') and the size of the committed heap
('SizeOfHeapCommit').
The 'reserved' amounts are address space (not real RAM) that is reserved
for the specific purpose; at program startup, the 'committed' amount is
actually allocated in RAM. The 'committed' value is also the amount by
which the committed stack or heap grows if necessary.
So, as an example, if a program has a reserved heap of 1 MB and a
committed heap of 64 KB, the heap will start out at 64 KB and is
guaranteed to be enlargeable up to 1 MB. The heap will grow in
64-KB-chunks.
The 'heap' in this context is the primary (default) heap.
DLLs don't have a stack or heap of their own, so the values are ignored
for their images.

After these stack- and heap-descriptions, we find 32 bits of
'LoaderFlags', which I didn't find a useful description of. I only found
that you may set bits that automatically invoke a breakpoint or a
debugger after loading the image; however, this doesn't seem to work.

Then we find 32 bits of 'NumberOfRvaAndSizes', which is the number of
valid entries in the directories that follow immediatly. I've found this
value to be unreliable; you might wish use the constant
IMAGE_NUMBEROF_DIRECTORY_ENTRIES instead, or the lesser of both.

After the 'NumberOfRvaAndSizes' there is an array of
IMAGE_NUMBEROF_DIRECTORY_ENTRIES (16) IMAGE_DATA_DIRECTORYs.
Each of these directories describes the location (32 bits RVA called
'VirtualAddress') and size (also 32 bit, called 'Size') of a particular
piece of information, which is located in one of the sections that
follow the directory entries.
For example, the security directory is found at the RVA and has the size
that are given at index 4.
The directories that I know the structure of will be discussed later.
Defined directory indexes are:

    IMAGE_DIRECTORY_ENTRY_EXPORT (0)
        The directory of exported symbols; mostly used for DLLs.
        Described below.

    IMAGE_DIRECTORY_ENTRY_IMPORT (1)
        The directory of imported symbols; see below.

    IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
        Directory of resources. Described below.

    IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
        Exception directory - structure and purpose unknown.

    IMAGE_DIRECTORY_ENTRY_SECURITY (4)
        Security directory - structure and purpose unknown.

    IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
        Base relocation table - see below.

    IMAGE_DIRECTORY_ENTRY_DEBUG (6)
        Debug directory - contents is compiler dependent. Moreover, many
        compilers stuff the debug information into the code section and
        don't create a separate section for it.

    IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
        Description string - some arbitrary copyright note or the like.

    IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
        Machine Value (MIPS GP) - structure and purpose unknown.

    IMAGE_DIRECTORY_ENTRY_TLS (9)
        Thread local storage directory - structure unknown; contains
        variables that are declared "declspec(thread)".

    IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
        Load configuration directory - structure and purpose unknown.

    IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
        Bound import directory - see description of import directory.

    IMAGE_DIRECTORY_ENTRY_IAT (12)
        Import Address Table - structure and purpose unknown

As an example, if we find at index 7 the 2 longwords 0x12000 and 33, and
the load address is 0x10000, we know that the copyright data is at
address 0x10000+0x12000 (in whatever section there may be), and the
copyright note is 33 bytes long.
If a directory of a particular type is not used in a binary, the Size
and VirtualAddress are both 0.

Section directories
-------------------

The sections consist of two major parts: first, a section description
(of type IMAGE_SECTION_HEADER) and then the raw section data. So after
the data directories we find an array of 'NumberOfSections' section
headers.

A section header is actually a very *small* struct. It contains:

An array of IMAGE_SIZEOF_SHORT_NAME (8) bytes that make up the name
(ASCII) of the section. If all of the 8 bytes are used there is no 0-
terminator for the string! The name is typically something like ".data"
or ".text" or ".bss". There need not be a leading '.', the names may
also be "CODE" or "IAT" or the like.
Please note that the names are not at all related to the contents of the
section. A section named ".code" may or may not contain the executable
code; it may just as well contain the import address table; it may also
contain the code *and* the address table *and* the initialized data.
To find information in the sections, you will have to look it up via the
data directories of the optional header. Do not rely on the names, and
do not assume that the section's raw data starts at the beginning of a
section.

The next member of the IMAGE_SECTION_HEADER is a 32-bit-union of
'PhysicalAddress' and 'VirtualSize'. In an object file, this is the
address the contents is relocated to; in an executable, it is the size of
the contents. In fact, the field seems to be unused; There are linkers
that enter the size, and there are linkers that enter the address, and
I've also found a linker that enters a 0, and all the executables run
like the gentle wind.

The next member is 'VirtualAddress', a 32-bit-value holding the RVA to
the section's data when it is loaded in RAM.

Then we have got 32 bits of 'SizeOfRawData', which is the size of the
secion's data rounded up to the next multiple of 'FileAlignment'.

Next is 'PointerToRawData' (32 bits), which is incredibly usefull
because it is the offset from the file's beginning to the section's data.
If it is 0, the section's data are not contained in the file and will be
arbitrary.

Then we have got 'PointerToRelocations' (32 bits) and
'PointerToLinenumbers' (also 32 bits), 'NumberOfRelocations' (16 bits)
and 'NumberOfLinenumbers' (also 16 bits). All of these are information
that's only used for object files. Executables have a special base
relocation directory, and the line number information, if present at
all, is usually contained in a special purpose debugging segment or
elsewhere.

The last member of a section header is the 32 bits 'Characteristics',
which is a bunch of flags describing how the section's memory should be
treated:

    If bit 5 (IMAGE_SCN_CNT_CODE) is set, the section contains
    executable code.

    If bit 6 (IMAGE_SCN_CNT_INITIALIZED_DATA) is set, the section
    contains data that gets a defined value before execution starts. In
    other words: the section's data in the file is meaningful.

    If bit 7 (IMAGE_SCN_CNT_UNINITIALIZED_DATA) is set, this section's
    data is uninitialized data and will be initialized to all-0-bytes
    before execution starts. This is normally the BSS.

    If bit 9 (IMAGE_SCN_LNK_INFO) is set, the section doesn't contain
    image data but comments, description or other documentation.

    If bit 11 (IMAGE_SCN_LNK_REMOVE) is set, the data is part of an
    object file's section that is supposed to be left out when the
    executable file is linked.

    If bit 12 (IMAGE_SCN_LNK_COMDAT) is set, the section contains
    "common block data", which are packaged functions of some sort.

    If bit 15 (IMAGE_SCN_MEM_FARDATA) is set, we have far data -
    whatever that means. This bit's meaning is unsure.

    If bit 17 (IMAGE_SCN_MEM_PURGEABLE) is set, the section's data
    is purgeable - but I don't think that this is the same as
    "discardable", which has a bit of its own, see below.
    The same bit is apparently used to indicate 16-bit-information as
    there is also a define IMAGE_SCN_MEM_16BIT for it.
    This bit's meaning is unsure.

    If bit 18 (IMAGE_SCN_MEM_LOCKED) is set, the section should not be
    moved in memory? This bit's meaning is unsure.

    If bit 19 (IMAGE_SCN_MEM_PRELOAD) is set, the section should be
    paged in before execution starts? This bit's meaning is unsure.

    Bits 20 to 23 specify an alignment that I have no information
    about. There are #defines IMAGE_SCN_ALIGN_16BYTES and the like. The
    only value I've ever seen used is 0, for the default 16-byte-
    alignment. I suspect that this is the alignment of objects in a
    library file or the like.

    If bit 24 (IMAGE_SCN_LNK_NRELOC_OVFL) is set, the section contains
    some extended relocations that I don't know about.

    If bit 25 (IMAGE_SCN_MEM_DISCARDABLE) is set, the section's data is
    not needed after the process has started. This is the case,
    for example, with the relocation information. I've seen it also for
    startup routines of drivers and services that are only executed
    once.

    If bit 26 (IMAGE_SCN_MEM_NOT_CACHED) is set, the section's data
    should not be cached. Don't ask my why not. Does this mean to switch
    off the 2nd-level-cache?

    If bit 27 (IMAGE_SCN_MEM_NOT_PAGED) is set, the section's data
    should not be paged out. This may be interesting for drivers.

    If bit 28 (IMAGE_SCN_MEM_SHARED) is set, the section's data is
    shared among all running instances of the image. If it is e.g. the
    initialized data of a DLL, all running instances of the DLL will at
    any time have the same variable contents.
    Note that only the first instance's section is initialized.
    Sections containing code are always shared.

    If bit 29 (IMAGE_SCN_MEM_EXECUTE) is set, the process gets
    'execute'-access to the section's memory.

    If bit 30 (IMAGE_SCN_MEM_READ) is set, the process gets
    'read'-access to the section's memory.

    If bit 31 (IMAGE_SCN_MEM_WRITE) is set, the process gets
    'write'-access to the section's memory.

After the section headers we find the sections themselves. They are, in
the file, aligned to 'FileAlignment' bytes (that is, after the optional
header and after each section's data there will be padding bytes). When
loaded (in RAM), the sections are aligned to 'SectionAlignment' bytes.

As an example, if the optional header ends at file offset 981 and
'FileAlignment' is 512, the first section will start at byte 1024. Note
that you can find the sections via the 'PointerToRawData' or the
'VirtualAddress', so there is hardly any need to actually fuss around
with the alignments.

I will try to make an image of it all:

    +-------------------+
    | DOS-stub          |
    +-------------------+
    | file-header       |
    +-------------------+
    | optional header   |
    |- - - - - - - - - -|
    |                   |----------------+
    | data directories  |                |
    |                   |                |
    |(RVAs to direc-    |-------------+  |
    |tories in sections)|             |  |
    |                   |---------+   |  |
    |                   |         |   |  |
    +-------------------+         |   |  |
    |                   |-----+   |   |  |
    | section headers   |     |   |   |  |
    | (RVAs to section  |--+  |   |   |  |
    |  borders)         |  |  |   |   |  |
    +-------------------+<-+  |   |   |  |
    |                   |     | <-+   |  |
    | section data 1    |     |       |  |
    |                   |     | <-----+  |
    +-------------------+<----+          |
    |                   |                |
    | section data 2    |                |
    |                   | <--------------+
    +-------------------+

There is one section header for each section, and each data directory
will point to one of the sections (several data directories may point to
the same section, and there may be sections without data directory
pointing to them).

Sections' raw data
------------------

general
-------
All sections are aligned to 'SectionAlignment' when loaded in RAM, and
'FileAlignment' in the file. The sections are described by entries in
the section headers: You find the sections in the file via
'PointerToRawData' and in memory via 'VirtualAddress'; the length is in
'SizeOfRawData'.

There are several kinds of sections, depending on what's contained in
them. In most cases (but not in all) there will be at least one
data directory in a section, with a pointer to it in the optional
header's data directory array.

code section
------------
First, I will mention the code section. The section will have, at least,
the bits 'IMAGE_SCN_CNT_CODE', 'IMAGE_SCN_MEM_EXECUTE' and
'IMAGE_SCN_MEM_READ' set, and 'AddressOfEntryPoint' will point somewhere
into the section, to the start of the function that the developer wants
to execute first.
'BaseOfCode' will normally point to the start of this section, but may
point to somewhere later in the section if some non-code-bytes are
placed before the code in the section.
Normally, there will be nothing but executable code in this section, and
there will be only one code section, but don't rely on this.
Typical section names are ".text", ".code", "AUTO" and the like.

data section
-------------
The next thing we'll discuss is the initialized variables; this section
contains initialized static variables (like "static int i = 5;"). It will
have, at least, the bits 'IMAGE_SCN_CNT_INITIALIZED_DATA',
'IMAGE_SCN_MEM_READ' and 'IMAGE_SCN_MEM_WRITE' set. Some linkers may
place constant data into a section of their own that doesn't have the
writeable-bit. If part of the data is shareable, or there are other
peculiarities, there may be more sections with the apropriate section-
bits set.
The section, or sections, will be in the range 'BaseOfData' up to
'BaseOfData'+'SizeOfInitializedData'.
Typical section names are '.data', '.idata', 'DATA' and so on.

bss section
-----------
Then there is the uninitialized data (for static variables like "static
int k;"); this section is quite like the initialized data, but will have
a file offset ('PointerToRawData') of 0 to indicate its contents is not
stored in the file, and 'IMAGE_SCN_CNT_UNINITIALIZED_DATA' is set
instead of 'IMAGE_SCN_CNT_INITIALIZED_DATA' to indicate that the
contents should be set to 0-bytes at load-time.
The length will be 'SizeOfUninitializedData'.
Typical names are '.bss', 'BSS' and the like.

These were the section data that are *not* pointed to by data
directories. Their contents and structure is supplied by the compiler,
not by the linker.
(The stack-segment and heap-segment are not sections in the binary but
created by the loader from the stacksize- and heapsize-entries in the
optional header.)

copyright
---------
To begin with a simple directory-section, let's look at the data
directory 'IMAGE_DIRECTORY_ENTRY_COPYRIGHT'. The contents is a
copyright- or description string in ASCII (not 0-terminated), like
"Gonkulator control application, copyright (c) 1848 Hugendubel & Cie".
This string is, normally, supplied to the linker with the command line.
This string is not needed at runtime and may be discarded. It is not
writeable; in fact, the application doesn't need access at all.
So the linker will find out if there is a discardable non-writeable
section already and if not, create one (named '.descr' or the like). It
will then stuff the string into the section and let the
copyright-directory-pointer point to the string. The
'IMAGE_SCN_CNT_INITIALIZED_DATA' bit should be set.

exported symbols
----------------
The next-simplest thing is the export directory,
'IMAGE_DIRECTORY_ENTRY_EXPORT'. This is a directory typically found
in DLLs; it contains the entry points of exported functions (and the
addresses of exported objects etc.). Executables may of course also have
exported symbols but usually they don't.
The containing section should be "initialized data" and "readable". It
can not be "discardable" because the process might call "GetProcAddress()"
to find a function's entry point at runtime.
The section is normally called '.edata' if it is a separate thing; often
enough, it is merged into some other section like "initialized data".

The structure of the export table ('IMAGE_EXPORT_DIRECTORY') comprises a
header and the export data, that is: the symbol names, their ordinals
and the offsets to their entry points.

First, we have 32 bits of 'Characteristics' that are unused and normally
0. Then there is a 32-bit-'TimeDateStamp', which presumably should give
the time the table was created in the time_t-format; alas, it is not
always valid (some linkers set it to 0). Then we have 2 16-bit-words of
version-info ('MajorVersion' and 'MinorVersion'), and these, too, are
often enough set to 0.

The next thing is 32 bits of 'Name'; this is an RVA to the DLL name as a
0-terminated ASCII string. (The name is necessary in case the DLL file is
renamed - see "binding" at the import directory.)
Then, we have got a 32-bit-'Base'. We'll come to that in a moment.

Now we have got sort of a problem, because the next two 32-bit-values are
the number of exported functions ('NumberOfFunctions') and the number of
exported names ('NumberOfNames'). These values are *not* always equal,
and weird things happen if you try to decipher the export directory past
the minimum of both. In *most* export-directories the values are equal
and we're fine. It seems to be always safe to decipher up to the minimum
of both values.
I've got a suspicion that it is somehow possible do export symbols
without a name (just by ordinal), but I didn't find anything about this
topic.

The next 3 32-bit-values are RVAs to 3 arrays. The arrays run parallel:
the array of entry points 'AddressOfFunctions' (given as 32-bit-RVAs),
the array of 32-bit-RVAs to symbol names 'AddressOfNames', and the array
of 16-bit-ordinals 'AddressOfNameOrdinals'.

To get information about the i-th exported symbol, find the export
directory, follow the 3 RVAs, skip to index i in the 3 arrays, and
you have got a) the RVA of the function's entry point; b) the RVA to the
function's name (a 0-terminated ASCII-string) and c) the function's
ordinal (you have to add the 'Base' to the value to get the real
ordinal).

For functions, the entry-point-RVA will normally point into the code
section; for objects, it will almost always point into the data- or
bss-section.

imported symbols
----------------
This is one for the tough.

When the compiler finds a call to a function that is in a different
executable (mostly in a DLL), it will, in the most simplistic case, not
know anything about the circumstances and simply output a normal
call-instruction to that symbol, the address of which the linker will
have to fix. The linker uses an import library to supply the addresses,
and that library has stubs for all the exported symbols, each of which
consists of a jump-instruction; the stubs are the actual call-targets.
These jump-instructions will actually jump to an address that's fetched
from the import address table we're discussing here.
In more sophisticated applications (when "declspec(dllimport)" is used),
the compiler knows the function is imported, and outputs a call to the
address that's in the import address table, bypassing the jump.

Hm. So far for that matter. You are kindly referred to [1] and [2].

However, the address of the function in the DLL is always necessary and
will be supplied by the loader when the application is loaded. The
loader knows which symbols in what libraries have to be looked up and
their addresses fixed by searching the import directory.

I will better give you an example. The calls with or without
__declspec(dllimport) look like this:

    source:
        int symbol(char *);
        __declspec(dllimport) int symbol2(char*);
        void foo(void)
        {
            int i=symbol("bar");
            int j=symbol2("baz");
        }

    assembly:
        ...
        call _symbol                 ; without declspec(dllimport)
        ...
        call ptr cs:0xdeadbeef       ; with declspec(dllimport)
        ...

    the stub provided by the import library (not used with
                __declspec(dllimport)) is located in the code section
                and is the above mentioned jump-instruction:
        _symbol: jmp dword ptr [0xdeadbeef]

So however you do it, there is a call or a jump to a location the
address of which is at 0xdeadbeef (in our example). This 0xdeadbeef is
right in the middle of the import directory; it is in the entry for the
function "symbol" in the DLL "foo.dll".

Now we'll look how an import directory is made up.

The import directory should reside in a section that's "initialized
data" and "readable".
The import directory is an array of IMAGE_IMPORT_DESCRIPTORs, one for
each used DLL. The list is terminated by a IMAGE_IMPORT_DESCRIPTOR
that's entirely filled with 0-bytes.
An IMAGE_IMPORT_DESCRIPTOR is a struct with these members:

    OriginalFirstThunk
        An RVA (32 bit) to a 0-terminated array of RVAs to
        IMAGE_THUNK_DATAs, each describing one imported function. The
        array will never change.

    TimeDateStamp
        A 32-bit-timestamp that has several purposes. Let's pretent that
        the timestamp is 0, and handle the advanced cases later.

    ForwarderChain
        The 32-bit-index of the first forwarder in the list of imported
        functions. Forwarders are also advanced stuff.

    Name
        A 32-bit-RVA to the Name (a 0-terminated ASCII string) of the
        DLL.

    FirstThunk
        An RVA (32 bit) to a 0-terminated array of RVAs to
        IMAGE_THUNK_DATAs, each describing one imported function. The
        array will change.

Each IMAGE_IMPORT_DESCRIPTOR in the array gives you the name of the
exporting DLL and, apart from the forwarder and timestamp, it gives you
2 RVAs to arrays of IMAGE_THUNK_DATAs, using 32 bits. (The last member
of each array is entirely filled with 0-bytes.)
Each IMAGE_THUNK_DATA is, for now, an RVA to a IMAGE_IMPORT_BY_NAME
which describes the imported function.
The interesting point is now, the arrays run parallel, i.e.: they point
to the same IMAGE_IMPORT_BY_NAMEs.

No need to be desparate, I will draw another picture. This is the
essential contents of one IMAGE_IMPORT_DESCRIPTOR:

     OriginalFirstThunk      FirstThunk
            |                    |
            |                    |
            |                    |
            V                    V

            0-->    func1     <--0
            1-->    func2     <--1
            2-->    func3     <--2
            3-->    foo       <--3
            4-->    mumpitz   <--4
            5-->    knuff     <--5
            6-->0            0<--6      /* the last RVA is 0! */

where the names in the center are the yet to discuss
IMAGE_IMPORT_BY_NAMEs. Each of them is a 16-bit-number (the ordinal of
the imported symbol) followed by an unspecified amount of bytes, being
the 0-terminated ASCII name of the imported symbol.
Mind you, the ordinal may be wrong. It is only a hint where to start
looking for the symbol name in the export table of the exporting DLL;
you may not compare the ordinals and take the matching entry, but you
must find the matching symbol name.

To summarize, if you want to look up information about the imported
function "foo" from DLL "knurr", you first find the entry
IMAGE_DIRECTORY_ENTRY_IMPORT in the data directories, get an RVA, find
that address in the raw section data and now have an array of
IMAGE_IMPORT_DESCRIPTORs. Get the member of this array that relates to
the DLL "knurr" by inspecting the strings pointed to by the 'Name's.
When you have found the right IMAGE_IMPORT_DESCRIPTOR, follow its
'OriginalFirstThunk' and get hold of the pointed-to array of
IMAGE_THUNK_DATAs; inspect the RVAs and find the function "foo".

This was the basic structure, for simple cases. Now we'll learn about
tweaks in the import directories.

First, the bit IMAGE_ORDINAL_FLAG (that is: the MSB) of the
IMAGE_THUNK_DATA in the arrays can be set, in which case there is no
symbol-name-information in the list and the symbol is imported purely by
ordinal. You get the ordinal by inspecting the lower word of the
IMAGE_THUNK_DATA.
Obviously, in this case the warning about unreliable ordinals doesn't
apply because all you have *is* the ordinal.

Ok, now, why do we have *two* lists of pointers to the
IMAGE_IMPORT_BY_NAMEs? Because at runtime the application doesn't need
the imported functions' names but the addresses. The loader will look up
each imported symbol in the export-directory of the DLL in question and
replace the IMAGE_THUNK_DATA in the 'FirstThunk'-list with the linear
address of the DLL's entry point. Remember the stubs "jmp dword ptr
[0xdeadbeef]"; this address 0xdeadbeef is exactly the address of the
IMAGE_THUNK_DATA in the 'FirstThunk'-list.
The 'OriginalFirstThunk' remains untouched, so you can always look up
the original list of imported names via the 'OriginalFirstThunk'-list.

Now we get the so-called "bound imports".
Think about the loader's task: when a binary that it wants to execute
needs a function from a DLL, the loader loads the DLL, finds its export
directory, looks up the function's RVA and calculates the function's
entry point. Then it patches the so-found address into the 'FirstThunk'-
list.
Given that the programmer was clever and supplied unique preferred load
addresses for the DLLs that don't clash, we can assume that the
functions' entry points will always be the same. They can be computed
and patched into the 'FirstThunk'-list at link-time, and that's what
happens with the "bound imports". (The utility "bind" does this.)

Of course, one must be cautious: The user's DLL may have a different
version, or it may be necessary to relocate the DLL, thus invalidating
the pre-patched 'FirstThunk'-list; in this case, the loader will still
be able to walk the 'OriginalFirstThunk'-list, find the imported symbols
and re-patch the 'FirstThunk'-list. The loader knows that this is
necessary if a) the versions of the exporting DLL don't match or b) the
exporting DLL had to be relocated.

To decide whether there were relocations is no problem for the loader,
but how to find out if the versions differ? This is where the
'TimeDateStamp' of the IMAGE_IMPORT_DESCRIPTOR comes in. If it is 0, the
import-list has not been bound, and the loader must fix the entry points
always. Otherwise, the imports are bound, and 'TimeDateStamp' must match
the 'TimeDateStamp' of the exporting DLL's 'FileHeader'; if it doesn't
match, the loader assumes that the binary is bound to a "wrong" DLL and
will re-patch the import list.

This was the so-called "old-style" binding.

There is an additional quirk about "forwarders" in the import-list. A DLL
can export a symbol that's not defined in the DLL but imported from
another DLL; such a symbol is said to be forwarded. Now, obviously you
can't tell if the symbol's entry point is valid by looking into the
timestamp of a DLL that doesn't actually contain the entry point. So the
forwarded symbols' entry points must always be fixed up, for safety
reasons. In the import list of a binary, imports of forwarded symbols
need to be found so the loader can patch them.

This is done via the 'ForwarderChain'. It is an index in the thunk-
lists; the import at the indexed position is a forwarded export, and the
contents of the 'FirstThunk'-list at this position is the index of the
*next* forwarded import, and so on, until the index is "-1" which
indicates there are no more forwards.

At this point, we should sum up what we have had so far :-)

Ok, I will assume you have found the IMAGE_DIRECTORY_ENTRY_IMPORT and you have
followed it to find the import-directory, which will be in one of the
sections. Now you're at the beginning of an array of
IMAGE_IMPORT_DESCRIPTORs the last of which will be entirely 0-bytes-
filled.
To decipher one of the IMAGE_IMPORT_DESCRIPTORs, you first look into the
'Name'-field, follow the RVA and thusly find the name of the exporting
DLL. Next you decide whether the imports are bound or not;
'TimeDateStamp' will be non-zero if the imports are bound. If they are
bound, now is a good time to check if the DLL version matches yours by
comparing the 'TimeDateStamp's.
Now you follow the 'OriginalFirstThunk'-RVA to go to the
IMAGE_THUNK_DATA-array; walk down this array (it is be 0-terminated),
and each member will be the RVA of a IMAGE_IMPORT_BY_NAME (unless the
hi-bit is set in which case you don't have a name but are left with a
mere ordinal). Follow the RVA, and skip 2 bytes (the ordinal), and now
you have got a pointer to a 0-terminated ASCII-string that's the name of
the imported function.
To find the supplied entry point addresses in case it is a bound import,
follow the 'FirstThunk' and walk it parallel to the
'OriginalFirstThunk'-array; the array-members are the linear addresses
of the entry points.

There is one thing I didn't mention until now: Apparently there are
linkers that exhibit a bug when they build the import directory (I've
found this bug being in use by a Borland C linker). These linkers set
the 'OriginalFirstThunk' in the IMAGE_IMPORT_DESCRIPTOR to 0 and create
only the 'FirstThunk'-array. Obviously, such import directories cannot
be bound (else the necessary information to re-fix the imports were
lost). In this case, you will have to follow the 'FirstThunk'-array to get
the imported symbol names, and you will never have pre-patched entry point
addresses.

The last tweak about the import directories is the so-called "new style"
binding (it is described in [3]), which can also be done with the
"bind"-utility. When this is used, the 'TimeDateStamp' is set to
all-bits-1 and there is no forwarderchain; all imported symbols get their
address patched, whether they are forwarded or not. Still, you need to
know the DLLs' version, and you need to distinguish forwarded symbols
from ordinary ones. For this purpose, the
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT directory is created. This will, as
far as I could find out, *not* be in a section but in the header, after
the section headers and before the first section. (Hey, I didn't invent
this, I'm only describing it!)
This directory tells you, for each used DLL, from which other DLLs there
are forwarded exports.
The structure is an IMAGE_BOUND_IMPORT_DESCRIPTOR, comprising (in this
order):
A 32-bit number, giving you the 'TimeDateStamp' of the DLL;
a 16-bit-number 'OffsetModuleName', being the offset from the beginning
of the directory to the 0-terminated name of the forwarded-to DLL;
a 16-bit-number 'NumberOfModuleForwarderRefs' giving you the number of
DLLs that this DLL uses for its forwarders.

Immediatly following this struct you find 'NumberOfModuleForwarderRefs'
structs that tell you the names and versions of the DLLs that this DLL
forwards from. These structs are 'IMAGE_BOUND_FORWARDER_REF's:
A 32-bit-number 'TimeDateStamp';
a 16-bit-number 'OffsetModuleName', being the offset from the beginning
of the directory to the 0-terminated name of the forwarded-from DLL;
16 unused bits.

Following the 'IMAGE_BOUND_FORWARDER_REF's is the next
'IMAGE_BOUND_IMPORT_DESCRIPTOR' and so on; the list is terminated by an
all-0-bits-IMAGE_BOUND_IMPORT_DESCRIPTOR.

Sorry for the inconvenience, but that's what it looks like :-)

Now, if you have a new-bound import directory, you load all the DLLs,
use the directory pointer IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT to find the
IMAGE_BOUND_IMPORT_DESCRIPTOR, scan through it and check if the
'TimeDateStamp's of the loaded DLLs match the ones given in this
directory. If not, fix them in the 'FirstThunk'-array of the import
directory.

It is easy if you know how it works :-)

resources
---------
The resources, like dialog boxes, menus, icons and so on, are stored in
the data directory pointed to by IMAGE_DIRECTORY_ENTRY_RESOURCE. It is in
a section that has, at least, the bits 'IMAGE_SCN_CNT_INITIALIZED_DATA'
and 'IMAGE_SCN_MEM_READ' set.

A resource base is a 'IMAGE_RESOURCE_DIRECTORY's; it contains several
'IMAGE_RESOURCE_DIRECTORY_ENTRY's which in turn may point to a
'IMAGE_RESOURCE_DIRECTORY'. This way, you get a tree of
'IMAGE_RESOURCE_DIRECTORY's with 'IMAGE_RESOURCE_DIRECTORY_ENTRY's as
leafs; these leafs point to the actual resource data.

In real life, the situation is somewhat relaxed. Normally you won't find
convoluted trees you can't possibly sort out.
The hierarchy is, normally, like this: one directory is the root. It
points to directories, one for each resource type. These directories
point to subdirectories, each of which will have a name or an ID and
point to a directory of the languages provided for this resource; for
each language you will find one resource entry, which will finally point
to the data.

The tree, without the pointer to the data, may look like this:

                           (root)
                              |
             +----------------+------------------+
             |                |                  |
            menu            dialog             icon
             |                |                  |
       +-----+-----+        +-+----+           +-+----+----+
       |           |        |      |           |      |    |
    "main"      "popup"   0x10   "maindlg"    0x100 0x110 0x120
       |            |       |      |           |      |    |
   +---+-+          |       |      |           |      |    |
   |     |     default   english   default    def.   def.  def.
german english

A IMAGE_RESOURCE_DIRECTORY comprises:
32 bits of unused flags called 'Characteristics';
32 bits 'TimeDateStamp' (again in the common time_t representation),
giving you the time the resource was created (if the entry is set);
16 bits 'MajorVersion' and 16 bits 'MinorVersion', thusly allowing you
to maintain several versions of the resource;
16 bits 'NumberOfNamedEntries' and another 16 bits 'NumberOfIdEntries'.

Immediatly following such a structure are
'NumberOfNamedEntries'+'NumberOfIdEntries' structs which are of the
format 'IMAGE_RESOURCE_DIRECTORY_ENTRY', those with the names coming first.
They may point to further 'IMAGE_RESOURCE_DIRECTORY's or they point to
the actual resource data.
A IMAGE_RESOURCE_DIRECTORY_ENTRY consists of:
32 bits giving you the id of the resource or the directory
it describes;
32 bits offset to the data or offset to the next sub-directory.

The meaning of the id depends on the level in the tree; the id may be a
number (if the hi-bit is clear) or a name (if the hi-bit is set). If it
is a name, the lower 31 bits are the offset from the beginning of the
resource section's raw data to the name (the name consists of 16 bits
length and trailing wide characters, in unicode, not 0-terminated).

If you are in the root-directory, the id, if it is a number, is the
resource-type:
    1: cursor
    2: bitmap
    3: icon
    4: menu
    5: dialog
    6: string table
    7: font directory
    8: font
    9: accelerators
    10: unformatted resource data
    11: message table
    12: group cursor
    14: group icon
    16: version information
Any other number is user-defined. Any resource-type with a type-name is
always user-defined.

If you are one level deeper, the id is the resource-id (or resource-
name).

If you are another level deeper, the id must be a number, and it is the
language-id of the specific instance of the resource; for example, you
can have the same dialog in english, french and german localized forms,
and they all share the same resource-id. The system will choose the
dialog to load based on the process's locale, which in turn will usually
reflect the user's "regional setting".
(If the resource cannot be found for the process locale, the system will
first try to find a resource for the locale using a neutral sublanguage;
if it still can't be found, the instance with the smallest language id will
be used.)
To decipher the language id, split it into the primary language id and
the sublanguage id using the macros PRIMARYLANGID() and SUBLANGID(),
giving you the bits 0 to 9 or 10 to 15, respectivly. The values are
defined in the file "winresrc.h".
Language-resources are only supported for accelerators, dialogs, menus,
rcdata or stringtables; other resource-types should be
LANG_NEUTRAL/SUBLANG_NEUTRAL.

To find out whether the next level below a resource directory is another
directory, you inspect the hi-bit of the offset. If it is set, the
remaining 31 bits are the offset from the beginning of the resource
section's raw data to the next directory, again in the format
IMAGE_RESOURCE_DIRECTORY with trailing IMAGE_RESOURCE_DIRECTORY_ENTRYs.

If the bit is clear, the offset is the distance from the beginning of
the resource section's raw data to the resource's raw data description,
a IMAGE_RESOURCE_DATA_ENTRY. It consists of 32 bits 'OffsetToData' (the
offset to the raw data, counting from the beginning of the resource
section's raw data), 32 bits of 'Size' of the data, 32 bits 'CodePage'
and 32 unused bits.
(The use of codepages is discouraged, you should use the 'language'-
feature to support multiple locales.)

The raw data format depends on the resource type; descriptions can be
found in the MS SDK documentation. Note that any string in resources is
always in UNICODE.

relocations
-----------
The last data directory I will describe is the base relocation
directory. It is pointed to by the IMAGE_DIRECTORY_ENTRY_BASERELOC entry
in the data directories of the optional header. It is typically
contained in a section if its own, with a name like ".reloc" and the
bits IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_DISCARDABLE and
IMAGE_SCN_MEM_READ set.

The relocation data is needed by the loader if the image cannot be
loaded to the preferred load address 'ImageBase' mentioned in the
optional header. In this case, the fixed addresses supplied by the
linker are no longer valid, and the loader has to apply fixups for
absolute addresses used for locations of static variables, string
literals and so on.

The relocation directory is a sequence of chunks. Each chunk contains
the relocation information for 4 KB of the image. A chunk starts with a
'IMAGE_BASE_RELOCATION' struct. It consists of 32 bits 'VirtualAddress'
and 32 bits 'SizeOfBlock'. It is followed by the chunk's actual
relocation data, being 16 bits each.
The 'VirtualAddress' is the base RVA that the relocations of this chunk
need to be applied to; the 'SizeOfBlock' is the size of the entire chunk
in bytes.
The number of trailing relocations is
('SizeOfBlock'-sizeof(IMAGE_BASE_RELOCATION))/2
The relocation information ends when you encounter a
IMAGE_BASE_RELOCATION struct with a 'VirtualAddress' of 0.

Each 16-bit-relocation information consists of the actual relocation
position in the lower 12 bits and a relocation type in the high 4 bits.
To get the relocation RVA, you need to add the IMAGE_BASE_RELOCATION's
'VirtualAddress' to the 12-bit-position. The type is one of:
    IMAGE_REL_BASED_ABSOLUTE (0)
        This is a no-op; it is used to align the chunk to a 32-bits-
        border.
    IMAGE_REL_BASED_HIGH (1)
        The relocation must be applied to the high 16 bits of the DWORD
        in question.
    IMAGE_REL_BASED_LOW (2)
        The relocation must be applied to the low 16 bits of the DWORD
        in question.
    IMAGE_REL_BASED_HIGHLOW (3)
        The relocation must be applied to the entire 32 bits in question.
    IMAGE_REL_BASED_HIGHADJ (4)
        Unknown
    IMAGE_REL_BASED_MIPS_JMPADDR (5)
        Unknown
    IMAGE_REL_BASED_SECTION (6)
        Unknown
    IMAGE_REL_BASED_REL32 (7)
        Unknown

As an example, if you find the relocation information to be
    0x00004000      (32 bits, starting RVA)
    0x00000010      (32 bits, size of chunk)
    0x3012          (16 bits reloc data)
    0x3080          (16 bits reloc data)
    0x30f6          (16 bits reloc data)
    0x0000          (16 bits reloc data)
    0x00000000      (next chunk's RVA)
    0xff341234
you know the first chunk describes relocations starting at RVA 0x4000 and
is 16 bytes long. Because the header uses 8 bytes and one relocation
uses 2 bytes, there are (16-8)/2=4 relocations in the chunk.
The first relocation is to be applied to the DWORD at 0x4012, the next
to the DWORD at 0x4080, and the third to the DWORD at 0x40f6. The last
relocation is a no-op.
The next chunk has a RVA of 0 and finishes the list.

Now, how do you do a relocation?
You know that the image *is* relocated to the preferred load address
'ImageBase' in the optional header; you also know the address you did
load the image to. If they match, you don't need to do anything.
If they don't match, you calculate the difference
actual_base-preferred_base
and add that value to the relocation positions, which you will find with
the method described above.

Acknowledgments
---------------
Thanks go to David Binette for his debugging and proof-reading.
(The remaining errors are entirely mine.)

Copyright
---------
This text is copyright 1998 by B. Luevelsmeyer. It is freeware, and you
may use it for any purpose but on you own risk. It contains errors and
it is incomplete. You have been warned.

Bug reports
-----------
Send any bug reports (or other comments) to
bernd.luevelsmeyer@iplan.heitec.net

Literature
----------

[1]
"Peering Inside the PE: A Tour of the Win32 Portable Executable File
Format" (M. Pietrek), in: Microsoft Systems Journal 3/1994

[2]
"Why to Use _declspec(dllimport) & _declspec(dllexport) In Code", MS
Knowledge Base Q132044

[3]
"Windows Q&A" (M. Pietrek), in: Microsoft Systems Journal 8/1995

[4]
"Writing Multiple-Language Resources", MS Knowledge Base Q89866

[5]
"The Portable Executable File Format from Top to Bottom" (Randy Kath),
in: Microsoft Developer Network

Appendix: hello world
---------------------
In this appendix I will show how to make programs by hand. The example
will use Intel-assembly, because I don't speak DEC Alpha.

The program will be the equivalent of

    #include <stdio.h>
    int main(void)
    {
        puts(hello,world);
        return 0;
    }

First, I translate it to use Win32 functions instead of the C runtime:

    #define STD_OUTPUT_HANDLE -11UL
    #define hello "hello, world\n"

    __declspec(dllimport) unsigned long __stdcall
    GetStdHandle(unsigned long hdl);

    __declspec(dllimport) unsigned long __stdcall
    WriteConsoleA(unsigned long hConsoleOutput,
                    const void *buffer,
                    unsigned long chrs,
                    unsigned long *written,
                    unsigned long unused
                    );

    static unsigned long written;

    void startup(void)
    {
        WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE),hello,sizeof(hello)-1,&written,0);
        return;
    }

Now I will fumble out the assembly:
    startup:
                ; parameters for WriteConsole(), backwards
    6A 00                     push      0x00000000
    68 ?? ?? ?? ??            push      offset _written
    6A 0D                     push      0x0000000d
    68 ?? ?? ?? ??            push      offset hello
                ; parameter for GetStdHandle()
    6A F5                     push      0xfffffff5
    2E FF 15 ?? ?? ?? ??      call      dword ptr cs:__imp__GetStdHandle@4
                ; result is last parameter for WriteConsole()
    50                        push      eax
    2E FF 15 ?? ?? ?? ??      call      dword ptr cs:__imp__WriteConsoleA@20
    C3                        ret

    hello:
    68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A   "hello, world\n"
    _written:
    00 00 00 00

I need to find the functions WriteConsoleA() and GetStdHandle(). They
happen to be in "kernel32.dll".

Now I can start to make the executable. Question marks will take the
place of yet-to-find-out values; they will be patched afterwards.

First the DOS-header, starting at 0x0 and being 0x40 bytes long:
    00 | 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    20 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    30 | 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00
As you can see, this isn't really a MS-DOS program. It's just the header
with the signature "MZ" at the beginning and the e_lfanew pointing
immediatly after the header, without any code. That's because it isn't
intended to run on MS-DOS.

Then the signature, starting at 0x40 and being 0x4 bytes long:
        50 45 00 00

Now the file-header, which will start at byte 0x44 and is 0x14 bytes long:
    Machine                     4c 01       ; i386
    NumberOfSections            02 00       ; code and data
    TimeDateStamp               00 00 00 00 ; who cares?
    PointerToSymbolTable        00 00 00 00 ; unused
    NumberOfSymbols             00 00 00 00 ; unused
    SizeOfOptionalHeader        e0 00       ; constant
    Characteristics             02 01       ; executable on 32-bit-machine

And the optional header, which will start at byte 0x58 and is 0x60 bytes long:
    Magic                       0b 01       ; constant
    MajorLinkerVersion          00          ; I'm version 0.0 :-)
    MinorLinkerVersion          00          ;
    SizeOfCode                  20 00 00 00 ; 32 bytes of code
    SizeOfInitializedData       ?? ?? ?? ?? ; yet to find out
    SizeOfUninitializedData     00 00 00 00 ; we don't have a BSS
    AddressOfEntryPoint         ?? ?? ?? ?? ; yet to find out
    BaseOfCode                  ?? ?? ?? ?? ; yet to find out
    BaseOfData                  ?? ?? ?? ?? ; yet to find out
    ImageBase                   00 00 10 00 ; 1 MB, chosen arbitrarily
    SectionAlignment            20 00 00 00 ; 32-bytes-alignment
    FileAlignment               20 00 00 00 ; 32-bytes-alignment
    MajorOperatingSystemVersion  04 00      ; NT 4.0
    MinorOperatingSystemVersion  00 00      ;
    MajorImageVersion           00 00       ; version 0.0
    MinorImageVersion           00 00       ;
    MajorSubsystemVersion       04 00       ; Win32 4.0
    MinorSubsystemVersion       00 00       ;
    Win32VersionValue           00 00 00 00 ; unused?
    SizeOfImage                 ?? ?? ?? ?? ; yet to find out
    SizeOfHeaders               ?? ?? ?? ?? ; yet to find out
    CheckSum                    00 00 00 00 ; not used for non-drivers
    Subsystem                   03 00       ; Win32 console
    DllCharacteristics          00 00       ; unused (not a DLL)
    SizeOfStackReserve          00 00 10 00 ; 1 MB stack
    SizeOfStackCommit           00 10 00 00 ; 4 KB to start with
    SizeOfHeapReserve           00 00 10 00 ; 1 MB heap
    SizeOfHeapCommit            00 10 00 00 ; 4 KB to start with
    LoaderFlags                 00 00 00 00 ; unknown
    NumberOfRvaAndSizes         10 00 00 00 ; constant

As you can see, I plan to have only 2 sections, one for code and one for
all the rest (data, constants and import directory). There will be no
relocations and no other stuff like resources.
The section alignment is the same in the file and in RAM (32 bytes);
this helps to keep the task easy.

Now we set up the data directories, beginning at byte 0xb8 and being 0x80 bytes long:
    Address        Size
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_EXPORT (0)
    ?? ?? ?? ??    ?? ?? ?? ??         ; IMAGE_DIRECTORY_ENTRY_IMPORT (1)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_SECURITY (4)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_DEBUG (6)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_TLS (9)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_IAT (12)
    00 00 00 00    00 00 00 00         ; 13
    00 00 00 00    00 00 00 00         ; 14
    00 00 00 00    00 00 00 00         ; 15
Only the import directory is in use.

Next are the section headers. First we make the code section, which will
contain the above mentioned assembly. It is 32 bytes long, and so will
be the code section. The header begins at 0x138 and is 0x28 bytes long:

    Name            2e 63 6f 64 65 00 00 00     ; ".code"
    VirtualSize         00 00 00 00             ; unused
    VirtualAddress      ?? ?? ?? ??             ; yet to find out
    SizeOfRawData       20 00 00 00             ; size of code
    PointerToRawData    ?? ?? ?? ??             ; yet to find out
    PointerToRelocations 00 00 00 00            ; unused
    PointerToLinenumbers 00 00 00 00            ; unused
    NumberOfRelocations  00 00                  ; unused
    NumberOfLinenumbers  00 00                  ; unused
    Characteristics     20 00 00 60             ; code, executable, readable

The second section will contain the data. The header begins at 0x160 and
is 0x28 bytes long:

    Name            2e 64 61 74 61 00 00 00     ; ".data"
    VirtualSize         00 00 00 00             ; unused
    VirtualAddress      ?? ?? ?? ??             ; yet to find out
    SizeOfRawData       ?? ?? ?? ??             ; yet to find out
    PointerToRawData    ?? ?? ?? ??             ; yet to find out
    PointerToRelocations 00 00 00 00            ; unused
    PointerToLinenumbers 00 00 00 00            ; unused
    NumberOfRelocations  00 00                  ; unused
    NumberOfLinenumbers  00 00                  ; unused
    Characteristics     40 00 00 c0             ; initialized, readable, writeable

The next byte is 0x188, but the sections need to be aligned to 32 bytes
(because I chose so), so we need padding bytes up to 0x1a0:

    00 00 00 00 00 00       ; padding
    00 00 00 00 00 00
    00 00 00 00 00 00
    00 00 00 00 00 00

Now the first section, being the code section with the above mentioned
assembly, *does* come. It begins at byte 0x1a0 and is 0x20 bytes long:
    6A 00                    ; push      0x00000000
    68 ?? ?? ?? ??           ; push      offset _written
    6A 0D                    ; push      0x0000000d
    68 ?? ?? ?? ??           ; push      offset hello_string
    6A F5                    ; push      0xfffffff5
    2E FF 15 ?? ?? ?? ??     ; call      dword ptr cs:__imp__GetStdHandle@4
    50                       ; push      eax
    2E FF 15 ?? ?? ?? ??     ; call      dword ptr cs:__imp__WriteConsoleA@20
    C3                       ; ret

Because of the previous section's length we don't need any padding
before the next section (data), and here it comes, beginning at 0x1c0:

    68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A  ; "hello, world\n"
    00 00 00                                ; padding to align _written
    00 00 00 00                             ; _written

Now all that's left is the import directory. It will import 2 functions
from "kernel32.dll", and it's immediatly following the variables in the
same section. First we will align it to 32 bytes:

    00 00 00 00 00 00 00 00 00 00 00 00     ; padding

It begins at 0x1e0 with the IMAGE_IMPORT_DESCRIPTOR:
    OriginalFirstThunk      ?? ?? ?? ??     ; yet to find out
    TimeDateStamp           00 00 00 00     ; unbound
    ForwarderChain          ff ff ff ff     ; no forwarders
    Name                    ?? ?? ?? ??     ; yet to find out
    FirstThunk              ?? ?? ?? ??     ; yet to find out

We need to terminate the import-directory with a 0-bytes-entry (we are at 0x1f4):
    OriginalFirstThunk      00 00 00 00     ; terminator
    TimeDateStamp           00 00 00 00     ;
    ForwarderChain          00 00 00 00     ;
    Name                    00 00 00 00     ;
    FirstThunk              00 00 00 00     ;

Now there's the DLL name left, and the 2 thunks, and the thunk-data, and
the function names. But we will be finished real soon now!

The DLL name, 0-terminated, beginning at 0x208:
    6b 65 72 6e 65 6c 33 32 2e 64 6c 6c 00  ; "kernel32.dll"
    00 00 00                                ; padding to 32-bit-boundary

The original first thunk, starting at 0x218:
    AddressOfData   ?? ?? ?? ??             ; RVA to function name "WriteConsoleA"
    AddressOfData   ?? ?? ?? ??             ; RVA to function name "GetStdHandle"
                    00 00 00 00             ; terminator

The first thunk is exactly the same list and starts at 0x224:
(__imp__WriteConsoleA@20, at 0x224)
    AddressOfData   ?? ?? ?? ??             ; RVA to function name "WriteConsoleA"
(__imp__GetStdHandle@4, at 0x228)
    AddressOfData   ?? ?? ?? ??             ; RVA to function name "GetStdHandle"
                    00 00 00 00             ; terminator

Now what's left is the two function names in the shape of an
IMAGE_IMPORT_BY_NAME. We are at byte 0x230.
    01 00                                      ; ordinal, need not be correct
    57 72 69 74 65 43 6f 6e 73 6f 6c 65 41 00  ; "WriteConsoleA"
    02 00                                      ; ordinal, need not be correct
    47 65 74 53 74 64 48 61 6e 64 6c 65 00     ; "GetStdHandle"

Ok, that's about all. The next byte, which we don't really need, is
0x24f. We need to fill the section with padding up to 0x260:
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; padding
    00

------------

We are done. Now that we know all the byte-offsets, we can apply fixups
to all those addresses and sizes that were indicated as "unknown" with
'??'-marks.
I won't force you to read that step-by-step (it's quite
straightforward), and simply present the result:

------------

DOS-header, starting at 0x0:
    00 | 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    10 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    20 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    30 | 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00

signature, starting at 0x40:
        50 45 00 00

file-header, starting at 0x44:
    Machine                     4c 01       ; i386
    NumberOfSections            02 00       ; code and data
    TimeDateStamp               00 00 00 00 ; who cares?
    PointerToSymbolTable        00 00 00 00 ; unused
    NumberOfSymbols             00 00 00 00 ; unused
    SizeOfOptionalHeader        e0 00       ; constant
    Characteristics             02 01       ; executable on 32-bit-machine

optional header, starting at 0x58:
    Magic                       0b 01       ; constant
    MajorLinkerVersion          00          ; I'm version 0.0 :-)
    MinorLinkerVersion          00          ;
    SizeOfCode                  20 00 00 00 ; 32 bytes of code
    SizeOfInitializedData       a0 00 00 00 ; data section size
    SizeOfUninitializedData     00 00 00 00 ; we don't have a BSS
    AddressOfEntryPoint         a0 01 00 00 ; beginning of code section
    BaseOfCode                  a0 01 00 00 ; RVA to code section
    BaseOfData                  c0 01 00 00 ; RVA to data section
    ImageBase                   00 00 10 00 ; 1 MB, chosen arbitrarily
    SectionAlignment            20 00 00 00 ; 32-bytes-alignment
    FileAlignment               20 00 00 00 ; 32-bytes-alignment
    MajorOperatingSystemVersion  04 00      ; NT 4.0
    MinorOperatingSystemVersion  00 00      ;
    MajorImageVersion           00 00       ; version 0.0
    MinorImageVersion           00 00       ;
    MajorSubsystemVersion       04 00       ; Win32 4.0
    MinorSubsystemVersion       00 00       ;
    Win32VersionValue           00 00 00 00 ; unused?
    SizeOfImage                 c0 00 00 00 ; sum of all section sizes
    SizeOfHeaders               a0 01 00 00 ; offset to 1st section
    CheckSum                    00 00 00 00 ; not used for non-drivers
    Subsystem                   03 00       ; Win32 console
    DllCharacteristics          00 00       ; unused (not a DLL)
    SizeOfStackReserve          00 00 10 00 ; 1 MB stack
    SizeOfStackCommit           00 10 00 00 ; 4 KB to start with
    SizeOfHeapReserve           00 00 10 00 ; 1 MB heap
    SizeOfHeapCommit            00 10 00 00 ; 4 KB to start with
    LoaderFlags                 00 00 00 00 ; unknown
    NumberOfRvaAndSizes         10 00 00 00 ; constant

data directories, starting at 0xb8:
    Address        Size
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_EXPORT (0)
    e0 01 00 00    6f 00 00 00         ; IMAGE_DIRECTORY_ENTRY_IMPORT (1)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_RESOURCE (2)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_EXCEPTION (3)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_SECURITY (4)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_BASERELOC (5)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_DEBUG (6)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_COPYRIGHT (7)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_GLOBALPTR (8)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_TLS (9)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG (10)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (11)
    00 00 00 00    00 00 00 00         ; IMAGE_DIRECTORY_ENTRY_IAT (12)
    00 00 00 00    00 00 00 00         ; 13
    00 00 00 00    00 00 00 00         ; 14
    00 00 00 00    00 00 00 00         ; 15

section header (code), starting at 0x138:
    Name            2e 63 6f 64 65 00 00 00     ; ".code"
    VirtualSize         00 00 00 00             ; unused
    VirtualAddress      a0 01 00 00             ; RVA to code section
    SizeOfRawData       20 00 00 00             ; size of code
    PointerToRawData    a0 01 00 00             ; file offset to code section
    PointerToRelocations 00 00 00 00            ; unused
    PointerToLinenumbers 00 00 00 00            ; unused
    NumberOfRelocations  00 00                  ; unused
    NumberOfLinenumbers  00 00                  ; unused
    Characteristics     20 00 00 60             ; code, executable, readable

section header (data), starting at 0x160:
    Name            2e 64 61 74 61 00 00 00     ; ".data"
    VirtualSize         00 00 00 00             ; unused
    VirtualAddress      c0 01 00 00             ; RVA to data section
    SizeOfRawData       a0 00 00 00             ; size of data section
    PointerToRawData    c0 01 00 00             ; file offset to data section
    PointerToRelocations 00 00 00 00            ; unused
    PointerToLinenumbers 00 00 00 00            ; unused
    NumberOfRelocations  00 00                  ; unused
    NumberOfLinenumbers  00 00                  ; unused
    Characteristics     40 00 00 c0             ; initialized, readable, writeable

(padding)
    00 00 00 00 00 00       ; padding
    00 00 00 00 00 00
    00 00 00 00 00 00
    00 00 00 00 00 00

code section, starting at 0x1a0:
    6A 00                    ; push      0x00000000
    68 d0 01 10 00           ; push      offset _written
    6A 0D                    ; push      0x0000000d
    68 c0 01 10 00           ; push      offset hello_string
    6A F5                    ; push      0xfffffff5
    2E FF 15 28 02 10 00     ; call      dword ptr cs:__imp__GetStdHandle@4
    50                       ; push      eax
    2E FF 15 24 02 10 00     ; call      dword ptr cs:__imp__WriteConsoleA@20
    C3                       ; ret

data section, beginning at 0x1c0:
    68 65 6C 6C 6F 2C 20 77 6F 72 6C 64 0A  ; "hello, world\n"
    00 00 00                                ; padding to align _written
    00 00 00 00                             ; _written
padding:
    00 00 00 00 00 00 00 00 00 00 00 00     ; padding
IMAGE_IMPORT_DESCRIPTOR, starting at 0x1e0:
    OriginalFirstThunk      18 02 00 00     ; RVA to orig. 1st thunk
    TimeDateStamp           00 00 00 00     ; unbound
    ForwarderChain          ff ff ff ff     ; no forwarders
    Name                    08 02 00 00     ; RVA to DLL name
    FirstThunk              24 02 00 00     ; RVA to 1st thunk
terminator (0x1f4):
    OriginalFirstThunk      00 00 00 00     ; terminator
    TimeDateStamp           00 00 00 00     ;
    ForwarderChain          00 00 00 00     ;
    Name                    00 00 00 00     ;
    FirstThunk              00 00 00 00     ;
The DLL name, at 0x208:
    6b 65 72 6e 65 6c 33 32 2e 64 6c 6c 00  ; "kernel32.dll"
    00 00 00                                ; padding to 32-bit-boundary
original first thunk, starting at 0x218:
    AddressOfData   30 02 00 00             ; RVA to function name "WriteConsoleA"
    AddressOfData   40 02 00 00             ; RVA to function name "GetStdHandle"
                    00 00 00 00             ; terminator
first thunk, starting at 0x224:
    AddressOfData   30 02 00 00             ; RVA to function name "WriteConsoleA"
    AddressOfData   40 02 00 00             ; RVA to function name "GetStdHandle"
                    00 00 00 00             ; terminator
IMAGE_IMPORT_BY_NAME, at byte 0x230:
    01 00                                      ; ordinal, need not be correct
    57 72 69 74 65 43 6f 6e 73 6f 6c 65 41 00  ; "WriteConsoleA"
IMAGE_IMPORT_BY_NAME, at byte 0x240:
    02 00                                      ; ordinal, need not be correct
    47 65 74 53 74 64 48 61 6e 64 6c 65 00     ; "GetStdHandle"
(padding)
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; padding
    00
First unused byte: 0x260

--------------

Alas, this works on NT but didn't on windows 95. windows95 can't run
applications with a section alignment of 32 bytes, it needs an
alignment of 4 KB and, apparently, a file alignment of 512 bytes. So for
windows95 you'll have to insert a large number of 0-bytes (for padding)
and adjust the RVAs. Thanks go to D. Binette for testing on windows95.

        -- end of text --


Erreur

Pages web | Images | Audios | Vidéos tout le web en français
Pages web | Images | Audios | Vidéos tout le web en français
Pages web | Images | Audios | Vidéos tout le web en français
Pages web | Images | Audios | Videos tout le web en français

En savoir plus
Meta moteur
S'ABONNER EN LIGNE
Freebox
Free 50 heures
Free Accès Gratuit
Freebox
• WIFI / MIMO - Routeur
• Téléphonie
• Télévision
• Plateforme multimédia
Espace Abonnés
• Identifiez-vous
Plus d'infos
• Nos offres
• Le dégroupage
Assistance
• Documentations
• Faq

Erreur 404 - Document non trouvé

Page précédente

Linux
Protection de la dignité humaine | Protection de l'enfance | Signaler un contenu illicite | Nous contacter | © Free 2007


Accueil

Accueil CHM

Normal, dit comme ça,
vous ne risquez pas de comprendre.