2007. március 24., szombat

Assertion Magic


Problem/Question/Abstract:

The ASSERT function shows you the line number and unit name from which it was launched. How can you get this functionality for different useages in your code

Answer:

Any information in this article has been checked on Delphi5 and Delphi 6 only (on Pentium II processor). It might not work on any other configuration !

First, we need to understand how the ASSERT fucntion works. As you've probably noticed, when an assertion occures, the error raised includes the line number and unit name. How does this happen? Where does the ASSERT function get that info from?

The answer is very simple. From Delphi's compiler. When Delphi compiles your code it addes a few lines in assembler code before the call to ASSERT. These assembler instructions contain the extra information the ASSERT function needs.
That is, the line number from which it was called, and the unit's name. But what if you want to have that info? How can you retreive the line number and unit name of a specific statment in your code? The answer is : you can't (at least as far as I know), at least not strait forwardly, but you can get it in some tricky way.

To solve this, we'll use Delphi's compiler. Since the compiler adds the extra information we're looking for before an ASSERT function, we'll put an ASSERT function in our code, and have the compiler add the needed information. Now we need to find a way to avoid the ASSERT function call (sinc we don't want a simple assertion error to happen). In order to do that, we'll add some assembler code to skip the ASSERT function. You might wonder why to put the ASSERT fucntion in the first place, if we're going to skip it in any case. The answer is, in order to read the extra information (line number and unit name) we have to let the compiler add it to our code, and that can be done only by adding the ASSERT function.

After skipping the ASSERT function, we'll read the information added by the compiler (using assembler) and put it in some global variables. Afterwards you can do what ever you wish with that info.

Here is the needed code :

var
  // Global variables to hold the result
  GLineNumber: Integer;
  GError, GUnitName: string;

asm

// Save EAX and EBX cause we're going to change them
    push eax
    push ebx
// The following 3 lines are in order to get the value of EIP
// "call" pushes EIP to the stack
    call @Temp
    @Temp:
// now we POP EIP into EBX
    pop ebx
// Add to EBX the value needed inorder to skip the ASSERT function
    add ebx, $1A
// Skip the ASSERT function by jumping to the code after it
    jmp ebx
  end;
ASSERT(False, 'Your Error Message Here');
  asm
// Make EBX point to the line number value that the compiler inserted
    sub ebx, $13
// Read that value into EAX
    mov eax, [ebx]
// Put the line number into GLineNumber
    mov GLineNumber, eax

// Same as above but for GUnitName
    add ebx, 5
    mov eax, [ebx]
    mov GUnitName, eax

// Same as above but for GError (the assertion error message)
    add ebx, 5
    mov eax, [ebx]
    mov GError, eax

// Restore the values of EBX and EAX
    pop ebx
    pop eax
end;

Let's look back again on what this code does.

Delphi's compiler adds some info before any ASSERT function it finds in the  code.
We add a call to ASSERT to make the compiler add the wanted information
We add assmbler code to skip the call to ASSERT
We add code (after the call to ASSERT) that reads to information that the compiler added.
We do what ever we wish with this information, for example - raise a better excpetion.

Note :

1) You might think this is to much code for such a simple task. Inorder to make this code shorter, you can but the assmbler code in a file, and use the {$I} include compiler directive. That way the code will look like this :

{$I PreAssert.INC}
ASSERT(False, 'YourMessage');
{$I PostAssert.INC}

2) You might want to override the assertion handling method in System.Pas instead of all of this, but then you won't be able to use regular ASSERT functions when they are needed.

3) This code is not meant to replace ASSERT. It just uses ASSERT inorder to retrieve the line number and unit name. This code is meant to get that information for a specific loacation in your code.

4) You must pass "False" as the first parameter of the ASSERT function you write. If you pass "True" then the compiler completly ignores the function, and if you pass a condition (eg. i = 5) the compiler will generate more code then expected, and the assembler instructions that I've provided won't work properly.

Nincsenek megjegyzések:

Megjegyzés küldése