All code is written for
PLAIN C.
This tutorial will briefly explain different types of object systems you can use in C or C++ and how to implement them.
With these, you can implement object systems better than C++ style objects.
For each example code, these compiler definitions are used:
Code:
#define NEW(type) (type*)malloc(sizeof(type));
In each object system, I try to explain the following associated with the object system:
- Class definitions
- Casting & Field access
- Type checking
- Inheritance
- Method invocation
Pick an object system you like.
Data Structure Objects
Pros
- Fast and simple
- Easy to implement
Cons
- Not very dynamic
- Limited type checking capabilities
- There is only simple inheritance
- Not much support for plugin systems
- Doesn't even surpass C++ objects
- Code is awkwardly typed
- Difficult to enforce garbage collection
You lose a lot of functionality in return for a very simple and fast system.
1. Class definition
This is the most simple type of object system in which each class is represented by a
struct. Functions are statically defined in some header file.
typedef struct {
float x;
float y;
} Point;
Point *point_create(float x, float y) {
Point *self = NEW(Point);
self->x = x;
self->y = y;
return self;
}
typedef struct {
Point topLeft;
Point bottomRight;
} Rectangle;
Rectangle *rectangle_create(float x1, float y1, float x2, float y2) {
Rectangle *self = NEW(Rectangle);
self->topLeft.x = x1;
self->topLeft.y = y1;
self->bottomRight.x = x2;
self->bottomRight.y = y2;
return self;
}
2. Inheritance
Only simple inheritance is possible. Not only that, you must know the object's parent field to access any super fields.
Fortunately, you can inherit objects that inherit other objects and it will work the same.
Code:
#define as(type, object) ((type*)(object))
#define extend(object, type) (type*)realloc(object, sizeof(type));
//as is used to access inherited fields
//extend is needed for multi constructing objects
typedef struct {
char *name;
float x;
float y;
} Entity;
Entity *entity_create(char *name, float x, float y) {
Entity *entity = NEW(Entity);
entity->name = name;
entity->x = x;
entity->y = y;
return entity;
}
typedef struct {
Entity super; //Parent object
unsigned int hp;
} Enemy;
Enemy *enemy_create(char *name, unsigned int hp, float x, float y) {
Enemy *self = (Enemy*)entity_create(name, x, y);
extend(self, Enemy);
self->hp = hp;
as(Entity, self)->x++; //As an example...
as(Entity, self)->x--; //You must know when you are referencing an inherited field
return self;
}
3. Type checking
Type checking is very limited or tedious to implement. In the base class of an object, you can include a bit-field representing the type of object, or put booleans in the base object.
Code:
typedef union {
struct {
unsigned char Person:1; //A single bit in the unsigned char represents the Person type
unsigned char Student:1; //This bit will represent the Student type
unsigned char reserved:6; //Fill the remaining bits
} types;
unsigned char value;
} Type;
typedef struct {
Type data;
} Object;
Object *object_create() {
Object *self = NEW(Object);
self->data.value = 0;
return self;
}
#define instanceof(object, type) (((Object*)(object))->data.types.type)
typedef struct {
Object super; //inherit Object
char *name;
} Person;
Person *person_create(char *name) {
Person *self = (Person*)object_create();
self = extend(self, Person); //Allocate the rest of the Person struct
as(Object, self)->data.types.Person = 1;
self->name = name;
return self;
}
typedef struct {
Person super; //inherit Person which inherits Object
float grade;
} Student;
Student *student_create(char *name, float grade) {
Student *self = (Student*)person_create(name); //Construct person
self = extend(self, Student); //Think "extend object to Student struct"
as(Object, self)->data.types.Student = 1;
self->grade = grade;
}
Export/Shared Objects
Pros
- Objects can be easily shared between functions and programs without casting
- Very dynamic
- Objects can have multiple inheritances
- Not much awkward casting
- Works across multiple languages and platforms
- Very easy to implement garbage collection
- Easy to serialize objects and import them into any object
Cons
- A little speed loss, but not much
- Objects must be functionally accessed
- Fields must constantly be casted (At least... primitive fields must)
- Requires much more memory
This is my favorite object system thanks to all the pros. It's great for plugin systems.
Now all your functions for accessing objects should be put into a DLL. This way, you can load external DLLs that call the Object DLL to create a dynamic plugin system.
In this object system, every object is a dictionary. Every field is named with a string, and has an ABSTRACT native field. The size of abstract is dependent on the pointer size of the machine. Abstract values can be pointers, primitives, or methods. I won't provide much code for the more advanced stuff (garbage collection, serialization, etc), but I will provide the structs so you can get an idea of how to implement it.
typedef void* ABSTRACT;
typedef ABSTRACT (*Method)(Object *self, ...); //Methods of an object with cdecl calling conventions
typedef struct {
unsigned char isRef; //"isReference" This one is optional and is for garbage collection and/or type checking purposes!
char *key;
ABSTRACT value;
} Field;
typedef struct {
unsigned int refCount; //Optional field. "reference count" It is for garbage collectors that work on reference counts.
unsigned char protected; //If 1, this object is protected from garbage collection. This is for native access purposes.
unsigned int inheritCount;
char **inherits;
unsigned int fieldCount;
Field *fields;
} Object;
char *_StringClone(char *s) {
char *ret = (char*)malloc(sizeof(char)*(strlen(s)+1));
strcpy(ret, s);
return ret;
}
void ObjectAddType(Object *self, char *type) {
self->inheritCount++;
self->inherits = (char**)malloc(sizeof(char*)*self->inheritCount);
self->inherits[self->inheritCount-1] = _StringClone(type);
}
unsigned char ObjectInstanceOf(Object *self, char *type) {
unsigned int i;
for(i = 0; i < self->inheritCount; i++) {
if(!strcmp(self->inherits[i], type)) {
return -1;
}
}
return 0;
}
unsigned char ObjectSetField(Object *self, char *key, ABSTRACT value) {
unsigned int i;
for(i = 0; i < self->fieldCount; i++) {
if(!strcmp(self->fields[i].key, key)) {
self->fields[i].value = value;
return 0;
}
}
//Field not found, let's create one
self->fieldCount++;
self->fields = (Field*)realloc(sizeof(Field)*self->fieldCount);
self->fields[self->fieldCount-1].key = _StringClone(key);
self->fields[self->fieldCount-1].value = value;
return -1;
}
ABSTRACT ObjectGetField(Object *self, char *key) {
unsigned int i;
for(i = 0; i < self->fieldCount; i++) {
if(!strcmp(self->fields[i].key, key)) {
return self->fields[i].value;
}
}
return NULL; //or throw an exception / set error
}
Method ObjectGetMethod(Object *self, char *key) {
return (Method)ObjectGetField(self, key); //This is exactly the same as GetField, but does the cast for you
}
void ObjectDelete(Object *self) {
unsigned int i;
Method m;
ABSTRACT deconstruct = ObjectGetField(self, "finalize");
if(deconstruct != NULL) {
m = (ABSTRACT)deconstruct;
m(self);
}
for(i = 0; i < self->inheritCount; i++) {
free(self->inherits[i]);
}
for(i = 0; i < self->fieldCount; i++) {
free(self->fields[i].key);
}
free(self->fields);
free(self->inherits);
free(self);
}
ABSTRACT ObjectToString(Object *self) {
//I recommend changing this to return a String object for garbage collection purposes
return self->inherits[0];
}
ABSTRACT ObjectProtect(Object *self) {
self->protected = -1;
return NULL;
}
Object *ObjectCreate() {
Object *self = NEW(Object);
self->refCount = 0;
self->protected = 0;
self->fieldCount = 0;
self->fields = (Field*)malloc(0);
self->inheritCount = 0;
self->inherits = (char**)malloc(0);
ObjectAddType(self, "Object");
ObjectAddType(self, "Serializable");
ObjectSetField(self, "protect", ObjectProtect); //Method that protects the object from garbage collection
// Some default methods to give you ideas on how to improve this object system
ObjectSetField(self, "finalize", NULL); //Deconstructor
ObjectSetField(self, "equals", ObjectEquals); //Test if two objects are equal
ObjectSetField(self, "clone", ObjectClone); //Clone the object
ObjectSetField(self, "toString", ObjectToString); //Convert object to string representation
ObjectSetField(self, "serialize", ObjectSerialize); //Serialize the object (save as a structured byte field)
return self;
}
Just pack these functions in a dll and export the Set/GetField, AddType, and Delete functions. Other DLLs can use these functions to interact with the objects in your program.
And an example code on how to use it
Code:
ABSTRACT PersonSayThis(Object *self, char *msg) {
printf("%s: %s\n", (char*)ObjectGetField(self, "name"), msg);
return NULL;
}
ABSTRACT PersonSayHello(Object *self) {
printf("%s: Hello! I am a person.\n", (char*)ObjectGetField(self, "name"));
return NULL;
}
Object *Person(char *name) {
Object *self = ObjectCreate();
ObjectAddType(self, "Person");
ObjectSetField(self, "name", name);
ObjectSetField(self, "sayThis", PersonSayThis);
ObjectSetField(self, "sayHello", PersonSayHello);
return self;
}
ABSTRACT StudentSayHello(Object *self) {
printf("%s: Hello! I am a student.\n", (char*)ObjectGetField(self, "name"));
return NULL;
}
Object *Student(char *name) {
Object *self = Person(name);
ObjectAddType(self, "Student");
ObjectSetField(self, "sayHello", StudentSayHello); //overwrite
return self;
}
int main() {
Object *person = Person("Bob");
Object *student = Student("Steve");
ObjectGetMethod(person, "sayHello")(person);
if(ObjectInstanceOf(student, "Person")) { //type checking isn't required, but just to make sure it works!
ObjectGetMethod(student, "sayHello")(student); //Uses StudentSayHello
ObjectGetMethod(student, "sayThis")(student, "The sky is falling!"); //Uses PersonSayThis
}
return 0;
}
Protected Shared Objects
Pros
- All the great features of shared objects
- Tries to combine the speed of data structure objects with the sharing capabilities of shared objects by only exporting the needed fields
- Protects the fields you don't want visible to the programmer
- Reduces the amount of calls to ObjectGetField
Cons
- Lose some inheritance functionality
- Objects with protected fields need their own serialization methods
- Re-introduces data structure objects
- Might confuse the programmer about the size of the object during memory moves or copies
- Should type check more often
In this object system, the programmer will always see objects as Object*. In the object methods, however, there is some casting to gain access to the protected fields of the object. This can effectively be more user friendly at the cost of potentially buggy code if the creator is not careful.
#define extend(object, type) (type*)realloc(object, sizeof(type));
typedef union {
ABSTRACT a;
float f;
} AbstractFloat; //float/abstract conversion factory
typedef struct {
float x;
float y;
} Point_t;
typedef struct {
Object base;
Point_t topLeft;
Point_t bottomRight;
} Rectangle_t;
ABSTRACT RectangleGetArea(Object *self) {
Rectangle_t *rect = (Rectangle_t*)self;
AbstractFloat af;
af.f = (rect->bottomRight.x - rect->topLeft.x) * (rect->bottomRight.y - rect->topLeft.y);
return af.a;
}
ABSTRACT RectangleSetDimensions(Object *self, float x1, float y1, float x2, float y2) {
Rectangle_t *rect = (Rectangle_t*)self;
rect->topLeft.x = x1;
rect->topLeft.y = y1;
rect->bottomRight.x = x2;
rect->bottomRight.y = y2;
return NULL;
}
Object *Rectangle(float x1, float y1, float x2, float y2) {
Rectangle_t *self = (Rectangle_t*)ObjectCreate();
extend(self, Rectangle_t);
self->topLeft.x = x1;
self->topLeft.y = y1;
self->bottomRight.x = x2;
self->bottomRight.y = y2;
ObjectSetField(self, "getArea", RectangleGetArea);
ObjectSetField(self, "setDimensions", RectangleSetDimensions);
return (Object*)self;
}
DLL Objects
Pros
- Probably the fastest way to utilize shared objects
- Extremely modular and modifiable
- Easily allows for static functions
Cons
- Lots of files required to maintain objects
- Awkward type checking unless using a class hierarchy system
- Good luck with garbage collection
No code here, since it would be all the over place. Export your functions for your object in a DLL header. Use data structures to represent your objects. Any field of an object must be accessed through a function in the DLL.
Each object system ranks as so:
SPEED -> Data Structure Objects
FUNCTIONALITY -> Shared Objects
MODULAR DESIGN -> DLL Objects
HYBRID OF ALL OF THE ABOVE -> Protected Shared Objects
Hope this gave you some ideas!!! And post what you think is best
