Download |
© 2013-2024 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?
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); |
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
struct
but as with C++, the details are visible in the header file. - Encapsulation: member variables are located inside the object
struct
and 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; |
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 && \ |
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); |
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) \ |
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) \ |
#define DECLARE_STRING_IVARS \ |
In your derived class, layout the class struct and object struct like this:
typedef struct { |
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.
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.