CRuntimeJavalike - C oder C++ oder beides

CRuntimeJavalike - C oder C++ oder beides

Inhalt


1 CRuntimeJavalike für C und C++ verwendbar

Topic:.C_Cpp.UsingCandCpp.

Das CRuntimeJavalike-Framework eignet sich grundsätzlich zum Einsatz sowohl in reinen C++-Umgebungen, in gemischten Umgebungen oder in reiner C-Programmierung ohne Nutzung eines C-Compilers. Nachstehend ist die Anwendung der Klasse Thread_Jc dargestellt:

C++

C

Java

 /* MyTestThread.h */
 #include "Thread_Jc.h"
 //
 /**CLASS_C-Definition. */
 class MyThread: public Thread_Jc
 {
   public: MyThread(const char* sName);
   private: int counter;
   private: virtual void run(void);
 };
 //
 //
 /***************************************************/
 /* MyTestThread.c */
 //
 //
 //
 //Reflection definition is not necessary,
 //but it is able to use it anyway.
 //It is not defined in this example.
 //The definition of reflection is adequate like C
 //
 //
 //
 //
 //
 //
 //
 //
 //
 //
 //
 //
 //
 /**Constructor. */
 MyThread::MyThread(const char* sName)
 : Thread_Jc(sName), counter(0)
 {
 }
 //
 //
 //
 //
 //
 //
 //
 //
 /**run-Method. */
 void MyThread::run(void)
 {
   counter = 0;
   while(counter < 10)
   { printf("\nMyThread::run(): %i", counter);
     sleep(1000);
     counter +=1;
   }
 }
 //
 /********************************************/
 /* Test */
 //
 METHOD_C void testSpecial()
 {
   MyThread* thread1
   = new MyThread("MyThread");
   printf("\nspecial(): starting MyTestThread ...");
   thread1->start();
   while(thread1->isAlive())
   { Thread_Jc::sleep(100);
   }
   printf("\nspecial(): MyTestThread finished.\n");
 }
 /* MyTestThread.h */
 #include "Thread_Jc.h"
 //
 /**CLASS_C-Definition. */
 typedef struct MyTestThread_t
 { Thread_Jc super;
   int counter;
 } MyTestThread;
 MyTestThread* ctor_MyTestThread(MyTestThread*ythis, const char* sName);
 void run_MyTestThread_F(Thread_Jc* ythis);
 //
 //
 /*************************************************************************/
 /* MyTestThread.c */
 #include "Reflection_Jc.h"
 //
 /**Reflection-Image necessary for virtual table. */
 #undef REFLECTION_IMAGE
 #define REFLECTION_IMAGE reflectionImage_MyTestThread
 struct ReflectionImage_MyTestThread
 { Object_Jc object;
   struct { Class_Jc clazz; Vtbl_Thread_sJc; }reflect_MyTestThread;
   struct{ ObjectArray_Jc array; Field_Jc data[1];} attributes_MyTestThread;
 } reflectionImage_MyTestThread
 =
 { CONST_Object_Jc_REFLECTION_IMAGE()
 , { CONST_data_Class_Jc("MyTestThread", MyTestThread)
   , { CONST_VtblStd_Object_Jc
     , run_MyTestThread_F
   } }
 , { CONST_ARRAYHEAD_Field_Jc(MyTestThread)
   , CONST_Field_Jc(MyTestThread, "counter",counter, int, 0)
 } };
 //
 /**Constructor. */
 MyTestThread* ctor1_MyTestThread( MyTestThread*ythis, const char* sName )
 { REF_Runnable_Jc runable = NULLREF_Jc;
   ctor2_Thread_sJc(&ythis->super, runable, sName);
   setReflection_Object_Jc
   ( &ythis->super.object
   , &reflectionImage_MyTestThread.reflect_MyTestThread.clazz
   , sizeof(MyTestThread)
   );
   ythis->counter = 0;
   return ythis;
 }
 //
 /**run-Method. */
 void run_MyTestThread_F(Thread_Jc* othis)
 { MyTestThread* ythis = (MyTestThread*)(othis);
   ythis->counter = 0;
   while(ythis->counter < 10)
   { printf("\nMyTestThread::run(): %i", ythis->counter);
     sleep_Thread_Jc(1000);
     ythis->counter +=1;
   }
 }
 //
 /************************************************************************/
 /* Test */
 #include "RuntimeHeap_Jc.h"
 METHOD_C void testSpecial()
 {
   MyTestThread* thread1
   = NEW1_Jc(MyTestThread, "MyTestThread");
   printf("\nspecial(): starting MyTestThread ...");
   start_Thread_Jc(&thread1->super);
   while(isAlive_Thread_Jc(&thread1->super))
   { sleep_Thread_Jc(100);
   }
   printf("\nspecial(): MyTestThread finished.\n");
 }

Die Variante machen beide das Gleiche: Es wird eine Klasse abgeleitet von Thread_Jc angelegt. Die Methode run() ist überladen. Wenn die Methode start() von Thread_Jc gerufen wird, dann wird im System ein neuer Thread angelegt. Im neuen Thread wird die ggf und in diesem Fall überladene Methode run() gestartet. Solange run() läuft, ist der zweite Thread aktiv. Das ist das Gleiche was in Java mit der Klasse java.lang.Thread gemacht werden kann.

In beiden Fällen werden die selben Files aus CRuntimeJavalike verwendet. Der Unterschied ist, dass innerhalb dieser CRuntimeJavalike-Header und -Sources verschiedene Files CRuntimeJavalike_SysConventions.h includiert werden, weil die Include-Pathes verschieden gesetzt sind. Dieser Headerfile enthält einige Defines, die die interne Compilierung für C oder C++ steuern.

Auffällig ist, das die C++-Variante sehr viel kürzer ist. Das, was nicht geschrieben werden braucht, erledigt der viel leistungsfähigere C++-Compiler:

Aber:

In C++ steht mitten in den Anwenderdaten ein Zeiger auf die virtuelle Tabelle der zugehörigen Klasse. Zum einen ist damit die Struktur der Daten nicht klar definiert, beispielsweise bei Übertragung der Daten aus einem anderen Speicherbereich, Netzwerkkommunikation, File und dergleichen. Das funktioniert überhaupt nicht in C++, wohl aber in C. Zum anderen läuft die Programmabarbeitung unmittelbar über diesen Zeiger mitten in den Daten. Wenn es einen (versteckten) Softwarefehler gibt, der Daten falsch kopiert, den Zeiger verschiebt oder ähnlich, dann kann es passieren, dass der Zeiger auf eine falsche virtuelle Tabelle oder auf eine falsche Position in der berstehenden virtuellen Tabelle zeigt. Die Programmabarbeitung könnte dann immernoch als funktionsfähig erscheinen - es gibt keinen sichtbaren Absturz. Gegebenenfalls werden die gestörten virtuellen Methoden nur selten gerufen. Ein solches Szenario ist gar nicht so unwahrscheinlich. Ähnliches passiert auch, wenn ein Modul compiliert wird mit einem modifizierten Inhalt in einer Klassendefinition in einem Headerfile, ein zugehöriges Modul das den selben Headerfile einzieht, wird aber aus verschiedenen Gründen (in einer lib eingebunden, die beim Makeprozess nicht erfasst wird) nicht neu compiliert. Dann werden gegebenenfalls wegen einer verschobenen virtuellen Tabelle die falschen Methoden aufgerufen. Dass das passiert, ist Praxiserfahrung.

Man stelle sich vor, in einer Ausnahmesituation wird ein Reaktor hochgefahren, weil shutup() gerufen wird, anstelle von shutdown().

Das ist ein generelles Sicherheitsrisiko in C++, funktionsprinzip-bedingt. In C lässt sich das vermeiden:

Wenn der Maschinencode in einen schreibgeschützten Bereich gespeichert wird, ist eine Änderung aufgrund von Softwarefehlern ausgeschlossen. Wenn über den schreibgeschützten Bereich zyklisch ein Speichertest (Prüfcode, CRC) läuft, dann kann ein Hardwarefehler frühzeitig detektiert werden. Damit kann ein Programm grundsätzlich nur in vorgegebenen getesteten und geprüften Bahnen laufen. Wird der Datenspeicher modul- oder threadübergreifend aufgrund von Softwarefehlern gestört, dann kann das richtige Programm gegebenenfalls zwar mit falschen Daten arbeiten. Für solche Fälle ist aber in sicherheitskritischen Anwendungen eine redundante Datenhaltung mit fest programmierten Vergleichsalgorithmen einbaubar. Eine durch einen Fehler verursachte unsystematische Änderung wird damit detektiert. Für systematische Änderungen (Fehler liegen so, dass redundante Informationen gleichermaßen beeinflusst werden) hilft Tests in übergreifenden Kontexten oder aus verschiedenen algorithmischen Ansätzen heraus.

Für die Implementierung der virtuellen Methodenaufrufe wie in DynamicCall_in_C beschrieben steht ein Index in den Daten. Dieser Index kann verfälscht sein, was auch dort zum Aufruf der falschen Methode führt. Aber hier kann zusätzlich ein algorithmisch verarbeiteter Vergleichswert (Methoden-Signatur-Identifikator) getestet werden. Damit ist dieser Aufruf viel sicherer aus der dirkete Gang der Programmabarbeitung über einen Zeiger einer Sprungadresstabelle mitten in den Daten.

Ergo: Mit C lässt sich sicherheitsrelevante Software bauen, mit C++ nicht.

Der Makel des "mehr schreiben müssens" in C wird ausgeglichen, wenn der C-Code generiert wird, beispielsweise aus Java, oder aus einer Metasprache eines UML-Tools, Außerdem werden Verschreiber vom C-Compiler meistenteils als Compilerfehler detektiert.


2 Quellen für C++ und C als C++-class

Topic:.C_Cpp.Cpp_CppUsing.

Es kann nützlich sein, Quellen so zu schreiben, dass sie für die C-Compilierung und für die C++-Compilierung geeignet sind, ohne dass die Quellen geändert werden müssen. Das trifft zu beispielsweise bei Treibern, die für beide Plattformen funktionsgleich sein sollen, oder bei einem Test in einer C++-Umgebung und einer anschließenden Implementierung in einer C-Umgebung. Es ist dabei möglich, dass Instanzen in der C++-Umgebung auch für echte C++-Umsetzung gedacht sein sollen, also class sind, die beispielsweise virtuellen Methoden enthalten. Für die C-Umgebung sind die selben Instanzen jedoch C-Strukturen mit der Abbildung von virtuellen Methoden in C, wie in DynamicCall_in_C beschrieben.

Oft erscheint es als einfacher, für beide Implementierungsplattformen entweder getrennte Quellen zu führen. Die Inhalte der Methoden, die eigentlichen Algorithmen, sind aber identisch und auch in der C- und C++-Syntax einfach identisch zu halten. Daher die Idee, nur eine Quelle zu pflegen.

Möglicherweise ist auch für eine Zielgruppe nur eine C++-Implementierung interessant, eine andere Zielgruppe braucht aber adäquate Dinge in C. In solchen Fällen wird oft so vorgegangen, dass es jedenfalls eine C-Implementierung der Funktionalität gibt. Die meisten API von Betriebssystemen, auch Windows, bieten zunächst alle Schnittstellen in C an. Die C++-Implementierung ist dann darum gebaut (wrapper), eigentlich nur deshalb, damit der nutzende C++-Programmierer diese Funktionalitäten wie in C++ gewohnt sehen kann.

Solche Lösungen sind relativ einfach handhabbar und in der CRuntimeJavalike für alle Klassen ebenfalls realisiert. Die class-Definition für C++ wird bedingt compiliert hinzugefügt, in deren Methoden werden jeweils unmittelbar die C-Methoden gerufen. Ein gekürztes Beispiel aus Object_Jc.h:

 #if defined(__CPLUSPLUS_Jcpp) && defined(__cplusplus)
   class  Object_Jcpp: public ObjectifcBase_Jcpp, public Object_Jc
   { public virtual void toString(StringBuffer_Jc* buffer);
     /*...*/
   };
 #endif  /*__CPLUSPLUS_Jcpp*/

Im Compilerschalter wird hier berücksichtigt, dass die C++-Compilierung erwünscht ist (__CPLUSPLUS_Jcpp) und auch tatsächlich ausgeführt wird (__cplusplus). Es ist möglich, dass ein C++-Compiler benutzt wird, aber nur ein C-Abbild compiliert werden soll. Andererseits könnten in C++-Umgebungen *.c-Files nur als C compiliert werden, dieser Quellcode aber als Header eingezogen werden. Wenn dann das Makro __CPLUSPLUS_Jcpp global gesetzt ist, sollen dennoch die C++-Anteile dafür unberücksichtigt bleiben. Beides wird hier gewährleistet.

Die Lösung, die Daten der Klasse hier in Object_Jc C-like zu halten und über die Vererbung in der C++-Klasse genauso zu haben, ist sehr einfach und damit genial. Selbst eine private-Datenhaltung ist möglich. Damit enthält die C++-Version eigentlich nur die Deklaration der Methoden in C++-Manier. Vorsicht: In der Datenabbildung gibt es noch den Zeiger auf die virtuelle Tabelle. Damit ist ein entscheidender Datenunterschied in C und C++ vorhanden. Aber C++ umfasst exakt alle C-Daten. Damit können die Daten auch gemischt für C-Anteile dieser C++-Instanzen genutzt werden. Das ist ganz gut. Teile in C, andere Teile einer komplexen Software über mehrere Gewerke in C++ - kein Problem.

Anhand der virtuellen Methode toString ist aber eine Problematik sichtbar. Für reine C++-Anwendungen ist es kein Problem, solche Methoden zu nutzen . Soll aber ein nutzender Algorithmus sowohl für C als auch für C++ laufen, dann muss der virtuelle Aufruf für C über eine C-Lösung wie in DynamicCall_in_C beschrieben realisiert werden. Möglicherweise ist das aber für die C++-Anwendungen nicht tragbar. Immerhin muss man selbst virtuelle Tabellen pflegen, die doch der C++-Compiler automatisch erzeugt. Das scheint als Widersinn. Einem Anwender muss es zu verständlich und einfach wie möglich gemacht werden.

Insgesamt kann die Vorgehensweise wie folgt sein:

TODO Beispiele

Das zum Definitionsteil in Headerfiles. Diese können nun für getrennte Anwendungen in C und C++ includiert werden.

Möchte man auch die Anwendung für C und C++ gemeinsam pflegen, dann klingt das sehr stark nach bedingter Compilierung. Diese ist aber oft nicht schön lesbar und -pflegbar. Einige geschickte Makros können aber helfen. Dazu grundsätzlich zu Makros: Sie sind für C++ oft berechtigterweise verpöhnt, weil der Compiler Fehler nach der Expansion der Makros meldet und die Fehler kaum nachvollziehbar sind. Makros wirken ansonsten wie Spracherweiterungen und müssen daher kurz, syntaktisch verständlich und einfach sein.

Im folgenden werden Makros genannt für Allokieren von Speicherplatz, Zugriff auf Referenzen und Aufrufe dynamischer (virtueller) Methoden. Diese Makros laufen in der CRuntimeJavalike in jeder Plattform, C,C++, mit und ohne erweiterte Referenzen (Stichwort Garbage Collector, mit oder ohne). Bei den meisten hier genannten Makros gibt es auch eine Variante mit _Jc anstatt _Jcpp als Suffix. Diese Jc-Makros expandieren auch in echtem C++ die C-Realisierungen, also nicht diesem Thema entsprechend.

NEW_Jcpp(TYPE)

Dieses Makro entspricht in C++ einem new TYPE() ohne weitere Parameter. In C wird hier der Konstruktor-Aufruf definitiv expandiert, nachdem Speicherplatz angelegt wurde. Das Makro liefert je nachdem die Referenz auf die C- oder C++-Instanz.

TODO: alle Makros