ClassJc_en.html: Reflection and Types
applstdef_emC_h.html … Why variants are necessary
1. What is ObjectJc
The struct ObjectJc
is a basic struct that can be used for all data instances
(struct
and class
) with some common maybe important information with less effort.
To use ObjectJc
the application should
#include <applstdef_emC.h>
Depending on the compiler switches in applstdef_emC_h.html
/**Define the granularity of the ObjectJc base class: */ //#define DEF_NO_ObjectJc_emC #define DEF_ObjectSimple_emC #define DEF_ObjectJc_SYNCHANDLE #define DEF_ObjectJcpp_REFLECTION #define DEF_ObjectJc_OWNADDRESS //#define DEF_ObjectJc_LARGESIZE /**Define of the offering of Reflection information: */ //#define DEF_REFLECTION_NO #define DEF_REFLECTION_SIMPLE //#define DEF_REFLECTION_OFFS //#define DEF_REFLECTION_FULL
different approaches for ObjectJc
are possible.
1.1. Simple application - without usage of specific headerfiles
If DEF_NO_ObjectJc_emC
is defined, you don’t need any emC/Base/Object*.*
in your sources.
It means it is for startup or for simple applications.
The struct ObjectJc
is nevertheless defined in emC/Base/applstded_common.h
also with some interesting features:
typedef struct ObjectJc_T { int identSize; } ObjectJc;
as well as the type / reflection class
typedef struct ClassJc_T { int id; } ClassJc;
Because this types are used in some emC sources and maybe also in the application sources, it is not possible to prevent it. But the content is reduced to only one integer value, 16 bit for cheap controller, which is also usable for a proper identification number. That should not be a disadvantage.
All relevant macros which are used in all sources of emC are properly defined, usual empty. Hence all sources of emC are able to compile.
There are some ObjectJc-relevant simple operations, which are nevertheless useable:
-
set… / isInitialized_ObjectJc(…)
: to detect a successfully intialization, important for distinguish init- and run mode. See chapter The isInitialized_ObjectJc(…) state of an object -
setReflection_ObjectJc(…, ID)
/getID_ObjectJc(…)
: This both routines sets and gets a numeric ID to recognize a specific instance. The instance can only have 10 bit, from 1..1023. -
lock / islocked / unlock_ObjectJc(…)
: See chapter Simple lock in Interrupt / backloop systems. This is interesting and maybe necessray especially in simple Interrupt / backloop applications. -
synchronized / wait / notify_ObjectJc
: Also this features are supported, but only for experience with no more as three mutex instances, see chapter Mutex, data access synchronization between threads and following.
The ObjectJc-relevant functionality for size and instance check are not given.
This is regarded in tests, see especially Test_emC/src/test/cpp/emC_Test_ObjectJc/*
The adequate operations returns always true. It means they can be used,
for compatibility, but not with really checking capability.
1.2. Using ObjectSimple_emC.h
If the compiler switch
#define DEF_ObjectSimple_emC
is set, the emC/Base/ObjectSimple_emC.h
defines the ObjectJc
class
depending of some more switches:
-
either as simplest if no other switch is defined:
typedef struct ObjectJc_T { uint32 identSize; } ObjectJc;
-
with an own 16 bit field for synchronization handle if
DEF_ObjectJc_SYNCHANDLE
is set:typedef struct ObjectJc_T { uint32 identSize;
uint16 offsetToInstanceAddr; uint16 handleBits; } ObjectJc;
in this case the
offsetToInstanceAddr
is unnecessary, left 0. -
if
DEF_REFLECTION_NO
is not set, including a reference to a type informationClassJc
:typedef struct ObjectJc_T { uint32 identSize; uint16 offsetToInstanceAddr; //optional depending on DEF_ObjectJcpp_REFLECTION uint16 handleBits; //optional depending on DEF_ObjectJc_SYNCHANDLE struct ClassJc_t const* reflection; } ObjectJc;
The other both defines can be set. Especially for C++ an offset from the position of the
ObjectJc
struct in a class and the start address of the class is necessary. For C-usage this is anytime 0.For the
DEF_ObjectSimple_emC
the reflection information contains either only the type with optional one base class, or a so named Reflection-offset-Field, see ClassJc_en.html#DEF_REFLECTION -
if
DEF_ObjectJc_OWNADDRESS
is set, the last element is a reference to the instance itself:typedef struct ObjectJc_T { uint32 identSize; //optional offsetToInstanceAddr, handleBits, reflection see above void const* ownAddress; } ObjectJc;
The
ownAddress
is usefull for a data dump to detect the address of data for references. The address of itself can be used if data are copied to any file (a memory map), and references between the data should be readjusted.Depending on the memory layout the reflection and the ownAddress has 2 Byte (in 16-bit-Systems), 4 Byte or 8 Byte (for 64-bit-Adressing). In all cases the alignment is correct. Note that 64-bit-addresses should aligned to a memory word boundary wich is usually 8 Byte.
The idea for ObjectJc
came from Java. In Java all instances have a base ('super')
class java.lang.Object
with adequate information. It is a proven concept.
The simplest form contains in only 32 bit:
-
An identifier for the type, important for type check on downcast.
-
Information about initialized status: init or run, one bit. An instance should be switched to the run mode if all aggregations are complete.
-
Information whether it is an head of an array
-
A lock bit. This lock information can be used for example for a polling of the lock state in an interrupt.
-
Size information.
For these pieces of information only 4 Byte (an uint32_t
) is necessary;
//in src_emC/emC/Base/ObjectSimple_emC.h: typedef struct ObjectJc_T { /**The identSize is helpfull to recognize the instance. * The bit31 is used to detect whether it is initialized or not. */ uint32 identSize; #define mInitialized_ObjectJc 0x80000000 #define mArray_ObjectJc 0x40000000 #define mLocked_ObjectJc 0x20000000 #define mInstanceType_ObjectJc 0x1fff0000 #define kBitInstanceType_ObjectJc 16 #define mSize_ObjectJc 0x0000ffff
For this basic features the type identification has 13 bits, which can distinguish between 8191 types qualified by a number. With this information RTTI (Run Time Type Information) for a qualified checked dynamic cast<…>
may be unnecessary, a normal cast can be done with the user programmed simple type check, for example for data variants. Note that casts may be a source of programming errors and/or unreliability. A check is usual a good decision. The disadvantage, definition of the type-identification number in the users sources, can be seen as advantage, because the numbers can be synchronized over more applications in a project.
The possible additionally reflection information, also available for the ObjectSimple_emC.h
usage, offer more possibilities (allow check also of a base class, also possible to use for C++ derivation), and it enables using the 13 Bits instead for the type for an instance identification. Hence the mask mInstance_ObjectJc
is given:
#ifndef DEF_REFLECTION_NO #define mInstance_ObjectJc 0x1fff0000 #define kBitInstance_ObjectJc 16 /**The reference to the type information. */ struct ClassJc_t const* reflection; #endif } ObjectJc;
The here shown ClassJc
doesn’t contain information about fields, for the simple variant, only the type but basic types too. See also ClassJc_en.html
typedef struct ClassJc_t { /**Index of the class in the reflectionOffsetArrays array if DEF_REFLECTION_OFFS is used, * elsewhere a simple Type ID. It may be used to compare types in different executables * (via connections). */ uint32 idType; #ifndef DEF_NO_StringUSAGE char const* name; #endif #ifdef DEF_REFLECTION_OFFS /**Refers the table with offset and type of the elements of this class. */ int32 const* reflOffs; #endif struct ClassJc_t const* superClass; } ClassJc;
The ClassJc
contains a textual name, it is proper for debugging. For small embedded systems with less memory all textual information can be prevented. For test of this software on PC or usage in a larger target hardware it can be present.
1.3. Using ObjectRefl_emC.h
The file ObjectRefl_emC.h
offers a more complex system:
The header file <emC/Base/ObjectSimple_emC.h
is also included on a proper position
the struct ObjectJc
is defined there, it means with the same capability.
But
-
reflection can be defined in a more complex way, see above.
-
the usage of the size can be more fine gradual bit usage for the size. The size for some instances can be more as 64k: This depends on the compiler switch
DEF_ObjectJc_LARGESIZE
in theapplstdef_emC.h
:#define DEF_ObjectJc_LARGESIZE
-
4095 instances(0x0fff) with < = 64 kByte,
-
255 instances (0x0ff0) with < = 1 MByte,
-
31 instances (0x1F00) with < = 16 MByte
This are the bit ranges with a designation inside the identSize
variable:
#define mSizeBits_ObjectJc 0x30000000 #define kIsSmallSize_ObjectJc 0x00000000 #define kIsMediumSize_ObjectJc 0x10000000 #define mIsLargeSize_ObjectJc 0x20000000
Reflection:
-
The
ClassJc
is defined with complete reflection information, especially fields (elements) of astruct
orclass
. -
Possibility to manage Mutex and Notify Objects for Multithreading operations (similar as in Java for
java.lang.Object
:synchronized
andwait
/notify
) - it is only an ident numer for the Mutex and Notify Instance managed in the RTOS adaption. -
Offset for Reflection- und ObjectJc-usage in C++
-
Address of the instance (necessary for memory images, association of data)
-
Some support for a
BlockHeap
concept
For usage of ObjectJc
in a C++ context with symbolic information about fields
in the derived class an additional offset between start of the data class
and position of the ObjectJc
inside the data are necessary. Additionally a field
for handle values is intended here. This both fields are available if the following
compiler switch is set:
#ifdef DEF_ObjectJcpp_REFLECTION /**Offset from the data-instance start address to the ObjectJc part. * It is especially for symbolic field access (reflection) in {cpp}. */ uint16 offsetToStartAddr; /**Some handle bits to use an ObjectJc for lock (mutex). */ uint16 handleBits; #define kNoSyncHandles_ObjectJc 0x0fff; #endif
In this case, but also independently if not DEF_REFLECTION_NO
is defined,
a reference from ObjectJc
to ClassJc
named reflection
is available, as also in the simple form of ObjectJc.
This improves the type test capability, especially recognizing base type references
(derivation), and it opens the possibility to support full symbolic information
about the fields in the data. It is the 'reflection' capability, see ClassJc_en.html.
#ifndef DEF_REFLECTION_NO .... #define mInstance_ObjectJc 0x3fff0000 .... /**The reference to the type information. */ struct ClassJc_t const* reflection; #endif
If a ClassJc
instance can be referenced, the identifier in the first word
identSize
is now used as instance identifier.
2. Using ObjectJc for C struct
The usage of ObjectJc
is independent of its definition (Simple, with Refl, Jc). The capability is different of course but the sources are indentically.
A C struct for C and C++ compilation should be defined as:
typedef struct MyData_T { union { MyBaseData super; ObjectJc obj; } base; int32_t anyData; } MyData_s;
-
The usage of
typedef
is recommended. Some compilers expect it, it is the clarified form. -
The
MyData_T
is the tag name. The tag name should not be the same as the type name, some compilers may have problems elsewhere! It can be used for forward-declaration.struct MyData_T; .... extern struct MyData_T anyData; //data are only declared ..... struct MyData_T* ref = getRef(...) //only use the reference without access
-
The type name
MyData_s
is written with suffix_s
to offer the possibility for a wrapping C++ class which should be namedMyData
. This writing rules are regarded by ClassJc_en, chapter "The reflection generator".
The ObjectJc
is arranged as the last or only one element inside a union. The other parts
of the union should be base struct
(super struct
),
whereby the immediate super struct
should be arranged first, necessary for
INIZ_…
initialization with { … }
.
This writing rule enables the access to ObjectJc
in an unified form independent
of super struct
nesting (inheritance in C) writing:
ObjectJc* obj = &myDataRef->base.obj;
For C usage it is the same as a simple pointer casting ((ObjectJc*)myDataRef)
because the ObjectJc is the first part in memory.
But usage pointer castings is not recommended because it is an additional
(supposed unsafe) cast. Secondly it may be faulty if myDataRef
is a C++ class
where the ObjectJc
is member of.
Unnecessary casting is an example of dirty software which runs
some years, then somebody extends it, and the assumption for the cast is no longer true.
Hence an important rule for C-programming is: "Avoid unchecked casting of pointers!".
3. Using ObjectJc in C++ classes
It is possible and may be recommended for state variables which may be exported (serialized) to define the data of a C++ class as C-struct
.
Then this struct
contains ObjectJc
in the form above.
There are generally three forms to inherit from a C-struct
:
class MyData: public MyData_s { ...
with possible access to data and immediately to myDatab→base.obj
.
class MyData: proctected MyData_s { ... public: ObjectJc const* toObject() { return this->base.obj; }
It has protected access to the data, but a individual operation toObject()
which returns the ObjectJc const*
reference only to the read only ObjectJc
data.
This form does not need virtual operations for that.
class MyData: public ObjectJcpp, proctected MyData_s { ... public: ObjectJc const* toObject() { return this->base.obj; }
The interface ObjectJcpp
contains the operation toObject()
as virtual,
hence a reference of type ObjectJcpp
is generally useable to access the ObjectJc
data.
But this form needs virtual operations, it may not be desired in some embedded applications.
It is a question of C++ using philosophy:
-
If C++ should be used only because of some C++ language features, for example operator definition (
float operator+(…)
), but virtual operations are forbidden by style guide for safety than the first or second form is appropriate. -
For common C++ usage the third form is recommended.
See test sources, it contains some casting situations too:
emC_Base/src/test/cpp/emC_Test_ObjectJc/test_ObjectJcpp.cpp
.
4. Initializing of data with ObjectJc
4.1. static and const initializing with initializer list in C
To get const data in a const memory section (Flash Rom) only
a const
initializing can be done with an so named initializer list.
Thas is the same situation in C as in C++ (!).
Type const myData = { ..... }; //hint: write const right side. const Type myData = { ..... }; //it is the same
In C it is not possible to initialize const data in any operations in runtime, other than in C++. The immediately initializing C-style is necessary if data should be stored in a const memory section (on Flash-ROM, for embedded Processors). This topic is irrelevant for C++ programming on a PC platform,
For non const
data the same initializing with an initializer list
is possible for all non-allocated data (not from heap).
If static data are used an initializing may be seen as recommended.
Type myData; //The initial data are undefined - prone of error Type myData = {0}; //at least forced 0-initialization.
An initializer list with given data is often complex to write, it is a challenge for the programmer. Macros to initialize some parts of nested data are helpful.
For example some struct
may be defined as:
typedef struct BaseType_T { union{ ObjectJc obj;} base; int32 data1; float data2; } Base_Type; // typedef struct InnerData_T { float x,y,z; } InnerData; // typedef struct MyType_T { union { BaseType BaseType; ObjectJc obj; } base; int32 m, n; InnerData data1; int p,q; } MyType_s;
then a initializer is complex.
Especially if some types are defined in another module or component it is difficult to handle.
At least for ObjectJc a macro INIZ_ObjectJc
can be used.
It is recommended to write such an INIZ…
macro for any type:
#define INIZ_VAL_BaseType( OBJ, REFL, ID, VAL) \ { { INIZ_ObjectJc(OBJ, REFL, ID) } \ , (int32)VAL, (float)VAL \ }
#define INIZ_InnerData( ) \ { 3.14f, 42.0f, -3.0f } //it is only a const initizalization
#define INIZ_VAL_MyType( OBJ, ID, VAL1, VAL2) \ { { INIZ_VAL_MyBaseType(OBJ, refl_MyType, ID, VAL1) } \ , VAL2, -(VAL2) \ , INIZ_InnerData() \ , 0,0 \ }
This macros should be written near to the struct definitions, to see the association.
-
The arguments of the macro may have a free meaning and order But the initializing values have to be able to calculate on compile time.
-
Because the
BaseType
hasbase.obj
, it uses theINIZ_ObjectJc(…)
. -
Because the
BaseType
is used as base type, it is necessary to give the reflection information as argumentREFL
here. -
The
INIZ_VAL_MyType(…)
does not need information about the data arrangement of the inner struct data. It invokes only theINIZ…
macro of the nested data. Hence the information about the data arrangement is encapsulated. -
The first
INIZ…
macro insideINIZ_VAL_MyType(…)
should have aREFL
-argument. Because theMyType
is never used as base class the reflection are not given as argument, but they are given immediately.
The INIZ_ObjectJc
macro is defined depending on the variants of ObjectJc
in different forms. The arguments are the same in any case.
As special feature the REFL
argument is used in case of DEF_ObjectJc_SIMPLE
as
#define INIZ_ObjectJc(OBJ, REFL, ID) \ { ((((uint32)(ID_##REFL))<<kBitInstanceType_ObjectJc) & mInstanceType_ObjectJc) \ | (sizeof(OBJ) & mSize_ObjectJc) \ }
It means, the identifier for the reflection class is used as identifier
for the numerical ID_refl_MyType
because the simple variant of an ObjectJc
has not a reference to the reflection but only the ID. The definition of an
ClassJc const refl_MyType = INIZ_ClassJc(refl_MyType, "MyType");
it not necessary and may not be given if DEF_REFLECTION_NO
is set. The type-ID
already stored in a also given ClassJc const
cannot be used for a const initialization
because it is not able to calculate on compile time:
Getting a const
value from a given another const
instance inside an
initializer list is not possible in C
and not possible for C++ const
-memory-segment-initialization.
The access to refl→idType
fails though it is a instance defined before.
It is too complex for the compiler’s initializer value calculation.
Adequate it is not possible to use the address of the instance shifted and masked for the correct bit position. An address value inside a constant initializer list is only possible by linker replacement, the address value can only be set as const reference as a whole from the linker. Some numeric calculations afterwards cannot be done with it because they would need to be done by the compiler.
4.2. CTOR_/ctor_ObjectJc(…) - initialization on run time
The ObjectJc
part on an instance is the core part but it contains information
for the whole instance: The type and size. Hence it should be initialized firstly
with respect to the instance:
MyType_s data; CTOR_ObjectJc( &data.base.obj, &data, sizeof(data) , refl_MyType, ID_Obj);
It uses the first argument as argument to the ObjectJc
part inside the data, and the second argument as void*
instance pointer. This is necessary for C++ usage in derived classes, where ObjectJc
is not on top of the data.
Then the construction of the instance can be done:
ctor_MyType(&data, 42.0f, 234);
The CTOR_ObjectJc
is a macro which regards DEF_REFLECTION_NO
. In this case
it uses the given identifier for the reflection type and invokes
ctor_ObjectJc(&data.base.obj, &data, sizeof(data), null, ID_refl_MyType);
It does not assume the existence of a ClassJc
instance. But the ID_refl…
should be defined, see ClassJc_en.html#ID_refl. But if a ClassJc
instance is given anyway,
the
ctor_ObjectJc(&data.base.obj, &data, sizeof(data), refl_MyType, ID_Obj);
can be used. In case of DEF_ObjectJc_SIMPLE
the type-ID is taken from the ClassJc
instance and the ID_Obj
is not use.
The constructor of the user types should not invoke the ctor_ObjectJc(…)
.
Instead a check of consistence can be done, which assures that the given instance
has a proper size and the given type is matching. This can be done as assertion:
ASSERT_emC( CHECKstrict_ObjectJc( &thiz->base.obj, sizeof(*thiz) , refl_MyType, 0) , "not matching instance and type", 0,0);
It has the advantage that the check-code is not existing if ASSERT_IGNORE_emC
is set.
On embedded platforms usually the construction is done only on startup. The startup can
be tested well on PC platform with assertion check and with Exception handling,
so errors are detected on PC-test.
The CHECKstrict_ObjectJc(…)
is a macro again which invokes in case of DEF_REFLECTION_NO
:
checkStrict_ObjectJc(OTHIZ, SIZE, null, ID_##REFL)
It does only test, an Exception is thrown only in conclusion with ASSERT_emC
.
Some details to the arguments both for ctor_… and CTOR_…
-
REFL
For the reflection argument (See chapter "Reflection and Types"). -
ADDR
The second argument of the ctorADDR
is expected of typevoid*
and should be the address of the instance itself. It has the same value for C-compilation as theObjectJc
reference becauseObjectJc
is the first element in astruct
. But for C++ there may be small differences between the address of the instance and theObjectJc
data part. This is if inheritance and virtual tables are used. The difference between both address values are stored in theObjectJc::offsetToInstanceAddr
, which requires settingDEF_ObjectJcpp_REFLECTION
. If it is not set butDEF_REFLECTION_FULL
is set, and C++ compiling is used, then an compiler error message is forced (#error …
). TheoffsetToInstanceAddr
is necessary to access data via reflection (FieldJc
). Hence in C++ this form of initializing should be used. The initializer list is not suitable for use. -
SIZE
: TheObjectJc
part stores the size of the whole instance. Hence it can be tested only with knwoledge of theObjectJc
reference whether a safe access to memory is possible with a given reference. Faulty pointer castings can be detected on runtime. It is important that the memory bounds are resepected. Data error because of software errors are acceptable (can be still found), but memory violations causes dubios behavior and ard hard to debug. -
ID
The last argumentID
of theCTOR_ObjectJc
-Macro is not used in case ofDEF_ObjectJc_SIMPLE
because the type-ID is stored in the only oneidentSize
element. But if theID
contains themArrayId_ObjectJc
flag bit, it is set in theObjectJc
. It is necessary for theObjectArrayJc
which contains the reflection reference to the elements, not for the whole instance. Note that the size information in theObjectJc
part is for the whole array, it is always for the whole instance.
4.3. Initializing for C++
In C++ allocation and construction are combined. It is true in both kinds of creation:
MyData* data = new MyData(...); MyData data(...); //ctor is invoked with data definition
That is a consequent C++ feature and prevents errors because of non-initialized data.
The concept of the ObjectJc
as core part which contains information
about the whole instance seems to be primary not regarded.
Some special constructs and style guides are necessary:
typedef struct MyPlainData_T { //Data in C manner, plain union { ObjectJc obj; } base; //with ObjectJc as core int32 d1; //:Any data float d2; //Note: padding any struct to 8-Byte-align if possible, } MyPlainData_s;
class MyBaseClass: protected MyPlainData_s //contains ObjectJc as core { public: MyBaseClass(int idObj); public: MyBaseClass(ObjectJc const* othis = null); }
class MyClass: protected MyBaseClass //contains ObjectJc as core too { public: MyClass(int idObj); protected: MyClass(ObjectJc const* objectJc); }
The ObjectJc should contain the size of all data, but only of the plain data,
except organization data of C++ (vtable pointer). It should be initialized firstly,
because the ctor of C data parts may check the size and type.
To initialize ObjectJc firstly, its CTOR_ObjectJc(…)
should be called fistly.
This is done in the following way:
-
The public constructor which should only act as instance constructor have not an
ObjectJc*
argument but it may need an argument for theidObj
, the ident designation. It should callCTOR_ObjectJc(…)
with size and reflection argument of the whole instance, from the C++ class.-
either in the argument preparation of the base constructor to fullfil its
ObjectJc*
argument,MyClass::MyClass(int idObj) //firstly call the base ctor in C++ syntax: : MyBaseClass( CTOR_ObjectJc(&this->base.obj , this, sizeof(*this), refl_MyClass, idObj)) { //...further special init, for this derived data ... }
-
or, if has not a further C++ base class, it should call
CTOR_ObjectJc(…)
in the body of the constructor.MyBaseClass::MyBaseClass(int idObj) { CTOR_ObjectJc(&this->base.obj, this, sizeof(*this), refl_MyBaseClass, idObj); //...further init, especially call the plain data constructor in C manner: ctor_MyPlainData(&this->base.obj); }
-
-
A constructor able to use as base constructor should have an argument
ObjectJc* objectJc
. It should be protected to designate it as base class constructor. Or, to simplify it may have anull
as default value:-
Either this class has a further C++ base class, then it forwards it:
MyBaseClass::MyBaseClass(ObjectJc const* othiz) : MyBaseBase(othiz) { //...further init }
-
Or it has not a further C++ class. Then it should check (assert) whether the given
othiz
is proper:MyBaseClass::MyBaseClass(ObjectJc const* othiz) { if(othiz == null) { //assumes that this class is the instance class: CTOR_ObjectJc(&this->base.obj, this, sizeof(*this), refl_MyBaseClass, idObj); } else { ASSERT_emC(othiz == &this->base.obj, "C++ ObjectJc initialization error", 0,0); } //...further init, especially call the plain data constructor in C manner: ctor_MyPlainData(&this->base.obj); }
-
4.4. The isInitialized_ObjectJc(…) state of an object
This is not related to the rest of the chapter above. The operations
-
setInitialized_ObjectJc(ObjectJc* othiz)
-
isInitialized_ObjectJc(ObjectJc* othiz)
are related to the instance data, not to the core data of the ObjectJc
struct.
The setInitialized_ObjectJc(ObjectJc* othiz)
should be called if the instance is proper to work.
Then also isInitialized_ObjectJc(ObjectJc* othiz)
returns true.
Usual software have an 'initialization' phase after construction. That is used in praxis but to less mentioned in the ObjectOrientation theory. Basic data should be calculated, and especially all aggregations should be set.
If an application has aggregations only as a tree (there is a correct order which classes refer which other) then aggregations can all set only in the constructor. This is the best and maybe theoretical correct case. It is especially supported for example in Java by using final references. But such a system does not allow aggregations between instances one another or circular. Such is defacto often necessary. To succeed theoretical approaches sometimes UML tools handle such situations as aggregation only for one direction, and the other direction is an association. But this is not the desired truth. The aggregations are one another or circular in fact. They are aggregations because they are never changed in runtime.
For that the 'initialization' phase after construction is necessary.
In this phase all aggregations are set of all known and hence constructed instances,
and maybe some values are calculated using the associations, for example derived parameters of controlling.
If this is done, then the initialization should be ended with setInitialized_ObjectJc(ObjectJc* othiz)
for the appropriate instance. For value calculations you may test isInitialized_ObjectJc(ObjectJc* othiz)
and get values only from already initialized instances.
Because even the success of initialization may depend on a succeeded initialization of another object,
and the order of initializations cannot be pre-defined, this should be done in a while loop.
If all instances have the data isInitialized_ObjectJc(ObjectJc* othiz)
,
checked in this while-loop, the while loop for initialization can be left, initialization is finished.
The situation that the successing initialization depends on the succeeded initialization of another instance and vice versa is of course a design error. Then the loop is endless. It should be checked by a maximum of loops, which is usual less (1..3.. 10) or at maximum the number of instances if the order of initializing is stupidness exact reverse to the necessary one.
Not that getting the reference to an instance does not need the succeeded initialization of this instance. Elsewhere one another or circular aggregations will not be possible. But using values for initial calculation of the own values should be gotten only from initialized instances. For that a tree approach is necessary.
5. ALLOC_ObjectJc()
The macro-wrapped function call of
ALLOC_ObjectJc(SIZE, REFL, ID)
is for C-usage or for creation of non-class
-data based on ObjectJc
in C++.
Depending on DEF_ObjectJc_SIMPLE
it expands either / or to
allocReflid_ObjectJc(SIZE, ID_##REFL, ID, _thCxt) allocRefl_ObjectJc(SIZE, &(REFL), ID, _thCxt)
adequate to CTOR_ObjectJc
in the chapter above. Additionally it requires the
pointer to a Thread context
see Stacktrace, ThreadContext and Exception handling
because generally the allocation can fail, then an Exception handling is recommended.
The core prototype with reflection reference is
extern_C ObjectJc* alloc_ObjectJc ( const int size, const int32 typeInstanceIdent struct ClassJc_T const* refl , struct ThreadContext_emC_t* _thCxt);
This routine allocates and initializes the core data of the ObjectJc
.
6. Using of the ObjectJc data
6.1. Type check for casting and safety
In classic C programming, sometimes in C++ too, often a pointer is stored and/or transferred as void*
-pointer if the precise type is not known in the transfer or storing environment.
Before usage a casting to the required type is done.
But such casting turns off the compiler error checking capability.
An unchecked cast is a leak for source safety.
A void* pointer should only be used for very general things. For example for memcpy
.
In C++ some casting variants are present. The static_cast<Type*>(ref)
checks on compile time
whether the cast is admissible in an inheritance of classes, and adjusts the correct
address value toward the start address of the part inside the instance which is adequate to the given type. If there isn’t an inheritance relation between the given type and the cast destination type, it forces a compiler error. But the static_cast<Type*>
does not check
the really given instance on runtime. On downcast (toward to a derived class) it assumes that the instance is of this type. A upcast (toward to the base class) is always true.
It means the static_cast<Type*>(ref)
can cause runtime errors if the assumption of the instance type is false.
The dynamic_cast<Type*>(ref)
does the same for 'downcast', but additionally the type is checked. This requires activation of RTTI (RunTime Type Information).
If the type is faulty, either a null pointer is delivered or an Exception is thrown,
depending on the compiler version.
The reinterpret_cast<Type*>(ref)
or a ordinary C-Cast (Type*)(ref)
delivers faulty results if it is used for inheritance class Types. It is an lackadaisical programming error to use reinterpret or C casts for class inheritance. Such an error is inconspicuous so long as no virtual operations are present. Unchecked or lax usage of C-casts or reinterpret-casts are a prone of error. Because a simple C-cast can be used by accident, a C++ compiler emits a warning. To assure compatibility between C and C++ a macro CAST_C(Type, ref)
is defined in emC/Base/types_def_common.h
which is adapted for C++ to a reinterpret_cast<Type*>
.
In C only the known (Type*)(ref)
written via macro as C_CAST(Type*, ref)
is available. The capability of static and dynamic casts are only necessary in respect of class hierarchie in C++. The problem is the same: Unchecked or lax usage of C-casts or reinterpret-casts are a prone of error.
Independently of the question C or C++ or with or without RTTI
the ObjectJc
base class delivers the type information.
It works for C++ too either using the ObjectJcpp
-Base class
or with immediate access to the C data which contains ObjectJc
.
The type check can be done with
extern_C ClassJc const refl_MyType; ..... bool bTypeOk = instanceof_ObjectJc((&myDataObj->base.obj, &refl_MyType);
This routine recognizes and returns true
for a base type too if a type information is available using DEF_REFLECTION_…
not NO
. The base type is referred in the ClassJc
instance referred as reflection
type information.
See Variants of emC-usage in Applications - chapter ClassJc and Reflection.
Note that for class inheritance in C++ with multiple inheritance or with virtual operations
a static_cast<TYPE>(OBJ)
has to be used for cast because addresses should be tuned.
For C inheritance using a base type struct
as first element of the inherited struct
of course a C_CAST(TYPE, OBJ)
is only possible and necessary.
The cast seems to be safe and might not be necessarily be tested if the type is known
in the user programming environment, because the
same software module stores the instance pointer, and gets it back.
But there may be programming errors, if the algorithm is enhanced etc.etc.
Hence it is recommended to check the type too, but with an assertion,
which can be switched off for fast runtime request.
With a side glance to Java the type is always checked on runtime for castings.
In Java a casting error is never possible.
For that the reflection info in java.lang.Object
is used.
Because castings are not the operations most commonly used in ordinary programs,
a little bit of calculation time is admissible for that.
The type check only as safety check, as assertion should be written as:
ASSERT_emC(INSTANCEOF_ObjectJc((&myData->base.obj, reflection_MyType)) , "faulty instance", 0, 0); MyType* myData = C_CAST(MyType*, myData); ...
The assertion ASSERT_emC(…)
macro is empty if assertions are
not activated, for fast real time execution. If assertions are activated, it causes a THROW, see ThCxtExc_emC.html. It means the program is continued on the CATCH
statement in a calling level, not aborted as assert()
of standard C/++.
The C_CAST
is an reinterpret_cast
for C++ usage
and a normal ((MyType*) myData)
for C usage.
The reflection_MyType
is the type information, see next chapter.
If the type of an instance is really unknown, especially if a base reference is delivered and the derived type should be a point of interest, the
if(INSTANCEOF_ObjectJc&myData->base.obj, reflection_MyType) { MyType* myDataderived = static_cast<MyType*>(myDate); ...
can be a part of the functional code. This example shows a C++ class reference where obj is member on.
For DEF_ObjectJc_SIMPLE
whereby ObjectJc
contains only an int32 value
only the really instance is able to check.
If the instance in this example is derived from MyType
the INSTANCEOF_ObjectJc(…)
returns false though the instance has MyType
as base class. It is a restriction,
but nevertheless often useful. If at least DEF_ObjectJc_DEFLREF
is defined and
the reflection are generated via DEF_REFLECTION_FULL
all information of base types
are contained there. Then INSTANCEOF_ObjectJc(…)
returns true also for base classes.
Ot is a question of effort and a question of necessities in the application.
6.2. Lock, mutex, notify
6.2.1. Simple lock in Interrupt / backloop systems
There is a very simple functionality for locking basically supported by ObjectJc
:
//any algorithm for example in interrupt: if(!islocked_ObjectJc(&myData->base.obj) { lock_ObjectJc(&myData->base.obj); //consistently access to data unlock_ObjectJc(&myData->base.obj); } //if locked
This is a mechanism for polling. It needs only one bit in the ObjectJc
data,
no mechanism of an Operation System. The principle is:
-
The consistency of data is requested.
-
A simple application usual with interrupt and main loop without Operation System is given.
-
The data access can be omitted, then new data are not checked and used. It’s admissible.
The usage is:
-
If the data are attempt to change in the backloop or a lower prior interrupt, this interrupt locks firstly. If the higher using interrupt comes in that time, it detects the lock state and prevents the access.
After writing new data the unlock frees after a short time. The next higher interrupt can access.
-
If the data should be written in a higher prior interrupt, the reading in a lower level locks it. Then the higher interrupt do not write new data. It writes the new data usual in the next cycle.
Often, this approach is possible. The advantage is, no more support from an operation system is necessary.
6.2.2. Mutex, data access synchronization between threads
Mutex means "mutual exclusion". It is a known mechanism to access data controlled by an operation system. The necessity is: If a data portion is locked (have a mutex or monitor), then another access should be prevented. This is done with waiting for access. Often the term "Semaphore" is used for that. It is really like a Semaphore on the railway: The train should wait till the track is free. In Operation Systems there are traditionally similar and multifaceted operations which hinder a compatibility of the application software for different Operation Systems. The next disadvantage is, the association between a mutex or monitor or a semaphore to the relevant data are not immediately, so mistakes are possible.
In Java this problem was solved with a simple approach already in the first versions:
synchronized(theData) { theData.set...(...) }
The important decision is: The mutex is related to the data.
The base class java.lang.Object
contains the capability of the mutex.
Of course it uses the underlaid Operation System organized from the Java Runtime Environment.
In this kind the binding of mutex to the data is clarified. This is the first idea.
The second important idea is, that the mutex or monitor or semaphore is wrapped with this
synchronized
operation.
And the third is, that there is a program block enclosed by the curly braces.
Hence the freeing of the mutex cannot be forgotten or held a too long time.
This system is transferred to he ObjectJc
class for C/++ programming to ensure
also a compatible access to the operation system:
synchronized_ObjectJc(&myData->base.obj); { myData->... //access } endSynchronized_ObjectJc(&myData->base.obj);
Of course the curly braces here are only a style guide. In Java it is controlled by the compiler. But it’s the same style.
For synchronization or mutex / monitor / semaphore of course there is an instance of that necessary.
This is not part of the ObjectJc
base data. It will be too much, and not compatible.
Furthermore, the most data based on ObjectJc
don’t need a mutex.
Hence, the solution is: Threre are only a few bits, 12 for the full capability
if DEF_ObjectJc_SYNCHANDLE
is set, enough for 4094 different mutex / monitor / semaphore instances.
If an ObjectJc
basing instance was one time associated to a mutex instance, then it is for all time.
Usual the mutex is used repeatedly or cyclically.
It means there is only one time an effort to get the instance from the Operation System,
and the consistency is guaranteed. The limitation is, the application should have no more as 4094 mutex instances.
But that seems to be really enough.
For a more simple system, either using the DEF_ObjectSimple_emC
or DEF_NO_ObjectJc_emC
there are only 3 instances for mutex possible (2 bit in the only one identSize
element).
But it is supposed that a system using threads can also use DEF_ObjectJc_SYNCHANDLE
.
It means this 2 bits are only for test and compatibility.
The implementation of the both synchronized_ObjectJc
and endSynchronized_ObjectJc
operations
are part of the OSAL - "Operation System Adaption Layer" and should be written proper
to the given Operation System. Sometimes an Operation System offers semaphore with more as one level or such,
this seems to be over engineered, not necessary.
If it might to be necessary, than with specific programming, which usual prevents the compatibility
(the multi platform idea of emC).
6.2.3. Wait and notify
The "wait - notify" is another approach often implemented using semaphores. It is also systematized in the first Java versions with:
synchronized(myData) { myData.setWaiting(true); myData.wait(timeout); myData.setWaiting(false); } .... //other thread: synchronized(myData) { if(myData.isWaiting()) { myData.notify(); } }
As you see on this principally examples, the problem is a little bit more complex. If it is solved using manual programming by semaphores, sometimes not all possibilities are considerate. So software fails after a longer time, if accidently situations comes together. This is also cared by the Java solution:
-
The establishing of a wait state should be a mutual excluded operation, hence wrapping in
synchronized
is necessary. -
Sometimes
notify
is bad, it the other process is not waiting. Then unclarified situations can be occur. Hence it is advisable to set the wait state able to quest in user level. May be also more data for notification should be set in this condition.
This proper and compatible (multi platform portable) solution is also supported
by ObjectJc
:
synchronized_ObjectJc(&myData->base.obj)); { myData->stateWait = val; wait_ObjectJc(&myData->base.obj, 100); myData->stateWait = 0; } endSynchronized_ObjectJc(&myData->base.obj); .... //other thread: synchronized_ObjectJc(&myData->base.obj)); { if(myData->stateWait !=0) { notify_ObjectJc(&myData->base.obj); } } endSynchronized_ObjectJc(&myData->base.obj);
As shown in this basically example also the timeout is used. This is often a proper idea to held the system running. After timeout there can be checked some conditions for example to abort the thread because missing a resource in the whole system. If the waiting has not a timeout, usual the reaction in the application may have deadlock situations, the application may hang.
Also the wait_ObjectJc
as the notify_ObjectJc
is programmed in the OSAL,
in an Operation System specific kind.
6.3. Plain Data exchange
Instances have a 'state'. The 'state' is contained in several elements of the instance. It can be the state variable of a state machine, the value of a controller intergral part or such other.
If there is any complex 'situation' in an application, the analyzes of the state variables can help to explore what was happen. This can be done off line later, after the occurrence and fast fixing the situation.
It can be helpfully to have a snapshot of the data on time of the situation. Then it is possible to load the snapped data in a simulation of the same software on PC, or a special data exploration software, which contains the same instances but maybe with special relationships.
-
Data from C++ classes cannot be copied because it contains virtual pointer etc.
-
Hence it is proper to separate the state data from the rest of the organization data. The state data should be defined in a
struct
which is based onObjectJc
. -
In the exploration software the instances can be created and wired (aggregation) on startup.
-
Then the state data are copied into from the snapshot of the data on the occurred situation. If the data contain associations one another, this addresses should be adjusted.
-
To recognize which instance were associated together in the originally snapped data, the element
ownAddress
helps. An association (variale connection) is a state, whereby an aggregation or composition (UML) is invariant, not a state. Hence the last one can be part only of the C++ class data. Associations may be part of the state data, its addresses should be adjusted with the other memory addresses using theownAdress
information. -
To check whether the data are compatible (version) the
size
andreflection
helps. It is stupid if the exploration software uses another version as the snapped data from a maybe older device which another, older version. -
The
ObjectJc
contains the head information for the data to correctly support exploration.
7. Reflection and Types
In the full capability of ObjectJc
reflections contains symbolic information
for all data elements.
A reflection instance of type ClassJc
contains the type information,
all base type information and the fields and maybe operations (methods) too.
With the information about base types (super types) the instanceof_ObjectJc(…)
can check whether a given instance is proper for a basic type too.
The construction of full reflection is described in ClassJc_en, chapter "The reflection generator".
For simple capability of ObjectJc use-able in embedded platforms maybe without String processing with fast realtime or less hardware resources there are four variant forms of reflections:
-
a) In the simplest form, only an
idType
is stored which is contained in the ObjectJc instance too to compare it. In this case theClassJc
is defined as:typedef struct ClassJc_t { int idType; // sizeReflOffs; } ClassJc;
-
b) Reflection access with Inspector target proxy. In this case reflection data are generated in form of positions of data in a
struct
and a number (index) of anystruct
type. In this case theClassJc
is defined as:typedef struct ClassJc_t { int idType; // sizeReflOffs; // int const* reflOffs; } ClassJc;
-
c) The reference
reflOffs
refers to the generated reflection data. As the reflection data are defined in succession in a "const" memory area, the low 16-bit of this pointer address can be used as a type identifier. -
d) No Reflection access,
DEF_REFLECTION_NO
is set: The reflections are only defined to have information about the type:typedef struct ClassJc_t { int idType; // sizeReflOffs; // char const* nameType; } ClassJc;
The nameType
is optional depending on DEF_NO_StringJcCapabilities
.
See org/vishia/emC/sourceApplSpecific/SimpleNumCNoExc/ObjectJc_simple.h
The kind to build the idType
depends on some possibilities on initialization
of the reflection_…Type
instance and can be defined by the users programming.
For example additional information, which can be used for debugging, are given outside
a fast realtime and low resource CPU, the idType
is a simple index.
It is important that the idType
of all reflection instances are unique.
The instanceof_ObjectJc(…)
compares only the idType
given with the reflection…
argument with the type information in ObjectJc
. It is the low 16 bit
of idInstanceType
for the simple ObjectJc
.
For the reflection with full capability see ClassJc_en.html.
8. Using of the sources for ObjectJc variants in Projects
If the emC approach should be used also in less and new projects. Firstly it may be recommended to use only a subset, not all possibilities. It is too much for starting. For example the reflection approach may be a novelty, which shouldn’t impose on a new user. Hence, the simple form of ObjectJc (see chapter What is ObjectJc) can be used firstly. It is defined in the header and source files emC/Base/ObjectSimple_emC.*
For complete usage of the capabilities of ObjectJc
the files emC/Base/ObjectRefl_emC.*
can be used.
Only for usage the full capability of Java-like approaches the emC/Jc/ObjectJc.*
offers more possibilities.
8.1. emC/Base/applstdef_common.h includes the correct headers
You should set the usage decision in applstdef_emC.h
which is able to include in your path:
#include <applstdef_emC.h>
Inside the <applstdef_emCh.h.>
the correct settings should be selected,
for example:
/**Define the granularity of the ObjectJc base class: */ //#define DEF_NO_ObjectJc_emC #define DEF_ObjectSimple_emC //#define DEF_ObjectJc_SYNCHANDLE //#define DEF_ObjectJcpp_REFLECTION //#define DEF_ObjectJc_OWNADDRESS //#define DEF_ObjectJc_LARGESIZE /**Define of the offering of Reflection information: */ //#define DEF_REFLECTION_NO #define DEF_REFLECTION_SIMPLE //#define DEF_REFLECTION_OFFS //#define DEF_REFLECTION_FULL ..... .....
and below, after all settings on end of this file:
#include <emC/Base/applstdef_common.h>
should be included. The applstdef_common.h
should be left unchange,
do not change it for project specifics. This file includes if necessary:
#if !defined(DEF_NO_THCXT_STACKTRC_EXC_emC) #include <emC/Base/Assert_emC.h> #include <emC_srcApplSpec/applConv/EnhanceRef_simple.h> #include <emC/Base/Exception_emC.h> #else .... .... #elif defined(DEF_ObjectSimple_emC) #include <emC/Base/ObjectSimple_emC.h> #else #define DEF_ObjectJc_FULLCAPABILITY //to compile content of ObjectRefl_emC.c #include <emC/Base/ObjectRefl_emC.h> #endif
It means for the simple case nothing more is included and necessary.
For working without full reflection capability only the ObjectSimple_emC.h
should be present with the ObjectSimple_emC.c
including as source.
If ObjectRefl_emC.h
is used, it needs ObjectRefl_emC.c
and some more files
which’s capabilities are declared.
Adequate it is with Exception, Assertion etc.
8.2. Which sources
Refer to the test environment. The test is done for all variants.
…TODO in test cases decide between DEF_ObjectSimple_emC to use different file sets.