Realisierung des dynamischen Bindens in C: C_RuntimeJavalike-Library für C


 

Zunächst muss in Frage gestellt werden, ob eine Verwendung des Konzeptes des dynamischen Bindens ( als virtuellen Methoden in C++ bekannt) für die Teile von Maschinensteuerungen, die ansonsten sowieso in C programmiert werden müssen, überhaupt notwendig ist.

Was ist "dynmamisches Binden", was sind "virtuelle Methoden"

Der Begriff "virtuelle Methoden" stammt aus C++, dort können Methoden, die mit dem Schlüsselwort virtual gekennzeichnet sind, in einer abgeleiteten Klasse überladen werden. In Java sind Methode grundsätzlich überladbar, benötigen also nicht dieses spezielle Schlüsselwort. Das ganze wird auch als "dynmamisches Binden" bezeichnet.

Das Grundprinzip sei hier erläutert:

Wenn eine Klasse D von einer anderen Klasse A erbt, die Basisklasse A hat eine Methode a(), die abgeleitete Klasse hat ebenfalls die Methode a(), dann sagt man: "in D wird die Methode a() überladen". Es gibt dann grundsätzlich zwei Methoden, nämlich A.a() und D.a(). Bei einer Instanz von D wird auch dann die Methode D.a() abgearbeitet, wenn diese Instanz nur als Referenz auf die Basisklasse A bekannt ist. Bei C++ geschieht dies nur, wenn A.a() als virtuell bezeichnet wird, bei Java immer. Liegt eine Instanz von A selbst vor, dann wird jedoch immer A.a() gerufen.

Wie geht das? Im aufrufenden Code steht:

D refD = new D();
A refA = refD;    //refA zeigt in wirklichkeit auf eine Instanz vom Typ D
refA.a();         //es wird D.a() aufgerufen.

Es kann also nicht sein, dass direkt die Funktion D.a() gerufen wird, im Kontext hat der Compiler davon keine Kenntnis, die Zuweisung der Instanz vom Typ D auf den refA erfolgt gegebenfalls an ganz anderer Stelle. Die Information, welche Methode aufgerufen wird, geht nur über Kenntnis der referenzierten Daten. Das ist in C nur lösbar durch einen indirekten Programmaufruf, in Assembler ist das ein indirekter Sprungbefehl. Bei C++ ist das maschinentechnisch ebenso gelöst: Die Indirektion wird über die sogenannte virtual table vermittelt.

Das Ganze ist ein Kernproblem, dass in C++ gelöst ist und in C nicht bekannt ist. Probleme solcher Art werden in C üblicherweise zu Fuß programmiert.

Wozu braucht man dynamisches Binden?

Es gibt zwei verschiedene und wesentliche Gründe, Benutzung von Interfaces und das Ansprechen der tatsächlichen instanzabhängigen Methode, wenn die Instanz nur über eine Basisklasse bekannt ist. Realisierungstechnisch entspricht beides dem gleichen Konzept, von der Intension her sind sie durchaus als verschieden zu betrachten.

Nutzung von Interfaces

Über ein Interface sind Instanz von Datenanordnungen (Klassen) ansprechbar, ohne diese Instanzen selbst zu kennen. Eine Klasse kann mehrere Interfaces haben, jeweils für bestimmte Zwecke, ein Interface kann von mehreren Klassen implementiert werden. Ein Interface (definiert neben Konstanten) nur Methodenaufrufe und enthält selbst keine Daten.

In Java sind Interfaces von Klassen unterschieden und als Grundkonzept der Sprache verankert. In C++ sind Interfaces als Klassen in gleicher Weise bildbar, dort aber nicht als fester Begriff bekannt. Den Interfaceklassen in C++ sind Klassen, die ausschließlich abstrakte Methoden

virtual methode(parameter)=0;

enthalten. Die Interfaceklassen enthalten Methoden, die nur definiert werden, aber ohne methoden-body. Welche Methode dann tatsächlich gerufen wird, hängt davon ab, welche Klasse das Interface implementiert. Über ein Interface kann eine Instanz bekannt sein, und zwar in diesem Fall über einen sehr vereinfachten pauschalisierten Zugang. Vorteil: Außen braucht nichts von Internas bekannt zu sein, Kapselung, Objektorientierung.

Aus der Assemblerprogrammierung sind die sogenannten "Sprungleisten" bekannt, das ist ein Konzept, mit dem verschiedene Implementierungen ohne Linker-Lauf auf die selbe Schnittstelle ausgerichtet werden können. Diese Sprungleisten sind eine alte Erscheinung dieses Konzeptes.

Die Definition von ...TODO

Das Konzept der Interfaces

Aufruf der zur Instanz passenden Methode ohne Kenntnis der Instanz selbst

Die Arbeit mit den überladenen dynamisch gebundenen Methoden ist ein Grundkonzept der Objectorientierung:

Oft möchte man ein Object nicht in seiner tatsächlichen Ausprägung kennen und bearbeiten, sondern möchte 'nur' eine gewissen Grundausprägung, programmtechnisch eine der Basisklassen, kennen. Damit wird dieser Programmteil unabhängig von einem anderen Programmteil, der Objekte spezifisch ausprägt. Aber dennoch soll immer die tatsächlich für den jeweiligen Objekttyp vorgesehene Methode abgearbeitet werden, (ohne den Objekttyp kennen zu müssen!!!).

Ein Schulbeispiel dazu: Eine Gehaltsabrechnung iteriert über alle Mitarbeiter und berücksichtigt dabei nicht den Unterschied Lohnempfänger / Gehaltsempfänger. Es wird immer eine Methode calcWage() der Basisklasse Mitarbeiter gerufen. Aber je nachdem der (Datensatz des) Mitarbeiter tatsächlich vom Typ Lohnempfänger oder Gehaltsempfänger ist, wird die passende Berechnung ausgeführt.

Wie kann das dynamische Binden (virtuellen Methoden) in C abgebildet werden:

Grundsätzlich ist die Aufrufadresse einer Methode nicht direkt im aufrufenden Code verankert, sondern hängt an der Instanz der Daten.

In C++ gibt es in der Dateninstanz zwischen den eigentlichen Daten einen oder mehrerere Zeiger (je nach Vererbungs-Verhältnissen) auf die sogenannten virtuellen Tabellen. Die virtuellen Tabellen sind ein Vektor von Sprüngen zu den konkreten Methoden des Instanztypes. Dadurch, dass die Zeiger auf die jeweils richtigen Tabellen in der Instanz stehen und von dort gelesen werden, werden die instanztyprichtigen Methoden gerufen. Das ist bereits das Grundprinzip.

Beim Aufruf einer dynamisch gebundenen ("virtuellen") Methode baut der Compiler also nicht deren Startadresse direkt in den Code ein, sondern den Index innerhalb der virtuellen Tabelle. Über einen "indirekten call" unter Kenntnis der zutreffenden virtuellen Tabelle, ermittelt über die Typinformation in der Instanz, erfolgt der Aufruf.

Damit sieht der Aufrufer immer eine Tabelle virtueller Methoden in einer bestimmten Reihenfolge, die dem Datentyp, der dem Aufrufer bekannt ist (Interface, Basisklasse) entspricht. Der Zeiger auf die Daten führt der Aufrufer mit und übergibt sie den Methoden, selbst kann er diesen bei Interfaces niemals gebrauchen, bei Basisklassen nur bedingt (private-Kapselung der Daten).

Vergleich mit C++-Lösung

In C++ wird der beschriebene Zeiger auf die virtuellen Methoden innerhalb der Daten der Instanz geführt. Je nach Anzahl der Basisklassen (Interfaces sind hier ebenfalls Basisklassen, aber ohne Daten) enthalten die Instanzdaten mehrere Zeiger, am Beispiel:

class B: A, I, J{...}

Die class B soll die betrachtete Klasse sein, class A ist eine Basisklasse, class I und class J könnten Interfaces sein. Die Daten einer Instanz von B enthalten dann folgende Bestandteile:

vfptr_AB: Zeiger auf virtuelle Tabelle der Klasse B, diese beinhaltet die in B überladenen Methoden aus A
data_A:  Daten der Basisklasse A
vfptr_IB: Zeiger auf virtuelle Tabelle des Interfaces I für Implementierung in B
vfptr_JB: Zeiger auf virtuelle Tabelle des Interfaces J für Implementierung in B
data_B:  Daten der Klasse B

Das ist ein Beispiel mit Einfachvererbung von Klassen mit Daten, die Mehrfachvererbung bezieht sich nur auf die Interfaces. Bei echter Mehrfachvererbung, beispielsweise wenn die Klassen I und J auch Daten enthalten, stehen deren Daten jeweils zwischen den vptr-Zeigern.

Wenn eine Instanz über ein Interface gezeigert wird, dann zeigt der Datenzeiger auf diejenige Stelle in den Daten, an denen der Zeiger auf die entsprechende virtuelle Tabelle steht. Beim Aufruf der Methode wird dieser Zeiger korrigiert, die Korrektur ist Bestandteil der aufgerufenen Methode, nicht des Aufrufes, und wird in der Regel in die Folge der Aufrufanweisungen eingebaut. Das ist beim Debuggen zu beachten. Die eingebauten Debugger (zum Beispiel Visual Studio) berücksichtigen dieses und zeigen die Daten so an, als ob der Zeiger am Anfang der Klassendaten stünde. Für eine manuelle Auswertung eines Zeigers auf eine Instanz muss das aber berücksichtigt werden.

In C: Kenntnis des Datenaufbaus kann wichtig sein!

In C möchte man sehr häufig die byteweise Struktur der Daten, deren Abbildung im RAM kennen. Beispielsweise wenn Daten als Byteabbild herausgelesen werden sollen und dann von einem anderen Tool, gegebenenfalls auch manuell, analysiert werden sollen. Es kann in diesem Zusammenhang oder für den Datenaustausch über Kommunikationskanäle auch wichtig sein, dass die Datenstrukturen von verschiedenen Compilern bei den selben Headerfiles gleichartig erzeugt werden. Beispielsweise werden Daten in einer Maschinensteuerung benutzt, die Daten werden als Bytepaket auf einem PC aufbereitet und über eine Netzwerkkommunikation der Maschinensteuerung geschickt. Beide Programme, der Erzeuger auf dem PC und der Verwender im embedded Controller, sollen den selben Headerfile für die Datenbeschreibung benutzen und die Daten unmittelbar auswerten. Es muss damit gerechnet werden, dass beide Systeme sehr verschiedene Compiler verwenden, beispielsweise auf dem embedded Controller einen Compiler für 16-bit-CPU. Für solche Aufgabenstellungen kann man auch die Daten auf dem PC beispeilsweise auch in Java aufbereiten. Es ist möglich, die Informationen aus dem Headerfile unmittelbar in ein Java-Programm umzucodieren, dieses bereitet damit die Daten in einem Bytefeld auf.

Aus C++-Sicht werden solche Art von Daten "Plain Old Data" POD genannt. In C++ und viel stärker noch in Java ist die Sicht auf Daten so, dass die Datenanordnung als intern betrachtet wird. Ein Datenaustausch über das direkte Abbild dieser internen Daten soll nicht erfolgen. In Java gibt es als Ersatz das System der Serialization mit dem Interface Serilizable zum Abbild der Daten, nebst der Möglichkeit mit Reflections und Byte-Datenfeldern zu arbeiten. In C++ gibt es allerdings eine solche standardisierte Lösung nicht.

C-Lösung: Virtuelle Tabelle in den Reflection

Aus anderen Gründen basieren alle Klassen (adäquat zu in Java) auf der Klasse Object_Jc. Die Daten von Object_Jc stehen am Anfang der Datenstruktur einer C-Klasse. Object_Jc bildet wie Object in Java grundsätzliche Eigenschaften aller Instanzen ab, darunter auch die Möglichkeit, Typinformationen über die Instanz zu erhalten. Das ist das Prinzip der Reflection. Die virtuelle Tabelle kann sich dort einordnen. Konkret ist Folgendes formulierbar:

Der Anwender definiert über ein Makro die Datenstruktur der Reflection zu jeder seiner Klasse im h-File. Das Makro ist einfach zu schreiben:

TYPEDEF_REFLECTION_Jc(UserClass);

Das Makro ist in Class_Jc.h definiert und expandiert für den Compiler folgendes:

typedef struct Reflection__##TYPE##_t
{ Class_Jc reflection; 
  Vtbl_##TYPE vtbl;
} Reflection__##TYPE; 
 
extern const Reflection__##TYPE reflection__##TYPE;

Das heißt: Der konkrete Reflectiontyp der Klasse enthält neben der aus Java bekannten Datenstruktur Class_Jc (Java.lang.Class) eine Datenstruktur vtbl. Das ist die Tabelle der virtuellen Methoden. Der Datentyp Vtbl_UserClass zuvor im entsprechendem Headerfile definiert werden, und zwar nach folgendem Muster:

/** The virtual table useable for C-like-virtual methods.*/
typedef struct Vtbl_ExampleB_t
{ Vtbl_ExampleA         ExampleA;   //the super class
  Vtbl_iExample         iExample;   //interface
  Vtbl_iExample2        iExample2;  //interface
  MT_methodB_ExampleB*  methodB;    //own methods
  MT_methodC_ExampleB*  methodC;
} Vtbl_ExampleB;

Die hier benutzten virtuellen Tabellen der Basisklassen sind in Headerfiles, die includiert werden müssen, adäquat definiert. Die unterste alle Basisklassen ist Object_Jc mit der in Object_Jc.h definierten

typedef struct Vtbl_Object_Jc_t
{ MT_clone_Object_Jc*    clone;
  MT_equals_Object_Jc*   equals;
  MT_finalize_Object_Jc* finalize;
  MT_hashCode_Object_Jc* hashCode;
  MT_toString_Object_Jc* toString;
} Vtbl_Object_Jc;

Die Typen MT_... sind über typedef definierte Funktionsprototypen, also beispielsweise

typedef String_JcRef MT_toString_Object_Jc(Object_c* ythis);

Diese Prototypen werden auch beim Aufruf der virtuellen Methoden benötigt, also einmalig im entsprechendem Header definiert, siehe Folgeabschnitt und Übersicht im Abschnitt Gesamtmuster für virtuelle Methoden und Interfaces in C und C++.

Im Code der Klasse (c-File) muss diese Tabelle gefüllt werden. Das sieht etwa wie folgt aus:

const Reflection__UserClass reflection__UserClass =
{{ ... Reflection-Definitionen ...
, { { { clone_Object_F      //methods of object
      , equals_Object_F
      , finalize_Object_F
      , hashCode_Object_F
      , toString_ExampleB_F
      }
    , methodA_ExampleB_F    //extends ExampleA
    }
  , { methodA_ExampleB_F   //implements iExample
    , methodB_ExampleB_F
    }
  , { methodB_ExampleB_F   //implements iExample2
    }
  , methodB_ExampleB_F      //methods of this
  , methodC_ExampleB_F  
  }
};

Im Beispiel stehen am Anfang geschachtelt Methoden, die aus Object_Jc dynamisch gebunden werden, wobei alle Methoden außer toString() aus Object_Jc original übernommen werden. Das Suffix _F deutet darauf hin, das es sich um die finalen Methoden der entsprechenden Klasse handelt. Demgegenüber wird die selbe Bezeichnung ohne _F hinten für die virtuell definierten Methoden verwendet, siehe Notationskonventionen.

C-Lösung: Definition und Aufruf der virtuellen Methoden von Klassen

Für jede Methode gibt es dann eine Typdefinition und ein define nach folgender Art:

typedef void MT_userMethod_UserClass(UserClass* ythis, int val1, float val2);
#define userMethod_UserClass(OBJ, P1, P2) \ VCALL_CLASS_Jc(userMethod, UserClass, MT_userMethod_UserClass, OBJ) , P1, P2 _endVCALL_Jc

Das Makro VCALL_CLASS_Jc hat es etwas in sich, setzt aber letzlich nur minimalen Maschinencode ab. Es ist wie folgt definiert (Class_Jc.h):

#define VCALL_CLASS_Jc(NAME, CLASS, METHODTYPE, OBJP)
 ( (METHODTYPE*)
   ( (  (ReflectionVtbl_Jc*)( ((Object_Jc*)(OBJP))->reflectionClass )
     )->vMethods[ IDX_VTBL_Jc(NAME,CLASS) ]
   )
 )
 VCALLparam_Jc (OBJP)

In diesem Makro wird ein weiteres Makro verwendet:

#define IDX_VTBL_Jc(TAG, CLASS)
 ( ( (int) &( ((Vtbl_##CLASS*)(0x1000))->TAG )
     - 0x1000
   )
   /sizeof(Void_MethodVoid)
 )

Das ist eine einfache Adressrechnung, die zur Compilezeit ausgeführt eine Konstante ergibt. Dabei wird die Position der jeweiligen virtuellen Methode oder Sub-Tabelle als Index ermittelt. Die 0x1000 ist lediglich eine Hausnummer, die wieder subtrahiert wird. Manche Compiler streiken, wenn man

(int) &( ((Vtbl_##CLASS*)(0))->TAG )

angibt.

Zur besseren Lesbarkeit sind die Makros mehrzeilig geschrieben, im Original ist es eine lange Zeile. Was wird bei VCALL_CLASS_Jc getan?

An der Aufrufstelle wird wie üblich bei C-like-Methodenaufrufen geschrieben:

userMethod_UserClassName(instance, param1, param2);

Das ist also recht einfach und überschaubar. Der Funktionsaufruf ist in Wirklichkeit ein Makro. Der Aufrufer braucht das nicht zu wissen, es könnte auch eine nichtvirtuelle Methode sein. Daher wird hier nicht die für Makros übliche Großbuchstabenschreibweise verwendet.

Das Makro wird expandiert über zwei Stufen, der Präprozessor macht daraus eine recht lange Folge. Die Frage ist, was kommt im Maschinencode an, wie komplex ist der Aufruf für die wirkliche Abarbeitung. Interessanterweise sehr einfach und viel kürzer als der zu vermutende C-Code. Das liegt daran, das eigentlich nur ein Zeiger besorgt wird, darüber eine Tabelle ermittelt und mit einem konstantem Index darauf zugegriffen. Das sind zwei indirekte Adressoperationen, dann erfolgt der Aufruf. Die Organisation der Parameter im Stack erfolgt genauso wie bei einem normalen Methodenaufruf. Der dem Compiler bekannte Funktionstyp (METHODTYP), sorgt für die richtige Organisation.

Folgend wird dargestellt, welche ein Visual-Studio-6-Compiler im Debugmode (!) an Befehlen für den oben gezeigten Aufruf erzeugt:

336:      methodC_ExampleB(data, 45, 3.14F);
00403244   mov         esi,esp
00403246   push        4048F5C3h                   //die Zahl 3.14F (param2) wird in den Stack gelegt
0040324B   push        2Dh                         //die Zahl 45 (param1) wird in den Stack gelegt
0040324D   mov         eax,dword ptr [ebp-4]       //Der Zeiger zur Instanz, hier data als Parameter
00403250   push        eax                         // wird in den Stack gelegt
00403251   mov         ecx,dword ptr [ebp-4]       //Zeiger zur Instanze als (Object_Jc*)
00403254   mov         edx,dword ptr [ecx+8]       //Laden des Zeigers auf reflectionClass
00403257   call        dword ptr [edx+58h]         //Indirekter Aufruf der Methode, da alle Indizierungen konstante sind,
                                                   //ergibt sich eine Konstante (58h) als Offset,
                                                   //die direkt im Aufrufbefehl notiert wird.
0040325A   add         esp,0Ch                     //Beenden des call, Stack aufräumen.

C-Lösung: Definition und Aufruf der virtuellen Methoden von Interfaces

Beim Aufruf von virtuellen Methoden von C-Klassen, auch von Basisklassen, genügt die Kenntnis der Instanz. Damit ist diese Lösung auch verwendbar in C++-Umgebungen, bei denen nur mit einfachen Referenzen (nicht mit dem Runtimeheap-GarbageCollector) gearbeitet wird. Bei der Verwendung des virtuellen Aufrufes von Methoden von Interfaces ist dagegen die erweiterte Referenz notwendig.

Zunächst soll nochmals das Verfahren in C++ gezeigt werden: In C++ stehen in den Daten der Instanz mehrere Zeiger auf virtuelle Tabellen, und zwar pro Basisklasse (ein Interface ist eine Basisklasse) ein Zeiger. Diese Zeiger sind in den Daten entsprechend der Vererbungsstruktur (Mehrfachvererbung) verteilt. Sie stehen nicht unbedingt am Anfang hintereinander. Wenn eine Instanz über einen Basisklassenzeiger (oder Interfacezeiger) referenziert wird, dann enthält dieser Zeiger nicht die direkt die Instanzadresse, sondern die Adresse innerhalb der Instanz, auf der die Adresse der jeweiligen virtuellen Tabelle steht. Diese Adresse liegt "etwas daneben". Da die virtuelle Tabelle direkt gezeigert wird, kann ohne Kenntnis des Instanztypes die virtuellen Methoden gerufen werden. In den virtuellen Methoden, die ihrerseits dem Instanztyp angepasst sind, erfolgt dann am Eingang die Korrektur des this-Zeigers auf die entsprechenden Instanzdaten. Beim Debuggen beispielsweise in Visual Studio 6 muss man schon genau beobachten, da der this-Zeiger in der Varaiblenanzeige vom Debugger korrigiert dargestellt wird, der tatsächliche Wert ist nur in der Registeranzeige beobachtbar.

Wenn also in C++ laut dem obigen Beispiel eine Instanz als Interface J bekannt ist, dann zeigt der Referenzzeiger auf die Stelle, an der vfptr_JB: steht. Es werden dort die virtuellen Methoden aus J für die Class B aufgerufen. Diese kennen die Stellung des Interfaces J innerhalb der Daten und korrigieren den this-Zeiger um einen entsprechenden Offset, damit er den Beginn der Instanzdaten entsprechend der Class B zeigt.

In der C-Lösung soll dieser Mechanismus nicht nachgebaut werden.

Stattdessen wird ein anderer Weg benutzt:

Wie in Java ist in C nur Einfachvererbung von Basisklassen vorgesehen. Das ist weniger komplex als die in C++ unterstützte komplexe Mehrfachvererbung. Damit kommt man bezüglich Klassen und Basisklassen mit nur einer virtuellen Tabelle aus. Diese enthält ab Anfang die virtuellen Methoden der jeweiligen Basisklassen in der Reihenfolge der Ableitung, zuletzt also die virtuellen Methoden der Klasse selbst, bereit für weitere Vererbungshierarchien.

Bei Interface liegt quasi eine Mehrfachvererbung vor, allerdings im Gegensatz zu C++ nicht mit eigenen Daten. Damit ist es auch diesbezüglich möglich, eine einfachere Lösung als in C++ zu implementieren.

Der Zeiger auf die Instanzdaten zeigt immer an den Anfang der Instanz, das sind die Daten der Basisklasse aller Klassen Object_Jc. Es gibt nur die eine virtuelle Tabelle, angebunden an die Reflections aus der Basisklasse Object_Jc. Damit ergibt sich das Problem, den richtigen Ausschnitt für die Interface-Sicht in der gesamten virtuellen Tabelle zu finden. Anstatt wie in C++ den Zeiger auf die Instanz zu variieren (er zeigt in C++ auf die Adresse der Referenz der jeweiligen virtuellen Tabelle innerhalb der Daten der Instanz) wird in der Referenz ein Index auf die virtuelle Tabelle geführt. Dazu (auch aus Gründen der Garbage Collection) besteht eine Referenz auf Intanzen nicht aus einem einfachen Zeiger, sondern ist erweitert.

Eine Referenz auf eine Instanz enthält insgesamt drei Informationen in zwei Speicherworten:

Beide Indizes passen auf ein Speicherwort, in einem kleinen Speichermodell auch auf ein 16-bit-Wort.

Eine Referenz ist nach folgendem Schema gebaut:

typedef struct Type_r_t
{ /** The base data of references*/
  ObjectRefValues_c refbase;
  /** The object be referenced from here*/
  struct Type_c_t* ref;
}Type_r;

Diese Struktur ist so definiert, dass sie in zwei Prozessor-Register passt. Innerhalb ObjectRefValues_Jc, definiert in Object_Jc.h, steht der Zeiger auf die virtuelle Tabelle.

Die Möglichkeit, eine Referenz in eine Struktur zu packen, ohne dass ein bedeutsamer Effektivitätsverlust in der Maschinencode-Abarbeitung die Folge ist, gepaart mit der Notwendigkeit, in der Referenz sowieso eine weitere Information neben dem Zeiger auf die Daten speichern zu müssen (für Garbage Collection), führt zur Idee, auch die Referenz auf die virtuelle Tabelle aus den Daten auszulagern und in die Referenz zu packen, damit die Daten von diesen nicht zugehörigen Ballast zu befreien.

Aufgrund der Tatsache, dass in einer Referenz in einem Prozessor-Register neben dem Zeiger auf die Virtuelle Tabelle auch noch den Index für die Rückwärtsreferenz für die Garbage Collection hineinpassen muss, führt allerdings dazu, dass nicht die einfache Referenz auf die virtuelle Tabelle gespeichert werden kann. Ohne bedeutsamen Rechenzeitverlust beim Aufruf der virtuellen Methode kann dort auch ein Index gespeichert werden, aus dem die Adresse sich berechnen lässt.

Die Definition einer virtuellen Methode aus Interfaces sieht ähnlich wie die aus Klassen aus, das Define ist ein anderes:

typedef MT_userMethod_UserInterface(Object_Jc* ythis, int val1, float val2);
#define userMethod_UserInterface(REF, P1, P2) \ VCALL_IFC_Jc(userMethod, UserInterface, MT_userMethod_UserInterface, REF) , P1, P2 _endVCALL_Jc

Beim Aufruf der Methode wird der Zeiger vom Typ Object_Jc übergeben, da ein Interface keine Daten kennt. Beim Aufruf des Makros wird die Referenz übergeben.

Das Makro VCALL_IFC_Jc ist ähnlich aber etwas komplexer als VCALL_CLASS_Jc (definiert in Class_Jc.h):

#define VCALL_CLASS_Jc(NAME, CLASS, METHODTYPE, OBJP)
 ( (METHODTYPE*)
   ( (  (ReflectionVtbl_Jc*)( ((Object_Jc*)((REF).ref))->reflectionClass )
     )->vMethods[ ((REF).refbase & mIdxVtbl_ObjectRef) + IDX_VTBL_Jc(NAME,IFC) ]
   )
 )
 VCALLparam_Jc (Object_c*)( (REF).ref )
 

Zur besseren Lesbarkeit ist das Makro mehrzeilig geschrieben, im Original ist es eine lange Zeile. Im Vergleich zu VCALL_CLASS_Jc wird folgendes anders getan:

An der Aufrufstelle wird das gleiche wie bei C-like-Klassen-Methodenaufrufen geschrieben, hier muss aber eine Referenz (die Interfacereferenz) auf ein Object vorhanden sein:

userMethod_UserInterface(reference, param1, param2);

Die Befehlsfolge im Maschinencode ist etwas länger, es kommt die Addition des Offset aus refbase und ein indirekter Zugriff hinzu:

340:      methodB_iExample2(ifc2, 25);
0040328D   mov         esi,esp
0040328F   push        19h                        //die Zahl 25 (param1) wird in den Stack gelegt
00403291   mov         eax,dword ptr [ebp-8]      //Der Zeiger zur Instanz aus der Referenz, hier ifc2
00403294   push        eax                        // wird in den Stack gelegt. Die Referenz ifc2 steht auf dem Stack.
00403295   mov         ecx,dword ptr [ebp-8]      //Zeiger zur Instanze als (Object_Jc*)
00403298   mov         edx,dword ptr [ecx+8]      //Laden des Zeigers auf reflectionClass
0040329B   mov         eax,dword ptr [ifc2]       //der refbase wird gelesen, der Befehl ist anders dargestellt aber
0040329E   and         eax,0FFFFh                 //identisch mit ... ptr [ebp-12], es wird maskiert
004032A3   call        dword ptr [edx+eax*4+30h]  //Die Addition des offset und call erfolgt in einem Befehl.
004032A7   add         esp,8                      //Beenden des call, Stack aufräumen.

Es entsteht also auch hier kein großer Aufwand. Der Compiler und der Befehlssatz für die Intel-Architektur ist recht optimal. Bei einem weniger leistungsfähigem Prozessor werden die Offsetadditions-Befehle einzeln ausgeführt, es sind aber auch nur wenige Befehle. Bei extrem schnellen Echtzeitanforderungen wird man kaum Interfaces benutzen, wenn der Prozessor aber daneben in langsameren Abtastzeiten noch umfangreiche Verwaltungsaufgaben zu erledigen hat, sind die Interfaces in C durchaus optimal.

Definition von Interfaceklassen, Definition der virtuellen Tabelle für Interfaces

Interfaceklassen selbst bilden sich in C nicht als Struktur ab. In C++ gibt es die normale Class-Definition, die die notwendigen abstrakten Definitionen (virtual ... =0) enthält.

In C ist aber eine Referenz vom Typ der Interfaceklasse unumgänglich. Um die Anwendung zu vereinfachen, gibt es ein Define. Der Anwender schreibt in C lediglich:

TYPEDEF_INTERFACE_Jc(UserInterface)

Damit ist die Deklaration des Interfacetypes, des Referenztypes, der Reflection erledigt.

Es ist möglich, ein Interface auch so zu definieren, dass es in C++ als interface-class definiert wird, beim Aufruf der Methoden sind also keine erweiterten Referenzen und keine handgeschriebene virtuelle Tabelle notwendig, gleichzeitig ist das auch für C quellcodekompatibel. In diesem Fall soll der Anwender formulieren:

TYPEDEF_INTERFACE_Jcpp(UserInterface)
  #if defined(__CPLUSPLUS_Jcpp) && defined(__cplusplus)
    virtual void method1(param)=0;
    virtual int method12(param)=0;
  #endif
_endINTERFACE_Jcpp

Hier liefert das Makro TYPEDEF_INTERFACE_Jcpp adäquates wie TYPEDEF_INTERFACE_Jc ab, jedoch wird vom Makro eine öffnende Klammer für die class-Definition erzeugt. Der Block innerhalb der Klassendefinition kann in der C-Variante nicht gebraucht werden, daher muss er extra mit bedingter Compilierung umschlossen werden. Das Makro _endINTERFACE_Jcpp liefert nur die schließende Klammer der Klassendefinition.

Die Definition von TYPEDEF_INTERFACE_Jcpp ist davon abhängig, ob in C oder C++ gearbeitet wird und ob erweiterte Referenzen verwendet werden. Das ist mit bedingter Compilierung des Makro realisiert. Im Falle Verwendung von C ist das Makro TYPEDEF_INTERFACE_Jcpp identisch mit TYPEDEF_INTERFACE_Jc.

Insgesamt werden damit folgende Identifier deklariert:

Identifier Verwendung C C++ mit Ref C++ ohne Ref
UserInterface Als Zeigertyp in der Form UserInterface* auf eine Instanz Identisch mit Object_Jc. Damit wird eine Instanz als Interface mit Object_Jc* angesprochen. Die definierte Klasse wird so bezeichet
UserInterface_r Referenz vom Typ des Interfaces auf die Instanz

Das ist die erweiterte Referenz der Form

typedef struct UserInterface_r_t
{ ObjectRefValues_c refbase;
  Object_c* ref;
}UserInterface_r;
Wie in C, aber der ref-Zeiger ist mit UserInterface* typisiert. In C++ ohne Referenz ist die Referenz als UserInterface* definiert.

Zur Definition eines Interfaces in C ist die Definition der Tabelle der virtuellen Methoden und die Definition der virtuellen Methoden selbst notwendig. Der Anwender muss folgendes notieren:

typedef void MT_methodA_iExample(Object_c* ythis);


typedef void MT_methodB_iExample(Object_c* ythis, int val);


typedef struct Vtbl_iExample_t
{ MT_methodA_iExample*   methodA;
  MT_methodB_iExample*   methodB;
} Vtbl_iExample;

Damit wird die virtuelle Tabelle des Interfaces definiert. Dieser Typ wird später in der virtuellen Tabelle der implementierenden Klasse eingeordnet.

Gesamtmuster für virtuelle Methoden und Interfaces in C und C++

Nachfolgend sind Muster angegeben, wie in einem Anwenderprogramm zu formulieren ist. Die Muster können über Kopieren und Einfügen (copy and paste) diesem Text entnommen werden, entsprechende Passagen werden gestrichen oder dupliziert und Bezeichner umbenannt, dann hat man diesbezüglich eine passende Anwendersoftware. Anmerkungen stehen unter den hell-türkis markierten Muster-Blöcken. Bestimmte Passagen sind mit farbiger Schrift markiert und darunterstehend erläutert. Die Beispiele sind für den komplexeren Fall geschrieben, dass sowohl in C als auch in C++ gearbeitet werden soll. Bei einer reinen C-Anwendung können die dunkelblauen Passagen gestrichen werden.

Inhalt einer h-Datei für die Definition eines Interfaces

/**UserInterface helps something
*/
TYPEDEF_INTERFACE_Jcpp(UserInterface)
  #if defined(__CPLUSPLUS_Jcpp) && defined(__cplusplus)
    virtual void method1(int par1, Usertype2* par2)=0;
    virtual void method2(param)=0;
  #endif
_endINTERFACE_Jcpp

/** method1 makes something ... */
typedef int MT_method1_UserInterface(Object_c* ythis, int par1, Usertype2* par2);
#define method1_UserInterface(REF, P1, P2) \
  VCALL_IFC_Jcpp(method1, UserInterface, MT_method1_UserInterface, REF), P1, P2 _endVCALL_Jc

typedef struct Vtbl_UserInterface_t
{ MT_methodA_iExample*   method1;
  MT_methodB_iExample*   methodB;
} UserInterface;


/** Declaration of the reflection class and external reference of reflection instance.*/
TYPEDEF_REFLECTION_Jcpp(UserInterface)


Inhalt einer h-Datei für die Definition einer Klasse, die als Basisklasse dient und virtuelle Methoden enthält.

/** Definition of a base clase with virtual method
 * java: abstract class ExampleBase
 *       { int a; float b;
 *         abstract void methodA(float value);
 *       }
 */
typedef struct ExampleBase_t
{ Object_c object;
  int a;
  float b;
} ExampleBase;
 
TYPEDEF_REFERENCE(ExampleBase)

/** methodA makes something ... */
typedef void MT_methodA_ExampleBase(ExampleBase* ythis, float value);
#define methodA_ExampleBase(THIS, P1) VCALL_CLASS_Jc(methodA, ExampleBase, MT_methodA_ExampleBase, THIS), P1 _endVCALL_Jc
 
/** The virtual table useable for C-like-virtual methods.*/
typedef struct Vtbl_ExampleA_t
{ Vtbl_Object_Jc         Object_c;
  MT_methodA_ExampleBase*   methodA;
} Vtbl_ExampleBase;
 
TYPEDEF_REFLECTION_Jc(ExampleBase)

#if defined(__CPLUS_CRUNTIMEJAVALIKE__) && defined(__cplusplus)
class ExampleBasepp: public virtual ObjectBase_C, public ExampleBase
{ virtual void methodA(float value)=0;
};
#endif  //__CPLUS_CRUNTIMEJAVALIKE__

Da die Basisklasse abstrakt ist, brauchen in der C++-Variante hier nicht die Methoden aus Object_Jcpp definiert werden. Sie sind virtuell und bleiben es bis zur weiteren Vererbung.

Inhalt einer h-Datei für die Definition einer Klasse, die von einer Basisklasse erbt und Interfaces implementiert

#include "UserInterface.h"
#include "ExampleBase.h"
...:TODO:

Da

 

Inhalt einer c-Datei für die Definition einer Klasse, die von einer Basisklasse erbt und Interfaces implementiert

Mögliche Fehlerquellen in der Programmierung

Wenn dann die über die virtuelle Tabelle gerufene Methode die selben Parameter hat (was bei einer ordentlichen Programmierung der Fall ist), dann kann nichts schief gehen.

Sollte die falsche Methode mit den falschen Parameter (-typen) gerufen werden, dann kann das bei unachtsamer Programmierung passieren. Das bekommt man beim Debuggen dann mit. Das selbige kann aber auch bei C++ und den dort implizit verarbeitenden virtuellen Tabellen der Fall sein, und zwar immer dann, wenn nach einer Änderung einer Klasse im Headerfile bezüglich virtueller Methoden (Anzahl, Reihenfolge, Parametertypen) der zugehörige Cpp-File nicht nochmal übersetzt wird, weil ein Maker das beispielsweise nicht festgestellt hat (Arbeit mit Libraries ...). Dann bekommt man genau die vergleichbaren Effekte.

Typcasting - Zuweisung von und zu Interfacereferenzen

xxx