DynamicLinkedCall

DynamicLinkedCall

Inhalt


Topic:.DynCall.

.


1 Möglichkeit und Notwendigkeit des Dynamischen Methodenaufrufs in C

Topic:.DynCall.DynamicCallUsage.

Last changed: 2011-07-03

Das dynamische Binden ist in C++ wie auch in Java und anderen ähnlichen Programmiersprachen gängige Programmierpraxis zur Implementierung von abstrakten Klassen und Interfaces, In C++ wird von virtuellen Methoden gesprochen. Ein Kürzel vtbl bezeichnet die virtuelle Tabelle, eine Adresstabelle für die konkreten Implementierungen der deklarierten virtuellen Methoden einer Klasse.

Beim dynamischen Binden handelt es sich um einen dynamischen (datenabhängigen) Aufruf von Methoden/Subroutinen, nicht um dynamische Libraries (dll). Hier ist sprachlich eine Verwechslung möglich. Daher ist es besser mit Blick auf das Laufzeitverhalten und nicht auf den Software-Erstellungsprozess von einem dynamischen Methodenaufruf (dynamic call) zu reden. Der Begriff dynamisches Binden ist als Gegensatz zum statischen Binden aufzufassen, der festen Bindung von Aufrufen zu aufgerufenen Subroutinen zur Compile/Linktime.

Das dynamische Binden bzw. der dynamische Methodenaufruf hat zwei verschiedene Aspekte:

In der Implementierung fließen beide Aspekte zusammen. Auch wird das Interfacekonzept oft mit dem Basisklassen/Aufruf abgeleiteter Methoden-Konzept gleichgesetzt. Das Konzept a) ist aber bereits bei älteren C- und Assemblerprogrammen bekannt, und zwar in Form einer sogenannten Sprungleiste. Wegen einer wünschenswerten Trennung beispielsweise von Betriebssystem und Applikation hat man als Eingang zum Betriebssystem auf einer festen Adresse eine Struktur mit Einsprungadressen für verschiedene Betriebssystemdienste angeordnet. Somit kann man eine Applikation unabhängig vom Betriebssystem erstellen und Linken, und man kann beides unabhängig austauschen.


2 Dynamischer Call in C++ - virtueller Methodenaufruf

Topic:.DynCall.CppVtblCall.

Last changed: 2011-07-03

In C++ wird in einer meist bei allen Compilern gleichartigen Art und Weise ein dynamischer Call ähnlich wie ein Aufruf über eine Sprungleiste ausgeführt. Die Sprungleiste ist die oben bereits erwähnte Virtuelle Tabelle (Vtbl). Die Adresse der Sprungleiste steht immer in den Daten, wegen dem Aspekt b) der Objektorientierten Programmierung. Der Aufruf von virtuellen Methoden über diesen Weg ist schnell, fast nicht langsamer als der direkte statisch gebundene Aufruf. Der nachfolgende Code-Ausschnitt eines Listings von Visual-Studio 6 zeigt eine solche Umsetzung:

 ; 291  :     { myModulA->startSystem(nPrioSocketThread);
 0087d  8b 8e 28 04 00 00   mov   ecx, DWORD PTR [esi+1064]
 00883  8b 96 a0 00 00 00   mov   edx, DWORD PTR [esi+160]
 00889  52                 push  edx
 0088a  8b 01              mov   eax, DWORD PTR [ecx]
 0088c  ff 50 08           call  DWORD PTR [eax+8]

Der erste Maschinenbefehl läd den Zeiger auf die Instanz myModulA in ecx. Vor dem call wird dann eax mit dem Zeiger auf die virtuelle Tabelle in den Daten geladen, hier am Anfang der Daten. Auf diesen Zeiger bezieht sich dann der call. Da die Methode die zweite virtuelle Methode ist wird eax+8 gerufen.

Kritik des Zeigers auf die virtuelle Tabelle in den Daten: Software kann Fehler enthalten. Nicht alle Fehler werden gefunden. Insbesondere Datenbereiche, die von mehreren Threads zugänglich sind, können auch mal falsch belegt werden. Wird der Zeiger auf die virtuelle Tabelle irgendwie überschrieben, dann läuft der Maschinencode dennoch wie hier gezeigt ab, die Bearbeitung wird aber auf eine falsche Adresse geführt. Das ist unkontrolliert. Es ist wahrscheinlich, dass danach die Bearbeitung undefiniert weiterläuft bis zu einem Stillstand (gemeinhin als Absturz bezeichnet). Es ist aber auch denkbar dass beispielsweise wegen einem Inkrement an der falschen Stelle die Adresse auf eine andere Position in der virtuelle Tabelle oder auf eine benachbarte Tabelle zeigt, alle virtuellen Tabellen liegen meist in einem gemeinsamen Bereich. Die Bearbeitung wird dann nicht abstürzen sondern nur etwas falsches tun, unbemerkt. Fehler in den Daten als Folge von anderen Softwarefehlern können oft erkannt werden, in dem Signifikanztests mit den Daten ausgeführt werden. Damit wird ein Softwarefehler irgend geeignet früher oder später erkannt. Wenn aber die Abarbeitungsfolge unkontrolliert ist, dann ist es zu spät. Daher ist diese zwar schnelle aber bezüglich Datensicherheit sehr unsichere Methode eher nicht vertrauenswürdig. Ein nicht erkannter Fehler führt zu unkontrolliertem Programmlauf.

Eine andere Problematik mit virtuellen Methoden und C++ ist folgende: Ein geänderter Headerfile mit oft unbemerkt geänderter Reihenfolge von virtuellen Methoden und ein vergessenes Neucompilieren von Libraries ist schnell passiert. Die Folge ist, dass die falschen virtuellen Methoden gerufen werden. Im Listing des Beispiels ist die Methode lediglich durch die Offsetangabe eax+8 bestimmt, nicht durch ein Label des Linkers. Ein solcher Fehler wird oft erst bei einem Einzeltest entdeckt.

Ein durchschnittlicher Anwendungs-Programmierer weiß oft nichts von den Mechanismen der virtuellen Methoden. Der C++-Compiler ergänzt automatisch die Klassendaten mit Zeigern auf virtuelle Tabellen und legt die virtuellen Tabellen als Datenstruktur an. Von der Existenz der virtuellen Tabellen erfährt man gegebenenfalls erst dann, wenn solche Fehler wie oben beschrieben passieren.


3 Dynamischer Call in C - Grundlagen

Topic:.DynCall.C_MtblCall.

Last changed: 2011-07-03

In C ist ein dynamischer Methodenaufruf ebenfalls möglich.

Im einfachsten Fall kann man einen Zeiger auf eine Funktionsadressentabelle benutzen. Dieser Zeiger wird außen ermittelt beispielsweise aufgrund switch-case des Typs der Daten. Das ist die zu Fuß-Programmierung. Der Funktionstabellenzeiger ist in gleicher Weise sensibel wie der vtbl-Pointer in C++. Wird er wegen eines Programmierfehlers versehentlich falsch belegt, dann ist der Fehler schwer aufspürbar weil der Instructionpointer im Prozessor vollkommen undefinierte Wege gehen kann. Man sollte also den Funktionstabellenzeiger nicht im Datenbereich dauerhaft speichern sondern nur im Stack halten. Der Stackbereich ist insoweit sowieso besonders zu schützen (Memory-Protection gegen andere Threads), als das auch dort andere Daten wie Rücksprungadressen gleichermaßen sensibel sind.

Möchte man einen Objektorientierten Ansatz, dann muss quasi mit Angabe der Daten automatisch der Zeiger auf die Funktionstabelle mitgegeben werden können In C++ sind das die vtbl-Zeiger in den Daten, die wie oben dargestellt unsicher sind.

Kurzer Einschub - Folgende Begrifflichkeiten werden folgend verwendet:

Im Objektorientierten Ansatz muss aber letzlich die Auswahl der Methodentabelle aus den Daten möglich sein. Das ist ein Problem der Typerkennung der Daten, die Daten müssen eine Typinformation mitführen. Die Methodentabelle muss dem Datentyp entsprechen.

In der CRuntimeJavalike-Library sowohl für manuelle Anwendung als auch im Rahmen der Verwendung in Java2C wird dazu folgender Ansatz gewählt:

 ObjectJc* oData = (ObjectJc*)myData;

Es gibt damit mehrere Möglichkeiten der Formulierung von Methodentabellen. Es kommt darauf an wozu die Methodentabellen benutzt werden:

Eine Methodentabelle ist als Folge von Adressen auf C-Funktionsprototypen formulierbar. Zweckmäßigerweise sollten zuerst die Prototypen definiert werden, um danach die Struktur der Methodentabelle im Headerfile zu definieren:

typedef void MT_MethodA(int value); //Funktionsprototyp-Typ
typedef void MT_MethodV(float value); //Zweiter Funktionsprototyp
typedef struct Mtbl_Topic_t
{ MT_MethodA* MethodA;
  MT_MethodB* MethodB;
} Mtbl_Topic;

Die Schreibweise MT_MethodA* bezeichnet den Zeigertyp auf eine Funktion mit einer Signatur, wie sie oben darüber definiert ist. MethodA ist die Zeiger-Bezeichnung. Es kann auch sein, dass es mehrere verschiedene Methoden mit der selben Signatur gibt.

Die Methodetabelle ist dann als externe Variable definiert beim Linken irgend geeignet auffindbar. Das wäre die Sprungverteiler-Funktionalität nach a):

extern Mtbl_Topic const mtblTopic;

oder der Zeiger auf die Methodentabelle ist Bestandteil der Daten einer Klasse, was für die Variante b) notwendig ist:

typedef struct MyClass_t
{ int x;
  Mtbl_Topic const* mtblTopic;
} MyClass;

In beiden Fällen ist eine Methoden-Tabellen-Instanz als Konstante notwendig, die die Referenzen auf vorhandene Methoden (C-Funktionen) enthalten:

void myMethodA(int value){ ...Implementierung }
...

const Mtbl_Topic =
{ myMethodA
, myMethodB
};

Der C-Compiler gibt hier eine Warnung aus wenn etwas mit den Typen nicht stimmt. Ein C++-Compiler für diesen C-Code ist schärfer.

Der Aufruf einer Methode unter Kenntnis der instanziierten Methodentabelle erfolgt dann mit

mtblTopic->methodA(value);

Das sieht ähnlich aus wie in C++. Es ist aber das Schema in C für den Zugriff auf Datenstrukturen.

Es passiert hier das gleiche wie in C++, nur hand-programmiert und selbst-kontrolliert.


4 Erhöhung der Sicherheit beim Methodenaufruf: Test der Tabelle, Methodentabellenzeiger im Stack

Topic:.DynCall.SafetyTestMtbl.

Last changed: 2011-07-03

Wenn das Paradigma der Kontrolle des Programmflusses beispielsweise für Sicherheitsrelevante Programmierung oder auch nur für fehlersichere Programmierung ernst genommen wird, dann ist der einfache direkte Aufruf einer Subroutine über eine Methodentabellenadresse in den Daten zu unsicher.

Vor dem Aufruf kann auf einfache Weise getestet werden, ob die Adresse tatsächlich eine Adresse auf die Methodentabelle des erwarteten Typs ist. Die Sicherheit ist hier nicht absolut, jedoch ist ein Doppelfehler wesentlich unwahrscheinlicher. Die Lösung ist die zusätzliche Angabe einer Check-Adresse in der Methodentabelle:

extern char const sign_Mtbl_Topic[];  //Declaration check variable
typedef struct Mtbl_Topic_t
{ char const* sign      //Ckeckaddress = Pointer to check variable
  MT_MethodA* MethodA;
  MT_MethodB* MethodB;
} Mtbl_Topic;

Jede Instanz der Methodentabelle muss nun das einmalig definiertem Kennzeichen, was eine Adresse einer Zeichenkette ist, enthalten:

const Mtbl_Topic =
{ sign_Mtbl_Topic;      //Ckeckaddress
, myMethodA
, myMethodB
};

Die Adresse wird in einer Compilationseinheit definiert und über den Linker an alle betroffenen Stellen verteilt, das ist die Erfüllung der extern-Deklaration. Der Inhalt der Variablen spielt dabei keine Rolle. Die Angabe einer passenden Zeichenkette ist gut für das manuelle Debugging, ansonsten aber nicht notwendig. Das sign ist die einmal im System vorhandene Adresse der Variable:

char const sign_Mtbl_Topic[] = "sign_Mtbl_Topic";

Nun kann vor dem Aufruf dieses Kennzeichen getestet werden:

ASSERT(mtblTopic->sign == sign_Mtbl_Topic);
mtblTopic->methodA(value);

Die Restwahrscheinlichkeit, dass die Adresse mtblTopic falsch ist, aber dennoch auf dieser Adresse die korrekte Adresse zu dem sign_Mtbl_Topic vorgefunden wird, ist auch deshalb sehr gering, da diese Variable nur für diesen Zweck der Kennzeichnung und nicht anderweitig als Datenzeiger verwendet wird.

Was soll nun im Fehlerfall passieren: Der Fehlerfall ist entweder Resultat einer ungenügenden Programmierung, sign... verwechselt oder dergleichen. Dann fällt der Fehler relativ sofort auf und muss sofort korrigiert werden, wie bei anderen Schusselfehlern auch. Das ASSERT-Makro kann zu einem Programmstop mit Anzeige der Aufrufstelle gegebenenfalls mit Stacktrace führen, damit der Fehler auch sofort erkannt wird. - Oder der Fehler ist tatsächlich Resultat eines Datenfehlers in einem ganz anderen Modul, der den Methodentabellenzeiger ungerechtfertigt verändert hat. Dann ist es nur gut, dass der Datenfehler an dieser Stelle auffällt, hoffentlich zeitnah nach dessen Verursachung. Die Ursachenermittlung ist erfahrungsgemäß eher schwierig, daher ist diese Art des Signifikanztestes hilfreich. Jedenfalls ist es gut, wenn in diesem Fall die Abarbeitung ebenfalls anhält, bevor etwas falsches ausgeführt wird.

In einer sicherheitskritischen Anwendung im Anwendungsbetrieb sollte man statt ASSERT(...) mit Anhalten eine Fallback-Strategie fahren:

 if(mtblTopic->sign != sign_Mtbl_Topic) {
   showError(...);
   doFallBack();
 } else {
  mtblTopic->methodA(value);
 }

Damit wird zwar der dynamischer Methodenaufruf verkompliziert, aber ein solcher Aufruf ist eher an wenigeren Stellen notwendig. In einer sicherheitskritischen Programmierung ist ein dynamischer Methodenaufruf oft auch verboten, aus den oben dargestellten Gründen Datenveränderung und Verwechslungsmöglichkeiten. Mit den hier gezeigten Sicherheitsvorkehrungen kann man gegebenenfalls einen Gutachter überzeugen, dass es sich nicht um den gerechterweise verbotenen simplen dynamischen Call handelt.

Eine weitere Möglichkeit des Sicherheitschecks ist der Test, ob die Adresse mtblTopic->methodA identisch ist mit einer der bekannten (wenigen) Implementierungen. Das ist zwar gegen das Objektorientierte Programmierparadigma, für SIL aber ggf. zu empfehlen. Eine weitere Möglichkeit ist der Test, ob der Zeiger auf die Methodentabelle in einen Bereich zeigt, in der nur Methodentabellen stehen. Das lässt sich mit spezieller Segmentangabe beim Compilieren und Linken erreichen. Dann muss nur noch gesichert sein, dass alle Methodentabellen richtig gebildet werden, und der Check hat eine 100%-Sicherheit.

Wann muss der Sicherheitscheck des Methodentabellenzeigers in der Abarbeitungsreihenfolge eines Threads erfolgen, reicht nur einmal am Anfang? Wie hoch ist die Wahrscheinlichkeit, dass ein getesteter Zeiger danach vor der Benutzung noch verändert wird?

Die zweite Frage lässt sich mit folgender Überlegung beantworten: Wenn sich die Variable, die den Methodentabellenzeiger enthält, im Stack befindet, ist entweder von einer Sicherheit des Stack-Inhaltes auszugehen, oder die gesamte Software muss als Unsicher gegenüber Softwarefehlern in anderen Modulen bezeichnet werden. Vorausgesetzt ist, dass der unmittelbare Code im Umkreis der Verwendung des Methodentabellenzeigers selbst geprüft fehlerfrei ist. Letzteres lässt sich nachweisen mit einem lokalem Codereview oder beim Test des betreffenden Algorithmus.

Der aufgeworfene Fall der Unsicherheit ist der Fall eines Querschlägers: Ein Fehler in einem anderen Softwareteil beeinflusst fremde Datenbereiche und führt zu Folgefehlern. Handelt es sich um reine Datenbereiche, dann ist in kritischen Softwareanteilen ein Signifikanzcheck der Daten mit entsprechenden Algorithmen möglich. Das gilt aber nicht für Bereiche, in denen programmlaufbestimmende Werte und interne Zwischenwerte beispielsweise vom Sicherheitscheck stehen, also für den Stack. Der Stack enthält neben lokal definierten Variablen Rücksprungadressen und dergleichen. Ein Querschläger in den Stackbereich ist nicht beherrschbar. Folglich muss entweder gehofft werden dass es keinen Querschläger gibt, oder der Stack muss hardwaretechnisch, von der jeweiligen CPU unterstützt, besonders geschützt werden. Letzteres ist möglich, da der Stack jeweils nur einem Thread (und System-Interrupts) zugeordnet ist. Ein Schreibschutz aller fremden Stackbereiche und die Schreiberlaubnis jeweils des eigenen Stackbereiches kann vom Scheduler des Betriebssystemkernes immer relativ rechenzeitgünstig beim Threadwechsel mit erledigt werden.

Mit diesen Maßnahem kann bezüglich der Frage der Softwaresicherheit eine partielle Sicherheit festgestellt werden: Ein oder mehrere zusammenhängende Threads werden geprüft und freigegeben. Andere Teile der Software, die beispielsweise auf die selben Daten zugreifen (Auswertungen) werden nicht geprüft und freigegeben, der Prüfaufwand kann damit bedeutend vermindert werden und die betreffenden Softwareteile dürfen weiterhin geändert werden. Die sicherheitsrelevante Funktionalität ist in den freigegebenen Threads enthalten und diese ist mit dem oben genannten Maßnahmen sicher.

Damit kann auch die erste aufgeworfene Frage beantwortet werden:

Steht ein Zeiger auf eine Methodentabelle im Stack, dann genügt es, den Sicherheitscheck nur einmal am Anfang auszuführen. Der dynamische Call kann dann beispielsweise mehrmals in der Schleife oder wiederholt für verschiedene Methoden der selben Tabelle ausgeführt werden ohne nochmalige Überprüfung und ist damit sehr schnell, möglicherweise sogar schneller als ein statischer Subroutinenaufruf. Das ist wichtig für die Verwendung in harter Echtzeit.

Ein Zeiger auf eine Methodentabelle in Datenbereichen ist dagegen grundsätzlich unsicher und damit immer unbrauchbar. Es kann sein, dass eine Manipulation genau zwischen erfolgreichem Test und Verwendung passiert, wenn beides nicht in einer Threadwechselsperre nacheinander ausgeführt wird. Eine Threadwechselsperre müsste aber dann in der aufgerufenen Methode sofort freigegeben werden, sonst gibts andere Probleme wie Deadlock.

Damit dürfen bei Forderung nach Sicherheitsrelevanter Programmierung sich die Zeiger auf Methodentabellen nur im Stack befinden.


5 Methodentabellen in CRuntimeJavalike und von Java2C

Topic:.DynCall.JcMtbl.

Last changed: 2011-07-03

.

///


6 Aufruf der dynamischen Methoden in CRuntimeJavalike und Java2C

Topic:.DynCall.getMtbl.

Last changed: 2011-07-03

Wenn in C entsprechend der Objektorientierung wie in Java oder C++ mit datenabhängig überladbaren Methoden gearbeitet werden soll, dann muss die Information über die Methodentabelle mit den Daten verbunden sein. Nur so kann abhängig von konkreten Instanzen ein Aufruf der zugehörigen überladenen Methode erfolgen. Wird 'nur' mit einem Interfacekonzept gearbeitet, dann kann grundsätzlich der Zeiger auf die Methodentabelle einer Interfaceimplementierung auch unabhängig und neben den Daten geführt werden. Dass wäre bei einer manuellen Programmierung in C denkbar, macht das Problem aber nicht einfacher: Der Anwender müsste Daten und Interface unabhängig und richtig zueinander verwalten. Einfacher ist es, wenn die Referenz auf die für das Interface notwendige Methodentabelle in den Daten steht und daraus abgeleitet werden kann. Dann braucht es äußerlich gesehen wie sonst auch nur eine Referenzierung der Daten, um Methoden des Datentyps auf die Daten anzuwenden.

In der Jc-Biibliothek (CRuntimeJavalike) und für Java2C ist es vorgesehen, dass der Zeiger auf die Methodentabelle instanzbezogen in den Reflection-Informationen steht, die ebenfalls instanzbezogen in der Basisklasse ObjectJc referenziert werden.

Man hat damit grundsätzlich ein ähnliches Problem der Datensicherheit bei den sogenannten Querschlägern wie in C++: Ein Querschläger (Softwarefehler in anderen Softwareteilen) kann die Daten beeinflussen, also auch den Zeiger auf die Reflection, und damit den Zeiger auf die Methodentabelle. Damit wird der Programmlauf in undefinierte Maschinencodes geführt. Ein beliebiger Fehler in einem anderen Modul kann die sonst sicheren und geprüften Abläufe erheblich mit Fehlfunktionen stören.

Aber: Der Zugriff auf den Zeiger auf die Methodentabelle ist nicht direkt ausgeführt, sondern ist selbst eine Routine, die Sicherheitschecks ausführen kann. Das dauert zwar etwas länger, Kritik an der Rechenzeit, aber man wird Interfaces und überladene Methoden nicht in den schnellen Algorithmen verwenden (Abtastzeiten im Mikrosekundenbereich) sondern eher in langsamen Teilen wie Bedienerabfragen, Sollwertvorgaben von außen, die in Millisekunden ablaufen. Eine Beschleunigung mit adäquaten Werten wie virtuelle Methoden in C++ ist dann vorhanden, wenn der Zeiger auf die Methodentabelle einmalig ermittelt und in einer Stackvariablen gespeichert wird, um dann beispielsweise in einer Schleife mehrmals genutzt zu werden.

Um den Zeiger auf die Methodentabelle einer Instanz aus gegebenen Daten auszulesen, gibt es die Subroutine

MtblHeadJc const* getMtbl_ObjectJc(ObjectJc const* othiz
                                  , char const* sign);

Zurückgeliefert wird der Zeiger auf den verallgemeinerten Typ auf die Kopfdaten der Methodentabelle. Der Anwender muss also casten. Der Aufruf sieht aus Anwendersicht wie folgt aus:

Mtbl_BaseType const* mtbl = (Mtbl_BaseType const*)
  getMtbl_ObjectJc(&myData->base.object, sign_Mtbl_BaseType);

Der geforderte Typ der Methodentabelle kann ein beliebiger Typ sein, dem die Daten entsprechen sollten. Insbesondere sind Basis- und Interfacetypen zweckmäßig. Der Typ der Daten selbst ist meist nicht gefragt, weil virtuelle Aufrufe auf Basisdaten ausgerichtet sind.

Jedwedes casting ist möglicherweise fehlerträchtig. In diesem Fall wird aber auf genau denjenigen Typ gecastet, der mit dem Aufruf mit Angabe des sign_Mtbl... angegeben wird. Wenn die Methodentabellen selbst ordnungsgemäß sind, dann ist dieses cast immer zulässig.

Der erhaltene Zeiger auf die Methodentabelle ist dann null, wenn entweder die Reflection-Informationen in den Daten fehlen oder unvollständig sind, oder die Daten nicht dem gefordertem Typ entsprechen. Der Anwender muss also dann mit einem Null-Zeiger rechnen, wenn der Typ nicht zuvor geprüft wurde. Ansonsten kann der Methodentabellen-Zeiger nun für den Aufruf der abgeleiteten Methoden unmittelbar benutzt werden:

   mtbl->doSomething(&myData->base.object, parameter);

Der Aufruf sieht ähnlich wie in C++ aus. Als Zeiger vor dem Subroutinen- bzw. Methodennamen doSomething ist aber hier der Zeiger auf die Methodentabelle. Der Zeiger auf die Daten wird den Methoden als erster Parameter übergeben, wie sonst auch im Objektorientierten C. Dabei wird mindestens für Interface-Methoden der ObjectJc*-typisierte Zeiger verwendet. Der originale Typ kann nicht verwendet werden, da die dynamischen Methoden diesen Typ nicht kennen können und der Typ im Aufrufkontext gegebenenfalls auch nicht bekannt ist, entsprechend dem Konzept der dynamsichen Methodenbindung. Ein Interfacetype wird deshalb nicht verwendet, weil dann eine Methode, die in mehreren Interfaces gleicherweise verlangt wird, dann mehrfach implementiert werden müsste.

Die Methode selbst wird aus der Methodentabelle gelesen. Die Methode ist diejenige des tatsächlichen (abgeleiteten) Datentyps. Da die Methode alle Daten ihres Typs verwenden darf, erfolgt in der Methode ein casting des Datentyps:

int doSomething(ObjectJc* ithiz, int parameter)
{ MyDataType* thiz = (MyDataType*)ithiz;
  ...
}

Dieses casting ist ebenfalls gestattet, weil die Methode nur in der Methodentabelle eingetragen wird, die für den Typ zuständig ist. Folglich kann die Methode nur dann aufgefunden werden, wenn der Datentyp korrekt ist.


7 Aufbau von Methodentabellen in CRuntimeJavalike

Topic:.DynCall.MtblData.

Last changed: 2011-07-03

Eine Methodentabelle ist eine const struct und sollte im const-Speicher abgelegt werden. In kleineren embedded Plattformen kann das unmittelbar im ROM-Bereich (Flash) erfolgen. Der Vorteil des Schreibschutzes kann genutzt werden, um Änderungen des Inhaltes wegen Fehlern in anderen Softwareteilen auszuschließen.

Eine Methodentabelle ist einmal pro Typdefinition einer C-Klasse anzulegen, unabhängig von der Anzahl von Dateninstanzen im Anwenderprogramm. Damit ist die Anlage der Methodentabelle grundsätzlich unabhängig von der konkreten Anwenderprogrammierung. Die Methodentabelle einer Klasse, die etwas komplexer abgeleitet ist, kann eine etwas verschachtelte Struktur sein. Das liegt daran, dass für jede Basisklasse und jedes Interface eine eigene Teil-Methodentabelle darin enthalten ist. Im Vergleich mit C++: Dort enthält eine abgeleitete Klasse für jede Basisklasse einen eigenen vtbl-Pointer am Anfang des zugehörigen Datenbereiches zur Basisklasse.

Eine Methodentabelle besteht aus einem

Eine Gesamt-Methodentabellenstruktur wird dann noch mit einer End-Struktur vom Typ MtblHeadJc abgeschlossen.

Die Methodentabelle von ObjectJc ist im Headerfile Fwc/objectBaseC.h wie folgt definiert:

typedef struct Mtbl_ObjectJc_t
{ MtblHeadJc head;
  /**Methods of ObjectJc. */
  MT_clone_ObjectJc*    clone;
  MT_equals_ObjectJc*   equals;
  MT_finalize_ObjectJc* finalize;
  MT_hashCode_ObjectJc* hashCode;
  MT_toString_ObjectJc* toString;
} Mtbl_ObjectJc;

Hier sind diejenigen fünf virtuellen Methoden aus java.lang.Object wiederzufinden. Die Methode toString() ist dabei die wichtigste auch für C-Anwendungen. Insbesondere für Debug- oder Reportzwecke soll mit dieser Methode eine Kurzaussage über den Datengehalt einer beliebigen Instanz erzeugt werden können.

Die Methodentabelle der C-Klasse ThreadJc definiert in ThreadJc.h sieht darauf aufbauend wie folgt aus:

typedef struct Mtbl_ThreadJc_t
{ MtblHeadJc head;
  Mtbl_ObjectJc ObjectJc;
  //Method table of interfaces:
  Mtbl_RunnableJc RunnableJc;
} Mtbl_ThreadJc;

Diese Klasse ThreadJc hat keine eigenen dynamischen Methoden, zumindestens im Abbild ThreadJc. In Java sind alle nicht explizit als final gekennzeichneten Methoden dynamisch. Meist wird aber das dynamische Überladen nicht benötigt, teilweise ist es auch wenig zweckmäßig. Aus praktischen Erwägungen ist daher ist die Anzahl der dynamischen Methoden in der CRuntimeJavalike gegenüber dem Java-Original reduziert. Eine Änderung der dynamischen Methoden ist aber abwärtskompatibel machbar ohne Nachbearbeitung der nutzenden Quellcodes: Es dürfen beliebig dynamische Methoden hinzukommen.

Die Klasse ThreadJc hat allerdings ein Interface, im Java-Original java.lang.Runnable.

Die Methodentabelle für RunnableJc ist eine typische Interface-Methodentabelle. Sie sieht definiert in ThreadJc.h wie folgt aus:

typedef struct Mtbl_RunnableJc_t
{ MtblHeadJc head;
  MT_run_RunnableJc* run;
  Mtbl_ObjectJc ObjectJc;
} Mtbl_RunnableJc;

Es gibt die einzige und wichtige Methode run(), die Main-Routine für Threads. Ansonsten ist in dieser Methodentabelle wieder die Methodentabelle für ObjectJc enthalten. Ein Grundkonzept in Java ist, dass die Methoden von java.lang.Object auch über alle Interfacereferenzen aufrufbar sind.

Eine Anwenderklasse, die auf Thread basiert, hat dann eine zusammengesetzte Methodentabelle der Formulierung

typedef struct Mtbl_WayCtrlThread_MainController_t
{ MtblHeadJc head;
  //keine eigenen dynamischen Methoden
  Mtbl_ThreadJc ThreadJc;
} Mtbl_WayCtrlThread_MainController;

Die Klasse basiert auf Thread und überläd die Methode run. Die eben gezeigte struct-Definition ist auch geeignet für die Nutzung in einer nochmals überladenen Klasse. Die Definition der Methodentabelle der eigenen Klasse erfolgt wie folgt:

typedef struct MtblDef_WayCtrlThread_MainController_t
{ Mtbl_WayCtrlThread_MainController mtbl;
  MtblHeadJc end;
} MtblDef_WayCtrlThread_MainController;

Hier ist also noch die End-Datenstruktur hinzugesetzt. Die Konstantendefinition sieht dann wie folgt aus. Das Beispiel entstammt dem von Java nach C übersetztem Code der Klasse org.vishia.exampleJava2C.java4c.MainController.WayCtrlThread:

const MtblDef_WayCtrlThread_MainController mtblWayCtrlThread_MainController = {
{ { sign_Mtbl_WayCtrlThread_MainController//J2C: Head of methodtable.
  , (struct Size_Mtbl_t*)((0 +2) * sizeof(void*)) //size. NOTE: all elements are standard-pointer-types.
  }
, { { sign_Mtbl_ThreadJc//J2C: Head of methodtable.
    , (struct Size_Mtbl_t*)((0 +2) * sizeof(void*)) //size. NOTE: all elements are standard-pointer-types.
    }
  , { { sign_Mtbl_ObjectJc//J2C: Head of methodtable.
      , (struct Size_Mtbl_t*)((5 +2) * sizeof(void*)) //size. NOTE: all elements are standard-pointer-types.
      }
    , clone_ObjectJc_F //clone
    , equals_ObjectJc_F //equals
    , finalize_ObjectJc_F //finalize
    , hashCode_ObjectJc_F //hashCode
    , toString_WayCtrlThread_MainController //toString
    }
    /**J2C: Mtbl-interfaces of WayCtrlThread_MainController: */
  , { { sign_Mtbl_RunnableJc//J2C: Head of methodtable.
      , (struct Size_Mtbl_t*)((1 +2) * sizeof(void*)) //size. NOTE: all elements are standard-pointer-types.
      }
    , run_WayCtrlThread_MainController //run
    , { { sign_Mtbl_ObjectJc//J2C: Head of methodtable.
        , (struct Size_Mtbl_t*)((5 +2) * sizeof(void*)) //size. NOTE: all elements are standard-pointer-types.
        }
      , clone_ObjectJc_F //clone
      , equals_ObjectJc_F //equals
      , finalize_ObjectJc_F //finalize
      , hashCode_ObjectJc_F //hashCode
      , toString_WayCtrlThread_MainController //toString
      }
    }
  }
}, { signEnd_Mtbl_ObjectJc, null } }; //Mtbl

Die Typdefinition der Methoden sieht noch recht einfach und schlüssig aus, die hier gezeigte Konstantendefinition muss jedoch die Schachtelung der Strukturen in den Typdefinitionen wiederspiegeln. Im Beispiel sind die Methoden run() und toString() überladen. Die anderen Methoden von ObjectJc sind mit den Standardmethoden dieser Klasse belegt.

Wird eine solche Konstantendefinition manuell programmiert, dann helfen Compilerfehlermeldungen: Wird eine Methode an falscher Stelle benannt, dann passt der Typ des C-Funktionszeigers nicht oder die Klammernschachtelung wird als fehlerhaft gemeldet. Es ist nicht anders als sonst bei Initialisierern von stärker geschachtelten Strukturen. Im Sinne dessen, dass diese Struktur nicht einer ständigen Änderung unterworfen sein wird, ist der Programmieraufwand noch im Rahmen. Von Vorteil ist allerdings eine automatische Generierung, wie sie vom Java2C-Translator übernommen wird. Für die manuelle Programmierung aber mit automatischer Reflection-Generierung ist im Tool Header2Reflection derzeit (Stand 2011-06) noch nichts vorhanden. Eine Erweiterung der Reflectiongenerierung um die Generierung der Methodentabelle aus einer entsprechend gekennzeichneten Methodendefinition im Headerfile ist aber möglich und schon länger angedacht. In diesem Sinn ist bei Nicht-Java-Orientierung also auch eine Arbeit mit dynamischen Methoden möglich. Zweckmäßig ist dies bereits für die Bereitstellung von toString()-Methoden für die Dateninhaltsanzeige mit dem Inspector.


8 Kopf der Methodentabellen in ObjectJc.h

Topic:.DynCall.MtblHeadJc.

Last changed: 2011-07-03

Die Definitionen für den Bau von Methodentabellen sind im Header ObjectJc.h enthalten, weil dieser Header in der Regel jedenfalls eingebunden wird.

 typedef struct MtblHead_ObjectJc_t
 { char const* sign;
   struct Size_Mtbl_t* sizeTable;
 }MtblHead_ObjectJc;

Diese Struktur ist am Anfang jeder Teil-Methodentabelle aufgeführt, und am Ende der Gesamt-Tabelle.

Folgende Subroutine liefert die Referenz auf die angeforderte Teil-Methodentabelle eines Interfaces oder einer Basisklasse bei gegebener Gesamt-Tabelle:

MtblHead_ObjectJc const* getMtableObjectJc
(ObjectJc const* ythis, char const* sign);

Der Routine wird der allgemeine Zeiger auf die Instanz übergeben. Diese enthält innerhalb der Basisklasse ObjectJc den Zeiger auf die Methodentabelle. In dieser Gesamt-Tabelle wird diejenige Teil-Tabelle aufgesucht, die mit dem angegebenen sign gekennzeichnet ist.


9 Basis-Methodentabelle Mtbl_ObjectJc

Topic:.DynCall.Mtbl_ObjectJc.

Last changed: 2011-07-03

Die Methodentabelle von ObjectJc ist wie folgt zu definieren (siehe ObjectJc.h):

 typedef struct Mtbl_ObjectJc_t
 { MtblHeadJc head;
   /**Methods of ObjectJc. */
   MT_clone_ObjectJc*    clone;
   MT_equals_ObjectJc*   equals;
   MT_finalize_ObjectJc* finalize;
   MT_hashCode_ObjectJc* hashCode;
   MT_toString_ObjectJc* toString;
 } Mtbl_ObjectJc;

Diese Methodentabelle enthält die Methoden, die jede Instanz enthält und überschreiben kann. Auch Interfaces kennen diese Methodentabelle.


10 Aufbau einer Methodentabelle für eine Klasse mit Basisklassen und mehreren Interfaces

Topic:.DynCall.MtblWholeSuperAndIfc.

Last changed: 2011-07-03

Am Beispiel soll der Aufbau der zugehörigen Methodentabelle gezeigt werden. Die Beispiel-Klasse in Java-Schreibweise ist wie folgt definiert:

public class ImplIfc extends ExpandableDataStruct implements Ifc, Ifc2
{ ...
  int testOverrideAble(float value)
  { ...

In der Klasse selbst ist eine Methode definiert, die von einer erbenden Klasse überschrieben werden kann. In den Interfaces sind weitere Methoden definiert, die in dieser Klasse implementiert werden. Die Methodentabellen-Typdefinition im Headerfile sieht wie folgt aus:

extern const char sign_Mtbl_ImplIfc_Test[]; //marker for methodTable check
typedef struct Mtbl_ImplIfc_Test_t
{ MtblHeadJc head;
  MT_testOverrideAble_ImplIfc_Test* testOverrideAble;
  Mtbl_ExpandableDataStruct_Test ExpandableDataStruct_Test;
  //Method table of interfaces:
  Mtbl_Ifc_Test Ifc_Test;
  Mtbl_Ifc2_Test Ifc2_Test;
} Mtbl_ImplIfc_Test;

Die Methodentabelle beginnt mit dem Kopf MtblHeadJc. Darauf folgen alle Methoden, die unmittelbar in dieser Klasse erstmalig definiert worden sind. Methoden dieser Klasse, die Methoden aus Interfaces überladen, sind hier nicht nochmals erwähnt sondern sind in den Interfaces benannt. Darauf folgt die Methodentabelle der Superclass, die adäquat definiert ist:

extern const char sign_Mtbl_ExpandableDataStruct_Test[];
typedef struct Mtbl_ExpandableDataStruct_Test_t
{ MtblHeadJc head;
  Mtbl_ObjectJc ObjectJc;
} Mtbl_ExpandableDataStruct_Test;

Dies ist ein Beispiel für eine Methodentabelle einer Klasse, die keine weiteren Superclasses sondern nur ObjectJc enthält, keine eigenen überschreibbaren Methoden und keine Interfaces implementiert. Damit bleibt nur der MtblHeadJc und die Mtbl_ObjectJc ObjectJc übrig. Letztere ist in ObjectJc.h definiert und enthält die fünf überschreibbaren Methoden von ObjectJc wie toString().

Die Methodentabellen für die Interfaces sind adäquat definiert, hier für eines der Interfaces gezeigt. Die Interfaces basieren wie in Java alle auch auf Object, so dass die Mtbl_ObjectJc ObjectJc auch dort enthalten ist.

extern const char sign_Mtbl_Ifc_Test[]; //marker for methodTable check
typedef struct Mtbl_Ifc_Test_t
{ MtblHeadJc head;
  MT_testIfc_Ifc_Test* testIfc;
  Mtbl_ObjectJc ObjectJc;
} Mtbl_Ifc_Test;

Oben gezeigte Strukturen sind die Typdefinitionen der Methodentabellen, wie sie jeweils als Element auch in Methodentabellen der abgeleiteten Klassen wieder verwendet werden. Die Definition der Belegung einer Methodentabelle für eine Klasse ist eine Konstante in folgender Schreibweise:

const struct { Mtbl_ImplIfc_Test; MtblHeadJc end; } mtblImplIfc_Test = {
{ { sign_Mtbl_ImplIfc_Test//J2C: Head of methodtable.
  , (struct Size_Mtbl_t*)((1 +2) * sizeof(void*))
  }
, testOverrideAble_ImplIfc_Test_F //testOverrideAble
, { { sign_Mtbl_ExpandableDataStruct_Test//J2C: Head of methodtable.
    , (struct Size_Mtbl_t*)((0 +2) * sizeof(void*))
    }
  , { { sign_Mtbl_ObjectJc//J2C: Head of methodtable.
      , (struct Size_Mtbl_t*)((5 +2) * sizeof(void*))
      }
    , clone_ObjectJc_F //clone
    , equals_ObjectJc_F //equals
    , finalize_ObjectJc_F //finalize
    , hashCode_ObjectJc_F //hashCode
    , toString_ImplIfc_Test //toString
    }
  }
  /**J2C: Method table of interfaces: */
, { { sign_Mtbl_Ifc_Test//J2C: Head of methodtable.
    , (struct Size_Mtbl_t*)((1 +2) * sizeof(void*))
    }
  , testIfc_ImplIfc_Test_F //testIfc
  , { { sign_Mtbl_ObjectJc//J2C: Head of methodtable.
      , (struct Size_Mtbl_t*)((5 +2) * sizeof(void*))
      }
    , clone_ObjectJc_F //clone
    , equals_ObjectJc_F //equals
    , finalize_ObjectJc_F //finalize
    , hashCode_ObjectJc_F //hashCode
    , toString_ImplIfc_Test //toString
    }
  }
, { { sign_Mtbl_Ifc2_Test//J2C: Head of methodtable.
    , (struct Size_Mtbl_t*)((2 +2) * sizeof(void*))
    }
  , testIfc_ImplIfc_Test_F //testIfc
  , testIfc2_ImplIfc_Test_F //testIfc2
  , { { sign_Mtbl_ObjectJc//J2C: Head of methodtable.
      , (struct Size_Mtbl_t*)((5 +2) * sizeof(void*))
      }
    , clone_ObjectJc_F //clone
    , equals_ObjectJc_F //equals
    , finalize_ObjectJc_F //finalize
    , hashCode_ObjectJc_F //hashCode
    , toString_ImplIfc_Test //toString
    }
  }
}, { signEnd_Mtbl_ObjectJc, null } }; //Mtbl

Die Methodentabelle ist von Java2C generiert, wie auch die oben gezeigten Typdefinitionen. Man kann eine solche Strukturkonstante auch noch per Hand schreiben, mit etwas Sorgfalt. Die Konstantendefinition beginnt mit der als Typ definierten Methodentabelle, enthält aber noch als Abschluss MtblHeadJc end; Dieses Ende-Kennzeichen ist notwendig für die Ermittlung einer Methodentabelle einer Superclass oder eines Interfaces. Die Schreibweise ist mit den richtigen Einrückungen generiert, eine Hand-Schreibweise ist adäquat zu empfehlen, so dass die Zuordnung zu den Strukturelementen noch gelingen kann. Es muss beachtet werden, dass die Struktur beispielsweise von Mtbl_ExpandableDataStruct_Test ebenfalls wieder feiner strukturiert ist, erst in dieser Konstantendefinition zeigt sich dies. Welcher Konstantenteil zu welcher Stuktur gehört, ist ohne Kommentierung an dem jeweiligen sign-Element, beispielsweise sign_Mtbl_ExpandableDataStruct_Test erkennbar.

Das Element sizeTable aus der Kopfstruktur MtblHeadJc ist der Form nach ein Pointer auf eine nirgends weiter definierte struct, dem Inhalt nach ist es eine einfache Konstante. Pointer deshalb, damit alle Elemente einer Methodentabelle Pointer sind. Damit ist eine einfache überschaubare Adressrechnung in einer Methodentabelle möglich. Ein int-Typ könnte eventuell gleich viele Bytes umfassen wie ein Pointer, das ist aber nicht garantiert. Demzufolge wird der einfache int-Wert auf den Pointertyp Size_Mtbl_t* gecastet. Der jeweilige int-Wert selbst wird aus der Anzahl der Methoden dieses Tabellenabschnitts berechnet, plus 2 für die zwei Zeiger in MtblHeadJc multipliziert mit der Anzahl Bytes für einen Zeigertyp. Wenn man eine solche Methoden-Tabelle manuell codiert, kann man die Anzahl der Methoden-Zeiger zählen, meist eine geringe Anzahl. Operationen mit sizeof() scheitern deshalb, weil es die Teiltabelle ist, ohne die Anteile der Basisklassen und Interfaces.

Diese sizeTable wird auf zweierlei Weise genutzt:

///


11 Wirkungsweise der Methode getMtbl_ObjectJc(...) - Aufsuche der Methoden-Tabelle einer Instanz in deren Daten

Topic:.DynCall.MtblInReflect.

Last changed: 2011-07-03

Dieser Subroutine wird die Instanz über die Basisklasse ObjectJc übergeben. Das ist allgemeingültig. Zusätzlich wird das Kennzeichen der Methodentabelle übergeben. Die Routine ermittelt den Zeiger auf die Gesamt-Methodentabelle über die Referenz auf die Reflection, gespeichert in ObjectJc, und dann als Referenz in den Reflection:

MtblHeadJc const* getMtbl_ObjectJc(ObjectJc const* ythis, char const* sign)
{ MtblHeadJc const* head;
  STACKTRC_ENTRY("getMtbl_ObjectJc");
  if(ythis->reflectionClass == null) {
    head = null; //nullpointer exception outside possible.
  } else
  {
    head = ythis->reflectionClass->mtbl;

Bis zu dieser Stelle kann head auch null enthalten, wenn es für diese Instanz keine Reflection oder Methodentabelle gibt. Dieser Fall kann auftreten für manuell geschriebene Klassen. Weiter geht es mit:

   if(head != null)//nullpointer exception outside possible.
   {
     while(  head->sign != sign
          && head->sign != signEnd_Mtbl_ObjectJc
          )
     { int sizeTable = (int)head->sizeTable;
       ASSERT(sizeTable >0 && sizeTable < (302 * sizeof(void*)));
       //The next part of method table is found after the current.
       head = (MtblHeadJc const*)( (MemUnit*)head + sizeTable );
     }

Wenn die Methodentabelle des Instanz-Typs unmittelbar gesucht wrid, dann ist die erste while(...)-Abfrage bereits false und head ist korrekt. Ansonsten wird die Methodentabelle als flache Struktur aufgefasst, was sie auch ist. Über die Größenangabe der jeweiligen Teiltabelle sizeTable wird jeweils auf die folgende Teiltabelle gestepped und dessen sign getestet. Die sizeTable ist signifikanz-getestet. Das ist kein Angst-Test, sondern es wird gewährleistet dass der Zeiger auch bei falschem Speicherinhalt nur in kleinen Schritten vorwärts bewegt wird. Ohne Test kann auch ein negativer oder sehr großer Wert von sizeTable gelesen werden, womit im Speicher unkontrolliert hin- und hergesprungen wird, jeweils mit scheinbarem Erfolg. Der Fehlerfall ist derjenige, bei dem Daten aufgrund Quersschläger zerstört sind. Im Fehlerfall wird man relativ schnell einen ungültigen Wert für sizeTable vorfinden und daraufhin abbrechen, oder an eine Speichergrenze stoßen und damit eine Betriebssystemexception erzeugen. Die Angabe 302 ist nun relativ willkürlich, begrenzt die maximale Anzahl von Methoden einer Teiltabelle auf 300, was sehr reichlich ist, setzt aber andererseits einen relativ kleinen Gültigkeitsbereich: 4 bis 1204 (4 * 301) in einem möglichen Zahlenbereich von +/- 2 Milliarden (32 bit).

Die Schleife hört dann auf, wenn ordnungsgemäß alle Teil-Methodentabellen durchsteppt wurden und am Ende der letzte Eintrag gekennzeichnet mit signEnd_Mtbl_ObjectJc gefunden wurde. Das ist der Fall, wenn die Methodentabelle keine Teiltabelle mit dem gefordertem Sign enthält. Dieser Fall ist von außen gesehen nicht vorgesehen. Es wird getestet:

     if(head->sign == signEnd_Mtbl_ObjectJc){
       THROW_s0(ClassCastException, "baseclass not found", (int)sign);

Damit kann sich die äußere Routine darauf verlassen, dass immer die richtige Methodentabellenreferenz zurückgeliefert wird, da ansonsten eine Exception mit einer entsprechenden Meldung innen erzeugt wird. Man muss außen also nicht nochmal testen und spart Programmieraufwand.

Das System der Exception ist oft schon diskutiert und hinterfragt worden mit folgender Ansicht: Eine Anlage muss immer laufen, egal was ist. Jedoch: Eine Erwartung des Aufrufes einer überladbaren Methode muss dazu führen, dass diese auch ordnungsgemäß vorgefunden wird. Für sicherheitsrelevante Systeme ist es besser, eine Funktionalität versagt und meldet dies nach außen, als dass sie falsch ausgeführt wird. Nur dann kann eine fallback-Strategie ordnungsgemäß ausgeführt werden.

Nach Erhalt der Referenz auf die Methodentabelle kann entweder damit eine Methode daraus aufgerufen werden, wie es in der Beispielimplementierung gezeigt wird:

int32 testIfc_Ifc_Test(Ifc_Test_s* ythis, int32 input, ThCxt* _thCxt)
{ Mtbl_Ifc_Test const* mtbl = (Mtbl_Ifc_Test const*)
        getMtbl_ObjectJc(&ythis->base.object, sign_Mtbl_Ifc_Test);
  return mtbl->testIfc((Ifc_Test_s*)ythis, input, _thCxt);
}

oder diese Referenz wird in einer stacklokalen Variable gespeichert um dann mehrfach genutzt zu werden:

int32 val;
Ifc_TestMTB ifc22;
val = 0;
SETMTBJc(ifc22, ((/*cast*/Ifc_Test_s*)(&(ythis->implifc))), Ifc_Test);
{ int32 ii;
  for(ii = 0; ii < 1000; ii++)
    { val += ifc22.mtbl->testIfc(ifc22.ref, 5, _thCxt);
    }
}

In diesem Beispiel ist ifc22 eine Struktur aus Referenz auf die Daten und Referenz auf die Methodentabelle:

typedef struct Ifc_TestMTB_t
{ struct Mtbl_Ifc_Test_t const* mtbl;
  struct Ifc_Test_t* ref; } Ifc_TestMTB;

und das Makro SETMTBJc(...) definiert in ObjectJc.h vereinfacht die Schreibweise des Setzens dieser Referenz:

#define SETMTBJc(DST, REF, TYPE) { (DST).ref = REF; \\
  (DST).mtbl = (Mtbl_##TYPE const*) \\
  getMtbl_ObjectJc(&(DST).ref->base.object, sign_Mtbl_##TYPE); }

Die obigen Beispiele sind alle mit Java2C übersetzt. Die Bildung einer Referenz auf die Methodentabelle im Stack erfolgt dann, wenn im Java-Code eine stacklokale Interfacereferenz verwendet wird. Der Java-Code für das Beispiel sieht wie folgt aus:

 int testAccessIfc()            //Java-method
 { int val = 0;
   Ifc ifc22 = implifc;         //stacklocal reference to type Ifc
   for(int ii=0; ii<1000; ii++)
   { val += ifc22.testIfc(5);   //use it for dynamic call, in C too.
   }
   return val;
 }

12 Interface-Typ in C und dessen Nutzung

Topic:.DynCall.IfcType.

Last changed: 2011-07-03

.


13 Strukturdefinitionen für Daten und Interfaces

Topic:.DynCall.typedef_struct.

Last changed: 2011-07-03

Für die Nutzung des dynamischen Bindens in C im Zusammenhang mit Daten wird das gleiche Interface/Basisklassen-Konzept wie in Java verwendet. Das bedeutet:

Die Implementierungseigenschaften dieses Konzeptes in C sind:

Ein abgeleiteter Typ mit Interfaces wird wie folgt programmiert:

 typedef struct UserStruct_t
 { union { ObjectJc object;
           Interface1Typ Interface1Typ;
           Interface2Typ Interface2Typ;
           UserBaseType super;
         } base;
   int someUserData;
 } UserStruct_s;

Ein Typ für C-Interfaces ist wie folgt zu definieren:

 typedef struct Interface1Typ_t
 { Object object;
 } Interface1Typ_i;

Damit ist ein Interfacetyp vom Compiler unterscheidbar von einem anderen Interfacetyp und Verwechslungen des Programmierers somit zur Compilezeit erkennbar. Die union in der Anwenderstruktur spiegelt die Tatsache wieder, dass alle Interfaces im selben Speicher abgebildet sind wie object und auch wie die unmittelbaren Basisdaten. Ein Zugriff auf Basisdaten kann dann in der Form

 myData->base.super

geschrieben werden. Der Typ braucht hier nicht angegeben werden, da es nur Einfachvererbung gibt (ähnlich dem super in Java). Der Zugriff auf ein Interfacetyp aus einer Implementierungsinstanz erfolgt mit

 myData->base.Interface1Typ

unter Angabe des gewünschten Typs. Ein casting der Form (Interface1Typ*)(myData) liefert das selbe. Es wird aber hierbei nicht vom Compiler getestet, ob der Typ korrekt ist. Diese Schreibweise ist unsicherer.

Der Zugriff auf die ObjectJc-Daten kann immer mit

 myData->base.object

erfolgen, ohne dass die Tiefe einer Ableitung bekannt sein muss.


14 Implementierung von Methoden für Interfacetypen

Topic:.DynCall.implMethod.

Last changed: 2011-07-03

Eine Interfaceroutine, wie sie als MT_...(...) definiert ist und in der Methodentabelle verwendet wird, hat als erstes Argument einen Zeiger auf den Interfacetyp. Damit wird vom Compiler an dieser Stelle ebenfalls die Richtigkeit des Typs geprüft. In der jeweiligen Implementierung muss am Anfang auf die Anwenderdaten gecastet werden. Das ist unbedingt ohne Test möglich, wenn die Implementierung als static gekennzeichnet ist, folglich nicht von außen gerufen werden kann sondern nur in den Methodentabellen der Compilierungsunit verwendet wird. Es besteht allerding auch die Möglichkeit des Typtests über Reflection. Dieser ist etwas mehr Zeit-Aufwändig. Passt der Anwendertyp nicht zum Interface, dann ist das ein Programmierfehler und kann jedenfalls mit einer Exception beantwortet werden.

Muster/Beispiel:

 static RetType method_UserTypeF(InterfaceTyp* ithis, ...)
 { UserType* ythis = (UserType*)ithis;
   ASSERT(instanceof_ObjectJc(&ithis->object, &reflection_UserType));
   ....
   return;
 }
 //...
 Mtbl_UserType mtbl_UserType=
 { sign_InterfaceTyp;
   ...
   method_UserTypeF;
   ...
 };

Der Aufruf einer dynamisch bindenden Methode soll für den Anwender sich möglichst nicht unterscheiden vom Aufruf einer anderen Methode. Damit ist das Konzept der Methodentabellen nicht bis in die Anwendung gezogen sondern dort verborgen. Die Anwendung muss aber mit Reflection arbeiten, da die Methodentabelle auf diese Weise in den Daten verankert wird.

Für den Aufruf von Methoden von außen gibt es für Interfaces dann folgende Wrapper-Methoden (Beispiel, Schema):

 RetType method_InterfaceTyp(InterfaceTyp* ithis, ...)
 { Mtbl_InterfaceTyp* mtbl = (Mtbl_InterfaceTyp*)
     getMtbl_ObjectJc(&ythis->object, &reflection_LogMessageFW_i);
   ASSERT(mtbl != null && mtbl->sign == &sign_InterfaceTyp);
   return mtbl->method(ithis,..);
 }

15 Überladen einer Methode einer Basisklasse aus Java2C mit reiner C-Programmierung

Topic:.DynCall.implifc.

Last changed: 2015-08-16

Interessant ist es, in Java Klassen zu schreiben und nach Java2C zu übersetzen, dabei gerufene Methoden als überladene Variante in reinem C zu schreiben. Dann kann beispielsweise eine Standard (Library-) Klasse, die schon als Java2C-übersetzt vorliegt, in einer Applikation genutzt werden. Die gerufene Methode wird aber nicht in Java sondern in C programmiert, da anwenderspezifisch und C sein soll.

Ein Beispiel dafür ist die srcJava_vishiaRun/org/vishia/communication/InterProcessCommRxThread. Diese Klasse organisiert eine Kommunikation beispielsweise über Sockets einschließlich Öffnen, Schließen, Fehlerbehandlung. Der Anwender braucht nur noch eine Routine hineinzuhängen, die als callback gerufen wird, sobald Daten über den Socket empfangen wurden. Dies wird in Java über ein Interface srcJava_vishiaRun/org/vishia/communication/InterProcessCommRx_ifc realisiert. Das ganze ist auch in Java genauso verwendbar.

Die Anwendung in C muss nach folgendem Schema agieren:

Headerfile:

#include <Jc/ObjectJc.h>
#include <Ipc2C/InterProcessCommRxThread_Ipc.h>
#include <Ipc2C/InterProcessCommRx_Ifc_Ipc.h>

/**Any type in the user's context. */
typedef struct MyUserData
{ .....
  /**Implementation of the base class for overridden receive routine. */
  InterProcessCommRx_ifc_Ipc_s targetRx;

  /**This is the reference to the existing used InterProcessCommRxThread. */
  InterProcessCommRxThread_Ipc_s* targetIpc;
  ....

C-file:

//Declaration of the reflection to check. Note: That declaration may be placed in an header file.
extern_C const ClassJc reflection_Comm_MyUserData;

//method which should be invoked on receiving data.
//It is defined with 'static', because it is only used in this compilation unit.
METHOD_C static void execRxData_TargetRx(InterProcessCommRx_ifc_Ipc_s* thiz
  , PtrVal_int8 buffer, int32 nrofBytesReceived
  , struct Address_InterProcessComm_t* sender, ThCxt* _thCxt)
{
  ASSERT(instanceof_ObjectJc(thiz->data, &reflection_MyUserData));
  MyUserData* data = (MyUserData*)thiz->data;

  printf("execRxData_TargetRx\n");
}

Hinweis: Diese Methode bekommt einen Zeiger auf die Instanzdaten mitgeliefert. Dieser referenziert die Basisklasse für die Implementierung. Die dort enthaltene Referenz data kann dann die entsprechenden Anwenderdaten referenzieren. Als Sicherheitscheck wird beim upcasting ein Typtest über Reflection ausgeführt.

Um die Methode irgendwo zu verankern wird ein Makro eingesetz, das Reflection und eine Methodentabelle definiert. In Java-Denkweise wäre das ein Teil der Definition einer anonymen Vererbung:

IFC_IMPL_dataMETHOD1_ObjectJc(InterProcessCommRx_ifc_Ipc, execRxData_TargetRx);

Dieses Makro ist in Jc/ObjectJc.h.html enthalten, weil es universell für alle ähnlich definierten Basisklassen gilt. Es ist als Makro an der Großschreibung zumindestens im Anfangsteil erkennbar, wie bei Makros oft üblich.

Nunmehr kann die Instanz der anonymen Implementierung des InterProcessCommRx_ifc_Ipc angelegt werden, entweder vorzugsweise innerhalb einer struct als Member, im Konstruktor für diese struct:

ctor_MyUserData(.....)
{ .....
  ctorO_InterProcessCommRx_ifc_Ipc(&thiz->targetRx.base.object, _thCxt);
  setReflection_ObjectJc(&thiz->targetRx.base.object, &reflection_execRxData_TargetRx, 0);
  thiz->targetRx.data = &thiz->base;  //the own data, can be used in the callback routine.

Das ist also eine Variable vom Typ der bekannten struct InterProcessCommRx_ifc_Ipc_s, der aber initial die Reflection zugewiesen werden, die eben mit dem Makro definiert wurden. Damit wird die oben definierte Methode assoziiert.

In der Anwendung des Konzepts im Beispiel kann nun die Instanz der InterProcessCommRxThread_Ipc angelegt werden, der diese Instanz als Referenz übergeben wird:

  thiz->targetIpc = create_InterProcessCommRxThread_Ipc(z_StringJc("UDP:127.0.0.1:60199"), thiz->targetRx, _thCxt);
  if(start_InterProcessCommRxThread_Ipc(thiz->targetIpc, null)) {
    ...

Die InterProcessCommRxThread_Ipc liegt als fertig nutzbares Modul vor. Mit der dem Constructor übergebenen Referenz auf die oben angelegte Interface-Implementierungsinstanz kann nun beim Empfang eines Telegramms auf der angegebenen IP-Adresse die ganz oben stehende Routine execRxData_TestTarget(...) als callback aufgerufen werden.

Wie ist die Basisklasse in C definiert:

Die Basisklasse, die die zu überladene Methode enthält, ist entweder in Java gegeben und wird von Java2C in der notwendigen Art als Sekundär-Quelle bereitgestellt oder entsprechend manuell erstellt. Die Java-Form sieht wie folgt aus:

public abstract class TYPE
{ @Java4C.SimpleRef public Object data;
  public abstract void METHOD(...);
}

Dabei ist TYPE eine beliebige Typ-Bezeichnung und METHOD(...) eine beliebige Methodendefinition mit beliebigen Argumenten. Wichtig ist nur die Bezeichnung des Datenzeigers data und die Tatsache, dass es genau eine überladene Methode gibt. Wenn die Klasse noch weitere Daten und Methoden enthält dann ist das zulässig.

Manuell geschrieben oder von Java2C übersetzt gibt es eine struct-Definition der Form:

typedef struct TYPE_t
{ union { ObjectJc object; } base;
  struct ObjectJc_t* data;
} TYPE_s;

Die Reflection-Definition der Basisklasse automatisch von Java2C generiert wird nicht benutzt. Aber die Methodentabelle erscheint in folgender Form:

extern const char sign_Mtbl_TYPE[]; //marker for methodTable check
typedef struct Mtbl_TYPE_t
{ MtblHeadJc head;
  MT_METHOD_TYPE* METHOD;
  Mtbl_ObjectJc ObjectJc;
} Mtbl_TYPE;

Das MT_METHOD_TYPE ist eine beliebige Funktionstyp-Definition, die dann auf die Anwenderfunktion passen muss, beispielsweise:

typedef void MT_METHOD_TYPE(TYPE_s* thiz, int arg1, float arg2, ThCxt* _thCxt);

oder dem obigen Beispiel entsprechend:

typedef void MT_execRxData_InterProcessCommRx_ifc_Ipc(InterProcessCommRx_ifc_Ipc_s* thiz
  , PtrVal_int8 buffer, int32 nrofBytesReceived, struct Address_InterProcessComm_t* sender, ThCxt* _thCxt);

Wie arbeitet dies?:

Das Makro verbirgt einigen Schreib- und Denkaufwand und kann als Blackbox gehandhabt werden. Ein Blick in das Makro zeigt die Anlage einer Methodentabelle mit der überladenen Methode, aber ansonsten Standardmethoden für ObjectJc:

#define IFC_IMPL_dataMETHOD1_ObjectJc(TYPE, METHOD) \
Mtbl_##TYPE static const mtbl_##METHOD = \
{ { sign_Mtbl_##TYPE , (struct Size_Mtbl_t*)((0 +2) * sizeof(void*)) } \
, METHOD \
, { { sign_Mtbl_ObjectJc, (struct Size_Mtbl_t*)((5 +2) * sizeof(void*)) } \
  , clone_ObjectJc_F, equals_ObjectJc_F, finalize_ObjectJc_F, hashCode_ObjectJc_F \
  , toString_ObjectJc_F \
  } \
} /*, { signEnd_Mtbl_ObjectJc, null } }*/; \
\

Die Methodentabelle ist const und wird daher auf einem PC in ein anderes Segment als veränderliche Daten angelegt, möglicherweise sogar mit write protection. In einer embedded Hardware-Umgebung kann es in einem ROM platz finden.

Die Methodentabelle muss in den Reflections referenziert werden. Daher ist die Reflection-Definition ebenfalls im Makro enthalten, als const:

\
extern_C struct ClassJc_t const reflection_##METHOD; \
static const struct Reflection_Fields_##METHOD_t  \
{ ObjectArrayJc head; FieldJc data[1]; } reflection_Fields_##METHOD = \
{ CONST_ObjectArrayJc(FieldJc, 1, OBJTYPE_FieldJc, null, &reflection_Fields_##METHOD) \
, { { "data" \
    , 0  \
    , &reflection_ObjectJc \
    , kReference_Modifier_reflectJc  \
    , (int16)((int32)(&((TYPE##_s*)(0x1000))->data) - (int32)(TYPE##_s*)0x1000) \
    , 0   \
    , &reflection_##METHOD \
    } \
} }; \
static const ClassJc reflection_##METHOD =  \
{ CONST_ObjectJc(OBJTYPE_ClassJc + sizeof(ClassJc), &reflection_ObjectJc, &reflection_ClassJc)  \
, #METHOD \
,  0  \
, sizeof(TYPE##_s) \
, (FieldJcArray const*)&reflection_Fields_##METHOD \
, null /*methods*/  \
, null /*superclasses */  \
, null  \
, 0     \
, &mtbl_##METHOD.head   \
};

Die Reflections bestehen wie gewöhnlich aus den Feld-Definitionen und der Class-Definition. Die Feld-Definition definiert die data-Referenz. Das ist unabhängig von den tatsächlichen Elementen in der gegebenen Struktur. Diese kann auch noch weitere Elemente enthalten. Diese sind jedoch nicht über ide hier definierten Reflection sichtbar. Das Element data muss jedenfalls enthalten sein, sonst gibt's nachher einen Compilerfehler. Das Wesentliche an der Reflectiondefinition ist die Referenz auf die oben definierte Methodentabelle.

Die Verwendung von Makros ist, wie bekannt, umstritten. Die Compilerfehlermeldungen bei Schreibfehlern sind nach Auflösung des Makros bezogen. Man erkennt schlecht die eigentliche Fehlerursache in der Quelle. Das trifft insbesondere zu wenn das Makro selbst fehlerhaft ist. Vorraussetzung für die erfolgreiche Verwendung von Makros ist also dessen Fehlerfreiheit. Tip: Beim Erstellen des Makros kann man ggf. mehrfach das Ergebnis des Präprozessorlaufs per Zwischenablage in den compilierenden Quellcode direkt übertragen, in mehrere Zeilen umbrechen und auf diese Weise die Fehlerursachen besser erkennen. Letzlich, wenn das Makro fehlerfrei ist, dann ist es gut.