| Download |
© 2013-2024, 2026 by Zack Smith. All rights reserved.
- Introduction
- What does OOC code look like?
- How does OOC work?
- What can be accomplished?
- Conclusions
- Examples of OOC?
- Demo of OOC?
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 after the inventions of C++ and Objective C around 1980.
My OOC project is an experimental improvement upon C to make it object-oriented without changing the compiler itself. OOC relies partly on C preprocessor macros and manually-built object-oriented structures.
OOC is effectively a slow-motion agile project,
in which I am the sole client and the entire programming team.
Every time I encounter an issue, I don't rashly cancel the project
but rather step back, question assumptions and try solutions
(e.g. the compler issue encountered with C's unfinished blocks
feature).
The goal is to see how far I can take the concept.
Despite minor hurdles and slow advances, it remains a novel solution that is similar to neither C++ nor Objective-C nor Swift.
- Whereas C++ syntax can make it difficult to read, OOC is easy to read.
- Whereas Objective-C and Swift's method calls require a hash table lookup, OOC uses a standard vtable which is faster.
- Whereas C++'s memory management is risky and a source of confusion for some, OOC simply uses retain/release.
- Whereas Swift does away with supporting C, OOC is C.
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
- Abstraction: each object is represented as a
structbut as with C++, the details are visible in the header file. - Encapsulation: member variables are located inside the object
structand method pointers inside the classstruct. - Polymorphism: this is accomplished by overwriting method pointers in the (sub)class struct during class initialization.
- 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:
- Verify at runtime that the object pointer is non-NULL.
- Verify at runtime that the object pointer has an is_a pointer.
- Verify at runtime that the class struct has the method in question.
- If all above are true, perform the method call, else provide a 0 result.
- 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:
- The is_a pointer.
- The retain count.
- Instance variables of parent class(es).
- 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
Closures
There an extension of C that adds closures called blocks
which are equivalent to the Objective-C concept of the same name.
I've extended OOC to use this feature, but there are caveats:
- Only Clang supports the blocks extension.
- The blocks library (BlocksRuntime) isn't included with Debian's Clang package and has to be additionally installed (
libblocksruntime-dev). - The
$macro doesn't work inside blocks, which might be due to its nested ternary expressions confusing Clang, so I've provided a block-friendly alternative called$$. - The
$$macro excludes the usual checks for a NULL object pointer and NULLis_apointer.
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.
Conclusions
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.
The only known is how the C compiler optimizes the code.
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,
requiring a #pragma to prevent optimization of some sections of code.
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 if used correctly.
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 smart
pointer solution but
it's not a requirement, so lots of C++ code fails to use it,
and anyway the syntax is ugly and hides details.
Let us question an assumption though: Is it possible for C++ to use explicit retain-release, like OOC does? Yes certainly, as that is what I did in my FrugalWidgets project. Manually calling retain and release methods results in more readable, more debuggable code than using smart pointers.
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++, because C++ has compile-time type checking (OOC's is runtime) as well as 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. In OOC, the method implementation is located simply using the $ macro and the method's name.
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.39
Available soon. Generally you can find OOC inside my bandwidth project's 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.
Is there any demo?
I've got two simple demo programs. Here's test_opengl:

