Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / Objective C

CoreObjects/GoliahCore17: Effective OOP for plain C || 2

Rate me:
Please Sign up or sign in to vote.
3.86/5 (3 votes)
29 Dec 2018CPOL5 min read 4.1K   53   4  
Implementing a Canvas concept with OOP, in a fresh, polished and effective, pure and plain ANSI C

Introduction

In this article, I've spent my efforts to show in depth how it can be simple to adopt the OOP coding in pure plain C. To do so, I've written from scratch a simple class library that renders some geometric shapes.

Background

This is to be considered the natural continuation of "CoreObjects/GoliahCore17: Effective OOP for plain C". Concepts introduced there are assumed to be known, but not required to continue reading.

The Route

The article is constructed as a guided tour on the key points as they appear in the simple example attached.

I have implemented a graphics environment (Canvas) capable of collecting and rendering some basic geometric shapes.

The shape (also called Entity) collection implementation details is completely hidden from the user point of view. So only abstract interface for basic insertion, removal, and forward traversal is exposed.

In order to allow the rendering in any device, the rendering process is taken submitting all shapes to a rendering interface (CanvasRenderer) implemented externally around the device itself.

The Goal

The objective of the sample is to instantiate a Canvas, add some shapes, and finally render those on an arbitrary rendering device, using a proxy object implementing the CanvasRenderer interface.

The contents of main should appear more or less as follows:

C++
struct CanvasEntity* ent;
struct CanvasRenderer* renderer = (struct CanvasRenderer*)ConsoleRenderer_build(rendererRegion);
struct Canvas* canvas = Canvas_build(0);
ent = (struct CanvasEntity*)CanvasPoint_build(0,10,11,ColorModelARGB32_make(12,13,14,15));
canvas->invoke->Entities->insert(canvas, ent);
ent = (struct CanvasEntity*)CanvasRectangle_build(0,10,11,12,13,14,ColorModelARGB32_make(15,16,17,18));
canvas->invoke->Entities->insert(canvas, ent);
ent = (struct CanvasEntity*)CanvasCircle_build(0,10,11,12,ColorModelARGB32_make(15,16,17,18));
canvas->invoke->Entities->insert(canvas, ent);
canvas->invoke->Canvas->render(canvas, renderer);
CoreObject_free(canvas);

Canvas Classes Overview

First of all, an overview of the hierarchy:

Class Hierarchy

Canvas Shapes

The shape hierarchy is based on the abstract CanvasEntity class. Some other abstract classes are inserted to provide additional behaviours to the descendants.

Entity

CanvasEntity is the base class of the shape hierarchy. It provides some common attributes and defines the operations as pure methods.

The attributes are collected in the CanvasEntityAttributes Attributes member:

  • Visible
  • Selected
  • StrokeWidth
  • StrokeColor

The operations are defined by the CanvasEntityProtocol:

  • render
  • getBoundingBo
  • translate
  • rotate
  • scale

EntityLocated

CanvasEntityLocated adds 2D location information in the GeometricsLocation2D Location member.

Point, and Circle

CanvasPoint, and CanvasCircle are concrete classes based on CanvasEntityLocated.

CanvasCircle adds the radius attribute in the member GeometricsCircleExtent Extent.

Each implements its own specialization of CanvasEntityProtocol.

EntityTilded

CanvasEntityTilted extends CanvasEntityLocated and adds a rotation angle in the GeometricsTilt Tilt member.

Square, Rectangle, Ellipse, and Triangle

CanvasSquare, CanvasRectangle, CanvasEllipse, and CanvasTriangle are concrete classes based on CanvasEntityTilted. Each one adds to itself some attributes to complete each shape definition (see the code), and implements its own specialization of CanvasEntityProtocol.

Canvas Class

Canvas class acts as a container and glue of all others. Even so, it is not a final concrete class, because by requirements, we need a completely hidden and transparently replaceable data structure for collecting shapes.

For that reason, the Canvas is actually instantiated as its descendant CanvasImplementation, that embeds a simple linked list to collect shapes. It also provides an implementation of CanvasEntityIterator, the CanvasEntityListIterator, to traverse the list, and the full CanvasEntityCollectionProtocol.

CanvasRenderer Class

CanvasRenderer is an abstract interface that esposes methods to render each known shape, as follows:

  • renderPoint
  • renderCircle
  • renderSquare
  • renderRectangle
  • renderEllipse
  • renderTriangle

It must be implemented for any device, or in a way we want to render the canvas contents.

For the purpose of the sample, the render is implemented with the class ConsoleRenderer that simply prints to the console information on shape submitted to it.

Points of Interest

Apart of the hierarchy, that can be more or less well designed, here I want to focus on the way it is possible to manage these classes in plain C. In this issue, I want to focus on 2 aspects:

  • Macro helpers
  • Dynamic allocation

Macros Helpers

In order to address the intrinsic overwork generated due to the need to repeat tons of declarators at each class extension/interface implementation, I've introduced the use of preprocessor macros as integral part of the class definition process.

Class Component Definition Helpers

Used to collect all the data components of a specific class, and to be used in all of its descendants.

Some examples are CanvasEntity_c, CanvasEntityLocated_c, etc.

C++
struct CanvasEntity {
    {…}
    #define CanvasEntity_c
    struct CanvasEntityAttributes Attributes[1];
};

{…}

struct CanvasEntityLocated {

    {…}
    #define CanvasEntityLocated_c CanvasEntity_c\
        struct GeometricsLocation2D Location[1];
    CanvasEntityLocated_c
};

struct CanvasPoint {
    {…}
    CanvasEntityLocated_c
};

In the clip above, we can see how CanvasEntity_c definition is embedded CanvasEntity body, how it is inherited into the CanvasEntityLocated_c, and finally how the latter is used in CanvasPoint to include in it all members to be inherited.

Methods Argument List Definition Helpers

Used to collect part or all argument list declaration of a function, and to be used to speedup the implementation of those methods.

As an example, we can see the usage of the CanvassEntity_scale_al:

C++
{…}

#define CanvasEntity_scale_al GeometricsMeasure originX, 
GeometricsMeasure originY, GeometricsMeasure scaleX, GeometricsMeasure scaleY, CoreInt32 scaleUnit
CoreResultCode(*scale)(CoreTarget target, CanvasEntity_scale_al);

{…}

CoreResultCode CanvasPoint_Entity_scale(CoreTarget target, CanvasEntity_scale_al);

{…}

CoreResultCode CanvasCircle_Entity_scale(CoreTarget target, CanvasEntity_scale_al);

{…}
CoreResultCode CanvasSquare_Entity_scale(CoreTarget target, CanvasEntity_scale_al);

The CanvassEntity_scale_al is defined in the same place of the scale method in CanvasEntiryProtocol, and in the declaration of all scale implementations. The same usage.

Another example can be the usage of the macro pair CanvasEntityTilted_build_bal and CanvasEntityTilted_build_eal, used together to embed a common declaration, but leaving the possibility to insert some other argument between:

C++
#define CanvasEntityTilted_build_bal CanvasEntityLocated_build_bal
#define CanvasEntityTilted_build_eal GeometricsMeasure rotationAngle, CanvasEntityLocated_build_eal
struct CanvasEntityTilted* CanvasEntityTilted_init(CoreTarget target, CanvasEntityTilted_build_bal,
CanvasEntityTilted_build_eal);

{…}

struct CanvasRectangle* CanvasRectangle_build(CoreMemoryAddress target, CanvasEntityTilted_build_bal
                      , GeometricsMeasure width, GeometricsMeasure height, CanvasEntityTilted_build_eal);

struct CanvasRectangle* CanvasRectangle_init(CoreTarget target, CanvasEntityTilted_build_bal

                      , GeometricsMeasure width, GeometricsMeasure height, CanvasEntityTilted_build_eal);

Dynamic Allocation

In this sample, I've added dynamic allocation capabilities do the builders and a common way to deallocate.

Allocation

The allocation is done through the CoreObject_build utility function.

C++
struct CoreObject* CoreObject_build(CoreTarget target, CoreTarget descriptor) {
    struct CoreObject* self = (struct CoreObject*)target;
    const struct CoreObjectDescriptor* type = (const struct CoreObjectDescriptor*)descriptor;
    if(self == 0) {
        self = CoreMemory_allocBytes(type->Size);
    }
    CoreMemory_clearBytes(self, type->Size);
    self->invoke = type->Interface;
    return self;
}

Deallocation

The deallocation is issued by calling the CoreObject_free function providing to it the instance address to free. The function does nothing other than send a CoreObject_tell_Free message to the instance class tell method. It is the responsibility of the implementer of the class to eventually do the actual deallocation handling of that message.

The CoreObject_free function:

C++
CoreResultCode CoreObject_free(CoreTarget target) {
    struct CoreObject* self = (struct CoreObject*)target;
    if(self == 0) return CoreResultCode_Success;
    return self->invoke->Class->tell(target, CoreObject_tell_Free);
}

An example of tell function handling CoreObject_tell_Free message:

C++
CoreResultCode Canvas_tell(CoreTarget target, CoreWord16 message, ...) {
    switch(message) {
        case CoreObject_tell_Free:
            if(target) {
                Canvas_Collection_freeAll(target);
                CoreMemory_free((CoreMemoryAddress)target);
            }
    }
    return CoreResultCode_Success;
}

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 with it and enjoy.

To Be Continued...

The vastness of the topic can't be fitted in a couple of articles, 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.

History

License

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


Written By
Software Developer (Senior) AUTOMA SRL
Italy Italy
Over 30 years of passionate software development.
Spanning from web-based enterprise applications to (my)self designed multitasking real-time embedded operative system.
Speaking C & C++, Java, Python, and more.
There was a time to fit all in 640k, there was a time to fit all in a few Gigas..... and there is a time to fit all in less than 32k

Comments and Discussions

 
-- There are no messages in this forum --