Introduction
This is my first article, so let me introduce where my OOP nature begins.
In the past 3 decades, I worked very closely with OOP, beginning to understand its principles as soon I could get my hands on "Turbo Pascal 5.5" (1989). Not only to comprehend how it could be used, but pushing me to see how it worked up to assembly level. Over the years, I then approached the same way with C ++ from Borland, Watcom, Microsoft, etc. Up to define (ten years later, 1999) my self-made plug-in architecture capable of integrating objects DLLs produced with a mixture of compiler in the same application, and featuring a "load-as-needed" mechanism letting program startup instantaneously (I've always hated plugins loading waits at boot time, like Photoshop, Illustrator, Gimp, etc.). Since that, another 20 years have passed, and now is the time to go into the merit of the article.
Standing to Wikipedia (https://en.wikipedia.org/wiki/Object-oriented_programming#Class-based_vs_prototype-based), strongly class-object-oriented languages sports the following features:
- Objects and classes
- Dynamic dispatch/message passing
- Encapsulation
- Composition, inheritance, and delegation
- Polymorphism
- Open recursion
CoreObjects/GoliahCore17 provides all these features to pure C, offering:
- a clever organization (better of some OOP native languages)
- a standard and polished way to define and use objects (no magic macors, nor tortuous template twist)
- and a lesser overhead in term of assembly code generated/executed (than some C++ compilers)
With an only real downside, due to the needs to manually write the VMT (Virtual Methods Table).
Although (I've spent the last 15 years in very few resources embedded designs, so let me tell it) when only a few dozen KB are available, it is crucial to take complete control over that aspect too.
The Route
The article is constructed as a guided tour on the key points as they appear in the simple example attached: Implementing a console defined (in an imaginary pseudocode) as below.
- class Console
- extends CoreObject
- adds some data
- implements interface CoreStreamOutput defined as:
- putChar( int charValue )
- flush()
- implements interface CoreStreamInput defined as:
- bool hasChar()
- int getChar()
The input/output stream implementations are backed on the following 4 <conio.h> provided functions:
- putChar(charValue) => _putch(charValue)
- flush() => _putch('\n')
- hasChar() => _kbhit()
- getChar() => _getche()
The Goal
At the very end, a Console
was instantiated and its input/output streams are used to interact with the console.
void Demo1a_Console_main() {
CoreByte consoleMemoryRegion[sizeof(struct Console)];
struct Console* con = Console_build(consoleMemoryRegion);
struct CoreStreamOutput* out = con->Output;
out->invoke->putChar(out,'C');
StreamOutput_putChar(out, '\n');
StreamOutput_printf(out, "Hello %s!\n", "USER");
StreamOutput_printf(out, "Press a key to terminate...\n");
StreamInput_getChar(con->Input);
}
Concepts
To define an object class, we must deal with 7 aspect of that class:
- TypeId unique identifier
- Class
- Descriptor
- Protocol
- Interface
- Component
- Object
TypeId Unique Identifier
It can be a simple numeric constant, an UUID
, or whatever can be suitable to uniquely identify the class type.
For the purpose of this article, we use a simple enum
.
enum CoreObjectTypeId {
CoreObject_TypeId=1
, CoreStreamOutput_TypeId
, CoreStreamInput_TypeId
, Console_TypeId
};
Class
The class is an instance of struct CoreObjectClass
, defined as below:
struct CoreObjectClass {
CoreWord32 (*tell)(CoreTarget target, CoreWord16 message, ...);
};
In addition, we must define a function to assign to the tell
member. This function acts as a generic service routine for the class. For the purpose of the article, we have defined an empty function as below:
CoreResultCode Console_Class_tell(CoreTarget target, CoreWord16 message, CoreArgumentList* argList) {
return CoreResultCode_Success;
}
CoreResultCode Console_tell(CoreTarget target, CoreWord16 message, ...) {
CoreArgumentList argList[1];
CoreArgumentList_init(argList, message);
CoreResultCode resultCode = Console_Class_tell(target,message,argList);
CoreArgumentList_done(argList);
return resultCode;
}
Descriptor
This is a structure to fill with basic information guessed at runtime.
struct CoreObjectDescriptor {
const struct CoreObjectClass* Class;
const struct CoreObjectInterface* Interface;
CoreInt16 TypeId;
CoreInt16 Size;
CoreWord32 Options;
CoreInt32 Offset;
};
Protocol
This is a piece also known as VMT, in particular, the piece where we go to define the methods only added by the actual class. Below is an example:
struct CoreStreamOutputProtocol {
CoreResultCode(*putChar)(CoreTarget target, CoreInt16 data);
CoreResultCode(*flush)(CoreTarget target);
};
Interface
This is the complete VMT, composed by a pointer to the Class
, as defined above, and all protocols implemented in this interface. Below is an example:
struct ConsoleInterface {
const struct CoreObjectClass* Class;
const struct CoreStreamInputProtocol* Input;
const struct CoreStreamOutputProtocol* Output;
};
Component
This is the data counterpart of the protocol. We can define there the data members only added by the actual class:
struct ConsoleComponent {
CoreWord16 __placeHolder; };
Object
This is the actual type of the class instance. It combines together a pointer to the interface instance and all components:
struct Console {
const struct ConsoleInterface* invoke;
struct ConsoleComponent Console[1];
struct CoreStreamOutput Output[1];
struct CoreStreamInput Input[1];
};
Object Class Runtime-Type Information
Now we need a place to join all pieces together filling instances of type-ids
, class
, subclasses
, descriptors
, protocols
and interfaces
, involved with the actual class.
At first glance, it seems a bit complex:
const struct ConsoleType {
struct CoreObjectDescriptor Descriptor[1];
struct CoreObjectClass Class[1];
struct ConsoleInterface Interface[1];
struct ConsoleStreamOutputType {
struct CoreObjectDescriptor Descriptor[1];
struct CoreObjectClass Class[1];
struct CoreStreamOutputInterface Interface[1];
struct CoreStreamOutputProtocol Protocol[1];
} Output[1];
struct ConsoleStreamInputType {
struct CoreObjectDescriptor Descriptor[1];
struct CoreObjectClass Class[1];
struct CoreStreamInputInterface Interface[1];
struct CoreStreamInputProtocol Protocol[1];
} Input[1];
} ConsoleType[1] = {{
.Descriptor = { {
.Class = ConsoleType->Class
, .Interface = (const struct CoreObjectInterface*)ConsoleType->Interface
, .TypeId = Console_TypeId
, .Size = sizeof(struct Console)
, .Options = 0
, .Offset = 0
} }
, .Class = { {
.tell=Console_tell
} }
, .Interface = { {
.Class = ConsoleType->Class
, .Output = ConsoleType->Output->Protocol
, .Input = ConsoleType->Input->Protocol
} }
, .Output = { {
.Descriptor = { {
.Class=ConsoleType->Output->Class
, .Interface=(const struct CoreObjectInterface*)ConsoleType->Output->Interface
, .TypeId=CoreStreamOutput_TypeId
, .Size=sizeof(struct CoreStreamOutput)
, .Options=CoreObject_Option_Aggregate
, .Offset=offsetof(struct Console,Output)
} }
, .Class = { {
.tell=Console_tell
} }
, .Interface = {{
.Class = ConsoleType->Output->Class
, .Output = ConsoleType->Output->Protocol
, .putChar = Console_StreamOutput_putChar
, .flush = Console_StreamOutput_flush
}}
, .Protocol = {{
.putChar = Console_StreamOutput_putChar
, .flush = Console_StreamOutput_flush
}}
} }
, .Input = { {
.Descriptor = { {
.Class=ConsoleType->Input->Class
, .Interface=(const struct CoreObjectInterface*)ConsoleType->Input->Interface
, .TypeId=CoreStreamInput_TypeId
, .Size=sizeof(struct CoreStreamInput)
, .Options=CoreObject_Option_Aggregate
, .Offset=offsetof(struct Console,Input)
} }
, .Class = { {
.tell=Console_tell
} }
, .Interface = {{
.Class = ConsoleType->Input->Class
, .Input = ConsoleType->Input->Protocol
, .hasChar = Console_StreamInput_hasChar
, .getChar = Console_StreamInput_getChar
}}
, .Protocol = {{
.hasChar = Console_StreamInput_hasChar
, .getChar = Console_StreamInput_getChar
}}
} }
} };
The Builder
Now we need to instantiate the Console
class, and we can do so with the Console_build
function:
struct Console* Console_build(CoreTarget address) {
struct Console*self = (struct Console*)address;
CoreMemory_clearIndirect(self);
self->invoke = ConsoleType->Interface;
self->Input->invoke = ConsoleType->Input->Interface;
self->Output->invoke = ConsoleType->Output->Interface;
return Console_init(self);
}
struct Console* Console_init(struct Console* self) {
return self;
}
How to Use the Source Code
The source code attached is an agglomerate of code fragments pasted together in a unique C file, to instantly bring the focus on the subject. The package includes also the VisualStudio2017 Community Edition project and solution under which it was created. Feel free to mess up with it and enjoy.
To Be Continued...
The vastness of the topic can't be fitted in a single article, this is only a brief overview. If it were to receive some kind of interest, I will return to the topic to deal with some other aspects of the OOP
using CoreObjects
/ColiahCore17
, from common PC to embedded architectures with very few resources.
If that case, in a next future, I will start telling about the GoliahCore17
philosophy: