Title: OOP - Writing Classes with MASM and ATC Author: Evil Homer Dated: April 22, 2004 The topic of Object-Oriented Programming is very common these days. Languages like VB, C and its variants are practically built on it. I find that OOP is useful because it's a way of writing code modules which are reusable without alteration in other projects, but there are many benefits to OOP, some of which I will remark apon and others that I won't cover simply because they are not within the scope of this document. OOP is based on the notion of a "Class", where a class can be described as a small set of functions which belong in the same group. A class is more than a way of grouping functions though. Defining a class in your program is in some ways similar to defining a structure ("a struct"). We have a line to "open" the class definition, then the stuff inside, then another line to "close" the class definition. Inbetween the "Opener" and the "Closer" is our list of function names that are owned by this class we are defining. We can however define something else more than just a bunch of functions in the class definition. We can also (just like a struct) define some Named and Typed DATA fields inside the class definition. This will make sense soon :) Having our class defined in our program is a lot like having a struct defined in a program - by itself, it doesn't do anything.. we have to actually USE it to benefit from it. In order to do so, we need to create "an INSTANCE of the class" which is sometimes called a "Class Object". We do this by using the NEW macro: mov pMyInstance, new (ClassName) The NEW macro returns a 32bit pointer to the class instance (aka "object"), which is actually a little chunk of Heap memory. We can think of this Pointer as our Key to that object, using that pointer we can call any of the class functions or read and write the data fields WITH RESPECT TO THAT INSTANCE. What's this? We can create as many instances of the class as we like, as long as we keep all the pointers to the instances. Each instance owns its OWN copy of the data fields, and class functions can (and often do) act with reference to a given instance of the class! Huh? Well, as a useful example, imagine we write a lot of little win32 programs and are sick to death of writing code to create and handle messages to various common controls. Let's write a class to encapsulate some common code for a particular control, which will make it easier for us to create any number of them we like and not have to worry about duplication of similar code etc, and also be able to reuse this class in other programs. For this example (and because I need it soon), I choose to encapsulate the basic ListBox control. Class names are often decorated with a capital C prefix, and I do observe this naming convention in general. Thus: class CListBox, ,C++ compatible virtual Create virtual MessageX virtual Show long hList endclass Note there are TWO commas in the "Opener" line. This class is not "inheriting" from any other class (more on that later), but if it was, the name of the "ancestor" class would go between the commas, filling the empty place there. We can see that we use the word "virtual" to declare a function name. There is another way to do this, more on that later. We can also see that this class is "C++ compatible" - the only other option is to declare it as "COM compatible" which we use for VB compatible stuff, not something we are doing today - so "C++ compatible" it is. It's what we can't see which is interesting here of course. This class contains one data member, "hList", which is a "long" - type variable, ie, a DWORD. This variable will hold the window handle for the ListBox control after it's been created. The entire set of Data variables defined in a class is owned by each instance of the class object we create, not defined once like the static data in the .data segment of our regular asm source. We can (and should) imagine a struct that contains just the data members of the class definition like so: CListBox struct hList DWORD ? CListBox ends !!! No need to actually DEFINE such a struct anywhere, just imagine it. In our case there's only one data member, but the same logic would be applied to a class with more data members. The reason why we should imagine this struct is because each time we create an instance of the class object, it gets created in Heap memory and looks like that struct. That's why each instance gets its own copy of the data members. The function definitions don't exist in instances, they get defined just ONCE in a static way in our exe, and are shared by all instances that we create. It is possible however, to create "static" class data members just like in C++ as well. Now let's look at the code for the Class Functions, otherwise known as "Methods". Just like in C++, each class has a "Constructor" method and a "Destructor" method. We ALWAYS need them, even if they don't contain active code. Constructor Method: We define a procedure using the naming convention ClassName_ClassName so since our class is called CListBox, the constructor method looks like this: CListBox_CListBox proc ret CListBox_CListBox endp Simple, huh? Our class does not need active code in the constructor method. If we wanted to allocate some other resources per instance of the class, we would put the allocation code in there. Destructor Method: Counterpart to the Constructor Method, it uses a different naming convention, which is ClassName_$ClassName, so we have: CListBox_$CListBox proc .if [ecx].CListBox.hList icall ecx, CListBox, MessageX, WM_CLOSE, NULL, NULL .endif ret CListBox_$ClistBox endp If we allocated anything in the Constructor, we should deallocate it in the Destructor, think of it as "CleanUp before I Die". In our case, our destructor checks to see if this instance's window has been created by checking whether the hList member is NULL. If hList is valid (not null), then we send the window a message to close. The destructor will automatically deallocate the instance from the heap without any code from us required, it's part of the "delete" macro. NOTES All data members are automatically preset to zero.If we want to preset any of their values to something else,we can do so in the constructor. When we use "iCall" to call a class method, the first parameter is a pointer to the instance apon which the method should act, which is generally known as "pThis", ie, a pointer to THIS instance of the class. The second parameter is the class name, the third param is the method name, and then comes the method's params themselves if any. When we use "pCall" to call a class method, we can use a simpler convention using the instance pointer, a dot, the method name, and its params if any. We call this the "Dot Calling Convention", it's a lot like VB calling convention. On entry to any method, ecx contains the instance pointer that was in the first parameter in the icall or pcall we made - ecx = pThis. I find it helpful to define a local variable in each method's procedure definition called something like "me" or "pThis", and immediately storing ecx into it before doing anything else, so that it can be used in calls to methods for the remainder of that procedure. When we create an instance using "new", the constructor gets called. When we destroy an instance using "delete", the destructor gets called. We have no say in this. Worse, THESE METHODS MUST NOT HAVE ANY PARAMETERS !!! This is VERY important, and a major difference to C++. Any other class methods can have any params they like, but the constructor and destructor have this limitation in ATC. To get around it, we simply define a separate method that has the input params that we desired for our constructor, and we call it immediately after creating the instance. for example: mov pInstance, new (MyClass) set pInstance as MyClass ;If we want to use pcall we have to tell atc what class pInstance is pcall MyClass.Create, HAIR_BLONDE or CLOTHES_OFF As for the class methods we defined in our class definition, they simply use the naming convention ClassName_MethodName. Our Create method uses the following params: hParent=Parent's window handle, in most cases =hWnd hInst=hInstance Style=Combined window styles we want to use dID=The Control ID value you want to associate with the window xs,ys= upper-left window coordinates xd,yd=width and height of new listbox window Therefore we might have: .data szListBox db "LISTBOX",0 .code CListBox_Create proc uses ebx pTitle:DWORD,hParent:DWORD, hInstance:DWORD, Style:DWORD, dID:DWORD,x1,y1,x2,y2 local me:DWORD local hLB:DWORD local rc:RECT mov me,ecx invoke CreateWindowEx, WS_EX_CLIENTEDGE, addr szListBox, pTitle, Style, x1,y1,x2,y2,hParent,dID, hInstance, NULL mov ecx,me mov [ecx].CListBox.hList, eax ret CListBox_Create endp May as well define the other functions too :) CListBox_MessageX proc umsg:DWORD,wp:DWORD,lp:DWORD invoke SendMessage,[ecx].CListBox.hList,umsg,wp,lp ret CListBox_MessageX endp CListBox_ShowWindow proc bMode:DWORD invoke ShowWindow, [ecx].CListBox.hList, bMode ret CListBox_ShowWindow endp As you can see, MessageX method is just a wrapper for the SendMessage api function, using the stored window handle for THIS object instance. Is this beginning to make sense? I hope so, because that's complete. Stitched together we get: ;================================================== ;CLISTBOX CLASS ;Author: Evil Homer ;Dated: April 22, 2004 .data szListBox db "LISTBOX",0 class CListBox, ,C++ compatible virtual Create virtual MessageX virtual Show long hList endclass .code CListBox_CListBox proc ret CListBox_CListBox endp CListBox_$CListBox proc .if [ecx].CListBox.hList icall ecx, CListBox, MessageX, WM_CLOSE, NULL, NULL .endif ret CListBox_$ClistBox endp CListBox_Create proc uses ebx pTitle:DWORD,hParent:DWORD, hInstance:DWORD, Style:DWORD, dID:DWORD,x1,y1,x2,y2 local me:DWORD local hLB:DWORD local rc:RECT mov me,ecx invoke CreateWindowEx, WS_EX_CLIENTEDGE, addr szListBox, pTitle, Style, x1,y1,x2,y2,hParent,dID, hInstance, NULL mov ecx,me mov [ecx].CListBox.hList, eax ret CListBox_Create endp CListBox_MessageX proc umsg:DWORD,wp:DWORD,lp:DWORD invoke SendMessage,[ecx].CListBox.hList,umsg,wp,lp ret CListBox_MessageX endp CListBox_ShowWindow proc bMode:DWORD invoke ShowWindow, [ecx].CListBox.hList, bMode ret CListBox_ShowWindow endp ;================================================== Now all we need to do to use this in our applications is to include it in the source, then we can create any number of instances using new(), followed in this case by a call to the Create method to actually create the window and store its handle. mov pListBox, new (CListBox) set pListBox as CListBox pcall pListBox.Create,CTEXT("Test List"),hWnd,hInstance,LBS_HASSTRINGS or LBS_NOINTEGRALHEIGHT or LBS_DISABLENOSCROLL,1234,0,0,100,300 pcall pListBox.MessageX,LB_ADDSTRING,0,CTEXT("It works") pcall pListBox.ShowWindow,SW_SHOW Have Phun :)