zs3.me

Object-Oriented C Programming

Revision 38
Download

© 2013-2024 by Zack Smith. All rights reserved.

Introduction

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

The OOC project is my experimental improvement upon C to make it object-oriented without changing the compiler itself. OOC relies partly on C preprocessor macros and manually-built OOP structures.

What does Object-Oriented C look like?

Others have attempted object-oriented C, but in my variant on this concept, the code looks like this:

 MutableImage *image = MutableImage_withSize (300, 200);
 $(image, clearWithColor, RGB_YELLOW);
 Font *font = FontBuiltin_new (14);
 String *string = String_withCString ("Test");
 $(image, drawString, string, 50, 50, font, RGB_BLUE);
 $(image, drawLine, 0, 0, 299, 199, RGB_RED);
 $(image, writeToBMP, "image.bmp");
 release (string);
 release (font);
 release (image);

A few things to notice from this:

  • The $ symbol is simply a variadic C preprocessor macro that I deployed for making method calls.
  • Class methods like MutableImage_withSize are called with the class name, an underbar, and the class method name.
  • OOC uses manual retain/release for object lifecycle management.

How does OOC work?

Basics

  1. Abstraction: each object is represented as a struct but as with C++, the details are visible in the header file.
  2. Encapsulation: member variables are located inside the object struct and method pointers inside the class struct.
  3. Polymorphism: this is accomplished by overwriting method pointers in the (sub)class struct during class initialization.
  4. Inheritance: this is accomplished by simply using inherited method pointers copied from superclass struct to subclass struct during the latter's class initialization.

Note:
As in C++, OOC's encapsulation has the problem that a client can see the private ivars of a class, because they are expressed in the header file, so it is not true encapsulation which hides the details. Compare to Objective-C, where private ivars are typically declared in the .m file.

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. The class struct should contain the vtable i.e. pointers to methods.

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;

In this example, the object itself is only 8+4+4+4 = only 20 bytes.

Method calls

Methods pointers in the class struct.

The $ 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 at runtime that the object pointer is non-NULL.
  2. Verify at runtime that the object pointer has an is_a pointer.
  3. Verify at runtime 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 written using TDD, 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 = new(Shape);
 myShape->width = 10;
 myShape->height = 12;
 float area = Shape_area (myShape);

In this case Shape_area need not by called using the $ macro. This will give the fastest execution time.

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

In this case, 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;

Otherwise, to find a superclass's inherited method we'd have to follow links to each successive superclass's struct, which is too time-consuming.

Class structs are allocated and initialized once per program run and they are small, so there is no need to sacrifice execution performance to save memory.

Therefore any inherited super-class methods pointers can copied into the derived class's struct when each class struct is initialized by simply calling the superclass' initializer.

Object struct layout

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

This ordering:

  1. The is_a pointer.
  2. The retain count.
  3. Instance variables of parent class(es).
  4. Instance variables of your class.

This supports Liskov Substitution Principle i.e.

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

To facilitate this ordering, 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;

In your derived class, layout the class struct and object struct like this:

 typedef struct {
  DECLARE_OBJECT_METHODS(struct mutable_string)
  DECLARE_STRING_METHODS(struct mutable_string)
  DECLARE_MUTABLE_STRING_METHODS(struct mutable_string)
 } MutableStringClass;
 //
 typedef struct mutable_string {
  MutableStringClass *is_a;
  DECLARE_OBJECT_IVARS
  DECLARE_STRING_IVARS
  DECLARE_MUTABLE_STRING_IVARS
 } MutableString;

Templates

Lately, I've found a way to implement single-type templates in OOC. It requires very careful coding but it works.

Thus given my template class VectorTemplate, I can create a variant for any scalar type e.g.

  • VectorUnsigned32
  • VectorByte
  • VectorFloat

What can be accomplished?

When I started playing around with this idea, I didn't expect that I would take it as far as I now have. But at this point, I've ported many of my previous hobby projects over to my OOC project, including much of my FrugalWidgets (C++) code. It is also the destination of some of my Leetcode solutions, as well as new ideas. As a result there are now many basic types (array, dictionary, templates), a little networking code, and there now a simple working GUI based on FrugalWidgets as you see below, although unlike that (which supported OpenGL, Xlib and Android) it is based solely on GL.

Object Oriented C GUI

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 manually shifted 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.

Compare to C++, where numerous unknown optimizations can completely remove sections of code, sometimes leading to surprising runtime behavior, or as they say, undefined behavior.

Compare to Objective-C, where the inner working of the language and its runtime are not meant to be examined. There is a just trust us approach.

2. Optimality

When object-oriented functionality is implemented by some unknown compiler writer, you just have to trust it was done well and is bug-free.

By adhering to the KISS rule, OOC's source code is fairly simple. It is public and its simple design can be verified, so there is no mystery, other than with the C compiler.

By comparison, there are times when the optimizations performed by a C++ compiler may actually degrade software quality when the compiler guesses your intentions wrongly.

OOC doesn't have any optimization feature that might lead to wrong guesses, because it is not a compiler.

If you wanted to speed up OOC code, you could turn off runtime object type verifications, but at your peril.

With OOC you can see what is implemented and how.

  • OOC allows very fast code.
  • OOC allows small objects.
  • OOC has a simple, pleasant syntax: $(object, method, parameters).
  • OOC can be mixed with C code, because it is C code.

When compiled with Clang, OOC could also support the use of closures i.e. blocks.

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. Security

Complexity is the enemy of security.

OOC is simpler in design than more user-friendly OOPLs.

There should be less opportunity for hidden mistakes within the OOC source code to become security vulnerabilities, because the OOC code fairly simple.

While its relative simplicity should reduce the risk of security flaws, it does still rely on pointers...

5. Ignored by AI

The probability that any AI will ever be updated to generate any flavor of OOC code is essentially zero.

In this respect, OOC will retain the authenticity of actual human-made programming.

The analogy might be of using Tesla's Autopilot, versus the experience of driving a vehicle stick-shift.

Those people who find programming to be tedious and frustrating instead of serendipitous, stimulating and even simultaneously relaxing will never see the advantage of the OOC approach.

Downsides?

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

Creating new classes

In the header files, you have to manually specify everything including manually building your class structs based on superclass struct(s).

It is best to use an existing class's files as a template and modify it to your new purpose.

Memory corruption

If there is ever a memory corruption issue, debugging that could be difficult if you aren't writing good code to begin with. But OOC uses retain/release, which is arguably much safer than manual memory management.

Compare to C++: Memory corruption is a bigger risk in C++, because C++ forces the programmer to manage objects' memory, and C++'s error messages are notoriously cryptic. Yes, there exists the shared pointer solution but it's not a requirement, so lots of C++ code fails to use it, and anyway the syntax is ugly.

Could C++ use explicit retain-release, like OOC does? Yes, that is what I did in my (mothballed) FrugalWidgets project.

Summary

OOC cannot rate highly in the area of maintainable-by-anyone due to the learning curve involved in understanding how to create new classes correctly.

The learning curve generally for OOC is less however than what is represented by the 1000-page book about C++. But the learning curve for OOC is certainly more than what's needed for Objective-C.

As for performance, OOC is bound to run slightly more slowly than C++, which has elaborate optimization in the compiler. OOC however runs quicker than Objective-C, because the latter has to use a hash table to look up each method implementation.

Despite the downsides of my OOC, with every refinement it is becoming more robust and ideal.

Download

The latest version of my OOC project is

  • OOC 0.49

Available soon. Generally you can find OOC inside my bandwidth tarball.

Real world examples of object-oriented C?

My own benchmark bandwidth is now implemented in Object-Oriented C and I've written several classes for it e.g. String, Array, MutableImage and a graphing class. My approach is evolving but is already fairly well refined.

1225208580