In my previous article I’ve talked about the basics of using ‘object like’ programming in C. Actually they rather were more like examples. To continue further I’ll discuss the stuff behind it more detailed, but first I’ll explain the basics of structs, if you know them already, just skip that part of this part.
Structs in C (and in the memory)
An example structure in C:
struct example{int a; int b;};
A structure is nothing more than a way of giving some parts in a fixed size amount of memory a specific name, and a recommended type.
When one would create a new instance of the example structure it would require 8 bytes of space in the memory, 4 for a
, the first integer and again 4 bytes for b
, the second integer:
00 a
01 a
02 a
03 a
04 a
05 b
06 b
07 b
08 b
An instance of the example struct could begin at the memory address0x0A001000
. In that case the value of a
would be at the memory adress 0x0A001000
too, for a
is the first field in the structure. The value of b
however would be located at 0x0A001004
for b
is the second field and is located 4 bytes after the start of the structure. I could use that knowladge to access b
in several different ways:
example* e = malloc(sizeof(example)); e->a = 1; e->b = 10;
printf("%d", e->b); // The usual way
printf("%d", *((int*)((int)e + 4)); // Converts e to an integer, adds 4 and then interprets the result as a pointer to an integer
printf("%d", *((int*)((int)(&(e->a)) + 4)); // Basicly the same way, but now via 'a' which is at the offset of the struct anyway
Structure inheritance
typedef struct {
int a;
int b;
int result;
} SumHandle;
void GetResult(SumHandle* handle){
handle->result = handle->a + handle->b;
}
The example SumHandle
struct is a very simple (and useless) structure to store 2 operands, and the result which is filled by calling the GetResult
function providing a pointer to the handle.
This works fine, but we could want to add more functionality, for instance why not want to know the result of multiplying those 2 operands? We could make a new struct defenition and a new GetResult
function which still uses the old GetResult
:
typedef struct {
int a;
int b;
int sumResult;
int multResult;
} SumHandle2;
void GetResult2(SumHandle2* handle){
handle->multResult = handle->a * handle->b;
GetResult((SumHandle*)handle);
}
As you can see changing the name of the originaly named result
field hasn’t got any effect for only the offset from the offset from the struct offset is used at runtime, therefore I could add the extra field and still use the original GetResult
function. This can be done for the original GetResult
function only uses the first 12 bytes of the pointed to piece of memory. Adding stuff after that does not effect the original function. Note that the oposite is not possible for changing memory outside your own allocated memory could do really nasty stuff causing the all-too-known GPF‘s.
Virtual functions
In the previous example only an extension has been made to the original behaviour. To overwrite the previous behaviour you need to use virtual functions to overwrite the previous call, the original struct would look like:
typedef void (*GetResultHandler)(void* result);
typedef struct {
int a;
int b;
int result;
GetResultHandler GetResult;
} SumHandle;
void GetResultImpl(void* handle){
((SumHandle*)handle)->result = ((SumHandle*)handle)->a + ((SumHandle*)handle)->b;
}
The GetResult
field is a function pointer which would be set in an initialization function of SumHandle
to the GetResultImpl
method.
Overwriting the behaviour would be as simple as writing a new initialization function used which would use another GetResult
implementation. This for calling resultHandleInstance->GetResult(resultHandleInstance)
is nothing more than calling the function at the address specified in the GetResult
field.
A type
Every inheritance at the moment still needs a custom constructor and destructor function. Therefore some functionality could not be ashieved like making a copy of a handle for that would need both the constructor function and the actual size of the struct, which both aren’t available when only a pointer is passed. Possibly a custom clone function would need to be provided.
If every object would expose a pointer to a type struct containing information about the struct this problem would be eliminated:
#include <malloc.h>
#include <stdio.h>
// Default defenitions
typedef struct _Type Type;
typedef struct _Object Object;
typedef void (*ConstructorHandler) (Type* type, Object* instance);
typedef void (*DestructorHandler) (Type* type, Object* instance);
struct _Type {
int size;
ConstructorHandler Construct;
DestructorHandler Destruct;
};
struct _Object {
Type* type;
};
Object* Construct(Type* type) {
Object* object = malloc(type->size);
object->type = type;
type->Construct(type, object);
return object;
}
void Destruct(Object* object) {
object->type->Destruct(object->type, object);
free(object);
}
Type* CreateType(int size, ConstructorHandler constructor, DestructorHandler destructor) {
Type* type = malloc(sizeof(Type));
type->size = size;
type->Construct = constructor;
type->Destruct = destructor;
return type;
}
// An example implementation
typedef struct _Example Example;
typedef void (*GenerateResultHandler)(Example* example);
struct _Example {
Type* type;
int a;
int b;
int result;
GenerateResultHandler GenerateResult;
};
void GenerateResultImpl(Example* example) {
example->result = example->a + example->b;
}
void ConstructExample (Type* type, Object* instance) {
((Example*)instance)->a = 0;
((Example*)instance)->b = 0;
((Example*)instance)->result = 0;
((Example*)instance)->GenerateResult = GenerateResultImpl;
}
void DestructExample (Type* type, Object* instance) {
}
Type* CreateExampleType() {
return CreateType(sizeof(Example), ConstructExample, DestructExample);
}
void main() {
Type* exampleType = CreateExampleType();
Example* example = (Example*)Construct(exampleType);
example->a = 5;
example->b = 5;
example->GenerateResult(example);
printf("%d", example->result);
Destruct((void*)example);
getchar();
}
To add an inheritance:
#include <malloc.h>
#include <stdio.h>
// Default defenitions
typedef struct _Type Type;
typedef struct _Object Object;
typedef void (*ConstructorHandler) (Type* type, Object* instance);
typedef void (*DestructorHandler) (Type* type, Object* instance);
struct _Type {
int size;
ConstructorHandler Construct;
DestructorHandler Destruct;
};
struct _Object {
Type* type;
};
Object* Construct(Type* type) {
Object* object = malloc(type->size);
object->type = type;
type->Construct(type, object);
return object;
}
void Destruct(Object* object) {
object->type->Destruct(object->type, object);
free(object);
}
Type* CreateType(int size, ConstructorHandler constructor, DestructorHandler destructor) {
Type* type = malloc(sizeof(Type));
type->size = size;
type->Construct = constructor;
type->Destruct = destructor;
return type;
}
// An example implementation
typedef struct _Example Example;
typedef void (*GenerateResultHandler)(Example* example);
struct _Example {
Type* type;
int a;
int b;
int result;
GenerateResultHandler GenerateResult;
};
void GenerateResultImpl(Example* example) {
example->result = example->a + example->b;
}
void ConstructExample (Type* type, Object* instance) {
((Example*)instance)->a = 0;
((Example*)instance)->b = 0;
((Example*)instance)->result = 0;
((Example*)instance)->GenerateResult = GenerateResultImpl;
}
void DestructExample (Type* type, Object* instance) {
}
Type* CreateExampleType() {
return CreateType(sizeof(Example), ConstructExample, DestructExample);
}
// An inheritance
typedef struct _Example2 Example2;
typedef void (*GenerateResult2Handler)(Example2* example);
struct _Example2 {
Type* type;
int a;
int b;
int result;
GenerateResult2Handler GenerateResult;
int result2;
};
void GenerateResult2Impl(Example2* example) {
example->result = example->a * example->b;
example->result2 = example->a + example->b;
}
void ConstructExample2 (Type* type, Object* instance) {
((Example2*)instance)->a = 0;
((Example2*)instance)->b = 0;
((Example2*)instance)->result = 0;
((Example2*)instance)->result2 = 0;
((Example2*)instance)->GenerateResult = GenerateResult2Impl;
}
void DestructExample2 (Type* type, Object* instance) {
}
Type* CreateExampleType2() {
return CreateType(sizeof(Example2), ConstructExample2, DestructExample2);
}
void main() {
Type* exampleType = CreateExampleType();
Type* example2Type = CreateExampleType2();
Example* example = (Example*)Construct(exampleType);
Example2* example2 = (Example2*)Construct(example2Type);
example->a = 5; example->b = 5;
example2->a = 5; example2->b = 5;
example->GenerateResult(example);
example2->GenerateResult(example2);
printf("example: result: %dn", example->result);
printf("example2: result: %d result2: %dn", example2->result, example2->result2);
Destruct((void*)example);
Destruct((void*)example2);
getchar();
}
Limitations
Although using types, and casts creates a lot more freedom, it still has its limitations:
- No multi inheritance possible
- No ‘new’ functions (functions that are only used when a pointer is specificly cast to a struct)
- No static support throughout types
- No type behaviour inheritance (a type still has its own basetype and isn’t an object itself)
I’ll explain some stuff about wrapping old implementations, or even multiple implementations in the next part, which will overcome these limitations.