New inheritance scheme
This change is driven by...
- plugin ratio of bullshit to real stuff is really bad: its a bad user experience for those who we really want to have a good experience.
- Higher levels of inheritance are unstable when lower level classes have modifications made to their virtual methods.
What we do now
Header file:
extern const Type Variable_Type
/* See Variable */
#define __Variable \
/* General info */ \
__Stg_Component \
\
/* Virtual info */ \
\
/* Variable info */ \
Index offsetCount; /**< Number of fields in this variable. */ \
...
struct _Variable { __Variable };
Variable* Variable_DefaultNew( Name name );
void _Variable_Delete( void* variable );
...
C file:
typedef ... ... _Construct ...
Plugin file:
blah
What's wrong with this? ...
Alan's Proposal
I hacked up a proof of concept of how we can approach the inheritance in the Sandbox with a few example classes: https://csd.vpac.org/svn/Sandbox/inheritence/POC-style3 This was a long time ago...
My aims were:
- Inheriting a new class should mean you write only things you have to - ie: the things you want to add/change
- A single constructor function for both stack and heap objects (rather than writing two)
- More features to query the hierarchy tree
- Implicit pickling and printing based on a single user defined virtual function
- Maintain similarity to reduce work during refactor
- Working with doxygen
- Working with tau/pdt
- --ansi --Wall --pedantic compatible
Header:
#define Array_Data \
Class_Data \
void** data; \
Index count; \
SizeT elementSize;
DeclareClass( Array, Class );
Array* Array_Init( void* instance, Index count, SizeT elementSize );
void _Array_Delete( void* instance );
Dictionary* _Array_Pickle( void* instance );
void Array_SetAt( void* instance, Index index, void* value );
void* Array_GetAt( void* instance, Index index );
C Source:
#include "Foundation.h"
ImplementClass( Array, Class ) {
GetSelf( Array, instance ); // creates the 'self' variable
self->_delete = _Array_Delete; // Assign the virtual functions you want
self->_pickle = _Array_Pickle;
}
Array* Array_Init( void* instance, Index count, SizeT elementSize ) {
GetSelf( Array, instance );
BeginConstructor( Array, self );
Class_Init( self ); // Constructor of parent class
self->data = (void**)malloc( count * elementSize );
self->count = count;
self->elementSize = elementSize;
EndConstructor( self );
}
Inherited class header :
[snip]
#define HashTable_Data \
Array_Data \
HashTable_HashFunction* Hash; \
HashTable_CompareKeyFunction* Compare; \
HashTable_DeleteDataFunction* DeleteData; \
\
Bool ownKeys; /**< whether to free the keys */
DeclareClass( HashTable, Array );
[snip]
Inherited class source:
[snip]
ImplementClass( HashTable, Array ) {
GetSelf( HashTable, instance );
self->_delete = _HashTable_Delete; // This is my new delete() def
self->DeleteData = _HashTable_DeleteData; // new virtual function assign
// notice that I wanted pickle() to remain the same, so I just don't write anything about it
}
[snip]
The Dictionary class is now part of the language requirements because it is used for the pickling
Dictionary* result = _Class_Pickle( self ); // Pickle my parent class
Dictionary_PickleLong( result, self->someData, 1 ); // add new member data in this class to the pickle process
Print() is a generic function which reads the pickle() Dictionary result.
Luke's Proposal
There is, in my view, two major problems with the existing inheritance design.
1. Way too much of both the header and source files are consumed with administration code for setting up the class and it's virtual methods. In addition to decreasing readability of the code, it makes creating new classes quite painful.
- As a sub-point, this issue can disenchant developers/users from attempting to implement their own classes/plugins. An easier, more intuitive interface would lessen the workload required of developers.
2. Modifying or addition of virtual methods to super-classes can easily become a nightmare in the current design. Consider the act of adding a virtual method to the Object class. This would require the addition of new administrative code to every source and header file of every class that inherits from Object; probably enough to depress even the most optimistic developer.
I've used X-Macros to hide the vast majority of administration code in a couple of header files, allowing for all that extra code to be replaced by an include command. This what a header file in the new system looks like:
#include "StGermain/Base/Foundation/ClassClear.h" #define CLASSDIR StGermain/Base/Foundation #define CLASSNAME NewClass #include "StGermain/Base/Foundation/ClassHdr.h"
And here is the administrative part of the C file:
#include "StGermain/Base/Foundation/NewClass.h" #include "StGermain/Base/Foundation/ClassDef.h"
Here's an example of the Object class header file:
#ifndef __StGermain_Base_Foundation_NewObject_h__ #define __StGermain_Base_Foundation_NewObject_h__ #include "StGermain/Base/Foundation/ClassClear.h" #define CLASSDIR StGermain/Base/Foundation #define CLASSNAME NewObject #include "StGermain/Base/Foundation/ClassHdr.h" #endif /* __StGermain_Base_Foundation_NewObject_h__ */
and source file:
#include <stdlib.h>
#include <string.h>
#include "types.h"
#include "debug.h"
#include "MemoryTag.h"
#include "Memory.h"
#include "NewClass.h"
#include "NewObject.h"
#include "StGermain/Base/Foundation/ClassDef.h"
void _NewObject_Init( void* _self ) {
NewObject* self = Class_Cast( _self, NewObject );
_NewClass_Init( self );
self->name = NULL;
}
void _NewObject_Destruct( void* _self ) {
NewObject* self = Class_Cast( _self, NewObject );
Class_Free( self, self->name );
_NewClass_Destruct( self );
}
void _NewObject_Copy( void* self, const void* op ) {
_NewClass_Copy( self, op );
NewObject_SetName( self, ((NewObject*)op)->name );
}
void NewObject_SetName( void* _self, const char* name ) {
NewObject* self = Class_Cast( _self, NewObject );
int len;
len = name ? strlen( name ) + 1 : 0;
self->name = Class_Rearray( self, self->name, char, len );
if( name )
strcpy( self->name, name );
}
const char* NewObject_GetName( void* self ) {
assert( self );
return ((NewObject*)self)->name;
}
It can be seen from the code that there is no longer any administration code involved in setting up a class; only the methods themselves need to be implemented. Of coarse, it's not quite as delightful as all this, we still need to define the class somewhere. I mentioned the use of X-Macros earlier, this is where they enter the equation, via a .def file. Here's the .def file for the Object class:
#include INHERIT( StGermain/Base/Foundation/NewClass )
#include "StGermain/Base/Foundation/ClassClear.h"
#define PARENTDIR StGermain/Base/Foundation
#define PARENT NewClass
#define CLASSDIR StGermain/Base/Foundation
#define CLASSNAME NewObject
#include "StGermain/Base/Foundation/ClassSetup.h"
MEMBER( char*, name )
VOIDOVERRIDE( Init, void,
(void* self), (self) )
VOIDOVERRIDE( Destruct, void,
(void* self), (self) )
VOIDOVERRIDE( Copy, void,
(void* self, const void* op), (self, op) )
METHOD( SetName, void,
(void* self, const char* name), (self, name) )
METHOD( GetName, const char*,
(void* self), (self) )
The class definition mechanism is now quite declarative. The code for implementing the virtual method calls is all hidden in the background.
An additional benefit of using X-Macros is the ability to automatically generate various common class operations. In the Object example above there are calls to Class_Cast, a method for casting a void pointer to a particular class while checking to ensure the memory pointed to is in fact either the class type in question or inherited from that class type. The code for performing this check is automatically generated for all classes in the new scheme.
To add...
- Component factory registration
- Alan's pickling interface
The plan
Header file:
foo
C file:
foo
New plugin file:
blah
