CRuntime_Javalike: Reflection


 

Was bietet das Reflection-Prinzip

Reflection in Java

Reflection - Anwendungen in C und C++

Unterstützte Methoden der Reflectionklassen in C

In C und C++ werden nicht die gesamte Funktionalität der Reflections aus Java unterstützt, folgende Teile sind ausgenommen:

Der Klassenlader ist eine typische Eigenschaft der virtuellen Maschine und für C(++)-Anwendungen nicht zutreffend. Die Security-check.Mechanismen sind insbesondere für den Einsatz von Java in unbekannten entfernten Umgebungen (Internet-Applikationen) gedacht und ebenfalls nicht zutreffend. Die strenge Unterscheidung der Zugriffsrechte sind ein Grundkonzept der Kapselung in der Objektorientierung. So ist es in Java zwar möglich, zu erfahren, dass es ein bestimmtes private-Attribute gibt, jedoch der Zugriff darauf wird mit einer Security-Exception beantwortet. Für die C(++)-Anwendung ist diese strenge Kapselung gelockert, in die Verantwortung des Anwendungsprogrammierers gelegt.

Nachfolgend werden für die drei Hauptklassen der Reflections die Methoden kurz benannt, die unterstützt werden.

class Class_Jc

siehe Quelldokumentation

class Field_Jc

siehe Quelldokumentation

class Method_Jc

siehe Quelldokumentation

Aufbau der Reflection-Daten

Die Reflectiondaten sollten im Anwenderprogramm vorliegen und eingebunden werden, wenn sie benutzt werden sollen. Grundsätzlich besteht aber auch die Möglichkeit, die Reflectiondaten offline beispielsweise in einer Datei zu halten und erst im Falle des Zugriffes zu laden. Dazu muss aber beachtet werden, dass die Reflectiondaten selbst verzeigert sind (Klasse zeigt auf ihre Attribut-Informationen, Attribut-Informationen zeigt auf die Klassen entsprechend dem Attributtyp usw.). Dazu müsste beim Laden erst die Verzeigerung korrigiert werden. Diese Arbeit übernimmt der Linker, wenn die Reflections gleich von vornherein zum Programmcode gehören.

Alle Daten sind Konstante

Grundsätzlich sind die gesamten Reflection-Infos als Konstante ausgeführt. Das bedeutet, dass kein zusätzlicher Code zum Erstellen der Daten, auch nicht beim Hochlauf nötig ist. Wichtiger ist gegebenenfalls noch, dass diese Konstanten sich auch in einem ROM-Bereich befinden können. Das ist wichtig, weil

Die konstanten Reflection-Infos sind so gestaltbar, das jeweils zusammengehörige Informationen zum Beispiel zu einem Modul, für alle außen auswertbaren Daten, oder auch alle Reflection-Infos in einer großen Struktur zusammengehalten werden. Damit ist es möglich, diese Struktur bytegenau in einen File zu übertragen als Strukturinformation zu ebenfalls ausgegebenen Daten.

Headerfile mit Strukturinformation und Zugriffsdefinition

Letzlich interessiert in anderen Modulen die Adresse der jeweiligen Class_Jc. Und zwar muss in jeder Instanz in Object_Jc ein Zeiger auf die Reflectionklasse Type Class_Jc const* vorhanden sein. Der interne Aufbau der Reflection-Infos ist nicht interessant. Da aber auch innerhalb der Reflections Zeiger auf andere Class_Jc const* notwendig sind, andererseits man alle Reflection-Infos in einer gesamten Struktur zusammenhalten will, muss diese Strukturinformation sich letztlich in einem Headerfile wiederfinden. Ein solcher Hederfile hat folgenden Aufbau, am Beispiel der Reflection für Object_Jc und umgebende Klassen:

#ifndef __Object_Jc_REFLECTION_h__
#define __Object_Jc_REFLECTION_h__
#include "Reflection_Jc.h"

typedef const struct Reflectiondata_Object_Jc_t
{ Object_Jc object;

  struct{ Class_Jc clazz; Vtbl_Object_Jc vtbl;} reflect_String_Jc;
  struct{ ObjectArray_Jc array; Field_Jc field[1];} attributes_String_Jc;
  struct{ ObjectArray_Jc array; Method_Jc field[1];} methods_String_Jc;

  struct{ Class_Jc clazz; Vtbl_Object_Jc vtbl;} reflect_Object_Jc;
  struct{ ObjectArray_Jc array; Field_Jc field[4];} attributes_Object_Jc;

  struct{ Class_Jc clazz; Vtbl_Object_Jc vtbl;} reflect_Substring_Jc;
  struct{ ObjectArray_Jc array; Field_Jc field[4];} attributes_Substring_Jc;

  struct{ Class_Jc clazz; Vtbl_Object_Jc vtbl;} reflect_StringBuffer_Jc;
  struct{ ObjectArray_Jc array; Field_Jc field[4];} attributes_StringBuffer_Jc;

}Reflectiondata_Object_Jc;

extern Reflectiondata_Object_Jc reflectiondata_Object_Jc;


/**Definition of all REFLECTION_Type there are found in this header,
 * may be used also from other REFLECTION.c-files including this header.
 */
#define REFLECTION_String_Jc_t &reflectiondata_Object_Jc.reflect_String_Jc.clazz
#define REFLECTION_String_Jc   &reflectiondata_Object_Jc.reflect_String_Jc.clazz

#define REFLECTION_Object_Jc_t &reflectiondata_Object_Jc.reflect_Object_Jc.clazz
#define REFLECTION_Object_Jc   &reflectiondata_Object_Jc.reflect_Object_Jc.clazz

#define REFLECTION_Substring_Jc_t &reflectiondata_Object_Jc.reflect_Substring_Jc.clazz
#define REFLECTION_Substring_Jc   &reflectiondata_Object_Jc.reflect_Substring_Jc.clazz

#define REFLECTION_StringBuffer_Jc_t &reflectiondata_Object_Jc.reflect_StringBuffer_Jc.clazz
#define REFLECTION_StringBuffer_Jc   &reflectiondata_Object_Jc.reflect_StringBuffer_Jc.clazz


#endif //__Object_Jc_REFLECTION_h__

In der Gesamtstruktur reflectiondata_Object_Jc sind für alle beteiligten Klassen jeweils die Daten für die Klasse selbst, die virtuellen Tabellen, die Daten aller Attribute und Methoden enthalten. Damit man mit einer einfachen und genormten Schreibweise von außen auf die Daten zugreifen kann, gibt es für jede Klasse Defines. Hier sind Problematiken der automatischen Codegenerierung mit berücksichtigt. Beispielsweise kann in einem Anwenderkontext ein Typ nur über seine Vorwärtsdeklaration bekannt sein. Diese wird als struct Type_t * geschrieben. Die Codegenerierung für diese Reflection möchte die dort stehende Typangabe formal übernehmen, daher gibt es jeweils auch die Type_t-Variante.

C-File mit den Konstantendefinitionen

Die im Headerfile definierte Strukturdefinition muss mit tatsächlichen Daten ausgefüllt werden. Hier ist nur ein kleiner Ausschnitt passend zum obigen Headerfile gezeigt:

#include "Object_Jc_REFLECTION.h"
#include "OtherUser_REFLECTION.h"

#undef BASE_REFLECTION_CURRENT_Jc
#define BASE_REFLECTION_CURRENT_Jc reflectiondata_Object_Jc

Reflectiondata_Object_Jc reflectiondata_Object_Jc
 =
{ CONST_Object_Jc(OBJTYPE_ReflectionImage_Jc + sizeof(reflectiondata_Object_Jc), &reflectiondata_Object_Jc, null)


  /*:NOTE: CLASS_CURRENT_Jc and REFLECTION_CURRENT_Jc are used inside CONST_FIELD_Jc and its related defines.
    REFLECTION_String_Jc is a define, representing the pointer to the reflection class.
  */
  #undef CLASS_CURRENT_Jc
  #define CLASS_CURRENT_Jc String_Jc
  #undef REFLECTION_CURRENT_Jc
  #define REFLECTION_CURRENT_Jc REFLECTION_String_Jc
, { { CONST_Object_Jc(OBJTYPE_Class_Jc + sizeof(reflectiondata_Object_Jc.reflect_String_Jc), &reflectiondata_Object_Jc.reflect_String_Jc, null)
    , "String_Jc"
    , 0     //posOwnData
    , sizeof(String_Jc)
    , (Field_JcArray const*)&reflectiondata_Object_Jc.attributes_String_Jc
    , null  //method
    , null  //superclass
    , null  //interfaces
    , 0     //modifier
    }//class_String_Jc
  , CONST_VtblStd_Object_Jc
  }
, { { CONST_Object_Jc(OBJTYPE_Field_Jc + sizeof(reflectiondata_Object_Jc.attributes_String_Jc), &reflectiondata_Object_Jc.attributes_String_Jc, null), 2, sizeof(Field_Jc), kDirect_ObjectArray_Jc }
  , { CONST_FIELD_Jc("refbase", refbase, REFLECTION_ObjectRefValues_Jc, 0)
    }
  }//attributes


  #undef CLASS_CURRENT_Jc
  #define CLASS_CURRENT_Jc StringBuffer_Jc
  #undef REFLECTION_CURRENT_Jc
  #define REFLECTION_CURRENT_Jc REFLECTION_StringBuffer_Jc
, { { CONST_Object_Jc(OBJTYPE_Class_Jc + sizeof(reflectiondata_Object_Jc.reflect_StringBuffer_Jc), &reflectiondata_Object_Jc.reflect_StringBuffer_Jc, null)
    , "StringBuffer_Jc"
    , 0     //posOwnData
    , sizeof(StringBuffer_Jc)
    , (Field_JcArray const*)&reflectiondata_Object_Jc.attributes_StringBuffer_Jc
    , null  //method
    , null  //superclass
    , null  //interfaces
    , 0     //modifier
    }//class_StringBuffer_Jc
  , CONST_VtblStd_Object_Jc
  }
, { { CONST_Object_Jc(OBJTYPE_Field_Jc + sizeof(reflectiondata_Object_Jc.attributes_StringBuffer_Jc), &reflectiondata_Object_Jc.attributes_StringBuffer_Jc, null), 2, sizeof(Field_Jc), kDirect_ObjectArray_Jc }
  , { CONST_FIELD_Jc("object", object, REFLECTION_Object_Jc, 0)
    , CONST_FIELD_Jc("count", count, REFLECTION_int16, 0)
    , CONST_FIELD_Jc("maxNumberOfChars", maxNumberOfChars, REFLECTION_int16, 0)
    , CONST_FIELD_Jc("valueDirect", valueDirect, REFLECTION_char, 0)
    }
  }//attributes


};

Im C-File wird auch bei automatischer Generierung mit Makros gearbeitet, CONST_FIELD_Jc ist letzlich wieder eine {...} - Konstruktion mit den Konstanten für ein Attribut. Das lässt sich leichter lesen, ein Compiler hat keine Probleme damit. Im C-File treten die Referenzen auf andere Reflections auf.

Die Reflections für die skalaren Basistypen, REFLECTION_char usw., sind in Reflection_Jc.h als Konstante im kleinen Bereich (bis max. 100) definiert und als (Class_Jc const *) umgecastet. Das hat den Vorteil, dass bei ausgelagerten Dateien diese Zeiger problemlos erkannt werden können, ohne diese Reflectiondaten selbst zu haben. Damit kommt ein Datenabbild nur mit seinen eigenen Reflectiondaten aus, wenn davon ausgegangen wird, das ein Datenabbild neben seinen eigenen Strukturen meist nur skalare Basistypen verwendet, und Zeiger auf fremde Typen (Interfaces), die dann aber sowieso nicht im Datenabbild dabei sind.

Für alle anderen Typen muss der Compiler die Definition des jeweiligen Types, enthalten in irgendeinem Headerfile wenn ein komplexes System vorliegt, kennen. Das bedeutet, dass jedes REFLECTION-c-Modul alle vorhandenen REFLECTION.h includieren sollte, die zur jeweiligen Executable dazugehören.

Automatische Generierung von Reflection aus Headerfiles

Die Reflection enthalten keine andere Information, als der Compiler aus den Informationen in Headerfiles sie zusammenstellt. Es wäre zusätzliche Mühe, diese Codes mit der Hand schreiben zu müssen. Daher gibt es folgendes Verfahren zur Generierung der .c und .h-Files:

 

Zugriff auf Daten von Strukturen in C


Zugriff auf Daten von Klassen in C++

Die drei C-like Klassen Class_Jc, Field_Jc und Method_Jc liegen in der C++-Form als Class_Jcpp, Field_Jcpp und Method_Jcpp vor. In C++ wird empfohlen, die C++-Klassen zu verwenden.

Bei den Datenzugriffen wird die Referenz auf die Instanz als Argument angegeben. In C ist auch bei C-like-Interfaces der Zeiger auf die Instanz immer identisch mit dem Zeiger auf die Basisklasse Object_Jc. Bei den Datenzugriffen auf Feldelemente, die selbst nicht auf Object_Jc basieren, sondern nur deren Feld-Kopfstruktur ObjectArrayHead_Jc, ist der Zeiger auf die Instanzdaten nicht auf Object_Jc* konvertierbar. Daher wird bei den Methoden des Zugriffes für C wie

METHOD_C int getInt_Field_Jc(const Field_c* ythis, void* obj, STACKTRCP);

der Zeiger auf die Instanz in der Form void* übergeben, siehe Abschnitt "Zugriff auf Daten von Arrays ohne Reflectioninfo der einzelnen Elemente"

Bei C++ ist dagegen der Zeiger auf die Instanzdaten nicht immer identisch mit dem Zeiger auf die unterste Basisklasse Object_Jcpp. Handelt es sich um eine Referenz von einem Interfacetyp oder einer Basisklasse in Mehrfachvererbung, dann zeigt die Referenz auf eine Stelle inmitten der Instanzdaten, an der die Referenz auf die virtuelle Tabelle dieses Basistyps angeordnet ist.

class Sample: public Object_Jcpp, public SampleIfc{ ...
  
    002F26C8  38 93 40 00  Zeiger auf virtuelle Tabelle von Object_Jcpp
--> 002F26CC  FF FF FF FF  Dateninhalt von Object_Jc: - Semaphore
    002F26D0  68 B2 40 00                             - Zeiger auf Reflection
==> 002F26D4  7C 94 40 00  Zeiger auf virtuelle Tabelle des Interfaces SampleIfc
    002F26DC  00 00 00 00  Beginn der Daten der Klasse

Im obigem Beispiel ist --> die Basis für den Offset der Daten, ==> ist dagegen die Adresse, die in der Referenz auf den Interfacetyp steht.

Würde die Referenz unmittelbar als Argument für die C-Methoden angegeben, dann würde keinerlei Zeigerkorrektor erfolgen, da ein void*-Typ verlangt wird. Damit erfolgt der Zugriff auf falsche Adressen. Daher gibt es folgende Regel:

Demzufolge müsste man die C-Methode in der Form

int val = getInt_Field_Jc(theField, instance->toObject_Jc(), STACKTRC);

aufrufen. Allerdings ist damit zu rechnen, dass der Anwender den Aufruf von toObject_Jc() vergisst und dass daher mit dem falschen etwas danebenliegenden Zeiger auf die Instanz gearbeitet wird. Dieser Fehler kann beim Compilieren nicht bemerkt werden, da void* an dieser Stelle verlangt wird.

Werden dagegen für C++-Klassen auch die C++-Variante Field_Jcpp verwendet, dann wird als Argumenttyp an der entsprechenden Stelle ObjectIfcBase_Jcpp* verlangt:

class  Field_Jcpp: public Field_c
{ ...
  public: int getInt(ObjectIfcBase_Jcpp* obj, STACKTRCP) const
  ...
}

Nach diesem Typ sind alle Interface- und Klassenreferenzen automatisch castbar (ohne manuelle casting-Angabe!), damit wird gleichzeitig kontrolliert, dass die entsprechende Basisklasse Object_Jcpp bzw. ObjectIfcBase_Jcpp verwendet wurde. Damit ist der Aufruf compilergesichert.

Folgendes Beispiel zeigt die Verwendung der C++-Typen:

    Class_Jcpp const* clazz = instance->getClass();                //instace may be also an interface type
    Field_Jcpp const* fieldData = clazz->getField(name, STACKTRC);
    int value = fieldData->get(instance, STACKTRC);


Zugriff auf Daten von Arrays ohne Reflectioninfo der einzelnen Elemente

In Java sind alle Feldelemente, die keine einfache skalare Variable darstellen (int, float, ...), grundsätzlich über Referenzen geführt. Die referenzierten Instanzen erben von Object und sind demzufolge mit Reflections zugänglich. Beispiel:

final class Data
{ int a;
  float f;
}
 
...
Data[] dataArray = new Data[12];

In dem gezeigtem Beispiel wird nicht etwa, wie in C(++) zu erwarten, ein Feld mit 12 Elementen vom Typ Data angelegt, sondern es wird nur ein Feld mit 12 nichtinitialisierten Referenzen angelegt. Würde Data nicht final sein, so könnten dort auch von Data abgeleitete Klassen referenziert werden. Um die Feldelemente tatsächlich anzulegen, ist nochmal Arbeit erforderlich:

for(int i = 0; i < dataArray.length(); i++){ dataArray[i] = new Data(); }

Erst damit bekommt man das in C erwartete Ergebnis. Java ist hier etwas komplizierter, aber dafür universell und sicher. Das entspricht dem Referenzkonzept, Java kennt keine "eingebetteten Daten", in C(++) sind eingebetteten Daten dagegen oft erwünscht und durchaus oppurtun. Ein Feld aus zusammengesetzten Elementen soll direkt angelegt werden:

typedef struct Data_t
{ int a;
  float f;
}Data;
 
Data* dataArray = new Data[12];  //(C++)
Data* dataArray = (Data*)(malloc(12*sizeof(Data));  //C-Variante

Dieses einfache und übliche Konstrukt aus C kennt jedoch weder eine Basisklasse Object, damit keine Reflection, noch ist eine Information über die Anzahl der Feldelemente direkt mit dem Feld verbunden vorhanden (muss geeignet extra programmiert werden).

Basisklasse ObjectArrayHead_Jc ist zu verwenden

Aus obigen Grund ist in der CRuntime_Javalike eine Basisklasse ObjectArrayHead_Jc definiert. Diese enthält die Kopfdaten eines beliebigen Feldes und ist von Object_Jc abgeleitet. Es ist möglich, dass die Feldelemente unmittelbar nach den Kopfdaten folgen.Dann bekommt man eine ähnlich kompakte Datenstruktur wie man es von C aus gewöhnt ist. Auf dieser Basis arbeitet das Reflectionprinzip für Arrays. Beispiel:

typedef struct Data_t
{ int a;
  float f;
}Data;
 

typedef struct DataArray_t
{ ObjectArrayHead_Jc array;
  Data data[100];    //NOTE: 100 is only a debug size, use dynamic size, see sizeof_DataArray.
}DataArray;
 
/*Constructor sets default values at the elements and set the head data of array.*/
DataArray* constructor_DataArray(DataArray* ythis, int size);
 
#define sizeof_DataArray(SIZE) (sizeof(ObjectArrayHead_Jc) + SIZE * sizeof(Data))
 
#define new_DataArray(SIZE) constructor_DataArray( (DataArray*)malloc(sizeof_DataArray(SIZE)), SIZE ))
 

DataArray* dataArray = new_DataArray(12);

Im Speicher werden die Kopfdaten und die angegebene Anzahl von Feldelementen des Typs Data in einem Block hintereinander angeordnet. Das ist die Basis für Reflections auf Arrays.

In C: Die Reflections des Arraytypes enthalten die Informationen der Elemente

In Java enthält die Reflectioninformation (Class) einer Feldinstanz lediglich den Namen des Feldes, keine weiteren Informationen. Auch das Attribut length, Bestandteil jeder Arrayinstanz, wird in den Reflections nicht angezeigt. Es ist selbstverständlich.

final class Data
{ int a;
  float f;
}
 
...
Data[] dataArray = new Data[12];
for(int i = 0; i < dataArray.length(); i++){ dataArray[i] = new Data(); }
 
Class classDataArray = dataArray.getClass();
Class classData      = dataArray[0].getClass();
System.out.println(classDataArray.getName());
Field[] fieldsDataArray = classDataArray.getDeclaredFields();
System.out.println(classData.getName());
Field[] fieldsData      = classData.getDeclaredFields();
Field field_a           = classData.getField("a");

Die erste Ausgabe für classDataArray.getName() liefert "[Lpackage.Data" als Anzeige, das es sich um ein Feld vom Referenztyp Data handelt. Das fieldsDataArray ist ein Feld der Länge 0. Die zweite Ausgabe für classData.getName() liefert "package.Data" als Typ des Feldelementes, das fieldsData enthält die Attribute von Data.

In C(++) ist für ein mit ObjectArrayHead_Jc gebautes Feld keine Reflection für die Elemente abrufbar, statt dessen ist beides kombiniert mit

Class_Jc* classDataArray = getClass_Object_Jc(&dataArray->array.object);
Field_Jca* fieldsDataArray = getDeclaredFields(classDataArray);
Field_Jc field_a           = getField_Class(classDataArray, const_String("a"));
println_PrintStream(&System.out, getName_Class_Jc(classDataArray);

abrufbar. Die Ausgabe liefert hier wie in Java "[LData", aber fieldsDataArray enthält die Attribute des Feldelementes. Das ist möglich, da die Elemente mit genau diesem Datentyp (und im Gegensatz zu Java nicht mit einem möglicherweise abgeleitetem Datentyp oder mit null) besetzt sind.

Probleme beim Test der Gültigkeit des angegebenen Feldelementes

In Java wird ein Wert eines Attributes gelesen oder gesetzt mit

Field field_a = dataArray[0].getClass().getField("a");
int a = field_a.getInt(dataArray[5]);
field_a.setInt(dataArray[5], 123); 

Das field_a ist dabei aus der Reflection eines Feldelementes bestimmt, aber nicht notwendigerweise aus dem des 5. Feldelementes, sondern beispielsweise aus dem ersten, da alle Elemente vom Aufbau her identisch sind. Grundsätzlich gilt in Java, dass beim Zugriff auf ein Element getestet wird, ob die angegebene Instanz dem selben Typ entspricht wie der class-Typ, der dem Field entspricht (ermittelbar auch manuell programmiert mit field_a.getDeclaringClass(). Nur so kann gesichert werden, dass keine unpassenden Instanzen beim Feldzugriff angegeben werden. Der Aufwand zur Runtime ist ähnlich kritisch/unkritisch zu bewerten wie die Tatsache in Java, dass jeder Index eines Feldes zur Runtime getestet wird. Die VM optimiert hier einiges, es handelt sich jeweils nur im Nanosekunden bei den üblichen Prozessoren.

In C(++) stehen beim adäquaten Aufruf

Field_Jc* field_a = getField_Class_Jc(getClass_Object_Jc(dataArray),const_String_Jc("a"));
int a = getInt_Field_Jc(field_a, dataArray[5]);
setInt_Field_Jc(field_a, dataArray[5], 123); 

die Reflection-Infos aus dataArray[i] nicht zur Verfügung, da ein Feldelement nicht auf Object basiert. Daher muss in diesem Fall der Test der Zulässigkeit des Zugriffes entfallen. Das entspricht aber dem allgemeinem Programmierparadigma in C(++). Es werden an vielen Stellen Zeiger verwendet, ohne nochmaligen expliziten Zulässigkeitstest.

Die Zulässigkeit kann per manueller Programmierung aber wie folgt getestet werden:

if(getDeclaringClass_Field_Jc(field_a) == getClass_Object_Jc(dataArray))
{...}

Das kann in Java ebenfalls genauso explizit erfolgen. Basis für den Test ist hier das Feld selbst, in Java steht an dieser Stelle adäquat wie oben bei der Ermittlung von field_a das Feldelement:

if(field_a.getDeclaringClass() == dataArray[5].getClass())
{...}

 

Ablauf des Aufrufes von Methoden

Ermittlung der Zugehörigkeit von Basistypen und Interfaces mit instanceof_Class_Jc(Object, Class)