Chapter 14
Programming with Objects

Object-Oriented Programming with TASM

Object-oriented programming, or OOP, has become the mainstay of high-level languages such as C++ and Borland Pascal. Until recently, if you wanted to use OOP, you had to write code with one of those languages or with a less well-known application-development system such as Smalltalk or Actor.

Beginning with TASM 3.0, however, you can now write object-oriented programs in assembly language. Exactly why you might want to do that is one of the most difficult aspects of learning to use OOP, so before digging into TASM's object-oriented features, read the following sections for an overview of OOP and its value to programmers.

Why Use OOP?

In a nutshell, OOP makes it possible to write computer programs largely by constructing objects. An object is simply a structure that relates data and code (see Figure 14.1), collectively known as the object's members. The object encapsulates its members in one handy package.

Figure 14.1. An object is a structure that encapsulates data and code.

Here are a couple of key points about the object in Figure 14.1.

Programming with objects offers several advantages over conventional techniques--but there are also a few drawbacks that you need to consider. The following sections describe many of OOP's features, advantages, and disadvantages.

Advantages of OOP
To understand the value of objects, consider how most programmers write conventional code. First, they define the program's data by reserving storage for bytes, words, and other structures. Then, they write subroutines to operate on that data. Or, they write statements that pass data to subroutines, or that pass addresses in registers or that push values onto the stack for a subroutine to use.

There is nothing wrong with this conceptual model for writing computer programs. But when programs grow beyond the moderately complex stage, one part of a program might inadvertently change data that another part requires, causing buggy twists and turns in the program's execution that can be difficult to unravel.

Even top-notch programmers are surprised to discover how easy it is to create such tangles. For example, you might define a global count variable, which you use in a loop that cycles a specified number of times. If that loop calls another subroutine, which calls other subroutines--a common situation--the danger exists that a statement somewhere deep inside the program might also use count for its own purposes. This critical but easily missed error results in a buggy loop that modifies its own controlling parameter and causes the program to fail.

Object-oriented programming can help prevent these kinds of conflicts. Because objects encapsulate code and data, the use of data is restricted to a defined set of subroutines. Encapsulation offers programmers two distinct advantages:

Disadvantages of OOP
Despite its rosy prospects, OOP has a few drawbacks. It is initially more difficult to design an object-oriented application. If you are the kind of programmer who, when freshly inspired by a great new idea can't wait to start typing instructions, OOP might be the wrong programming model for you. With OOP, careful planning is essential to achieving reliable results.

OOP tends to be of more value in large programs than in small ones. The sample listings in this chapter, for example, might seem to use overly complex methods for relatively simple operations. If you write medium to small programs, OOP might increase your code's complexity. (Even small programs, however, can often use libraries of existing objects advantageously.)

OOP and Turbo Assembler
Turbo Assembler's OOP features resemble those in Pascal and C++, although there are some important differences that I'll describe in this chapter. In assembly language, for instance, it is your responsibility to construct various tables, pointers, and to perform operations such as loading registers that are automatic in other languages.

It is also easier to get into trouble with OOP in Turbo Assembler than it is in other languages, which have built-in safeguards that can prevent mistakes. For example, C++ and Borland Pascal compilers can verify that statements use the correct types of objects. In assembly language, all bets are off and it's relatively simple to break OOP's rules (as it is to break conventional programming's rules).

One other disadvantage of OOP in Turbo Assembler is that object instances (that is, variables of a certain object data type) are incompatible with C++ classes and Pascal objects. If you intend to combine assembly and high-level OOP code, it is probably best to use the high-level language to construct your object-oriented modules. See Chapter 13, "Mixing Assembly Language with C and C++" for suggestions about mixing assembly language to high-level C++ OOP.


Note: Turbo Debugger's object-oriented commands (View|Hierarchy, for example) do not recognize TASM objects. You may inspect object instances in Turbo Debugger, but they are shown as structures, not objects.
Despite these drawbacks of using OOP in Turbo Assembler, there are many good reasons for selecting this programming model to write assembly language applications. As I suggested, OOP is tailor-made for large applications, especially those written by programming teams. Also, debugging, maintenance, and future revisions are potentially simpler due to OOP's design.

Another good reason to use OOP is to convert an existing high-level C++ or Pascal object-oriented program into pure assembly language. If you need to convert high level OOP code to assembly language, TASM's OOP features will greatly simplify the conversion.

OOP on Its Own Terms
Like all technologies, OOP comes with its own terms, many of which you will encounter in this chapter. Scan these terms now to become familiar with them, but don't be concerned if some of the concepts are unclear.


Note: The following glossary also explains differences and similarities between C++, Borland Pascal, and Turbo Assembler's OOP terminologies. Turbo Assembler's terms more closely resemble those used in Borland Pascal than in C++.
Base object
An object that is used to derive another object. The derived object inherits the properties of the base object. More than one derived object may inherit the properties of the same base object. For example, a graphics program might declare a general-purpose object TGraphics, and then use that object as a base to derive special-purpose objects such as TCircle and TRectangle. The base object provides data and code that are common to all related objects. The derived objects add data and code that are specific to their needs. Any object may be a base object. See also Derived object.

Class
The C++ term for Object as used in Turbo Assembler and Borland Pascal. See Object.

Constructor
A special method that initializes an object instance. Turbo Assembler does not support the concept of a constructor, although as I show in this chapter, you can program its equivalent. (In C++, constructors can be called automatically. In assembly language, it is your responsibility to call an object's constructor.)

Derived object
An object that inherits the properties (data and code) of another base object. A derived object may be used as a base object from which another object may be derived (see Base object). The collection of base and derived objects in an object-oriented program creates a hierarchy of related objects. Typical OOP code consists of many such object hierarchies. In Turbo Assembler, a derived object may inherit the properties of only one base object (see also Single and Multiple inheritance.)

Destructor
A special method that is used to destroy an object instance. Turbo Assembler does not support the concept of a destructor.

Encapsulation
The process of relating data and code in an object. Although not required to do so, an object's code (that is, its assembly language subroutines) usually performs some operation on or with the object's encapsulated data. Encapsulation restricts the use of data to a defined set of subroutines, which can simplify debugging, maintenance, and revisions.

Inheritance
The contents of an object that is derived from another object. The derived object inherits the base object's data and code. By using inheritance, you can enhance existing objects quickly and easily. See also Base object, Derived object, Single inheritance, and Multiple inheritance.

Instance
Storage for an object. Also called an Object instance. An instance of an object is similar to a variable of a data type such as a byte or a word. In Turbo Assembler, you define instances using the same syntax as for structures. (An instance is equivalent to a C++ class object.)

Member
Any component of an object. A method, for example, is a member of an object. A variable in an object is a data member.

Method
Another term for an object's subroutines. See also Static method and Virtual method. (A method is equivalent to a C++ member function.)

Multiple inheritance
A feature of some OOP languages that permits deriving new objects, using inheritance, from more than one base object. TASM does not support multiple inheritance (see Single inheritance).

Object
A special structure that relates data and code. It's important to understand that an object is merely a source-code description of related data and code. Objects exist solely in the program text; they do not exist at runtime. To use an object in a program, you must create an instance of it similar to the way you create variables of other data types such as bytes and words. (An object in Turbo Assembler is equivalent to a C++ class.)

Object instance
Same as Instance.

Polymorphism
The process by which an object instance can determine an action to be performed on or for that object. The action is implemented as a virtual method. A pointer (the ds:si registers, for example) might address a instance of a graphics object derived from a common base. Calling that instance's virtual Draw method draws a circle if the pointer addresses a Circle instance, or a rectangle if the pointer addresses a Rectangle instance. The correct function is selected at runtime without the program explicitly stating the object's type in a call instruction. With polymorphism, you modify the actions of existing code by writing new objects and virtual methods. See also Virtual method.

Single inheritance
The technique of building a derived object from a single base object. All OOP languages, including Turbo Assembler, support single inheritance. See also Multiple inheritance.

Static method
An object's subroutine. Calls to static methods are identical to calls to non-object-oriented subroutines. The addresses of static methods are bound into call instructions at link time.

Virtual method
An object's subroutine. Calls to virtual methods are made indirectly to addresses stored in an object's virtual method table. The addresses of virtual methods are bound into the call instruction at run time. See also Polymorphism.

Virtual method table (VMT)
A table of virtual method addresses. Every object that has one or more virtual methods must have an associated virtual method table. It is your responsibility to create this table and to insert and initialize a pointer to the VMT in every object instance.
Fundamentals of TASM Objects
To learn how to use OOP in Turbo Assembler programs, you need to master three fundamental techniques. These are: You also need to learn how to combine those techniques using polymorphism to create objects that can determine their own actions. The rest of this chapter is devoted to these topics. I'll first explain the techniques of encapsulation, inheritance, and virtual methods in general terms, and then show how to implement those techniques using Turbo Assembler objects. Finally in this chapter, I'll explain how to create and use a list object that demonstrates the wonderful world of programming with polymorphism.


Note: Borland's user guide suggests using Ideal mode for object-oriented programs, but for unexplained reasons, all examples in the guide and on disk use MASM mode. Worse, many of the printed examples contain mistakes and do not work correctly. Needless to say, these facts have prevented many assembly language programmers from using TASM's object-oriented features. All example programming in this chapter uses Ideal mode. Because there is no official documentation on Ideal mode and OOP, I derived most of the syntax and example programs in this chapter by experimentation.
Encapsulation
Objects are similar to structures created with the STRUC directive. In case you need a refresher course on using assembly language structures, following is a quick review.

A STRUC associates multiple variables under a single name. For example, to create a STRUC named Point, you can use a declaration such as this:

STRUC Point
  x dw ?
  y dw ?
ENDS  Point
The declaration creates a structure named Point that contains two word variables, x and y. The structure is merely a description of a data type--it does not occupy any space at runtime. To use the structure, you must define a variable of its type. For example, you might insert these instructions in a data segment:
DATASEG
p1  Point  <>
p2  Point  <45, 68>
The first line starts the data segment. The second line defines a variable p1 of the Point structure--in other words, p1 is a memory space that consists of two word variables named p1.x and p1.y. The third line also defines a variable p2 of the Point structure. In addition, the third line initializes its two word variables to 45 and 68, respectively.

You create objects using a special form of the STRUC directive. Actually, objects are structures--but in addition to containing data, an object also specifies subroutines, called members, that usually operate on or with that data. Typically, some of those members assign values to the object's data. Other members might return the data's values. Members can perform additional tasks as well.

Following is a sample object, TPoint, that declares four methods: two for changing the object's x and y variables, and two for returning those values:

STRUC TPoint METHOD {
  getx:dword = TPoint_getx
  gety:dword = TPoint_gety
  setx:dword = TPoint_setx
  sety:dword = TPoint_sety
  }
  x  dw  ?
  y  dw  ?
ENDS  TPoint
Compare this STRUC with the non-object-oriented Point structure. The keyword METHOD tells the assembler that this structure specifies the names of subroutines to be associated with the object. Subroutine declarations in braces follow the METHOD keyword. Each declaration is in the form:

getx:dword = TPoint_getx
This states that the object has an associated method named getx, and that the address of that method is to be stored in a dword (32-bit) pointer. (Small memory model programs may use a word offset in place of dword.) The method pointer (getx) is initialized to the address of the actual subroutine (TPoint_getx), which you must write somewhere in the program using the PROC directive as you do for other subroutines (of course, a complete example would have additional instructions):
PROC    TPoint_getx PASCAL
        ret
ENDP    TPoint_getx
The naming convention that I use is arbitrary, but works well. I begin object names with T, which indicates the object is a data Type. The method name (getx for example) describes the purpose of the object's subroutine--in this case, to get the value of the object's x variable. The actual subroutine name in the PROC directive combines the object name, an underscore, and the method name (TPoint_getx). These conventions help me to recognize the relationships among objects, methods, and subroutines.

The other TPoint object methods--gety, setx, and sety--are declared similarly. Each is a dword pointer initialized to the address of an actual subroutine implemented elsewhere.

After the object's methods are any associated variables, in this case, two uninitialized words, x and y. Instances (that is, variables) of the TPoint object consist of those two words, just as in a common structure. Use the TPoint object as you would any structure. These statements, for example, define two TPoint instances:

p1  TPoint  <>
P2  TPoint  <12, 34>
It is important to understand that the TPoint object's methods are not stored in the object itself. The object merely associates code and data--it doesn't actually store code and data in the same place. The preceding two instances p1 and p2 occupy four bytes each--exactly enough room for each instance's two word variables, x and y.


Note: The preceding paragraph will make better sense if you think of objects as data types similar to those built into assembly language--bytes and words, for example. A byte is a data type, which merely describes the nature and size of a kind of information. To use a byte, you must define a variable of that type using the DB (define byte) directive. Operations such as addition and subtraction that you can perform on bytes aren't stored inside the byte variables. Those operations are instead written as subroutines or instructions to which you pass byte values. The difference in object-oriented programming is that, rather than pass data to subroutines, you call methods for object instances. In that sense, the instance "knows" how to perform operations on itself.
These facts lead to an important observation: objects and structures are really one and the same. They differ, however, in how you use them. You use structures as you do any other variables, but with objects, you call methods to operate on instance data. To help you understand how this works, the next two listings flesh out the full TPoint object.

Listing 14.1, TPOINT.INC, shows how to declare and implement a Turbo Assembler object. The file is stored in the OOP\ENCAPSUL directory. (All programs in this chapter are similarly stored in their own directories.) The module is designed to be included into a program with the INCLUDE directive, so don't attempt to assemble it just yet. Later, I'll explain how do that. Scan TPOINT.INC now, then turn to the line-by-line discussion following the listing.


Note: Borland suggests storing object declarations in files ending with the extension .ASO (for assembly language object). I use .INC instead because my text editors are programmed to recognize that filename extension. You can name your object module files using any other extension if you want.

Listing 14.1. oop\encapsul\TPOINT.INC.
 1: %TITLE "TPoint object -- by Tom Swan"
 2:
 3: GLOBAL TPoint_getx:PROC
 4: GLOBAL TPoint_gety:PROC
 5: GLOBAL TPoint_setx:PROC
 6: GLOBAL TPoint_sety:PROC
 7:
 8: STRUC TPoint METHOD {           ; Begin TPoint object declaration
 9:   getx:dword = TPoint_getx      ; Return object's x data
10:   gety:dword = TPoint_gety      ; Return object's y data
11:   setx:dword = TPoint_setx      ; Change object's x data
12:   sety:dword = TPoint_sety      ; Change object's y data
13:   }                             ; End of method declarations
14:   x  dw  ?                      ; Object's x data
15:   y  dw  ?                      ; Object's y data
16: ENDS  TPoint                    ; End TPoint object declaration
17:
18: CODESEG
19:
20: %NEWPAGE
21: ;---------------------------------------------------------------
22: ; TPoint_getx           TPoint getx method
23: ;---------------------------------------------------------------
24: ; Input:
25: ;       ds:si = instance address
26: ; Output:
27: ;       ax = instance.x data
28: ; Registers:
29: ;       ax
30: ;---------------------------------------------------------------
31: PROC    TPoint_getx PASCAL
32:         mov     ax, [(TPoint PTR si).x] ; Move instance x data into ax
33:         ret                             ; Return to caller
34: ENDP    TPoint_getx
35: %NEWPAGE
36: ;---------------------------------------------------------------
37: ; TPoint_gety           TPoint gety method
38: ;---------------------------------------------------------------
39: ; Input:
40: ;       ds:si = instance address
41: ; Output:
42: ;       ax = instance.y data
43: ; Registers:
44: ;       ax
45: ;---------------------------------------------------------------
46: PROC    TPoint_gety PASCAL
47:         mov     ax, [(TPoint PTR si).y] ; Move instance y data into ax
48:         ret                             ; Return to caller
49: ENDP    TPoint_gety
50: %NEWPAGE
51: ;---------------------------------------------------------------
52: ; TPoint_setx           TPoint setx method
53: ;---------------------------------------------------------------
54: ; Input:
55: ;       ds:si = instance address
56: ;       x (word) parameter
57: ; Output:
58: ;       none
59: ; Registers:
60: ;       ax
61: ;---------------------------------------------------------------
62: PROC    TPoint_setx PASCAL
63:         ARG     @@x:word                ; Create stack offset to param x
64:         USES    ax                      ; Preserve ax (optional)
65:         mov     ax, [@@x]               ; Move x param into ax
66:         mov     [(TPoint PTR si).x], ax ; Move x param into instance.x
67:         ret                             ; Return to caller
68: ENDP    TPoint_setx
69: %NEWPAGE
70: ;---------------------------------------------------------------
71: ; TPoint_sety           TPoint sety method
72: ;---------------------------------------------------------------
73: ; Input:
74: ;       ds:si = instance address
75: ;       y (word) parameter
76: ; Output:
77: ;       none
78: ; Registers:
79: ;       ax
80: ;---------------------------------------------------------------
81: PROC    TPoint_sety PASCAL
82:         ARG     @@y:word                ; Create stack offset to param y
83:         USES    ax                      ; Preserve ax (optional)
84:         mov     ax, [@@y]               ; Move y param into ax
85:         mov     [(TPoint PTR si).y], ax ; Move y param into instance.y
86:         ret                             ; Return to caller
87: ENDP    TPoint_sety

Lines 8-16 declare the TPoint object, which has four methods and two variables. The module also has four GLOBAL statements at lines 3-6, which publish method subroutine names such as TPoint_getx so other modules can call them.


Note: When used to export a symbol as done here for TPoint's methods, GLOBAL is interpreted as a PUBLIC directive. When used to import a symbol, as might be done by another module that needs to use the TPoint object, GLOBAL is interpreted as an EXTRN directive. You could use PUBLIC and EXTRN directives with object methods, but the dual-purpose GLOBAL directive is more convenient.
After these declarations, at line 18 the module begins or continues the program's code segment. Following that are the object's method implementations--in other words, its subroutines, which are stored along with the program's other code. The TPoint_getx method, for example, is implemented as a subroutine at lines 31-34.

This subroutine has only two instructions. Line 32 moves the value of an object instance's x variable into the ax register. Line 33 returns to the method's caller. As this part of the listing demonstrates, you write object methods the same way you write conventional subroutines.

There is, however, one major difference between TPoint_getx and conventional code. Like all methods, TPoint_getx must be called in reference to an instance of the TPoint object. By convention, registers ds:si address this instance.

Line 32, for example, obtains the value of the instance's x variable by addressing the object instance with ds:si. Carefully examine the syntax in this line--it differs from the syntax in Borland's User Guide, which doesn't explain how to use Ideal mode with TASM's OOP features. You must use parentheses around the subexpression (TPoint PTR si) so that the assembler treats this as a unit. You also must tell the assembler the type of object addressed by ds:si (TPoint in this example). Finally, you must include a PTR directive to indicate an indirect reference to memory.


Note: Calling TPoint_getx requires a special form of the call instruction provided by the directive CALL...METHOD that is unique to Turbo Assembler. Following the next listing, I'll explain how to use this directive.
The next method in TPOINT.INC, TPoint_gety, is identical to TPoint_getx but returns the value of an object instance's y variable (see lines 46-49).

Two more methods, TPoint_setx and TPoint_sety, complete the implementation of TPoint's methods. The method at lines 62-68 demonstrates how to receive arguments passed by instructions that call the method. In this case, TPoint_setx requires its caller to pass a 16-bit word of data to store in an object instance's x variable (line 63).

You may pass information to methods using any technique you wish in a register, for example, as a global variable, or on the stack. The demonstration method uses a stack argument, declared as:

ARG @@x:word
The directive tells the assembler to calculate the offset into the stack of a 16-bit word parameter, and to give that offset the name @@x. You may use any name you want--because of its local-symbol preface (@@), the symbol is limited for use in the current PROC. This means that another PROC may define an argument named @@x without conflicting with this one.


Note: When using ARG, it is important to select a consistent language in addition to the memory model. All methods in the TPoint object (and others in this chapter) use the PASCAL model, which makes the called subroutines responsible for cleaning up their own stack frames.
Following the ARG directive, TPoint_setx also tells the assembler that it uses the ax register (line 64). The USES directive automatically inserts push and pop instructions to save and restore registers. You don't have to use USES, but it's convenient for ensuring that a subroutine saves and restores critical registers. Separate multiple registers with commas as in:
USES  ax, cx, si, es
By virtue of the ARG directive, it's a simple matter to refer to arguments passed on the stack. For example, to load the value of the x argument into ax, the subroutine executes this instruction at line 65:
mov  ax, [@@x]
Line 66 then stores that value in the object instance's x variable. The TPoint_sety method at lines 81-87 resembles TPoint_setx, but inserts a 16-bit argument into an object instance's y variable.

The next step is to use the TPoint object by including its module in a host program. Using an object involves three key techniques:

Listing 14.2, ENCAPSUL.ASM, demonstrates these techniques. You may now assemble the program, which includes the TPOINT.INC module. Change to the OOP\ENCAPSUL directory, and type make to assemble and link the program. Or, you can enter the following two instructions. Either way, be sure to add debugging information to the ENCAPSUL.EXE program, which, like many of this book's example programs, doesn't produce any on-screen output. You need to use Turbo Debugger, as described after the listing, to investigate how the program works. tasm /zi encapsul

tlink /v encapsul

Listing 14.2. oop\encapsul\ENCAPSUL.ASM.

 1: %TITLE "TPoint object demonstration -- by Tom Swan"
 2:
 3:         IDEAL                   ; Select Ideal mode syntax
 4:
 5:         JUMPS                   ; Enable auto-conditional jumps
 6:
 7:         LOCALS  @@              ; Enable block-scoped labels
 8:
 9:         MODEL   large, PASCAL   ; Select a memory model and language
10:
11:         STACK   1000h           ; Allocate program stack
12:
13:         INCLUDE "tpoint.inc"    ; Include TPoint object module
14:
15:         DATASEG                 ; Start of data segment
16:
17: exCode  DB      0               ; Program exit code
18:
19: ;----- Define TPoint instances
20:
21: p1      TPoint  <>              ; Default TPoint instance
22: p2      TPoint  <01h, 02h>      ; Initialized TPoint instance
23:
24:         CODESEG                 ; Start of code segment
25:
26: Start:
27:         mov     ax, @data       ; Initialize DS to address
28:         mov     ds, ax          ;  of data segment
29:
30: ;----- Call TPoint methods
31:
32:         mov     si, offset p1               ; Address instance with ds:si
33:         CALL    si METHOD TPoint:getx       ; Call object method
34:
35:         mov     si, offset p2               ; Address instance with ds:si
36:         CALL    si METHOD TPoint:gety       ; Call object method
37:
38: ;----- Pass literal arguments to methods
39:
40:         mov     si, offset p1               ; Address instance with ds:si
41:         CALL    si METHOD TPoint:setx, 03h  ; Pass argument to method
42:
43:         mov     si, offset p1               ; Address instance with ds:si
44:         CALL    si METHOD TPoint:sety, 04h  ; Pass argument to method
45:
46: ;----- Pass register arguments to methods
47:
48:         mov     si, offset p2               ; Address instance with ds:si
49:         mov     dx, 05h                     ; Load argument into dx
50:         CALL    si METHOD TPoint:setx, dx   ; Pass dx on stack to method
51:
52:         mov     si, offset p2               ; Address instance with ds:si
53:         mov     cx, 06h                     ; Load argument into cx
54:         CALL    si METHOD TPoint:sety, cx   ; Pass cx on stack to method
55:
56: Exit:
57:         mov     ah, 04Ch        ; DOS function: Exit program
58:         mov     al, [exCode]    ; Return exit code value
59:         int     21h             ; Call DOS.  Terminate program
60:
61:         END     Start           ; End of program / entry point
Several directives are required at the beginning of an object-oriented assembly language program. You can experiment with variations on the types and numbers of directives, but I've found these to work best in most cases:
IDEAL
JUMPS
LOCALS @@
MODEL large, PASCAL
STACK 1000h
You'll find these same directives in other listings in this chapter (see lines 3-11 in Listing 14.2). The first line selects Turbo Assembler's Ideal mode. In addition to its other benefits (discussed elsewhere in this book), Ideal mode makes a structure's symbols local to that structure. In MASM mode, a structure's symbols are global and must be unique throughout the entire program. This is why Ideal mode requires GLOBAL directives, but despite this added complication, local structure symbols simplify programming by eliminating possibly conflicts among different structures.

The JUMPS directive enables automatic conditional jumps, making it possible for the assembler to generate more efficient code. The LOCALS directive declares @@ as the local-symbol prefix. You will use many local symbols in OOP, and the use of a local prefix will prevent conflicts that would probably arise if you declared symbols such as @@x and @@y globally. Also, some OOP directives generate code that requires this local-symbol preface.

The MODEL directive in this example (line 9) selects the large memory model. Because object-oriented programs tend to be large, this is usually the correct model to use. It is possible, however, to write small and huge memory-model OOP code as I'll explain later in this chapter, but the addressing details in small-model code can be tricky. For best results, use the large model until you know your way around.


Note: The MTA.LIB library on the book's disk is assembled for the small memory model. If you link an object-oriented large-model program to this library, you must first create large-model versions of all library modules by editing the MODEL directives. For example, to create a large-model version of the STRINGS module, change the MODEL to large in STRINGS.ASM, then reassemble and insert the module in MTA.LIB using the supplied MAKEFILE on disk.
The MODEL directive at line 9 also specifies the PASCAL language. This does not mean the program is written for Pascal. It merely changes the code inserted by the assembler for the PROC and ENDP directives. With the PASCAL language model, you declare and pass arguments on the stack in the same order. For example, if a method requires x and y arguments, you must declare and pass them in that order. In addition, the PASCAL model causes the assembler to delete all arguments from the stack by inserting a special form of the ret instruction that adjusts the stack pointer, sp. Other models (the C model, for example) require the caller to a subroutine to clean up the stack. Generally, this is inconvenient, and because OOP code tends to use lots of arguments passed to methods, PASCAL is the best choice.

Finally, the program defines a stack (line 11). Again, because of the heavy use of stack arguments in OOP code, a larger than normal stack may be required. I used 1000h for all programs in this chapter. You may have to increase this value in large programs with many objects.

To use the TPoint object, the sample program includes the TPOINT.INC module (line 13). If your program uses more than one object, it should include all modules at this location.

Following those steps, the sample program defines global variables, two of which are object instances. First, a DATASEG directive at line 15 begins the program's global data segment. The exit code variable at line 17 is the same as used in most of this book's programs. Lines 21-22 demonstrate two ways to define object instances.

The first line (21) creates an instance of the TPoint object named p1. Because the angle brackets are empty in this statement, the values of the instances x and y variables are uninitialized. When viewed in Turbo Debugger, they are set to zero, but in the program's normal use, they might equal any value left over in memory.

The second line (22) defines another object instance, but specifies initial values for the instance's variables. This line creates an instance with x set to 01h and y set to 02h.

The program next demonstrates how to address object instances and how to call object methods. There's one vital rule to memorize: you must call an object method in reference to an object instance. In other words, you never call methods out of context; instead, you must specify an object instance on which that method operates.

There are many ways to address object instances--you could pass their addresses as stack variables or you could address them using any combination of registers you choose. Register addressing is probably best, and for consistency, it's a good idea to use the same registers throughout the program to address all object instances. By convention, I use ds:si.

Because the sample program's instances are in the data segment, register ds is already initialized by the preparatory instructions at lines 27-28. Only one other step is required to address instance p1:

mov  si, offset p1
That instruction moves the offset address of instance p1 into si. Now, ds:si properly address a TPoint object instance, and the program can call any of that object's methods to perform operations on or for that instance. For example, to call the TPoint_getx method, which returns the instance's x variable, line 33 executes this special form of the call instruction:

CALL  si METHOD TPoint:getx
Actually, that's not an assembly language instruction--it's a CALL...METHOD directive, which is unique to Turbo Assembler. To distinguish the directive from common subroutine calls, I type it in uppercase, but you can use lowercase if you prefer. The CALL...METHOD directive's syntax is somewhat complex:

CALL <instance_ptr> METHOD {<object_name>:}
  <method_name> {USES
{segreg:}offsreg}{<extended_call_parameters>}
The first element, <instance_ptr>, can be the address of an object or a reference to a register. Because I always address instances with ds:si, I insert si between the CALL and METHOD keywords. This satisfies the syntax, but in this case, the register isn't otherwise used. (Later in this chapter, when you investigate virtual methods, this part of the CALL...METHOD syntax becomes more important.)

Next, CALL...METHOD permits you to specify an object name. Always do this. You must refer to an object by name (especially in Ideal mode) in order to also refer to any of that object's members. In this case, you need to insert the name of a method you want to call--the TPoint:getx method, for example, as demonstrated in line 33.


Note: Specify method names in CALL...METHOD statements by typing the object name, a colon, and the method name. Do not use the actual subroutine name. For example, as line 33 shows, TPoint:getx is the correct way to refer to the getx method in the TPoint object. The actual subroutine is named TPoint_getx in the TPOINT.INC module.
As the CALL...METHOD syntax indicates, you can specify a USES clause in the directive to preserve any registers that the method changes. I prefer to make the methods themselves preserve all registers except those used to pass back information to callers, so I rarely insert USES in CALL...METHOD statement. If you want to use this option, however, type it like this:
CALL  si METHOD TPoint:getx USES cx, di
Finally in a CALL...METHOD instruction, you may list any arguments to be pushed onto the stack. These arguments may be literal values, memory references, or registers. Regardless of form, however, they are always passed on the stack. For example, to pass the value 03h to the TPoint object's setx method, line 41 uses the instruction:
CALL  si METHOD TPoint:setx, 03h
From that directive, Turbo Assembler generates instructions to push 03h onto the stack. The setx method, as I explained for the TPOINT.INC module, uses an ARG directive to access that argument.

You can also pass register values to methods. For example, you can move a value into cx (or another register) and pass that value with the instruction:

mov   cx, 04h
CALL  si METHOD TPoint:setx, cx
Despite appearances, however, the second line does not pass a value in cx to the setx method. It pushes cx's value onto the stack, and the method still must use an ARG directive to access that value. (See also lines 48-50 for another example of passing a register value to a method.)
Note: Methods may use values passed in registers. If you specify those registers as CALL...METHOD arguments, however, they still will be pushed onto the stack, and you must declare an ARG directive for those arguments. This enables Turbo Assembler to generate a return instruction that deletes the pushed argument bytes by adjusting the stack pointer. If you don't use ARG, the stack will overflow, and you should check that all methods specify ARG directives for every argument in CALL..METHOD directives.
It is highly instructive at this point to run the ENCAPSUL demonstration program in Turbo Debugger. Follow these suggestions to investigate how the program works:
  1. Change to the OOP\ENCAPSUL directory, and type make to create the ENCAPSUL.EXE code file if you haven't done so already. Enter td encapsul to start Turbo Debugger and load the demonstration program.

  2. Use the arrow keys to move the flashing cursor up to the p2 instance, and press Ctrl+W to add it to the Watches window. Do the same for p1. The Watches window should now have two TPoint entries. Notice that they are shown as "struc" variables, which in reality is what object instances are. Notice also that p2's x and y variables are initialized to the values in angle brackets in the instance's definition.

  3. Press F7 three times to execute the instructions that initialize ds and that address p1 with ds:si. Press F7 again to execute the first CALL...METHOD instruction. The display changes to the TPOINT.INC module, and the cursor is poised at the mov instruction in the TPoint_getx method.

  4. Press Alt+VR to bring up the Registers window, then press F7 to execute the mov instruction. Notice that ax changes to the value of the addressed instance's x variable. Press F7 again to execute the method's ret instruction, which ends this CALL...METHOD.

  5. Press F7 four more times to execute the next CALL...METHOD, and observe the use of modified registers, which Turbo Debugger highlights. These steps return the y variable value for the p2 instance.

  6. The program is now paused at the instruction that moves the offset of instance p1 to si. Press F7 to execute that instruction. Registers ds:si now address the p1 instance.

  7. Before executing the next CALL...METHOD, open the CPU window (press Alt+VC and hit F5 to expand the window to full screen). You will find instructions that look something like these:

    push   ax 
    push   bp 
    mov    bp,sp 
    mov    word ptr [bp+02],0003 
    pop    bp 
    push cs 
    call   tpoint_setx 
    nop
    You are viewing the actual instructions that Turbo Assembler generates for the CALL...METHOD command (the one at line 41 in the listing). The first five instructions "punch a hole" in the stack, creating a space for the argument to be passed to the method. The push cs instruction simulates a far call, after which, a near call performs the actual call to the method subroutine. The nop is a placeholder, left over from the optimization that TASM performs to convert far calls to efficient push cs and near call instructions. This nop wastes a byte, but the end result is faster than the equivalent far call. (The assembler makes this modification for all far subroutine calls, not only for object-oriented CALL...METHOD directives.)
Use Turbo Assembler's F7 key to run the remaining instructions. You may do this while viewing the Module or CPU windows. In the Module window, you execute CALL...METHOD and other instructions as individual commands, even though as you have seen, they might actually contain multiple steps. In the CPU window, you execute those steps individually. Try running the program both ways to further investigate how it works. Press Alt+X to exit the debugger.

Before continuing with the next section, be sure you understand: