Inhalt
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:
Die Methoden-Labels für den Linker werden bei C nur aus dem Methodennamen gebildet, folglich muss man bei C mit einer aufwendigeren Bezeichner-Schreibweise die Methoden unterscheiden. In C++ wird dagegen der interne Methodenname (Linklabel) auch mit aus dem Kontext gebildet: Die zugehörige Klasse wird aus der aufrufenden Instanz ermittelt, die Typen der Aufrufargumente bestimmen den internen Methodennamen mit.
In C muss man die Aufruftabelle dynamisch gerufener (virtueller) Methoden selbst bilden, in C++ ist das ist ein Kernfeature des Compilers. Die virtuelle Tablle in C ist in der bestehenden Implementierung innerhalb der Reflections definiert. Die Reflection müsste man in C++ in etwa genauso bilden, wenn man sie bräuchte. Für Standardanwendungen braucht man sie aber nicht.
In C muss man die eigenen Klassenvariablen explizit hier mit einem Zeiger, hier ythis->
adressieren, in C++ ist das implizit.
Ob man die aufrufende Instanz in C++ davor schreibt: instanze->name
oder als erstes Argument einer Methode name(instance)
in C, ist dagegen kein großer Aufwandsunterschied. Die C++-Variante liest sich aber entsprechend den Gewohnheiten besser.
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.
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:
Die Kern-Daten werden als C-Struktur definiert:
TODO Beispiele
Die Kern-Methoden werden als C-Prototypen definiert. Das ist auch für einen C++-Programmierer verständlich:
Virtuelle Methoden werden in einer C-like-virtuellen Tabelle definiert. Diese VTbl_Example wird in der C++-compilierung nicht benutzt. Sie kann aber compiliert werden, oder auch nicht. Im letzteren Fall kann es einen speziellen Headerfile dafür geben:
Es erfolgt die Definition der Reflection, diese ist ebenfalls nur in C notwendig und damit ebenfalls im speziellen ergänzenden C-Headerfile zu pflegen:
Es erfolgt die Definition einer C++-class unter bedingter Compilierung, Die C-Struktur wird dabei vererbt. Die Methoden werden inline mit Aufruf der C-Methoden gerufen.
Für die C-Variante wird der C++-Bezeichner der Klasse mit dem C-Bezeichner der Struktur gleich gesetzt, für den Aufruf wird dann der C++-Bezeichner verwendet:
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.
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