Program Control (Object Pascal)
Go Up to Object Pascal Language Guide Index
The concepts of passing parameters and function result processing are important to understand before you undertake your application projects Treatment of parameters and function results is determined by several factors, including calling conventions, parameter semantics, and the type and size of the value being passed.
This following topics are covered in this material:
- Passing Parameters.
- Handling Function Results.
- Handling Method Calls.
- Understanding Exit Procedures.
Parameters are transferred to procedures and functions via CPU registers or the stack, depending on the routine's calling convention. For information about calling conventions, see the topic on Calling Conventions.
By Value vs. By Reference
Variable (var) parameters are always passed by reference, as 32-bit pointers that point to the actual storage location.
Value and constant (const) parameters are passed by value or by reference, depending on the type and size of the parameter:
- An ordinal parameter is passed as an 8-bit, 16-bit, 32-bit, or 64-bit value, using the same format as a variable of the corresponding type.
- A real parameter is always passed on the stack. A Single parameter occupies 4 bytes, and a Double, Comp, or Currency parameter occupies 8 bytes. A Real48 occupies 8 bytes, with the Real48 value stored in the lower 6 bytes. An Extended occupies 12 bytes, with the Extended value stored in the lower 10 bytes.
- A short-string parameter is passed as a 32-bit pointer to a short string.
- A long-string or dynamic-array parameter is passed as a 32-bit pointer to the dynamic memory block allocated for the long string. The value nil is passed for an empty long string.
- A pointer, class, class-reference, or procedure-pointer parameter is passed as a 32-bit pointer.
- A method pointer is passed on the stack as two 32-bit pointers. The instance pointer is pushed before the method pointer so that the method pointer occupies the lowest address.
- Under the register and pascal conventions, a variant parameter is passed as a 32bit pointer to a Variant value.
- Sets, records, and static arrays of 1, 2, or 4 bytes are passed as 8-bit, 16-bit, and 32bit values. Larger sets, records, and static arrays are passed as 32-bit pointers to the value. An exception to this rule is that records are always passed directly on the stack under the cdecl, stdcall, and safecall conventions; the size of a record passed this way is rounded upward to the nearest double-word boundary.
- An open-array parameter is passed as two 32-bit values. The first value is a pointer to the array data, and the second value is one less than the number of elements in the array.
When two parameters are passed on the stack, each parameter occupies a multiple of 4 bytes (a whole number of double words). For an 8-bit or 16-bit parameter, even though the parameter occupies only a byte or a word, it is passed as a double word. The contents of the unused parts of the double word are undefined.
Pascal, cdecl, stdcall, and safecall Conventions
Under the pascal, cdecl, stdcall and safecall conventions, all parameters are passed on the stack. Under the pascal convention, parameters are pushed in the order of their declaration (left-to-right), so that the first parameter ends up at the highest address and the last parameter ends up at the lowest address. Under the cdecl, stdcall, and safecall conventions, parameters are pushed in reverse order of declaration (right-to-left), so that the first parameter ends up at the lowest address and the last parameter ends up at the highest address.
Under the register convention, up to three parameters are passed in CPU registers, and the rest (if any) are passed on the stack. The parameters are passed in order of declaration (as with the pascal convention), and the first three parameters that qualify are passed in the EAX, EDX, and ECX registers, in that order. Real, method-pointer, variant, Int64, and structured types do not qualify as register parameters, but all other parameters do. If more than three parameters qualify as register parameters, the first three are passed in EAX, EDX, and ECX, and the remaining parameters are pushed onto the stack in order of declaration. For example, given the declaration:
procedure Test(A: Integer; var B: Char; C: Double; const D: string; E: Pointer);
a call to Test passes A in EAX as a 32-bit integer, B in EDX as a pointer to a Char, and D in ECX as a pointer to a long-string memory block; C and E are pushed onto the stack as two double-words and a 32-bit pointer, in that order.
Register saving conventions
Procedures and functions must preserve the EBX, ESI, EDI, and EBP registers, but can modify the EAX, EDX, and ECX registers. When implementing a constructor or destructor in assembler, be sure to preserve the DL register. Procedures and functions are invoked with the assumption that the CPU's direction flag is cleared (corresponding to a CLD instruction) and must return with the direction flag cleared.
Note: Object Pascal language procedures and functions are generally invoked with the assumption that the FPU stack is empty: The compiler tries to use all eight FPU stack entries when it generates code.
When working with the MMX and XMM instructions, be sure to preserve the values of the xmm and mm registers. Object Pascal functions are invoked with the assumption that the x87 FPU data registers are available for use by x87 floating point instructions. That is, the compiler assumes that the EMMS/FEMMS instruction has been called after MMX operations. Object Pascal functions do not make any assumptions about the state and content of xmm registers. They do not guarantee that the content of xmm registers is unchanged.
Handling Function Results
The following conventions are used for returning function result values.
- Ordinal results are returned, when possible, in a CPU register. Bytes are returned in AL, words are returned in AX, and double-words are returned in EAX.
- Real results are returned in the floating-point coprocessor's top-of-stack register (ST(0)). For function results of type Currency, the value in ST(0) is scaled by 10000. For example, the Currency value 1.234 is returned in ST(0) as 12340.
- For a string, dynamic array, method pointer, or variant result, the effects are the same as if the function result were declared as an additional var parameter following the declared parameters. In other words, the caller passes an additional 32-bit pointer that points to a variable in which to return the function result.
- Int64 is returned in EDX:EAX.
- Pointer, class, class-reference, and procedure-pointer results are returned in EAX.
- For static-array, record, and set 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 EAX. Otherwise, the result is returned in an additional var parameter that is passed to the function after the declared parameters.
Handling Method Calls
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 instance or class in which the method is called. The Self parameter is passed as a 32-bit pointer.
- Under the register convention, Self behaves as if it were declared before all other parameters. It is therefore always passed in the EAX register.
- Under the pascal convention, Self behaves as if it were declared after all other parameters (including the additional var parameter sometimes passed for a function result). It is therefore pushed last, ending up at a lower address than all other parameters.
- Under the cdecl, stdcall, and safecall conventions, Self behaves as if it were declared before all other parameters, but after the additional var parameter (if any) passed for a function result. It is therefore the last to be pushed, except for the additional var parameter.
Constructors and destructors use the same calling conventions as other methods, except that an additional Boolean flag parameter is passed to indicate the context of the constructor or destructor call.
A value of False in the flag parameter of a constructor call indicates that the constructor was invoked through an instance object or using the inherited keyword. In this case, the constructor behaves like an ordinary method. A value of True in the flag parameter of a constructor call indicates that the constructor was invoked 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 EAX.
A value of False in the flag parameter of a destructor call indicates that the destructor was invoked using the inherited keyword. In this case, the destructor behaves like an ordinary method. A value of True in the flag parameter of a destructor call indicates that the destructor was invoked through an instance object. In this case, the destructor deallocates the instance given by Self just before returning.
The flag parameter behaves as if it were declared before all other parameters. Under the register convention, it is passed in the DL register. Under the pascal convention, it is pushed before all other parameters. Under the cdecl, stdcall, and safecall conventions, it is pushed just before the Self parameter.
Since the DL register indicates whether the constructor or destructor is the outermost in the call stack, you must restore the value of DL before exiting so that BeforeDestruction or AfterConstruction can be called properly.
Understanding Exit Procedures
Exit procedures ensure that specific actions such as updating and closing filesare carried out before a program terminates. The ExitProc pointer variable allows you to install an exit procedure, so that it is always called as part of the program's termination whether the termination is normal, forced by a call to Halt, or the result of a runtime error. An exit procedure takes no parameters.
Note: It is recommended that you use finalization sections rather than exit procedures for all exit behavior. Exit procedures are available only for executables. For .DLLs (Win32) you can use a similar variable, DllProc, which is called when the library is loaded as well as when it is unloaded. For packages, exit behavior must be implemented in a finalization section. All exit procedures are called before execution of finalization sections.
Units as well as programs can install exit procedures. A unit can install an exit procedure as part of its initialization code, relying on the procedure to close files or perform other clean-up tasks.
When implemented properly, an exit procedure is part of a chain of exit procedures. The procedures are executed in reverse order of installation, ensuring that the exit code of one unit isn't executed before the exit code of any units that depend on it. To keep the chain intact, you must save the current contents of ExitProc before pointing 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 shows a skeleton implementation of an exit procedure:
var ExitSave: Pointer; procedure MyExit; 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, then installs the MyExit procedure. When called as part of the termination process, the first thing MyExit does is reinstall the previous exit procedure.
The termination routine in the runtime 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 is not 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. In case of termination due to a runtime error, ExitCode contains the error code and ErrorAddr contains the address of the invalid statement.
The last exit procedure (the one installed by the runtime library) closes the Input and Output files. If ErrorAddr is not nil, it outputs a runtime error message. To output your own runtime error message, install an exit procedure that examines ErrorAddr and outputs a message if it's not nil; before returning, set ErrorAddr to nil so that the error is not reported again by other exit procedures.
Once the runtime library has called all exit procedures, it returns to the operating system, passing the value stored in ExitCode as a return code.