Einzelnen Beitrag anzeigen

choose

Registriert seit: 2. Nov 2003
Ort: Bei Kiel, SH
729 Beiträge
 
Delphi 2006 Architect
 
#13

Re: Interface referenzen auf gleichheit prüfen?

  Alt 12. Okt 2004, 00:10
Hallo Hagen,

Zitat von Hagen:
Mein oben vorgeschlagener Weg ist eine saubere Lösung, und für meine Begriffe die einzigst saubere überhaupt. Denn selbst wenn Delphi die Art und Weise wie es Interfaces mit Klassen/Objecten verbindet ändert, oder sogar wenn man auf diese Weise Interfaces die durch verschiedene Compiler erzeugt wurden, vergleicht, so funktioniert das immer sauber.
ich stimme Dir in diesem Punkt absolut zu! Allerdings bietet Sie nach wie vor keine Lösung für eine Situation, bei der die Implementierung der Basisklasse unveränderlich ist. Außerdem möchte ich auf einige Punkte Deiner Darstellung erneut eingehen, weil ich sie nicht nachvollziehen kann.

Der von Dir dargestellte Code
Delphi-Quellcode:
var
  A,B,C: IInterface;
begin
  A := TClassA.Create;
  
  B := A as IInterfaceType;
  C := A as IInterfaceType;

  Assert( B <> C );
end;
funktioniert bei mir mir in den Delphi Versionen 5, 6 und 7 so, wie von mir beschrieben: Die Zusicherung schlägt fehl, oder mit anderen Worten: Es gilt
B = C; Und das "Konzept" der Interfaces entspricht im Gedanken denen des "Typen" (vgl Booch), also "Sichten" auf Objekte. Sie abstrahieren vom Verhalten einer Klasse und führen lediglich das Protokoll (in Form der Signaturen der Nachrichten), mit dessen Hilfe der Klient mit Ihnen interagieren kann, ein. Trotz dieser Sicht verbirgt sich hinter einer solchen Interfacereferenz also ein (Proxy-)Objekt und hat somit eine Identität inne.
Objekte könnten also durch Stellvetreter (Proxies) dynamisch bei jeder Zuweisung generiert werden und ganze Programmkonzepte (zB dynamische Proxies unter Java oder div Persistenz-Frameworks unter Smalltalk arbeiten nach diesem Prinzip) beruhen auf diesem Ansatz. Der Delphi-Compiler allerdings bedient sich jedoch eines anderen Ansatzes, der einen Mittelweg zwischen Performance und Speicherbedarf beschreibt.



Im Folgenden möchte ich Zeigen, dass die Interfacereferenz nicht nur im allgemeinenen sondern auch bei der Delphi-Implementierung nach dm COM-Interface-Paradigma ein direkter Zusammenhang zur Objektidentität existiert.

Während bei einem virtuellen Aufruf eines Objekts einer Klasse der Compiler die Basisklasse eines Objekts bereits zur Übersetzungszeit kennt und er die Klasse eines Objekts zur Laufzeit ermitteln kann, hat er die Möglichkeit, in der VMT der Klasse nach der durch die Basisklasse angeführten Stelle die Referenz auf die Methode allein durch die indirekte Kenntnis der Objektklasse durch die Referenz auf das Objekt zu erlangen.
Bei einer Referenz auf ein Interface wurde wieder dieser weg Beschritten, weil es hier keine Basisklasse gibt, auch wurde kein Slot-Mechnismus wie bei dynamischen Methoden oder Message Methoden verwendet:

Code:
Jede Klasse besitzt eine "Methodentabelle" pro Interface, dass sie implementiert.
Weil zwei Interfaces voneinander erben können, die Implementierung einer Methode, die durch das "Oberinterface" jedoch gänzlich von der Implementierung des "Unterinterface" verschieden sein kann, ist der direkte Abgriff an der Stelle, die durch diesen "Vorfahren" eingeführt wurde, nicht möglich, so dass der Ansatz von Delphi etwas anders aussieht (ich beschreibe den letzten Teil des Castes, der, wie bereits von Dir beschrieben auf der Methode QueryInterface beruht, die ihrerseits GetInterface verwendet):
Delphi-Quellcode:
function TObject.GetInterface(const IID: TGUID; out Obj): Boolean;
var
  InterfaceEntry: PInterfaceEntry;
begin
  Pointer(Obj) := nil;
  InterfaceEntry := GetInterfaceEntry(IID);
  if InterfaceEntry <> nil then
  begin
    if InterfaceEntry^.IOffset <> 0 then
    begin
      Pointer(Obj) := Pointer(Integer(Self) + InterfaceEntry^.IOffset);
      if Pointer(Obj) <> nil then IInterface(Obj)._AddRef;
    end
    else
      IInterface(Obj) := InvokeImplGetter(Self, InterfaceEntry^.ImplGetter);
  end;
  Result := Pointer(Obj) <> nil;
end;
Die Methode GetInterface hat die Aufgabe, in den Parameter Out eine Interfacereferenz auf die durch IID beschriebene Schnittstelle zum betrachteten Objekt zurückzugeben, sofern vorhanden. Der Bool'sche Rückgabewert signalisiert den Erfolg der Operation.

InterfaceEntry := GetInterfaceEntry(IID); Dieser Aufruf der Klassenmethode GetInterfaceEntry ermittelt (indirekt) die bereits zuvor beschriebene VMT der Klasse zum Interface. Der Rückgabewert des Aufrufs ist für alle Exemplare der Klasse identisch und nil, falls die Klasse das Interface nicht implementiert. Betrachtet man den Aufbau der Struktur, auf die eine Referenz zurückgegeben wird, etwas genauer
Delphi-Quellcode:
type
  TInterfaceEntry = packed record
    IID: TGUID;
    VTable: Pointer;
    IOffset: Integer;
    ImplGetter: Integer;
  end;
fällt auf, dass sie eine Referenz auf die tatsächliche VMT enthält (VTable) und die GUID des Interface enthält, letztere ist für die Methoden GetInterfaceEntry erforderlich.
Interessant für die weitere Betrachtung ist das Feld IOffset. Betrachtet man die Implementierung von GetInterface weiter
Delphi-Quellcode:
if InterfaceEntry^.IOffset <> 0 then
begin
  Pointer(Obj) := Pointer(Integer(Self) + InterfaceEntry^.IOffset);
  if Pointer(Obj) <> nil then IInterface(Obj)._AddRef;
end
so erkennt man, dass dieser Offset einfach zu dem Speicherbereichsbeginn des Objekts selbst addiert wird. Der so ermittelte Wert ist (im Regelfall) dann die Interfacereferenz.

Wie also zu erkennen ist, muss der Wert von GetInterface bei jedem Aufruf an dasselbe Objekt den identischen Rückgabewert haben, weil IOffset von der zu einem (Delphi-)Objekt unveränderlichen Klasse und der andere Summand von dem (für Delphiobjekte unveränderlichen) Speicherort abhängt (der bei Delphiobjekte der Identität entspricht).


Um zu klären, wie der Compiler Code zum Aufruf einer Methode erzeugen kann, ohne Kenntnis über die Klasse eines Objekts, zu dem eine Interfacereferenz bekannt ist, zu haben, verfolgt man im Debugger, was an der Stelle
Verfolgt man im Debugger, was an der Stelle
Pointer(Integer(Self) + InterfaceEntry^.IOffset) zu finden ist.
Entdecken kann man bei einem Aufruf der Art
MyInterfaceRef.AnOperationIntrocuedByMyInterface; eine Technik, die auch bei diversen anderen Stellen durch den Delphi-Compiler erzeugt wird:
Code:
  ; MyInterfaceRef = InterfaceReferenz = Objektrefernz+IOffset
  ; Hier: IOffset = 16d = $10
  mov  eax, [MyInterfaceRef] ; eax => "magischer Zeiger" mit Objektreferenz+IOffset (s.o.)
  mov  edx, [eax]            ; edx => "magischer Bereich"
  call edx, [edx+$0c]        ; Sprung nach [edx+Methodenselektor]
Code:
  ; "magische Tabelle" hinter der Klasse
  ;  mit IOffset = 16d = $10
  add  eax, -$10              ; eax = "magischer Zeiger"-IOffset = Objektreferenz
  jmp  TMyClass.AnOperationIntroducedByMyInterface
Nach diesen Zeilen also wird die Methode AnOperationIntroducedByMyInterface aufgerufen und in eax befindet sich, gemäß der Delphi-Aufruf-Konvention nach Registern, die Referenz auf das Objekt, um Zugriffe auf die Pseudovariable Self zu ermöglichen.

Verfolgen wir den Verlauf von eax fällt auf, dass es zunächst mit der Interfacereferenz (=Objektreferenz+IOffset, siehe oben) beladen wurde und anschließend, in einer "magischen Sprungtabelle" durch harten Quelltext um den für die Klasse konstanten Wert von IOffset dekrementiert wird, um -wieder hart im Code pro Klasse- zur entsprechenden Methode zu springen.
Die tatsächliche Unterscheidung, welche Implementierung verwendet werden soll, wird also innerhalb des "magischen Bereichs" innerhalb des Speicherbereichs des Objekts getroffen: Hier wird pro Objekt eine Referenz auf den "Beginn" der "magischen Sprungtabelle", deren Struktur für alle Klassen identisch ist, abgelegt; Ein Zeiger pro Objekt.

Wir haben es also mit einer doppelten Indirektion zu tun, die dynamisch aufgelöst werden kann und deren veriablen Anteile die InterfaceReferenz, einem Zeiger auf die Objektreferenz plus einem für die Klasse-Interface-Kombination konstanten Offset IOffset sowie einem bei der Anlage eines Objekts erzeugten Verweis an dieser Stelle auf die "Methodentabelle" der Klasse für ein konkretes Interface, die, im Wissen um IOffset (weil konstant und zur Übersetzungszeit bekannt), die Objektreferenz rekonstruieren kann.


Ich hoffe, ich konnte zeigen, dass es keinen Grund für die Trennung zwischen der Identität eines Objekts und der eines Interface gibt und dass bei Delphi 5, 6 und 7 (ich habe die früheren Compiler leider nicht zur Hand) zumindest eine direkte Abhängigkeit zwischen den Objekt- und Interfacereferenzen bestehen, die unter Delphi der Identität entspricht.

Diese Erkenntnis:
Code:
In Delphi 5, 6 und 7 entspricht die Interfacereferenz [i]IRef[/i] des Interfacetyps [i]ITyp[/i] immer der Objektreferenz [i]ORef[/i] plus einem konstanten Wert [i]ClassTyp.IOffset(ITyp)[/i] seiner Klasse [i]ClassTyp[/i] in Abhängigkeit des betrachteten Interfacetyps.
Es gilt für beliebige aber feste [i]ORef[/i], [i]ITyp[/i] mit [i]ClassTyp = ORef.ClassTyp[/i] daher
  ORef.ClassTyp.IOffset(ITyp) = const
und somit
  IRef = ORef + ORef.ClassTyp.IOffset(ITyp) = const
kann nun mit dem Wissen, dass TInterfacedObject das Interface IInterface implementiert, verwendet werden, um von der Identität der Interfacereferenzen IRef1 und IRef2 eben dieses Typs auf die Objektidentität zu schließen, gemäß der Folgerung
Code:
 ORef1 = ORef2    =>   IRef1 = IRef2
und der aus Zeitgründen nicht hinreichend gezeigten Beschaffenheit von IOffset (es gilt: Für alle gültigen IOffset, ORef : IOffset<=ORef.InstanceSizePlusMagic, so dass insbesondere die Kombination niemals "in" den Speicherbereich eines von ORef verschiedenen Objekts zeigt) folglich gilt:
Code:
 ORef1 = ORef2    <=   IRef1 = IRef2
(man verzeihe mir bitte, diese etwas "pragmatische Darstellung").


Der Test nach
Delphi-Quellcode:
Result := (AnInterfaceRefOfAnObjectThatImplementsIInterface as IInterface)
  = (AnotherInterfaceRefOfAnObjectThatAlsoImplementsIInterface as IInterface);
ist somit auch unter Delphi möglich.


Interessant ist nun noch die Betrachtung von TInterfaceEntry.ImplGetter aber ich glaube, dass dies nach diesem trockenen Stoff niemanden mehr interessiert
gruß, choose
  Mit Zitat antworten Zitat