Control issues

This chapter describes in detail the various ways that Delphi implements program control. Included are calling conventions and exit procedures.

Calling conventions

Parameters are transferred to procedures and functions via the stack. Before calling a procedure or function, the parameters are pushed onto the stack in their order of declaration. Before returning, the procedure or function removes all parameters from the stack.

The skeleton code for a procedure or function call looks like this:

PUSH Param1

PUSH Param2

.

.

.

PUSH ParamX CALL ProcOrFunc

Parameters are passed either by reference or by value. When a parameter is passed by reference, a pointer that points to the actual storage location is pushed onto the stack. When a parameter is passed by value, the actual value is pushed onto the stack.

Variable parameters

Variable parameters (var parameters) are always passed by referencea pointer that points to the actual storage location.

Value and constant parameters

Value parameters are passed by value or by reference depending on the type and size of the parameter. In general, if the value parameter occupies 1, 2, or 4 bytes, the value is pushed directly onto the stack. Otherwise a pointer to the value is pushed, and the procedure or function then copies the value into a local storage location.

The 8086 does not support byte-sized PUSH and POP instructions, so byte-sized parameters are always transferred onto the stack as words. The low-order byte of the word contains the value, and the high-order byte is unused (and undefined).

An integer type or parameter is passed as a byte, a word, or a double word, using the same format as an integer-type variable. (For double words, the high-order word is pushed before the low-order word so that the low-order word ends up at the lowest address.)

A Char parameter is passed as an unsigned byte.

A Boolean parameter is passed as a byte with the value 0 or 1.

An enumerated-type parameter is passed as an unsigned byte if the enumeration has 256 or fewer values; otherwise, it is passed as an unsigned word.

A floating-point type parameter (Real, Single, Double, Extended, and Comp) is passed as 4, 6, 8, or 10 bytes on the stack. This is an exception to the rule that only 1-, 2-, and 4-byte values are passed directly on the stack.

A pointer-type, class-type, or class-reference-type parameter is passed as two words (a double word). The segment part is pushed before the offset part so that the offset part ends up at the lowest address.

A string-type parameter is passed as a pointer to the value.

For a set type parameter, if the bounds of the element type of the set are both within the range 0 to 7, the set is passed as a byte. If the bounds are both within the range 0 to 15, the set is passed as a word. Otherwise, the set is passed as a pointer to an unpacked set that occupies 32 bytes.

Arrays and records with 1, 2, or 4 bytes are passed directly onto the stack. Other arrays and records are passed as pointers to the value.

A global procedure pointer type is passed as a pointer.

A method pointer type is passed as two pointers. The instance pointer is pushed before the method pointer so that the method pointer ends up at the lowest address.

Open parameters

Open string parameters are passed by first pushing a pointer to the string and then pushing a word containing the size attribute (maximum length) of the string.

Open array parameters are passed by first pushing a pointer to the array and then pushing a word containing the number of elements in the array less one.

When using the built-in assembler, the value that the High standard function returns for an open parameter can be accessed by loading the word just below the open parameter. In this example, the FillString procedure, which fills a string to its maximum length with a given character, demonstrates this.

procedure FillString(var Str: OpenString; Chr: Char); assembler; asm

LES

DI,Str

{ ES:DI = @Str }

MOV

CX,Str.Word[-2]

{ CX = High(Str) }

MOV

AL,CL

CLD

STOSB

{ Set Str[0] }

MOV

AL,Chr

REP

STOSB

{ Set Str[1..High] }

end;

Function results

Ordinal-type function results are returned in the CPU registers: Bytes are returned in AL, words are returned in AX, and double words are returned in DX:AX (high- order word in DX, low-order word in AX).

Real-type function results (type Real) are returned in the DX:BX:AX registers (high- order word in DX, middle word in BX, low-order word in AX).

80x87-type function results (type Single, Double, Extended, and Comp) are returned in the 80x87 coprocessor's top-of-stack register (ST(0)).

Pointer-type, class-type, and class-reference-type function results are returned in DX:AX (segment part in DX, offset part in AX).

For a string-type function result, the caller pushes a pointer to a temporary storage location before pushing any parameters, and the function returns a string value in that temporary location. The function must not remove the pointer.

For array, record, and set type function results, if the value occupies one byte, it is returned in AL, if the value occupies two bytes, it is returned in AX, and if the value occupies four bytes, it is returned in DX:AX. Otherwise, the caller pushes a pointer to a temporary storage location of the appropriate size, and the function returns the result in that temporary location. Upon returning, the function leaves the temporary pointer on the stack.

A global procedure pointer type is returned in DX:AX.

A method pointer type is returned in BX:CX:DX:AX, where DX:AX contains the method pointer and BX:CX contains the instance pointer.

NEAR and FAR calls

The 80x86 family of CPUs supports two kinds of call and return instructions: NEAR and FAR. The NEAR instructions transfer control to another location within the same code segment, and the FAR instructions allow a change of code segment.

A NEAR CALL instruction pushes a 16-bit return address (offset only) onto the stack, and a FAR CALL instruction pushes a 32-bit return address (both segment and offset). The corresponding RET instructions pop only an offset or both an offset and a segment.

Delphi automatically selects the correct call model based on the procedure's declaration. Procedures declared in the interface section of a unit are farthey can be called from other units. Procedures declared in a program or in the implementation section of a unit are nearthey can only be called from within that program or unit.

For some specific purposes, a procedure can be required to be far. For example, if a procedure or function is to be assigned to a procedural variable, it must far. The $F compiler directive is used to override the compiler's automatic call model selection. Procedures and functions compiled in the {$F+} state are always far; in the {$F-} state, Delphi automatically selects the correct model. The default state is {$F-}.

Nested procedures and functions

A procedure or function is said to be nested when it is declared within another procedure or function. By default, nested procedures and functions always use the near call model, because they are visible only within a specific procedure or function in the same code segment.

When calling a nested procedure or function, the compiler generates a PUSH BP instruction just before the CALL, in effect passing the caller's BP as an additional parameter. Once the called procedure has set up its own BP, the caller's BP is accessible as a word stored at [BP + 4], or at [BP + 6] if the procedure is far. Using this link at [BP + 4] or [BP + 6], the called procedure can access the local variables in the caller's stack frame. If the caller itself is also a nested procedure, it also has a link at [BP + 4] or [BP + 6], and so on. The following example demonstrates how to access local variables from an inline statement in a nested procedure:

procedure A; near; var

IntA: Integer;

procedure B; far; var

IntB: Integer;

procedure C; near; var

IntC: Integer;

begin asm

MOV

AX,1

MOV

IntC,AX

{ IntC := 1 }

MOV

BX,[BP+4]

{ B's stack frame }

MOV

SS:[BX+OFFSET IntB],AX

{ IntB := 1 }

MOV

BX,[BP+4]

{ B's stack frame }

MOV

BX,SS:[BX+6]

{ A's stack frame }

MOV SS:[BX+OFFSET IntA],AX { IntA := 1 }

end; end;

begin C end; begin B end;

Nested procedures and functions can’t be declared with the external directive, and they cannot be procedural parameters.

Method calling conventions

Methods use the same calling conventions as ordinary procedures and functions, except that every method has an additional implicit parameter, Self, which is a reference to the class or instance for which the method is invoked. The Self parameter is always passed as the last parameter, and is always a 32-bit pointer. For regular methods, Self is a class type value (a pointer to an instance). For class methods, Self is a class reference type value (a pointer to a virtual method table).

For example, given the declarations

type

TMyObject = class(TObject) procedure Test(X, Y: Integer); procedure Foo; virtual;

class procedure Bar; virtual; end;

TMyClass = class of TMyObject;

var

MyObject: TMyObject;

MyClass: TMyClass;

the call MyObject.Test(10, 20) generates the following code:

PUSH 10

PUSH 20

LES DI,MyObject

PUSH ES

PUSH DI

CALL MyObject.Test

Upon returning, a method must remove the Self parameter from the stack, just as it must remove any normal parameters.

Methods always use the far call model, regardless of the setting of the $F compiler directive.

To call a virtual method, the compiler generates code that loads the VMT pointer from the object, and then calls via the slot associated with the method. Referring to the declarations above, the call MyObject.Foo generates the following code:

LES DI,MyObject

PUSH ES

PUSH DI

LES DI,ES:[DI]

CALL DWORD PTR ES:[DI]

the call MyObject.Bar generates the following code:

LES DI,MyObject

LES DI,ES:[DI]

PUSH ES

PUSH DI

CALL DWORD PTR ES:[DI+4]

and the call MyClass.Bar generates the following code:

LES DI,MyClass

PUSH ES

PUSH DI

CALL DWORD PTR ES:[DI+4]

Constructors and destructors

Constructors and destructors use the same calling conventions as other methods, except that an aditional word-sized flag parameter is passed on the stack just before the Self parameter.

A zero in the flag parameter of a constructor call indicates that the constructor was called through an instance or using the inherited keyword. In this case, the constructor behaves like an ordinary method.

A non-zero value in the flag parameter of a constructor call indicates that the constructor was called through a class reference. In this case, the constructor creates an instance of the class given by Self, and returns a reference to the newly created object in DX:AX.

A zero in the flag parameter of a destructor call indicates that the destructor was called using the inherited keyword. In this case, the destructor behaves like an ordinary method.

A non-zero value in the flag parameter of a destructor call indicates that the destructor was called through an instance. In this case, the destructor deallocates the instance given by Self just before returning.

Entry and exit code

This is the standard entry and exit code for a procedure or function using the near call model:

PUSH

BP

;Save BP

MOV

BP,SP

;Set up stack frame

SUB

SP,LocalSize

;Allocate locals (if any)

.
.
.

MOV

SP,BP

;Deallocate locals (if any)

POP

BP

;Restore BP

RETN

ParamSize

;Remove parameters and return

For information on using exit procedures in a DLL, see Chapter 12, "Dynamic-link libraries."

If the routine is compiled in the {$W-} state (the default), the entry and exit code for a routine using the far call model is the same as that of a routine using the near call model, except that a far-return instruction (RETF) is used to return from the routine.

In the {$W+} state, this is the entry and exit code for a routine using the far call model:

INC

PUSH

BP

BP

;Indicate FAR frame

;Save odd BP

MOV

BP,SP

;Set up stack frame

PUSH

DS

;Save DS

SUB

SP,LocalSize

;Allocate locals (if any)

.
.
.

MOV

SP,BP

;Remove locals and saved DS

POP

BP

;Restore odd BP

DEC

BP

;Adjust BP

RETF

ParamSize

;Remove parameters and return

This is the entry and exit code for an exportable routine (a procedure or function compiled with the export compiler directive):

MOV

NOP

AX,DS

;Load DS selector into AX

;Additional space for patching

INC

BP

;Indicate FAR frame

PUSH

BP

;Save odd BP

MOV

BP,SP

;Set up stack frame

PUSH

DS

;Save DS

MOV

DS,AX

;Initialize DS

SUB

SP,LocalSize

;Allocate locals (if any)

PUSH

SI

;Save SI

PUSH

DI

;Save DI

.
.
.

POP

DI

;Restore DI

POP

SI

;Restore SI

LEA

SP,[BP-2]

;Deallocate locals (if any)

POP

DS

;Restore DS

POP

BP

;Restore odd BP

DEC

BP

;Adjust BP

RETF

ParamSize

;Remove parameters and return

For all call models, the instructions required to allocate and deallocate local variables are omitted if the routine has no local variables.

Occasionally you might find {$W+} useful while developing a Windows protected- mode application. Some non-Borland debugging tools require this state to work properly.

By default, Delphi automatically generates smart callbacks for procedures and functions that are exported by an application. When linking an application in the

{$K+} state (the default), the linker looks for a MOV AX,DS instruction followed by a NOP instruction at every exported entry point and, for each such sequence it finds, it changes the MOV AX,DS to a MOV AX,SS. This change alleviates the need to use the Windows MakeProcInstance and FreeProcInstance API routines when creating callback routines (although it isn't harmful to do so), and also makes it possible to call exported entry points from within the application itself.

In the {$K-} state or when creating a dynamic-link library, the Delphi linker makes no modifications to the entry code of exported entry points. Unless a callback routine in an application is to be called from another application (which isn't recommended anyway), you shouldn't have to ever select the {$K-} state.

When loading an application or dynamic-link library, Windows looks for a MOV AX,DS followed by a NOP at each exported entry point. For applications, the sequence is changed into three NOP instructions to prepare the routine for use with MakeProcInstance. For libraries, the sequence is changed into a MOV AX,xxxx instruction, where xxxx is the selector (segment address) of the library's automatic data segment. Because smart callback entry points start with a MOV AX,SS instruction, they are left untouched by the Windows program loader.

Register-saving conventions

Procedures and functions should preserve the BP, SP, SS, and DS registers. All other registers can be modified. In addition, exported routines should preserve the SI and DI registers.

Exit procedures

By installing an exit procedure, you can gain control over a program's termination process. This is useful when you want to make sure specific actions are carried out before a program terminates; a typical example is updating and closing files.

The ExitProc pointer variable allows you to install an exit procedure. The exit procedure is always called as a part of a program's termination, whether it's a normal termination, a termination through a call to Halt, or a termination due to a run-time error.

An exit procedure takes no parameters and must be compiled with a far procedure directive to force it to use the far call model.

When implemented properly, an exit procedure actually becomes part of a chain of exit procedures. This chain makes it possible for units as well as programs to install exit procedures. Some units install an exit procedure as part of their initialization code and then rely on that specific procedure to be called to clean up after the unit.

Closing files is such an example. The procedures on the exit chain are executed in reverse order of installation. This ensures that the exit code of one unit isn't executed before the exit code of any units that depend upon it.

To keep the exit chain intact, you must save the current contents of ExitProc before changing it to the address of your own exit procedure. Also, the first statement in your exit procedure must reinstall the saved value of ExitProc. The following code demonstrates a skeleton method of implementing an exit procedure:

var

ExitSave: Pointer;

procedure MyExit; far; begin

ExitProc := ExitSave; { Always restore old vector first }

.

.

.

end;

begin

ExitSave := ExitProc;

ExitProc := @MyExit;

.

.

.

end.

On entry, the code saves the contents of ExitProc in ExitSave, and then installs the MyExit exit procedure. After having been called as part of the termination process, the first thing MyExit does is reinstall the previous exit procedure

The termination routine in the run-time library keeps calling exit procedures until ExitProc becomes nil. To avoid infinite loops, ExitProc is set to nil before every call, so the next exit procedure is called only if the current exit procedure assigns an address to ExitProc. If an error occurs in an exit procedure, it won't be called again.

An exit procedure can learn the cause of termination by examining the ExitCode

integer variable and the ErrorAddr pointer variable.

In case of normal termination, ExitCode is zero and ErrorAddr is nil. In case of termination through a call to Halt, ExitCode contains the value passed to Halt, and ErrorAddr is nil. Finally, in case of termination due to a run-time error, ExitCode contains the error code and ErrorAddr contains the address of the statement in error.

The last exit procedure (the one installed by the run-time library) closes the Input

and Output files. If ErrorAddr is not nil, it outputs a run-time error message.

If you wish to present run-time error messages yourself, install an exit procedure that examines ErrorAddr and outputs a message if it's not nil. In addition, before returning, make sure to set ErrorAddr to nil, so that the error is not reported again by other exit procedures.

Once the run-time library has called all exit procedures, it returns to Windows, passing the value stored in ExitCode as a return code.

C h a p t e r