zsmith.co

Object-Oriented C Programming

Revision 29
© 2013-2019 by Zack Smith. All rights reserved.

Introduction

Object-oriented programming was invented circa 1967 and is attributed to Alan Kay. OOP languages became dominant in the 1980's and 1990's with the simultaneous inventions of C++ and Objective C around 1980.

Object-Oriented C (OOC)

Due to the problems arising from certain object-oriented languages, it may be preferable to use a non-OOP language such as C but to augment it to support object-oriented principles. This is what I have done.

What does Object-Oriented C look like?

In my variation on this concept, code looks like this:

 MutableImage *image = MutableImage_newWithSize (300, 200);
 $(image, clear, RGB_YELLOW);
 Font *font = FontBuiltin_new (14);
 String *string = String_newWithCString ("Test");
 $(image, drawString, string, 50, 50, font, RGB_BLUE);
 $(image, writeToBMP, "image.bmp");
 release (string);
 release (font);
 release (image);

How can C support OOP?

  1. Encapsulation: by putting member variables inside an object struct and and method pointers inside a class struct.
  2. Polymorphism: by overwriting method pointers in the class struct.
  3. Inheritance: by including inherited method pointers into each class struct.

What are some real examples of object-oriented C?

My own benchmark bandwidth is now implmented in Object-Oriented C and I've written several classes for it e.g. string, array, image manipulation and graphing. My approach is evolving but is already fairly well refined.

GTK+, which uses glib, uses a type of object-oriented C.

Techniques for object-oriented C programming

Object and class structs

An object struct should be as small as possible, containing only instance variables and a pointer to a class struct i.e. is_a. I also use a retain count for memory management.

Here is an example of a class struct and an object struct.

 struct shape;
 typedef struct shapeClass {
  char *name;
  void (*destroy) (struct shape* self);
  struct shape* (*print) (struct shape* self, FILE *output);
  float (*area) (struct shape* self);
 } ShapeClass;
 typedef struct shape {
  ShapeClass *is_a;
  int32_t retainCount; // Inherited from Object class.
  float width;
  float height;
 } Shape;

The object itself is only 8+4+4+4=20 bytes, so you can fit 3 Shape objects in one typical 64-byte cache line. Presumably the ShapeClass is hanging around in the L1 cache as well and won't be too quicky displaced.

Method calls

Methods pointers in the class struct.

This macro provides a simple and readable syntax:

 float area = $(myShape, area);

There are a few different ways to implement the dollar sign macro.

Here is one that performs multiple safety checks:

 #define $(OBJ,METHOD,...) (OBJ && OBJ->is_a && OBJ->is_a->METHOD ? \
         OBJ->is_a->METHOD(OBJ, ##__VA_ARGS__):\
         : (typeof((OBJ->is_a->METHOD(OBJ, ##__VA_ARGS__))))0 \
        )

What this does:

  1. Verify that the object pointer is non-NULL.
  2. Verify that the object pointer has an is_a pointer.
  3. Verify that the class struct has the method in question.
  4. If all above are true, perform the method call, else provide a 0 result.
  5. It supports any number of arguments.

In well-tested production code, it could be simplified to just the method call:

 #define $(OBJ,METHOD,...) OBJ->is_a->METHOD(OBJ, ##__VA_ARGS__)

Call a non-overrideable method i.e. the non-polymorphic case.

Let's say a method will never be inherited and overridden in a derived class.

 Shape* myShape = Shape_new ();
 myShape->width = 10;
 myShape->height = 12;
 float area = Shape_area (myShape);

Calling an overrideable (i.e. polymorphic) super-class method.

It is necessary to put all methods, both parent class's and derived class's into the derived class's class struct, like so:

 #define DECLARE_OBJECT_METHODS(CLASS) \
  char *(*name)(void);
 #define DECLARE_STRING_METHODS(CLASS) \
  unsigned (*length)(void); \
  void (*print)(void); \
  wchar_t (*characterAt)(unsigned);
 #define DECLARE_MUTABLE_STRING_METHODS(CLASS) \
  void (*append)(wchar_t); \
  void (*truncate)(unsigned); \
  void (*reverse)(void); \
  void (*toupper)(void);
 //
 typedef struct {
  ObjectClass *parent_class;
  char *class_name;
  DECLARE_OBJECT_METHODS(struct mutable_string)
  DECLARE_STRING_METHODS(struct mutable_string)
  DECLARE_MUTABLE_STRING_METHODS(struct mutable_string)
 } MutableStringClass;

Any inherited super-class methods pointers can copied into the derived class's struct when each instance is initialized by simply calling the superclass' constructor.

Object struct layout

You should lay out your object structs such that instance variables of derived classes come after their parent classes' instance variables.

  1. The is_a pointer.
  2. Instance variables of parent class(es).
  3. Instance variables of your class.
  4. (Instance variables of further subclass here.)

To facilitate this, each class header file should provide macros that declare instance variables and methods, for example:

 #define DECLARE_OBJECT_METHODS(CLASS) \
  int32_t retainCount;

 #define DECLARE_STRING_IVARS \
  int stringLength; \
  wchar_t *characters;

For your derived class, layouts of class struct and object struct would look like this:

 typedef struct {
  DECLARE_OBJECT_POLYMORPHIC_METHODS(struct mutable_string)
  DECLARE_STRING_POLYMORPHIC_METHODS(struct mutable_string)
  DECLARE_MUTABLE_STRING_POLYMORPHIC_METHODS(struct mutable_string)
 } MutableStringClass;
 //
 typedef struct mutable_string {
  MutableStringClass *is_a;
  DECLARE_OBJECT_IVARS
  DECLARE_STRING_IVARS
  DECLARE_MUTABLE_STRING_IVARS
 } MutableString;

Conclusion

Does this approach really have advantages over C++ or another OOPL? Yes.

1. Transparency

Although it is a fully manual approach akin to driving a stick-shift car, because all object-oriented infrastructure is manually specified, there is no question about what is going on under the hood. The hood is open, or at least transparent. That level of certainty is reassuring.

2. Optimality

When OOP functionality is implemented by someone else, you have to trust it was done well. With Object-Oriented C, you can verify this for yourself.

  • OOC allows very fast code, as fast as C++.
  • OOC allows small objects.
  • My variant of OOC has a pleasing syntax: $(object, method, parameters).

3. Self-checks

Many self-checks and novel protections can be put in place that, if you were using C++ or Objective C, would be in a layer of code that you wouldn't be able to examine or improve.

4. When you have no OOPL

For systems where no C++ compiler exists, this approach also presents a viable means for object-oriented programming.

Downsides?

Object-oriented programming in C can be more difficult than programming in an OO language like C++ or Objective-C.

You have to manually specify everything including manually building your class structs.

If there is ever a memory corruption issue debugging that can be a nightmare.