In many cases, you will want to return a value from your assembler routines. In previous chapters, we have discussed how to pass parameters to a function and how to use local variables in your assembler code. This chapter will show you how you can return results to the caller.
The Delphi language provides several different integer types. We use the term integer in this chapter mostly in its general meaning of "whole number", using a standard font in lowercase. Delphi also has a generic type called Integer. When we refer to the Delphi-specific generic type, we will spell it Integer in a monospaced font and with an uppercase "I".
There are currently 8-bit, 16-bit, 32-bit and 64-bit integer types in Delphi. Some of these types are signed, whereas others are unsigned. Unsigned integers always represent positive whole numbers. Signed types have a sign bit, which if set indicates a negative value and when cleared indicates a positive value. Negative values are represented as the two's complement of the absolute value. The Delphi integer types are Shortint (8-bit, signed), Smallint (16-bit, signed), Longint (32-bit, signed), Int64 (64-bit, signed), Byte (8-bit, unsigned), Word (16-bit, unsigned) and Longword (32-bit, unsigned). In addition, Delphi also has the generic types Integer and Cardinal, which correspond on the 32-bit platform to respectively a signed 32-bit value and an unsigned 32-bit value. So, Integer is on 32-bit platforms the same as Longint, whereas Cardinal is the same on a 32-bit platform as Longword.
There are several other types in Delphi that map to one of the above integer types. Many of these additional types are provided to offer a type of the same name as in C-declarations, including when calling the Windows API. For example, DWORD and UINT are the same as Longword, whereas SHORT is the same as Smallint, etc.
In general, returning integers is very simple: you just store the value in the eax register. If the return type is smaller than eax, only the al (8 bits) or ax (16 bits) portion of the register is valid, the contents of the remainder of the register are irrelevant. See Table 4 for a detailed overview for all the integer types. The only exception to this rule are 64-bit integers. On the 32-bit platform, these are returned in edx:eax, with edx containing the most significant part.
Please note that the Comp type, which also represents a 64-bit integer, does not behave like other integers. It is a type that uses the floating point unit of the processor and as such follows the conventions for real types. You should have little use for the Comp type, which is maintained for backward compatibility. It is recommended to use Int64 instead. Int64 will also yield better performance, as it uses the CPU registers, which is faster than using the FPU.
The following code demonstrates how to return an integer from assembly code. It returns the number of set bits in the AValue parameter as an unsigned 8-bit value (we don't need the larger range of 16 or 32 bit integers, since the returned value will be between 0 and 32).
function CountBits(const AValue: Longword): Byte; asm mov ecx, eax xor al, al test ecx, ecx jz @@ending @@counting: shr ecx, 1 adc al, 0 test ecx, ecx jnz @@counting @@ending: end;
Returning a boolean value is quite simple as well. Again, the result goes in the eax register, or a subset of that register - similar to returning integers. Delphi has several boolean types. Boolean is the "proper" boolean type: it only knows two possible values, true and false. Within Delphi, you should always use this type. The other boolean types, ByteBool, WordBool and LongBool are provided for compatibility with other languages and for calling the Windows API.
The Boolean type is really an enumerated type. It occupies a byte of memory. As this is not an ordinal type, you should only use the predefined constants True and False in assignments. Do not assign ordinal values to a Boolean as this relies on an implementation feature, namely the values that the compiler uses to represent the True and False values of a Boolean. While it is unlikely that this will change in future versions of Delphi, relying on this is bad practice, architecturally ugly and also less clear than using the predefined boolean constants True and False. As a Boolean requires 8 bits, you return it from your code in al:
function DoSomething(...): Boolean; asm ... mov al, True ... end;
In contrast, ByteBool, WordBool and LongBool, provided for compatibility with other languages and calling the Windows API, are essentially ordinal types. They occupy respectively 8 bits, 16 bits and 32 bits and are thus returned accordingly in al, ax or eax. These are considered true if their ordinality is non-zero and false otherwise. The compiler will perform any necessary conversions between these types and Boolean where required.
Table 4 contains an overview of the rules for returning results.
Real numbers in Delphi are implemented as floating point values. Unfortunately, all too many programmers nowadays don't properly understand floating point representation. Another article of mine also on this website, Understanding floating point values in a Delphi environment is therefore highly recommended as prior reading before embarking on using floating point data.
The basic mechanism for returning real values from your assembler code is fairly simple: you just put the result in the ST(0) register of the FPU, which is the top of the FPU stack.
Even though the Delphi language supports several floating point formats, like for example single (7-8 significant digits, occupies 4 bytes) and double (15-16 significant digits, occupies 8 bytes of memory), internally the FPU always stores and handles floating point values as 80-bit values. Delphi's Extended type (19-20 significant digits, uses 10 bytes of memory) maps directly to this format. Note that all these real values are always returned to the caller as a value in ST(0). It is only when the result is subsequently stored in memory or passed along to another part of the program that it effectively is transformed in its 4, 8 or 10 byte encoding. The article on floating point values elsewhere on this site discusses these formats in some more detail.
This is however not the end of the story. The Intel FPU has a control register that can be used to control precision and rounding and also has exception masks to control FP exception handling. The different precision and rounding methods supported in this way are there for mostly historic reasons, i.e. to allow programmers to write code compatible with other systems. It is thus important to realise that it is not really sufficient to declare a variable of a certain type (say, single or double) to be assured of a certain level of precision.
The precision control bits in the FPU control register are highly relevant in this respect. These two bits ought to be set to 11 at all times, indicating 64-bit mantissa precision. If these precision control bits are set to a lower precision, the FPU will reduce precision during computation and hence your result will be less precise than you would have wanted. The theory of floating point arithmetic and the details of the Intel FPU are out of scope for this article, but you should familiarise yourself intimately with these topics before attempting to write elaborate floating point code. Delphi has several supporting functions and variables, like for example Get8087CW, Set8087CW, SetPrecisionMode, etc. See the online help for more information on these. Note that many libraries and even calls to OS functions could change the value of the FPU control word. Similarly, if you change the control word inside your own code, it is good practice to make sure you set it back to its previous state when you are done. This ought to be done outside your time critical FP code, since setting the control word causes on most processors a serious stall if the control word is read immediately afterwards, which is the case for most FP instructions.
In addition to single, double and extended, the types Real48, Comp and Currency are also returned in ST(0). Even though Comp represents a 64-bit integer, it is a type that uses the FPU, rather than the CPU's registers and as such is manipulated using FPU instructions. Currency is a fixed-point type mainly designed for monetary calculations, but as with Comp it is in fact a FPU based type. Note that Currency is scaled by 10,000. Hence, a Currency value of 5.4321 is stored in ST(0) as the whole number 54321.
Anyone wishing to use FP math for monetary applications ought to make sure they fully understand the nature of floating point arithmetic. The article Understanding floating point values in a Delphi environment elsewhere on this site is recommended reading and contains links to further reading material. Using scaled integers might be a better approach for such applications. Also, Intel CPUs support BCD encoding and arithmetic, which you might consider using. Unfortunately, the Delphi language has no support for BCD, so you'll have to do all the coding yourself.
Unless needed for compatibility with other applications or environments, you should avoid the non-native Real48 altogether. The hardware has no support for this type, so all manipulation has to be done in software, which makes it very slow. Therefore, you should convert a Real48 into a native float immediately after receiving it, using the System unit's _Real2Ext function. When invoking _Real2Ext, eax contains a pointer to the Real48 value. Upon return, ST(0) is loaded with the value. You can then use the FPU to perform the required calculations. If you need to hand the result back in Real48 format, call _Ext2Real, also in the System unit, which will convert the value in ST(0) back into a Real48 value. eax should contain a pointer to a 6-byte wide memory location in which the converted value will be stored. Note that in Delphi versions before D4, this non-native 6-byte format was called Real instead of Real48. From D4 onwards, Real is a generic type for real numbers, currently implemented as a Double.
Table 4 summarises the rules for returning results, including the real number types.
To conclude this section on returning real numbers, I will provide a full working example. The function CalcRelativeMass below demonstrates floating point arithmetic in assembler within a Delphi environment. The function takes two parameters, the mass and the velocity of a body, and calculates the relative mass according to the theory of relativity.
function CalcRelativeMass(m,v: Double): Double; register; const LightVelocity: Integer = 299792500; asm {Calculate the relative mass according to the following formula: Result = m / Sqrt(1-v²/c²), where c = the velocity of Light, m the mass and v the velocity of an object} fild LightVelocity fild LightVelocity fmulp {Calculate c²} fld v fld v fmulp {Calculate v²} fxch fdivp {v²/c²} fld1 fxch fsubp {ST(0)=1-(v²/c²)} fsqrt {Root of ST(0)} fld m fxch fdivp {divide mass by root result} end;
Delphi currently offers two fundamental character types: AnsiChar is an 8-bit character, whereas WideChar is a 16-bit Unicode character. There is also a generic type, Char, which is currently mapped to AnsiChar. As with integer types, it is good practice to use the fundamental types in your own assembly code. While assembler is a step above machine language, in practice assembler code is tied to a specific platform (in our case the Intel 32-bit processor family) and as such you should leave no ambiguity in your code with regard to types etc.
As AnsiChar is an 8-bit type, you return it in al. WideChar is a 16-bit Unicode character and as such is returned in ax.
The Delphi online help makes a bit of a mess of things by stating on the one hand that WideChar is a fundamental type, and on the other hand talking about its "current implementations [sic]", thereby alluding that future versions might change its 16-bit nature, as if it were some generic type. In practice, there is little choice but to consider WideChar as a fundamental, 16-bit Unicode type, so that is what we do in this article. It would be quite foolish (if not beyond the wit of man) to change WideChar in a future version to a different implementation. If a future version of Delphi were to introduce another character type, say a 32-bit Unicode character, it ought to do this by defining a new type for it.
See also Table 4, which provides an overview of the rules for returning results.
A very common type used in Delphi applications is the long string: AnsiString. This type is essentially an array of AnsiChar characters. The AnsiString type has two greatly important features from the point of view of the assembler programmer. Firstly, it is a reference counted type. The reference counting mechanism has several advantages. If the same string is used in different places, the same instance can be shared, rather than having to allocate memory for each identical string. By using reference counting with a copy-on-write algorithm, a copy of a string in memory is only made when it is absolutely necessary, which enhances performance. Reference counting also allows for automated memory management. When the reference count reaches zero, indicating no one is using the string anymore, it is automatically cleaned up. However, within our assembler code, we don't have the compiler's support for this, so we must make sure we deal with the reference count ourselves.
The second important feature of long strings is that they are stored on the heap. The string variable is this in fact nothing else but a pointer to the actual string on the heap. As with reference counting, and in contrast to Pascal code, we will need to explicitly deal with memory management issues for our long strings.
As we generate and manipulate long strings in our assembler code, we must ensure that the strings we create and manipulate continue to function properly within the rest of our Delphi code. This means that we will need to allocate the memory for the strings properly, through Delphi functions, and that we must consider the reference counting carefully when we create, process and pass on long strings.
In contrast to ordinal types, long strings are not returned in a register, rather the function behaves as if an additional var parameter was declared after all the other parameters. In other words, an additional parameter is passed to your function by reference. Paragraph 2.4 in chapter 2 describes passing by reference in more detail. You should also make sure you have carefully read and understood the rest of chapter 2 as you will also have to consider the calling convention in dealing with long string results. This might seem like a needlessly complicated process, but in fact it is quite clever: by passing this additional var parameter, responsibility for decreasing the reference count is handed to the caller. In fact, this makes it quite easy to return long strings from our own assembler code, yet make sure that the reference count is suitably adjusted after the caller is done with it, since the compiler will automatically generate such code when calling our routine.
In terms of allocating memory for long strings we generate in our own code, you should inspect the various routines in System.pas. For example, you can call LStrSetLength to set the length of the Result string, then fill it with content. A major drawback of this approach is that it can easily be broken as Delphi itself evolves. System.pas gets special treatment at compile time and all these internal routines might be changed at some point as Delphi evolves. One way around this is to create the string elsewhere in Pascal, then just hand an already allocated long string to your own assembler code. Writing code that ports well is an important consideration. Clearly the very choice for assembler means that code will be tied to a specific platform, but within that boundary, it should be written to be as portable as possible.
The following example illustrates how this could be done. The procedure FillWithPlusMinus simply fills the long string passed to it with a pattern. In this case, the string itself is allocated beforehand, which means that we can avoid calling specific System.pas routines.
procedure FillWithPlusMinus(var AString: Ansistring); register; asm push esi mov esi, [eax] {esi now points to our string} test esi,esi {if nil, then exit} jz @@ending mov edx, [esi-4] {edx now contains length of the string} mov eax,'+-+-' {pattern to use} mov ecx, edx {load length in counter register} shr ecx,2 {divide, as we process 4 bytes at once} test ecx,ecx jz @@remain @@loop: mov [esi],eax add esi,4 dec ecx jnz @@loop @@remain: {fill the remaining bytes in case the rest fraction of length/4 is not zero} mov ecx, edx and ecx, 3 jz @@ending @@loop2: mov BYTE PTR [esi],al shr eax,8 inc esi dec ecx jnz @@loop2 @@ending: pop esi end;
The example above does not need to call any of the System.pas routines that are not part of the official interface, but it still relies on some level of implementation specific code, namely where it retrieves the long string's length. Long strings are preceded by two extra dwords, a 32-bit length indicator at offset -4 and a 32-bit reference count at offset -8. There is no guarantee that this scheme will remain unchanged forever. To use the function above, you would need to allocate a string beforehand:
procedure DoSomething;
var
ALine: AnsiString;
begin
...
SetLength(ALine, {Required Length});
FillWithPlusMinus(ALine);
...
end;
Alternatively, we could opt to call the appropriate System.pas routine for setting a long string's length (LStrSetLength), as illustrated in the PlusMinusLine function for Delphi 7 below. From chapter 2 you should remember that with the register calling convention, LineLength as the first parameter will go in eax. The Result string is, as explained above, passed as an extra var parameter. In this case our Result parameter is the second parameter, so in case of the register calling convention it will go into edx. Note that we also call UniqueStringA to ensure our result has a reference count of 1. This is necessary, because our manipulations of the string's content are unknown to the compiler.
function PlusMinusLine(LineLength: Integer): Ansistring; register; asm push esi push ebx mov ebx, eax {ebx=LineLength, use ebx so it is not overwritten by LStrSetLength call} mov esi, edx {esi=pointer to location of Result} xchg edx, eax {eax=pointer to Result location, edx=required length} call System.@LStrSetLength {call the system routine for setting an Ansistring's length} mov ecx, [esi] jecxz @@ending {if nil, then exit} mov eax, esi call System.@UniqueStringA mov esi, [esi] {esi now points to first character of string} mov eax, '+-+-' {pattern to use for line} mov ecx, ebx {load length in counter register} shr ecx,2 {divide, as we process 4 bytes at once} test ecx,ecx jz @@remain @@loop: mov [esi],eax add esi,4 dec ecx jnz @@loop @@remain: {fill the remaining bytes in case the rest fraction of length/4 is not zero} mov ecx, ebx and ecx, 3 jz @@ending @@loop2: mov BYTE PTR [esi],al shr eax,8 inc esi dec ecx jnz @@loop2 @@ending: pop ebx pop esi end;
This example now uses the Result mechanism. You can simply call it with the required length to obtain a string:
procedure DoSomething;
var
ALine: AnsiString;
begin
...
ALine:=PlusMinusLine({Required Length});
...
end;
The example above was written for Delphi 7. You should be able to port it to most other versions of Delphi, although the names for the internal functions can differ between Delphi versions. Inspecting System.pas should help you in identifying the appropriate functions. You can also use the ctrl-left button shortcut on the name of Pascal functions like UniqueString to jump into their System.pas implementations.
(more content to be added shortly...)
Previous part: Chapter 3: Local Variables
Table of Contents
Copyright © Guido Gybels, 2001-2007, All rights reserved.
This page was last updated 3 January 2007.