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.

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.
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:
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.)
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.
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.
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 PointThe 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_getxThis 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.
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.
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
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.
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:wordThe 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.
USES ax, cx, si, esBy 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:
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 pointSeveral 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 1000hYou'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.
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 p1That 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:getxActually, 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.
CALL si METHOD TPoint:getx USES cx, diFinally 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, 03hFrom 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, cxDespite 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.)
push ax push bp mov bp,sp mov word ptr [bp+02],0003 pop bp push cs call tpoint_setx nopYou 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.)
Before continuing with the next section, be sure you understand: