Mega Code Archive

 
Categories / Delphi / Ide Indy
 

Inline Assembler in Delphi (V) Objects

Title: Inline Assembler in Delphi (V) - Objects Question: How to work with objects in inline assembler Answer: Inline Assembler in Delphi (V) An Introdunction to Objects By Ernesto De Spirito edspirito@latiumsoftware.com Objects are records From the assembler point of view, an object is like a record, whose fields are its own fields plus the fields of its ancestors, plus the pointer to the VMT (Virtual Methods Table). Let's see this with an example: type TClass1 = class FieldA: integer; FieldB: string; end; TClass2 = class(TClass1) FieldC: integer; end; In the example, TClass2 is somehow like a record with four fields: TClass2 = record VMT: pointer; // invisible field, always first FieldA: integer; // inherited from TClass1 FieldB: string; // inherited from TClass1 FieldC: integer; // declared in TClass2 end; Object variables are pointers ============================= An object variable is just a pointer to an object, i.e. a pointer to a record. var a, b: TClass2; begin a := TClass2.Create; b := a; // just a pointer assignment a.Free; end; A constructor allocates memory for an instance (object) of its class, initializes it, and returns a pointer to the allocated memory. Thus, after the call to TClass.Create the variable "a" points to the record (the object): +---+ +--------+ | a | ---------- | VMT | +---+ +--------+ | FieldA | +--------+ | FieldB | +--------+ | FieldC | +--------+ The assignment "b := a" doesn't create a new object, copy of the first, but actually makes both variables point to the same object: +---+ +--------+ +---+ | a | ---------- | VMT | +---+ +--------+ +---+ | FieldA | +--------+ | FieldB | +--------+ | FieldC | +--------+ Assembler methods Methods receive an invisible first parameter called Self, which is a pointer to the object upon which the method should operate. type TTest = class FCode: integer; public procedure SetCode(NewCode: integer); end; procedure TTest.SetCode(NewCode: integer); begin FCode := NewCode; end; var a: TTest; begin : a.SetCode(2); : end; The above Object Pascal code is somehow translated to standard Pascal as follows: type TTest = record VMT: pointer; FCode: integer; end; procedure SetCode(Self: TTest; NewCode: integer); begin Self.FCode := NewCode; end; var a: ^TTest; begin : SetCode(a, 2); : end; The example serves to explain that methods receive the Self pointer as their first parameter, i.e. they receive the Self pointer in the EAX register, and the first declared parameter is passed as a second parameter in EDX, and so on (the second declared parameter is passed as third in ECX, and the rest of the parameters are passed on the stack). The method SetCode can be written in assembler like this: procedure TTest.SetCode(NewCode: integer); asm // EAX = Self = Address of TTest instance // EDX = NewCode parameter // FCode := NewCode; mov TTest[eax].FCode, edx // TTest(EAX)^.FCode := EDX; end; As you can see, object fields are accessed in the same way as record fields. NOTE: Properties are not fields, and they cannot be accessed directly from inline assembler. Here's an example of a method calling another method: procedure TTest.Increment; asm // SetCode(Code+1); mov edx, TTest[eax].FCode // ECX := TTest(EAX)^.FCode; inc edx call TTest.SetCode; end; We didn't set the value of EAX before making the call since EAX already contains the desired value (Self), so the called method will operate on the same object. NOTES: Virtual methods can only be called statically, since a reference to the class is needed in the call statemet. Overloaded methods cannot be distinguished in inline assembler. Assembler constructors Constructors are very special methods. Constructors can be called to create an instance of a class (i.e. to allocate the memory for the object and initialize it), or simply to reinitialize an already created object: a := TTest.Create; // allocates memory a.Create; // only reinitializes an existing object To distinguish between these two situations, constructors are passed an invisible second parameter of byte type (i.e. in the DL register) which can be positive or negative respectively (the compiler uses 1 and -1 respectively). If we have to call a constructor from assembler code with DL = $01 (to allocate memory for the object), we have to pass a reference to the class in EAX. Since there is no symbol to access it directly from assembler, we have to do something similar to what we did with the type information for records: var TTest_TypeInfo: pointer; : initialization TTest_TypeInfo := TTest; Now that we have initialized a global variable with the reference to the class from our Pascal code, we can use it in our assembler code: var a: TTest; begin // a := TTest.Create(2); asm mov eax, TTest_TypeInfo mov dl, 1 mov ecx, 2 call TTest.Create mov a, eax end; : end; Calling a constructor to reinitialize the object is simpler, since we don't need a reference to the class: var a: TTest; begin : // a.Create(2); asm mov eax, a mov dl, -1 mov ecx, 2 call TTest.Create end; : end; We have nothing to worry about if we have to write an assembler constructor since Delphi handles the allocation thing for us upon entry to the constructor, and -after that- EAX will point to the object, as it happens with any other method, but what is relevant is that if the constructor has parameters, the first declared parameter will be internally passed third, i.e. in ECX (instead as second, i.e. in EDX, as it happens with other methods), and the rest of the parameters will be passed on the stack. constructor TTest.Create(NewCode: integer); asm // FCode := NewCode mov TTest[eax].FCode, ecx end; Previous: Inline Assembler in Delphi (IV) - Records Next: Inline Assembler in Delphi (VI) - Calling external procedures