object oriented code does not mean "language support for object oriented code".
Object oriented code is very common in many larger C libraries (or things like the linux kernel). The only difference is that you don't get any compiler support to prevent errors.
The standard model is:
typedef struct __MyType MyTypeRef;
struct MyTypeMethodTable {
// the destructor - honestly at an api level you should probably have retain/release instead
void (destroy)(MyTypeRef _this);
//
void (someMethod)(MyTypeRef _this, int someArg);
};
struct __MyType {
MyTypeMethodTable vtable;
};
void MyType_destroy(MyTypeRef _this) {
_this->vtable->destroy(_this);
}
void MyType_someMethod(MyTypeRef _this, int someArg) {
_this->vtable->someMethod(_this, someArg);
}
Then an actual type is implemented as
struct MyConcreteType {
struct __MyType base;
// some fields
};
void MyConcreteType_destroy(MyTypeRef value) {
MyConcreteType realValue = (MyConcreteType )value;
// cleanup anything you need to do
free(realValue);
}
void MyConcreteType_someMethod(MyTypeRef value, int someArg) {
printf("I: %d\n", someArg);
}
MyTypeMethodTable MyConcreteType_MethodTable {
.destroy = MyConcreteType_destroy,
.someMethod = MyConcreteType_someMethod
};
MyTypeRef CreateMyConcreteType() {
MyConcreteType *result = calloc(1, sizeof(MyConcreteType));
result->base.vtable = & MyConcreteType_MethodTable;
result->someField = whatever;
return &result->base; // Or similar. avoid UB in C can make this weird
}
You can see that trivially this is pretty much what notionally OO languages like C++, Java, Haskell, etc do.
An apparently not-uncommon error that happens in COM is:
someObject->whateverTheirMethodTableIsCalled->someMethod(theWrongObject)
Possibly with someObject and theWrongObject the other way around. The end result is sadness either way.
OO programming is super effective for many things, and generally better for a lot of design, especially for libraries and frameworks. But nothing about OO requires compiler/language support - indeed the first discussions of OO code predated OO languages - but it's hopefully obvious to see that if the compiler can manage this, it results in less code, and less opportunity for error, and because the compiler manages those semantics it should technically produce better code (e.g if the compiler knows that "vtable" is actually a vtable pointer, it knows there is no circumstance it can change[1]).
[1] Yes you could have incorrect code through UB, but in that case the compiler is allowed to do the "wrong" thing.