Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

CoreObjects/GoliahCore17: Effective OOP for plain C

3.67/5 (3 votes)
25 Dec 2018CPOL4 min read 7.1K   69  
A fresh, polished and effective way to deal with OOP in pure and plain ANSI C

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.

MC++
void Demo1a_Console_main() {
    /* Allocate a memory region to hold the Console instance */
    CoreByte consoleMemoryRegion[sizeof(struct Console)];
    /* Build and initialize the Console instance */
    struct Console* con = Console_build(consoleMemoryRegion);
    /* Take the CoreStreamOutptu interface implementation from the Console instance */
    struct CoreStreamOutput* out = con->Output;
    /* Directly invoke the interface member function putChar, to output a character to the console */
    out->invoke->putChar(out,'C');
    /* Invoke the interface putChar through an inline stub, 
       to output a line-feed character to the console  */
    StreamOutput_putChar(out, '\n');
    /* Invoke the printf-like function to output formatted string 
       to the console through the putChar member function of its CoreStreamOutput interface */
    StreamOutput_printf(out, "Hello %s!\n", "USER");
    StreamOutput_printf(out, "Press a key to terminate...\n");
    /* wait for a key */
    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.

C++
enum CoreObjectTypeId {
    CoreObject_TypeId=1
  , CoreStreamOutput_TypeId
  , CoreStreamInput_TypeId
  , Console_TypeId
};

Class

The class is an instance of struct CoreObjectClass, defined as below:

C++
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:

C++
CoreResultCode Console_Class_tell(CoreTarget target, CoreWord16 message, CoreArgumentList* argList) {
 //struct Console* self = (struct Console*)CoreObject_getSelf(ConsoleType->Class, target);
 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.

C++
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:

C++
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:

C++
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:

C++
struct ConsoleComponent {
  CoreWord16 __placeHolder; //A __placeHolder only for documentation purpose 
};

Object

This is the actual type of the class instance. It combines together a pointer to the interface instance and all components:

C++
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:

C++
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:

MC++
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);
}
//Console_init actually does nothing.
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:

GoliahCore17 - C for the Millennials

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)