Home > Writings > Programming > Using Assembler in Delphi > Chapter 4: Returning Results

Using Assembler in Delphi

Chapter 4: Returning Results

In many cases, you will return a result from your assembler routines to the caller. 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 cover returning results to the caller.

4.1. Returning integers as immediate values

Delphi 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 I refer to the Delphi-specific generic data type, I will spell it Integer in a monospaced font and with an uppercase "I".

There are 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 a 32-bit platform to respectively a signed 32-bit value and an unsigned 32-bit value. So, Integer is on a 32-bit platform the same as Longint, whereas Cardinal is the same on a 32-bit platform as Longword.

There are several other data 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, often for conformity with 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 straightforward: you store the value in the eax register before returning to the caller. 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 ignored. See Table 4 for a detailed overview. The only exception to this rule are 64-bit integers. On a 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. On several processors, Int64 will also yield better performance for integer arithmetic, as it uses the CPU registers, rather than 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 fall in the range 0-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;
4.2. Returning booleans as immediate values

Returning a boolean value is quite simple too. 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 has 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 it being future proof is still 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 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 correspondingly in al, ax or eax. They 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 provides the full overview.

4.3. Returning real numbers

Real numbers in Delphi are implemented as floating point types. Unfortunately, all too many programmers nowadays don't properly understand floating point representation. You can read my brief introduction on the topic.

The basic mechanism for returning real values from your assembler code is to put the result in the ST(0) register of the FPU, which corresponds to the top of the FPU stack.

Even though Delphi supports several floating point formats, such as 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 points as 80-bit values. Delphi's Extended type (19-20 significant digits, uses 10 bytes of memory) maps onto this format. Note that all these real types are 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. My article on floating point values elsewhere on this site discusses these formats in some more detail.

There are however other considerations to take into account when working with real numbers. The Intel FPU has a control register that controls precision and rounding and also has exception masks to steer FP exception handling. The different precision and rounding methods allow a programmer fine control over the behaviour of the FPU and can be important for code compatibility with other systems or legal and other standards. It is therefore important to understand that it might not be sufficient to declare a result of a certain data type (say, single or double) only. Instead, you might need to explicitly configure the control register.

The precision control bits in the FPU control register are highly relevant in this respect. Under normal circumstances, this two bit binary value ought to be set to 11 at all times, indicating 64-bit mantissa precision. If the 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 expected. 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 the topics before attempting to write elaborate floating point code or whenever you need to produce results in line with specific guidelines or standards. Delphi has several supporting functions and variables, such as Get8087CW, Set8087CW, SetPrecisionMode, etc. See online help for more information. Note that many libraries and even calls to OS functions can change the value of the FPU control word. If you change the control word inside your own code, it is good practice to make sure you restore it 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 many processors, considerable 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 Real48, Comp and Currency types 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 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 104. Hence, a Currency value of 5.4321 is stored in ST(0) as 54321.

Anyone wishing to use FP math for monetary applications ought to make sure they fully understand the nature of floating point arithmetic. My article on floating point values provides a brief introduction to the topic 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 is useful for these purposes. Unfortunately, the Delphi language has no support for BCD types, so you will need to encode and decode BCD data yourself.

Unless needed for compatibility with other applications or environments, you should avoid the non-native Real48 type altogether. This type is not supported in hardware, so all manipulation has to be done in code, which makes it very slow. 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 as a Real48 type, 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 where the converted value will be stored. Note that in Delphi versions before Delphi 4, this non-native 6-byte type was called Real instead of Real48. From Delphi 4 onwards, Real acts as a generic type for real numbers, at present implemented as a Double.

Table 4 summarises the rules for returning results, including real types.

To conclude this section on returning real numbers, below is a full working example. The function CalcRelativeMass below demonstrates floating point arithmetic in assembler within a Delphi environment. The function takes two parameters, mass and velocity of a body, and calculates the relative mass as per 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;
4.4. Returning characters

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 mapped to AnsiChar. As with integer types, it is good practice to use the fundamental types in your own assembly code.

As AnsiChar is an 8-bit type, you return it in al. WideChar is a 16-bit Unicode character and 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.

See also Table 4, which provides an overview of the rules for returning results.

4.5. Returning a long string

A very common type used in Delphi applications is the long string: AnsiString. This type is essentially an array of AnsiChar characters, but with some additional complexity. The AnsiString type has two important properties 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 reduces overhead and 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 often must handle memory allocation and reference counting manually.

The second important feature of long strings is that they are stored in heap memory. The string variable is a pointer to the 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 create and manipulate long strings in assembler code, we must ensure that those strings will continue to function properly for the rest of our Delphi code, outside the asm block. This requires us to allocate memory for the strings properly, through Delphi functions, and that we must consider the reference counting carefully when we create, process and return 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. chapter 2 describes parameter passing in detail, including the the issues related to passing variables by reference and the differences associated with the various calling conventions.

The approach of using an additional parameter might seem like a needless complication, but in fact it is quite clever: by passing this additional var parameter, responsibility for decreasing the reference count is handed over to the caller. This makes it quite easy to return long strings from assembler code, while at the same time making sure that the reference count is suitably adjusted after the caller is done with it.

With regard to the process of allocating memory for long strings in assembler code, you should study the various routines provided in System.pas for this purpose. For example, you can call LStrSetLength to set the length of the Result string, before filling 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 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 much more closely tied to a specific platform and compiler, but within those limitations, programmers should still endeavour to write code that is as readable and future proof as possible.

The following example illustrates how it can be done. The procedure FillWithPlusMinus fills the long string passed to it with a pattern. In this case, the string itself is already allocated before calling the procedure, thus avoiding the need to call 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 = length of the string}

  mov eax,'+-+-' {pattern to use}

  mov ecx, edx {length in counter register}

  shr ecx,2 {divide, 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}

  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 above example does not need to call any of the System.pas routines, but it still relies on some implementation specific behaviour, 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 above function, allocate a string and then call the routine:

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 will 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 manipulation 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}

  mov esi, edx  {esi=pointer to Result}

  xchg edx, eax {eax=pointer to Result, edx=length}

  call System.@LStrSetLength

  mov ecx, [esi]

  jecxz @@ending {if nil, then exit}

  mov eax, esi

  call System.@UniqueStringA

  mov esi, [esi] {esi = first character of string}

  mov eax, '+-+-' {pattern to use}

  mov ecx, ebx {length in counter register}

  shr ecx,2 {divide, 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}

  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;

The example now uses the standard Result mechanism for returning data. It is called simply with the required length to obtain a string:

procedure DoSomething;

var

  ALine: AnsiString;

begin

  ...

  ALine:=PlusMinusLine({Required Length});

  ... 

end;

The above example above was written in Delphi 7. It should be very similar for most other versions of Delphi, although the names for the internal functions do differ somewhat between Delphi versions. Inspecting System.pas should help you in identifying the appropriate function. You can also use the ctrl-left button shortcut on the name of Pascal functions like UniqueString to jump to their System.pas implementations.

Next: Further Reading