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:
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:
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.
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
:
{…}
#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:
#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.
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:
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:
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