Background

Part I: Good

C Opaque Type as an Object

In the C language, object-oriented abstraction can be implemented using structures and functions. In C, a pointer to a structure can be defined without disclosing the elements of the structure. For example, in the header file Shape.h we define opaque type

typedef const struct __Shape * ShapeRef;

This declares a new type, ShapeRef, which is a pointer to a hidden structure __Shape. While the messages we can send to (or functions we can call with) this structure pointer are defined in a C header file, i.e. Shape.h, we are not allowed to see nor access directly the elements of this structure.

You can read more about Opaque data types and Opaque pointers on Wikipedia. You can also learn more about Opaque types at Apple’s Core Foundation documentation.

Mutable vs Immutable Types

You may have noticed in the example on Opaque Types that the pointer was defined using the const keyword. The keyword const means that the contents of __Shape structure cannot be modified after it is created. This makes Shape an immutable type. This means that while we can define functions that reveal the internal state (i.e., structure elements) of Shape, we cannot define functions that change its internal state. Only by eliminating the keyword const in the type definition would we be able to define functions to change the internal state of the type. A type whose internal state can be modified after creation is called a mutable type. Thus, in the header file Shape.h we also define the opaque type

typedef struct __Shape * MutableShapeRef;

What are the advantages of an immutable type? Let’s say you want to use an instance of type A as a variable inside type B. You decide to do this by placing a copy of the pointer for the instance of A inside B. Unbeknownst to you, however, another type, let’s call it C, also has a copy of the pointer for the same instance of A. If A was mutable, then type C could change the value of A, and, in turn, change the instance variable A inside B, without B knowing what happened. There are two ways to handle this problem. If A is mutable, then type B has no choice but to make its own personal copy of the A instance. Depending on the size of A, this could be a memory and time consuming process. On the other hand, if A is immutable, then B can safely (and rapidly) copy only the pointer for A. As you might imagine, immutable types are highly useful in concurrent programs. General advice: don’t define an type as mutable unless you really need it.

Read more about Mutable and Immutable types at Wikipedia.

Implementation of Shape

Now, looking inside the source Shape.c, we find the structure

struct __Shape {
    // Shape Type attributes  - order of declaration is essential
    float xPosition;
    float yPosition;
    float orientation;
};

Creation and Destruction of the Shape Type is handled with these function:

static struct __Shape *ShapeAllocate()
{
    struct __Shape *theShape = malloc(sizeof(struct __Shape));
    if(NULL == theShape) return NULL;
    return theShape;
}

ShapeRef ShapeCreate(float xPosition, float yPosition, float orientation)
{
    struct __Shape *newShape = ShapeAllocate();
    if(NULL == newShape) return NULL;
    newShape->xPosition = xPosition;
    newShape->yPosition = yPosition;
    newShape->orientation = orientation;
    return newShape;
}

void ShapeFinalize(ShapeRef theShape)
{
    if(NULL == theShape) return;
    free((void *)theShape);
}

Comparison and Accessors are handled with these functions:

bool ShapeEqual(ShapeRef theShape1, ShapeRef theShape2)
{
    if(NULL == theShape1 || NULL == theShape2) return false;
    if(theShape1 == theShape2) return true;
    if(theShape1->xPosition != theShape2->xPosition) return false;
    if(theShape1->yPosition != theShape2->yPosition) return false;
    if(theShape1->orientation != theShape2->orientation) return false;
    return true;
}

float ShapeGetXPosition(ShapeRef theShape)
{
    if(NULL == theShape) return nan(NULL);
    return theShape->xPosition;
}

void ShapeSetXPosition(MutableShapeRef theShape, float xPosition) {
    if(NULL == theShape) return;
    theShape->xPosition = xPosition;
}

A shape can be translated and rotated. These methods are handled with these functions:

void ShapeTranslate(MutableShapeRef theShape, float xTranslation, float yTranslation)
{
    if(NULL == theShape) return;
    theShape->xPosition += xTranslation;
    theShape->yPosition += yTranslation;
}

void ShapeRotate(MutableShapeRef theShape, float angle)
{
    if(NULL == theShape) return;
    theShape->orientation += angle;
}

Usage of Shape

MutableShapeRef shape = ShapeCreateMutable(0.0, 0.0, 0.0);
ShapeShow(shape);

ShapeTranslate(shape, 10.0, 20.0);
ShapeRotate(shape, 180.);
ShapeShow(shape);
ShapeFinalize(shape);

Inheritance

Let’s examine how we can define a Square type that inherits from Shape. In source code we define the structure

struct __Square {
    // Shape Type attributes - order of declaration is essential
    float xPosition;
    float yPosition;
    float orientation;
    // Square Type attributes
    float width;
};

For this inheritance trick to work it is essential that the order of instance variable declarations be identical to those inside the Shape structure. Any additional instance variables must go after the variables matching Shape’s structure.

In the header file we define the opaque types

typedef const struct __Square * SquareRef;
typedef struct __Square * MutableSquareRef;
Creation and Destruction of the Square Type is handled with these function:

static struct __Square *SquareAllocate()
{
    struct __Square *theSquare = malloc(sizeof(struct __Square)); if(NULL == theSquare) return NULL;
    return theSquare;
}

SquareRef SquareCreate(float xPosition, float yPosition, float orientation, float width)
{
    struct __Square *newSquare = SquareAllocate();
    if(NULL == newSquare) return NULL;
    newSquare->xPosition = xPosition;
    newSquare->yPosition = yPosition;
    newSquare->orientation = orientation;
    newSquare->width = width;
    return newSquare;
}

void SquareFinalize(SquareRef theSquare)
{
    if(NULL == theSquare) return;
    free((void *)theSquare);
}
Comparison and Accessors are handled with these functions:

bool SquareEqual(SquareRef theSquare1, SquareRef theSquare2)
{
    if(!ShapeEqual((ShapeRef) theSquare1, (ShapeRef) theSquare2)) return false;
    if(theSquare1->width != theSquare2->width) return false;
    return true;
}

float SquareGetXPosition(SquareRef theSquare)
{
    return ShapeGetXPosition((ShapeRef) theSquare);
}

float SquareGetWidth(SquareRef theSquare)
{
    if(NULL == theSquare) return nan(NULL);
    return theSquare->width;
}

void SquareSetXPosition(MutableSquareRef theSquare, float xPosition)
{
    ShapeSetXPosition((MutableShapeRef) theSquare, xPosition);
}

void SquareSetWidth(MutableSquareRef theSquare, float width)
{
    if(NULL == theSquare) return;
    theSquare->width = width;
}

Notice how we type cast a Square into a Shape before calling Shape methods.

Usage of Square

MutableSquareRef square = MutableSquareCreate(0.0, 0.0, 0.0, 10.0);
ShapeShow((ShapeRef) square);
SquareShow(square);

ShapeTranslate((MutableShapeRef) square, 10.0, 20.0);
ShapeRotate((MutableShapeRef) square, 180.);
SquareShow(square);
SquareFinalize(square);

Part II: Better

In the last section we examined how Opaque types in C can be adapted to follow some Object Oriented Design Patterns. It is a sensible approach but it still lacks memory management, collections, strings, and many other useful object-oriented design features. To continue on our path towards a more complete approach let’s look how we can introduce retain count memory management.

Reference Counting

When an type is no longer needed it should be deallocated and its memory freed. But how will a type know when it’s no longer needed? Type A may hold a reference (pointer) to type B, but how does type A know that type B still exists? For example, type B could have initially been created to be part of type C. If type C gets destroyed along with all it’s constituent objects and type A doesn’t know, then type A could end up sending a message to (calling a function with) a non-existent type B, and crash the program.

The solution we adopt to solve this problem is called reference counting. When a type wants to hold a reference to another type it calls that type’s retain function. Every time an type’s retain function is called, it increments its internal retainCount variable. Conversely, when an type no longer needs to hold a reference to a type it calls that type’s release function. Every time an type’s release function is called, it decrements its internal retainCount variable. When a type’s retainCount hits zero, then the type self destructs. That is, it would call the release function of any types it had retained, and then deallocate itself.

With this in mind, we follow the conventions below.

  • if you create an type (either directly or by making a copy of another type—see The Create Rule), you own it. We will explicitly use the word Create or Copy in the name of any function that creates and returns a type with a retain count of 1.

  • if you get an type from somewhere else, you do not own it. If you want to prevent it being disposed of, you must add yourself as an owner (using a retain method).

  • if you are an owner of an type, you must relinquish ownership when you have finished using it (using a release method).

Read more about Reference counting at Wikipedia and at Apple’s Memory Management documentation.

Implementation

We begin by creating a fundamental opaque type called KFType, from which all other types will inherit. In KFType source code define structure

struct impl_KFType {
    u_int32_t retainCount;
    void (*finalize)(void *);
    bool (*equal)(void *, void *);
};

In KFType header define opaque type

typedef struct impl_KFType * KFTypeRef;

KFType Methods

bool KFTypeEqual(KFTypeRef theType1, KFTypeRef theType2)
{
return theType1->equal(theType1,theType2);
}

void KFRelease(KFTypeRef theType)
{
    if(NULL==theType) return;
    if(theType->retainCount == 1) {
        theType->finalize(theType); return;
    }
    theType->retainCount--;
    return;
}

OCTypeRef OCRetain(OCTypeRef theType)
{
    if(NULL==theType) return NULL;
    theType->retainCount++;
    return theType;
}

Now we can define KFShape to inherit from KFType

struct impl_KFShape {
    u_int32_t retainCount;
    void (*finalize)(void *);
    bool (*equal)(void *, void *);

    // Shape Type attributes
    float xPosition;
    float yPosition;
    float orientation;
};

static struct impl_KFShape *KFShapeAllocate()
{
    struct impl_KFShape *theShape = malloc(sizeof(struct impl_KFShape));
    if(NULL == theShape) return NULL;
    theShape->retainCount = 1;
    theShape->finalize = KFShapeFinalize;
    theShape->equal = KFShapeEqual;
    return theShape;
}

Usage of KFShape

KFMutableShapeRef shape = KFShapeCreateMutable(0.0, 0.0, 0.0);
KFShapeShow(shape);

KFShapeTranslate(shape, 10.0, 20.0);
KFShapeRotate(shape, 180.);
KFShapeShow(shape);

KFRelease((KFTypeRef) shape);

Part III: Best - OCTypes Framework

Inspiration from Core Foundation

In Apple’s Core Foundation, the opaque type we called KFType is actually called CFType. The Core Foundation framework provides a rich set of types and functions that implement reference counting, collections, strings, and many other useful object-oriented design features. It is a powerful and mature framework that has proven itself in building complex applications across macOS and iOS platforms.

However, Core Foundation has some limitations for general C development:

  • Size and Complexity: It’s a rather large framework with many components beyond what most projects need

  • Platform Dependencies: It’s not easily portable to non-Apple platforms

  • Learning Curve: The full API surface can be overwhelming for developers new to object-oriented C

The OCTypes Solution

Here, we provide a lightweight alternative to Core Foundation, called OCTypes. It is a small framework that implements the core object-oriented design patterns we’ve explored—reference counting, inheritance, polymorphism, and collections—while maintaining the essential benefits of type safety and memory management.

Key design principles of OCTypes:

  • Minimal Dependencies: Implemented in pure C with no external dependencies

  • Cross-Platform: Suitable for use across different operating systems and environments

  • Educational: Simple enough to understand and modify, making it an excellent learning tool

  • Practical: Robust enough for real-world applications while remaining lightweight

The OCBase Foundation

The heart of the OCTypes system is the OCBase structure, which serves as the foundation for all OCTypes objects. Let’s examine how this evolution improves upon our earlier approaches:

struct __OCShape {
    OCBase   _base;        // Always the first member

    // Shape-specific attributes
    float xPosition;
    float yPosition;
    float orientation;
};

The OCBase structure itself contains everything needed for sophisticated object-oriented behavior:

typedef struct impl_OCBase {
    OCTypeID typeID;                              // Unique identifier for runtime type checking
    uint32_t retainCount;                         // Reference count for memory management
    bool static_instance;                         // Indicates if the instance is static (never deallocated)
    bool finalized;                               // Indicates if the instance has been finalized

    // Virtual method table - enables polymorphism
    void (*finalize)(const void *);               // Finalizer called when retain count reaches zero
    bool (*equal)(const void *, const void *);   // Equality comparison function for this type
    OCStringRef (*copyFormattingDesc)(OCTypeRef);// Human-readable description of the object
    cJSON *(*copyJSON)(const void *);             // JSON serialization function
    void *(*copyDeep)(const void *);              // Immutable deep copy function
    void *(*copyDeepMutable)(const void *);       // Mutable deep copy function

    // Debug and introspection support
    const char *allocFile;                        // Source file where object was allocated
    int allocLine;                                // Source line where object was allocated
    bool tracked;                                 // Whether this object is tracked for leak detection
} OCBase;

Key Improvements Over Previous Approaches

1. Type Safety and Runtime Introspection

Unlike our earlier approaches, OCTypes provides runtime type checking through OCTypeID. This prevents many common casting errors:

// Safe: will return NULL if obj is not actually an OCShape
OCShapeRef shape = (OCShapeRef)obj;
if (OCGetTypeID(shape) == OCShapeGetTypeID()) {
    // Safe to use as OCShape
}

2. Comprehensive Virtual Method Table

The virtual method table in OCBase enables true polymorphism. Different types can have different implementations of the same conceptual operation:

// Works for any OCType - calls the appropriate implementation
OCStringRef description = OCTypeCopyFormattingDesc(anyOCTypeObject);
bool areEqual = OCTypeEqual(obj1, obj2);
void *copy = OCTypeDeepCopy(originalObject);

3. Built-in Debugging and Leak Detection

The framework includes sophisticated debugging capabilities that help developers track down memory leaks and other issues:

// Debug information is automatically captured
OCShapeRef shape = OCShapeCreate(1.0, 2.0, 45.0);  // __FILE__ and __LINE__ recorded

// Leak detection can report exactly where objects were allocated
int leakCount = OCLeakTrackerGetUnreleasedObjectCount();

4. Consistent API Patterns

OCTypes follows consistent naming and behavior patterns that make the API predictable and learnable:

  • Create functions: Return objects with retain count 1 (caller owns)

  • Copy functions: Return new objects with retain count 1 (caller owns)

  • Get functions: Return borrowed references (caller doesn’t own)

  • Retain/Release: Explicit memory management

A Complete OCShape Implementation

Let’s see how all these concepts come together in a complete OCShape implementation:

// Type registration - done once per type
static OCTypeID kOCShapeID = kOCNotATypeID;

OCTypeID OCShapeGetTypeID(void) {
    if (kOCShapeID == kOCNotATypeID) {
        kOCShapeID = OCRegisterType("OCShape");
    }
    return kOCShapeID;
}

// Structure definition with OCBase inheritance
struct impl_OCShape {
    OCBase base;           // Must be first for inheritance to work
    float xPosition;
    float yPosition;
    float orientation;
};

// Virtual method implementations
static bool impl_OCShapeEqual(const void *a_, const void *b_) {
    OCShapeRef a = (OCShapeRef)a_;
    OCShapeRef b = (OCShapeRef)b_;

    // First check type compatibility
    if (a->base.typeID != b->base.typeID) return false;
    if (a == b) return true;  // Same object

    // Compare actual values
    return (a->xPosition == b->xPosition &&
            a->yPosition == b->yPosition &&
            a->orientation == b->orientation);
}

static void *impl_OCShapeDeepCopy(const void *obj) {
    OCShapeRef source = (OCShapeRef)obj;
    if (!source) return NULL;
    return (void *)OCShapeCreate(source->xPosition, source->yPosition, source->orientation);
}

static void *impl_OCShapeDeepCopyMutable(const void *obj) {
    OCShapeRef source = (OCShapeRef)obj;
    if (!source) return NULL;
    return (void *)OCShapeCreateMutable(source->xPosition, source->yPosition, source->orientation);
}

static void impl_OCShapeFinalize(const void *obj) {
    // OCShape doesn't own any additional resources, so just free the memory
    // Note: OCBase cleanup is handled automatically by the framework
    free((void *)obj);
}

// Allocation function using the OCTypes framework
static struct impl_OCShape *OCShapeAllocate() {
    return OCTypeAlloc(struct impl_OCShape,
                       OCShapeGetTypeID(),
                       impl_OCShapeFinalize,
                       impl_OCShapeEqual,
                       OCShapeCopyFormattingDesc,
                       impl_OCShapeCopyJSON,
                       impl_OCShapeDeepCopy,
                       impl_OCShapeDeepCopyMutable);
}

// Public creation function
OCShapeRef OCShapeCreate(float x, float y, float orientation) {
    struct impl_OCShape *shape = OCShapeAllocate();
    if (!shape) return NULL;

    // Initialize shape-specific data
    shape->xPosition = x;
    shape->yPosition = y;
    shape->orientation = orientation;

    return (OCShapeRef)shape;
}

Benefits of This Approach

  1. Memory Safety: Automatic retain/release prevents most memory leaks and use-after-free bugs

  2. Type Safety: Runtime type checking prevents casting errors

  3. Polymorphism: Virtual method table enables object-oriented behavior in C

  4. Debugging: Built-in leak detection and allocation tracking

  5. Consistency: Predictable API patterns across all types

  6. Portability: Pure C implementation works everywhere

Public Interface

In the header file, we maintain the clean opaque interface while hiding all implementation details:

typedef const struct __OCShape * OCShapeRef;
typedef struct __OCShape * OCMutableShapeRef;

// Creation and memory management
OCShapeRef OCShapeCreate(float x, float y, float orientation);
OCMutableShapeRef OCShapeCreateMutable(float x, float y, float orientation);

// All OCTypes inherit these operations
OCShapeRef OCRetain(OCShapeRef shape);      // Increment retain count
void OCRelease(OCShapeRef shape);           // Decrement retain count
bool OCShapeEqual(OCShapeRef a, OCShapeRef b);  // Compare for equality
OCShapeRef OCShapeDeepCopy(OCShapeRef source);  // Create immutable copy

This approach provides all the benefits of object-oriented programming—encapsulation, inheritance, and polymorphism—while remaining true to C’s philosophy of simplicity and explicitness.