Friday, June 8, 2007

Object Orientation in C

If you ask a common programmer to list a few languages that are object oriented they will most likely list ones like, Java, SmallTalk, C++, Ruby, and so forth. You won't hear people say that C is an OO language, because it isn't. This doesn't mean it doesn't have the capability of doing it though. Working with libgnt, the console based graphical library Finch uses, I am using a lot of OO design built in C. I'd like to give just a short intro to how it all works because I think it's rather neat.

Classes are defined with structs. Thus all the member variables are simply put into a struct and methods that operate on a class are function pointers in a struct. Because C doesn't have a native concept of OO, if a class were declared with both member variables and member functions defined in the same struct, a waste of memory would ensue. Every time an instance of a class would be created, those pointers would be created again. So a separation of static members and instance members much be created. This can be done in many ways, but a common convention and the one used by libgnt is to create another struct called ${CLASSNAME}Class. This struct would contain all the function pointers for the given class, and would also hold any static variables.

The following is a simple example of a circle class that stores some basic circle information and a few methods that operate on circles.

/*circle.h*/

#ifndef __circle_h__
#define __circle_h__

typedef struct _Circle Circle;
typedef struct _CircleClass CircleClass;

struct _Circle {
float radius;
};

struct _CircleClass {
float pi;
void (*draw) (Circle * c);
float (*get_circumference) (Circle * c);

void (*set_radius) (Circle *c,float r);
float (*get_radius) (Circle *c);

Circle * (*new) (float r, int c);
void (*destroy) (Circle *);
};

CircleClass * get_circle_class();

#endif

#include "circle.h"

#include
#include

static void circle_draw(Circle * c){
/* Do the drawing here */
printf("Drawing a circle with radius %f\n",c->radius);
}

static float circle_get_circumference(Circle * c){
return get_circle_class()->pi * c->radius * 2.00;
}

static void circle_set_radius(Circle *c, float r){
c->radius = r;
}
static float circle_get_radius(Circle *c){
return c->radius;
}
static Circle * circle_new(float r, int color){
Circle *c = (Circle *)malloc(sizeof(Circle));
c->radius = r;
return c;
}

static void circle_destroy(Circle *c){
if(c){
free(c);
}
}

/********************
* Exported Methods *
********************/

CircleClass * get_circle_class(){
static CircleClass * klass = NULL;

if(klass == NULL){
klass = (CircleClass *)malloc(sizeof(CircleClass));
klass->draw = circle_draw;
klass->get_circumference = circle_get_circumference;
klass->get_radius = circle_get_radius;
klass->set_radius = circle_set_radius;
klass->new = circle_new;
klass->destroy = circle_destroy;

klass->pi = 3.14;
}
return klass;
}

/*client.c*/

#include
#include "circle.h"

int main(){

CircleClass * klass= get_circle_class();
Circle * c = klass->new(2.45,6);

float circum = klass->get_circumference(c);
float radius = klass->get_radius(c);

printf("Circle Data\n\tCircumference: %f\n\tRadius: %f\n",circum,radius);

klass->draw(c);

klass->destroy(c);

return 0;
}

Object orientation wouldn't be worth much without some inheritance. So let's see how that's done. It's actually very simple. The first member of the struct is the parent struct. So if you cast an object to it's parent-type, you have a pointer to it's first member, and parent class instance. Isn't that just neat! The following shows how to add a Shape parent class to Circle.

/*shape.h*/

#ifndef __shape_h__
#define __shape_h__

typedef struct _Shape Shape;
typedef struct _ShapeClass ShapeClass;

struct _Shape {
int color;
int border;
};

struct _ShapeClass {
void (*set_color) (Shape *c, int color);
int (*get_color) (Shape *c);

void (*set_border) (Shape *c, int border);
int (*get_border) (Shape *c);
};

void * shape_class_init(ShapeClass *sc);

#endif

/*shape.c*/

#include "shape.h"


static void shape_get_border(Shape *s){
return r->border;
}
static void shape_set_border(Shape *s, int border){
s->border = border;
}

static void shape_set_color(Shape *s, int color){
s->color = color;
}
static int shape_get_color(Shape *s){
return s->color;
}

/********************
* Exported Methods *
********************/

void * shape_class_init(ShapeClass *sc ){
ShapeClass *sc = ShapeClass *)malloc(sizeof(ShapeClass));

sc->get_color = shape_get_color;
sc->set_color = shape_set_color;

sc->set_border = shape_set_border;
c->get_border = shape_get_border;
}
And a few changes to the first few files.
/*circle.h*/

#include "shape.h"

struct _Circle{
Shape parent;
/* other members */
};

struct _CircleClass {
ShapeClass parent;
/* other members */
Circle * (*new) (float r, int color, int border);
};

/*circle.c*/
static Circle * circle_new(float r, int color, int border){
Circle *c = (Circle *)malloc(sizeof(Circle));
Shape *s = (Shape *)c;
c->radius = r;
s->color = color;
s->border = border;
return c;
}

CircleClass * get_circle_class(){
static CircleClass * klass = NULL;
ShapeClass *sc;

if(klass == NULL){
klass = (CircleClass *)malloc(sizeof(CircleClass));
klass->draw = circle_draw;
klass->get_circumference = circle_get_circumference;
klass->get_radius = circle_get_radius;
klass->set_radius = circle_set_radius;
klass->new = circle_new;
klass->destroy = circle_destroy;

sc = (ShapeClass*)klass;
/* override anything here */
klass->pi = 3.14;
}
return klass;
}

So there you can see how things are inherited. A simple cast gets you the parent object. There should only be one instance of CircleClass and it should contain a unique instance of ShapeClass that will be shared among all circles.

This demonstration was very rough and dirty and probably has a few problems since I wrote it up pretty fast and had a baby to deal with while I was piecing it all together, but I'm sure the basic concept makes sense. With the help of GLib and its object instantiation framework this design works a lot better. GLib gives you abstract classes, interfaces and much more, but things are still all in C.

This probably isn't the recommended way of doing OO, but the point is that C can do it. Hope this was informative and it has encourages you to dig a little deeper. If this is case, I recommend you read this paper on OO in C.

1 comment:

Johonn said...

Pretty cool! I had no idea that you could do that kind of stuff in c. Keep up the good work.