About This Tutorial

CREDITS: This is a reprint of «Dräc»’s multi-part tutorial PureBasic and the Object-Oriented Programming, also knwon as the OOP demystified, published in 2005 on drac.site.chez-alice.fr.

LICENSE: It was reprinted with explicit permission of the author, who released the tutorial text and source codes under Creative Commons Attribution (CC BY 4.0) in response to the permission request to reproduce it inside the PureBASIC Archives.

CHANGES: The tutorial was ported from HTML to AsciiDoc by Tristano Ajmone, who also polsihed the English text — introducing slight adjustments in line with the tutorial aims (using the original French tutorial as a reference) — and made minor changes to code examples (either aesthetic, or minor corrections, or to ensure compatibility with latest version of PureBASIC).

First republished: 2016/11/18 — by @tajmone.

Why OOP in PureBasic?

Since many object-oriented languages already exist, trying to achive OOP in PureBasic — which is a procedural language — might come as a surprise.

The fact that some languages are defined «Object-Oriented», while others are not, is merely indicating the presence of specific keywords which facilitate writing code according to the OOP paradigm. That is to say: object-oriented languages introduce richer semantics; but compilation-wise they are not introducing any new aspects compared to a non-OO language, they’re just adding a layer over the latter.

Therefore, we can fully implement the OO paradigm in PureBasic, but we have to pay a price in terms of accuracy — both in development and in notation. And here we face the immediate advantages of (natively) object-oriented languages.

Nevertheless, besides the possibility of programming in PureBasic according to the OO paradigm, implementing object-oriented concepts in PureBasic offers us the interesting chance to reveal some of the underlying mechanisms of OO languages keywords.

This paper presents a programming technique that allows large projects to benefit from object oriented design. It is not intended as a course on OOP, and it assumes that the reader has good knowledge of PureBasic language.

Object Concepts

The Object-Oriented Programming paradigm introduces concepts like: objects, inheritance and polymorphism. Before jumping into how these concepts can be implemented in PureBasic, we must first define them.

The Notion of Object

An object has an internal state:

  • The state is represented by the value of each inner component at a given moment,

  • A component is either a value or another object.

It uses and provides services:

  • It interacts with the outside through functions called «methods».

It is unique (notion of identity).

An object can be seen at two levels:

  • By the services which it provides: external view (specification). This is the User side,

  • By the way these services are implemented within it: internal view (implementation). This is the Developer side.

From the developer’s point of view, an object is a contiguous memory area containing information: variables called attributes and functions called methods.

The fact that we refer to an object’s functions as «its methods» is because they belong to it and allow manipulation of its attributes.

The Notion of Class

This is an extension of the concept of «type» found in PureBasic.

In a given context, multiple objects can have the same structure and behavior. They can therefore be grouped together in the same «Class».

From a developer’s point of view, a Class defines the type of contents that will be held by objects belonging to it: the nature of their attributes (type of each variable) and their methods (names, implementation).

Just as integer is the type of a variable, the type of an object is its Class.

The Notion of Instance

An instance is an object defined from a Class.

This process is called «instanciation».

It corresponds to the assignement of variables in PureBasic.

Normally, an object is initialized at the time of its instanciation.

Encapsulation

In theory, the manipulation of an object’s attributes should be possible only through its methods. This technique, which allows making visible to the user only a part of the object, is called «encapsulation».

The advantage of encapsulation is that it guarantees the integrity of attributes. Indeed, the developer is the only one who, through the methods provided to the user, manages the modifications allowed on an object.

At our level, this is the least that should be retained of the encapsulation concept.

Inheritance

Inheritance allows defining new Classes by using already existing ones.

From the developer’s point of view, it means being able to add/modify attributes and methods to/of an existing Class in order to define a new Class.

There are two kinds of inheritances:

  • Simple inheritance: the new Class is defined from a single existing Class.

  • Multiple inheritance: the new Class is defined from several existing Classes.

Multiple inheritance is complex to implement, and it will not be covered here. Thus, this papers deals only with simple inheritance.

Terminology:

  • The Class which inherits from another Class, is usually called Child Class.

  • The Class which gives its inheritance to a Child Class is usually called Parent Class.

Overloading

A method is overloaded if it carries out different actions according to the nature of the target objects.

Let us take an example:

The following objects: circle, rectangle and triangle are all geometrical shapes. We can define for these objects the same Class with the given name: Shape. Thus, these objects are all instances of the Shape Class.

If we want to display the objects, the Shape Class needs to have a Draw method.

So endowed, every object has a Draw method to display itself. Now, this method could not possibly be the same for each object, since we want to display a circle, in one case, a rectangle, in another, etc.

Objects of the same Class employ the same Draw method, but the object’s nature (Circle, Rectangle, or Triangle) dictates the actual implementation of the method. The Draw method is overloaded: for the user, displaying a circle or a rectangle is achieved in the same way. From the developer’s point of view, the methods implementations needs to be different.

Instead of overloaded methods, we can also speak of polymorphic methods (having several forms).

The Notion of Abstract Class

As we’ve seen, a Class includes the definition of both attributes and methods of an object. Let us suppose that we can’t provide the implementation of one of the Class methods. This method is just a name without code. We’re then speaking of an «abstract method». A Class containing at least one abstract method qualifies as an «abstract Class».

You might wonder why an abstract class should exist at all, since objects of such a Class can’t be created. Abstract Classes allow defining Object Classes, which are considered — by opposition — as being «concrete». The transition from the former to the latter occurs through inheritance, where the concrete Class takes care of providing the missing implementations to the abstract methods inherited.

Thus, abstract Classes are a kind of interface, because they describe the generic specification of all the Classes which inherit from them.

First Implementation

In this section, I shall demonstrate how the aforementioned object concepts can be implemented in PureBasic. This implementation doesn’t refer to what is programmed in object-oriented languages. Furthermore, this implementations is meant be improved upon, or adapted according to needs.

I’ll be presenting here one of these implementations, with all its pros and cons.

Concrete Class and Abstract Class

As seen, a Class defines the contents of an object:

  • Its attributes (each variable type)

  • Its methods (Names, implementation)

For example, if I want to represent Rectangle objects and display them on screen, I shall define a Rectangle Class including a Draw() method.

The Rectangle Class could have the following construction:

Structure Rectangle
  *Draw
  x1.l
  x2.l
  y1.l
  y2.l
EndStructure

Procedure Draw_Rectangle(*this.Rectangle)
  ; [ ...some code... ]
EndProcedure

where x1, x2, y1 and y2 are four attributes (the coordinates of the diametrically opposed points of the rectangle) and *Draw is a pointer referencing the drawing function which displays Rectangles.

Here *Draw is a function pointer used to contain the address of the desired function: @Draw_Rectangle(). Functions referenced in this manner can be invoked by using CallFunctionFast().

Thus, the proposed Structure is completely adapted to the notion of Class: * the structure stores the definition of the object’s attributes: here x1, x2, y1 and y2 are Long variables. * the structure stores the definition of the object’s method: here the Draw() function, through to a function pointer.

When a similar Class definition is followed by the implementations of its methods (in our example, Draw_Rectangle()’s Procedure : EndProcedure block statement), it becomes a concrete Class. Otherwise, it will be an abstract Class.

*this always refers to the object on which the method must be applied. This notation can be seen in the previous example, within the Draw_Rectangle() method.

Instanciation

Now, to create an object called Rect1 from the Rectangle Class, write:

Rect1.Rectangle

To initialize it, simple write:

Rect1\Draw = @Draw_Rectangle()
Rect1\x1 = 0
Rect1\x2 = 10
Rect1\y1 = 0
Rect1\y2 = 20

Next, to draw the Rect1 object, use:

CallFunctionFast(Rect1\Draw, @Rect1)

Encapsulation

In this implementation, encapsulation doesn’t exist, simply because there is no way to hide the attributes or the methods of such an object.

By writing Rect1\x1, the user can access the x1 attribute of the object. This is the way we used to initialize the object. The next implementation (Second Implementation section) will show how to fix this. Although significant, this feature is not essential in implementing OOP.

Inheritance

Now I want to create a new Class with the capability to Erase rectangles from the screen. I can implement this new Rectangle2 Class by using the existing Rectangle Class and by providing it with a new method called Erase().

A Class being a Structure, let’s take advantage of the extension property of structures. So, the new Class Rectangle2 could be:

Structure Rectangle2 Extends Rectangle
  *Erase
EndStructure

Procedure Erase_Rectangle(*this.Rectangle2)
  ; [ ...some code... ]
EndProcedure

The Class Rectangle2 includes the members of the previous Rectangle Class as well as those of the new Erase() method. To instanciate an object from this new Class write:

Rect2.Rectangle2

Rect2\Draw = @Draw_Rectangle()
Rect2\Erase = @Erase_Rectangle()
Rect2\x1 = 0
Rect2\x2 = 10
Rect2\y1 = 0
Rect2\y2 = 20

To use Rect2’s Draw() and Erase() methods, I shall proceed the same way as before: through CallFunctionFast().

This demonstrates that the Rectangle2 Class inherited the properties of the Rectangle Class.

Inheritance is a category of polymorphism. The object Rect2 can be also seen as an Object from the Rectangle Class — just don’t use the Erase() method! By inheritance, the object carries several forms: those of the objects coming from the Parent Classes. It is called inheritance polymorphism.

Overloading

During initialization of an object, its function pointers are initialized by assigning to them the addresses of the methods suiting the object.

So, given an object Rect from the Rectangle Class, by writing:

Rect1\Draw = @Draw_Rectangle()

I can invoke its Draw() method the following way:

CallFunctionFast(Rect1\Draw, @Rect1)

Now, imagine that it was possible to implement another method for displaying a rectangle (by using a different algorithm from the one in the first method).

Let us call this implementation as Draw_Rectangle2():

Procedure Draw_Rectangle2(*this.Rectangle)
  ; [ ...some code... ]
EndProcedure

It’s possible to initialize our object Rect1 with this new method effortlessly:

Rect1\Draw = @Draw_Rectangle2()

To use the method, write again:

CallFunctionFast(Rect1\Draw, @Rect1)

We can see that with both the former method (ie: Draw_Rectangle()) as well as the latter (ie: Draw_Rectangle2()) the use of the Rect1 method is strictly identical.

It isn’t possible to distinguish by the single line of code CallFunctionFast(Rect1\Draw, @Rect1) which one of the Draw() methods the Rect1 object is really using. To know this, it is necessary to go back to the object initialization.

The notion of function pointer allows overloading the Draw() method.

One limitation: the use of the CallFunctionFast() instruction implies paying attention to the number of parameters passed.

Conclusion:

In this first implementation, we produced an object capable of meeting the main object-oriented concepts, albeit with certain limitations.

We mainly just lay the foundations upon which we shall implement a more complete object — thanks to PureBasic’s Interface statement!

Interface instruction

Syntax
Interface <Name1> [Extends <Name2>]
  [Procedure1]
  [Procedure2]
  ...
EndInterface

The PureBasic Interface instruction allows grouping under the same Name (<Name1> in the above box) various procedures.

Example
Interface My_Object
  Procedure1(x1.l, y1.l)
  Procedure2(x2.l, y2.l)
EndInterface

It’s now sufficient to declare an element as being of the My_Object type in order to access all the procedures that it contains. The declaration is carried out in the same manner as with Structuretypes:

Object.My_Object

As a result, we can now acess the Object’s functions directly:

Object\Procedure1(10, 20)
Object\Procedure2(30, 40)

Thanks to the Interface instruction, procedures can be called via a very practical and pleasant notation. By writing Object\Procedure1(10, 20), the Procedure1() from Object is called. This notation is typical of the Object-oriented Programming paradigm.

Initialization

Any typed variable declaration is normally followed by initialization. The same applies when declaring an element whose type is an Interface.

Unexpectedly, naming the Interface : EndInterface block with the name of a desired Procedure isn’t enough to make it refer to its implementation — i.e., to reference the Procedure : EndProcedure block of the desired procedure.

In fact, we can rename procedures inside an Interface : EndInterface block: we can give any name we like to the procedures that we are going to use.

Then, how are we going to connect this new name with the desired real procedure?

As with overloaded methods, the solution is in function addresses. We must see the names inside the Interface : EndInterface block as function pointers to the desired function — i.e., as pointer holding function addresses.

However, to initialize the function pointers of an Interface typed element, the approach is different from that of a Structure typed element. Indeed, it isn’t possible to initialize individually each field defined by an Interface, because, you must remember, that writing Object\Procedure1() means calling that procedure.

Initialization occurs indirectly, by giving to the element the address of a pre-initialized variable storing functions pointers.

This kind of variable is called a method table.

Example:

Let us carry on with the Interface My_Object. Consider the following Structure describing the function pointers:

Structure My_Methods
  *Procedure1
  *Procedure2
EndStructure

and its associated method table:

Methods.My_Methods
Methods\Procedure1 = @My_Procedure1()
Methods\Procedure2 = @My_Procedure2()

where My_Procedure1() and My_Procedure2() are the desired procedure implementations.

Then, initialization of Object (of the My_Object type, an Interface) looks like this:

Object.My_Object = @Methods

Next, by writing

Object\Procedure2(30, 40)

the Object’s Procedure2() function is called — i.e., My_Procedure2().

When declaring elements of an Interface, it’s essential to initialize them before using their procedures. Therefore, it is strongly advisable to initialize elements at declaration time.

The method table’s Structure must reflect exactly the composition of its correlated Interface. It must contain the same number of fields, and preserve their order, to ensure the correct assignation of each function’s name and address. It is only under these conditions that the element will be properly initialized.

To summarize, using an Interface involves:

  • an Interface describing the required procedures to use,

  • a Structure describing the function pointers,

  • a method table: a structured variable initialized with the required function adresses.

And its benefits are:

  • an object-oriented notation

  • an easy way to rename procedures

Second Implementation

In our first implementation, object concepts were adapted in a more or less extended way. Now, it’s time to improve this first implementation thanks to the use of the Interface instruction.

Notion of Object Interface

The main purpose of encapsulation is to make visible, to the user, only part of an object contents. The visible part of an object’s contents is called its interface, the hidden part is called it implementation.

Therefore, an object’s interface is the only input/output access available to the user for interacting with it.

This is the aim that we are going to achieve through the use of the Interface instruction.

The Interface instruction allows to group, under the same name, all or part of an object’s methods which the user will have the right to access.

Object Instanciation and Object Constructor

Implementing an Interface involves three steps:

  1. An Interface describing the required methods,

  2. A Structure describing the pointers of the corresponding functions,

  3. A method table: a structured variable initialized with the required functions adresses.

Step 1, consists in specifying the object’s Interface; this is not difficult. Just name the methods.

Steps 2 and 3 are linked. In our object approach, we already have the adapted Structure: it’s the one that describes the Class of an object. Moreover, the Interface and the Class of an object are similar: both contain functions pointers. Simply, the Interface instruction doesn’t contain the Class attributes but only all or part of its methods.

Therefore it’s possible to use an object’s Class to initialize its Interface. This approach is the most natural one. Let’s not forget that an interface is the visible part of an object’s Class, so it is natural that the Class determines the Interface.

To see how this can be achieved, let’s carry on with the example of the Rectangle2 class, which provided the Draw() and Erase() methods.

The corresponding Class is:

Structure Rectangle2
  *Draw
  *Erase
  x1.l
  x2.l
  y1.l
  y2.l
EndStructure

Procedure Draw_Rectangle(*this.Rectangle2)
  ; [ ...some code... ]
EndProcedure

Procedure Erase_Rectangle(*this.Rectangle2)
  ; [ ...some code... ]
EndProcedure

The associated Interface is:

Interface Rectangle
  Draw()
  Erase()
EndInterface

Since the user can handle an object only through its Interface, the object must be created directly from the Rectangle Interface, rather than from the Rectangle2 Class.

The object will thus be created by writing:

Rect.Rectangle

instead of Rect.Rectangle2.

However, you should not forget to connect the Interface to the Class. For this, it is necessary to initialize the Rect object during its declaration. Correction made, the proper instruction to declare the object is the following one:

Rect.Rectangle = New_Rect(0, 10, 0, 20)

New_Rect() is a function which performs the initialization operation. We already know that its returned value is the memory address containing the functions addresses to be processed by the interface.

Here is the body of the New_Rect() function:

Procedure New_Rect(x1.l, x2.l, y1.l, y2.l)
  *Rect.Rectangle2 = AllocateMemory(SizeOf(Rectangle2))

  *Rect \Draw = @Draw_Rectangle()
  *Rect \Erase = @Erase_Rectangle(

  *Rect\x1 = x1
  *Rect\x2 = x2
  *Rect\y1 = y1
  *Rect\y2 = y2

  ProcedureReturn *Rect
EndProcedure

This function allocates a memory area with the same size as the object’ Class size. Then it initializes the methods and attributes of the object. Finally, it ends by returning the memory area’s address. Because the addresses of the Draw() and Erase() functions are positioned at the beginning of this memory area, the Interface is effectively initialized.

To access the methods of the Rect object, just write:

Rect\Draw()
Rect\Erase()

Therefore, we have ascertained that:

  • Class Rectangle2 allows initialization of the object’s Interface .

  • Rect — declared via Interface — is an object of the Rectangle2 Class, and can use the Draw() and the Erase() methods.

Thus the Interface instruction and the New_Rect() function perform the instanciation of a Rect object from the Rectangle2 Class. The New_Rect() function is the Constructor for objects of the Rectangle2 Class.

All the Methods implementations (Procedure : EndProcedure blocks) must contain, as first argument, the *this pointer of the object. On the other hand, the *this argument mustn’t appear at the Interface level. In fact, as this instruction allows to write Rect\Draw(), it knows that the Draw() method involves the Rect object: no ambiguity! Everything happens as if the object Rect was «aware» of its state.

The Constructor could receive, as parameters, the whole functions addresses which implement the methods. This is not the case here, because we know the implemented methods: the ones from the class. On the other hand the initial state desired by the user is unknown. Thus, the Constructor may contain parameters for attributes initialization. This case applyes here: the paramters required by New_Rect() are the two coordinates (x1, y1) and (x2, y2) of the diametrically opposite points of the rectangle.

Object Initialization

We’ve seen that after allocating the required memory area for an object, the Constructor initializes the various members of the object (methods and attributes). This operation can be isolated in a specific procedure, called by the Constructor. This precaution allows to make a distinction between an object’s memory allocation and its initialization. This approach will turn out to be very useful later on, when implementing the concept of Inheritance, because a single memory allocation is sufficient, but several initializations are required.

In addition, initialization of methods and attributes are separated too — because the methods implementation depends on the class, while the attributes initialization depends on the object itself (see previous remark).

In our example, the two initialization procedures will be implemented as:

Procedure Init_Mthds_Rect(*Rect.Rectangle2)
  *Rect\Draw = @Draw_Rectangle()
  *Rect\Erase = @Erase_Rectangle()
EndProcedure

Procedure Init_Mbers_Rect(*Rect.Rectangle2, x1.l, x2.l, y1.l, y2.l)
  *Rect\x1 = x1
  *Rect\x2 = x2
  *Rect\y1 = y1
  *Rect\y2 = y2
EndProcedure

and the Constructor becomes:

Procedure New_Rect(x1.l, x2.l, y1.l, y2.l)
  *Rect = AllocateMemory(SizeOf(Rectangle2))
  Init_Mthds_Rect(*Rect)
  Init_Mbers_Rect(*Rect, x1, x2, y1, y2)
  ProcedureReturn *Rect
EndProcedure

Object Destructor

An object Constructor is always associated with its counterpart: the object Destructor. During construction of an object, a memory area is allocated to store its method and attribute definitions. When an object becomes useless, it must be destroyed to free the computer memory. This process is performed by using a specific function, known as the object’s Destructor.

In our example of Rectangle2 objects, the Destructor will be:

Procedure Free_Rect(*Rect)
  FreeMemory(*Rect)
EndProcedure

and can be used by:

Free_Rect(Rect2)

The Destructor could be seen as a method of the object. But to avoid weighing down the object, and to preserve homogeneity with the Constructor, I have chosen to see it as a function of the Class.

To delete an object by its Destructor means releasing the memory area containing its information (the methods it uses, and the state of itsattributes) but not deleting the object’s infrastructure. So, in our example, after doing a:

Free_Rect(Rect2)

Rect2 can be reused without specify its type again:

Rect2 = New_Rect(0, 10, 0, 20)
Rect2\Draw()

Indeed, after we instantiate an object with:

Rect2.Rectangle

the life cycle of object Rect2 follows the same rules that apply to variables — because Rect2 is first of all a variable: it is a structured variable, holding the functions pointers of the object’s methods. (See also the following reminder)

Small reminder: the life cycle of a variable is linked to the life cycle of the program part where the variable is declared:

  • If the variable is declared inside a procedure, its life cycle will be linked to that of the procedure — i.e., it’s equal to the function’s time of use.

  • If the variable is declared outside any procedure, in the program’s main body, its life cycle is linked to that of the program.

Memory Allocations

At every new instanciation, the Constructor has to dynamically allocate a memory area the size of the information describing the object. For this purpose, the Constructor should use the AllocateMemory() command; and the Destructor should use its associated counterpart, the FreeMemory() command.

But there are also other candidates for achieving dynamic memory allocation. Under Windows OS, for example, the Windows API could be employed directly.

PureBasic’s standard library provides linked lists, which are also a good candidate for dynamically allocating some memory.

Encapsulation

Suppose now that we wanted to restrict the user’s access to just the Draw() method of the Class Rectangle. We shall begin by defining the desired interface:

Interface Rectangle
  Draw()
EndInterface

Instanciation of a new object reamins the same:

Rect.Rectangle = New_Rect()

with

Procedure Init_Mthds_Rect(*Rect.Rectangle2)
  *Rect\Draw = @Draw_Rectangle()
  *Rect\Erase = @Erase_Rectangle()
EndProcedure

Procedure Init_Mbers_Rect(*Rect.Rectangle2, x1.l, x2.l, y1.l, y2.l)
  *Rect\x1 = x1
  *Rect\x2 = x2
  *Rect\y1 = y1
  *Rect\y2 = y2
EndProcedure

Procedure New_Rect(x1.l, x2.l, y1.l, y2.l)
  *Rect = AllocateMemory(SizeOf(Rectangle2))
  Init_Mthds_Rect(*Rect)
  Init_Mbers_Rect(*Rect, x1, x2, y1, y2)
  ProcedureReturn *Rect
EndProcedure

It is similar to the previous example, because the first function address is that of the Draw() method.

Now, suppose that we wanted to give to the user only the access to the Erase() method. We shall begin by defining the new interface:

Interface Rectangle
  Erase()
EndInterface

Nevertheless, to instanciate the new object I cann’t use the New_Rect() Constructor above: doing so would yeld results identical to the previous case, and Rect\Erase() would call the Draw() method.

Thus, a new Constructor is needed, capable of returning the correct function address.

Here it is:

Procedure Init_Mthds_Rect2(*Rect.Rectangle2)
  *Rect\Draw = @Erase_Rectangle()
  *Rect\Erase = @Draw_Rectangle()
EndProcedure

Procedure Init_Mbers_Rect(*Rect.Rectangle2, x1.l, x2.l, y1.l, y2.l)
  *Rect\x1 = x1
  *Rect\x2 = x2
  *Rect\y1 = y1
  *Rect\y2 = y2
EndProcedure

Procedure New_Rect2(x1.l, x2.l, y1.l, y2.l)
  *Rect = AllocateMemory(SizeOf(Rectangle2))
  Init_Mthds_Rect2(*Rect)
  Init_Mbers_Rect(*Rect, x1, x2, y1, y2)
  ProcedureReturn *Rect
EndProcedure

Notice how the functions addresses were simply inverted at the initialization level. Certainly, it is not a very elegant solution to allocate the Draw field of Rectangle2’s Structure with an other function’s address. But it allows to preserve the same Structure of the Class; and it also underlines a point: function pointers’ names are less interesting than their values!

To solve this false problem, just rename the pointers of the Class as follows:

Structure Rectangle2
  *Method1
  *Method2
  x1.l
  x2.l
  y1.l
  y2.l
EndStructure

Indeed, it’s the Interface and the Constructor which give meaning to these pointers:

  • by giving them a name (task of the interface)

  • by allocating them the adequate functions addresses (task of the constructor)

In spite of this arrangement concerning the function pointers’ names, it remains more practical to keep an explicit name when not considering to hide methods (which is the most common scenario). This allows to modify a Parent Class without having to retouch the pointers’ numbering in Children Classes.

Inheritance

For our first implementation of the inheritance concept, let’s takes advantage of the fact that the Structure and Interface instructions can be extended thanks to the Extends keyword.

So, to pass from the Rectangle1 Class, which has a single Draw() method…

Interface
Interface Rect1
  Draw()
EndInterface
Class
Structure Rectangle1
  *Method1
  x1.l
  x2.l
  y1.l
  y2.l
EndStructure

Procedure Draw_Rectangle(*this.Rectangle1)
  ; [ ...some code... ]
EndProcedure

Procedure Init_Mthds_Rect1(*Rect.Rectangle1)
  *Rect\Method1 = @Draw_Rectangle()
EndProcedure
Constructor
Procedure Init_Mbers_Rect1(*Rect.Rectangle1, x1.l, x2.l, y1.l, y2.l)
  *Rect\x1 = x1
  *Rect\x2 = x2
  *Rect\y1 = y1
  *Rect\y2 = y2
EndProcedure

Procedure New_Rect1(x1.l, x2.l, y1.l, y2.l)
  *Rect = AllocateMemory(SizeOf(Rectangle1))
  Init_Mthds_Rect1(*Rect)
  Init_Mbers_Rect1(*Rect, x1, x2, y1, y2)
  ProcedureReturn *Rect
EndProcedure

…to a Rectangle2 Class, which has two methods: Draw() and Erase(), we write:

Interface
Interface Rect2 Extends Rect1
  Erase()
EndInterface
Class
Structure Rectangle2 Extends Rectangle1
  *Method2
EndStructure

Procedure Erase_Rectangle(*this.Rectangle1)
  ; [ ...some code... ]
EndProcedure

Procedure Init_Mthds_Rect2(*Rect.Rectangle2)
  Init_Mthds_Rect1(*Rect)
  *Rect\Method2 = @Erase_Rectangle()
EndProcedure
Constructor
Procedure Init_Mbers_Rect2(*Rect.Rectangle2, x1.l, x2.l, y1.l, y2.l)
  Init_Mbers_Rect1(*Rect, x1, x2, y1, y2)
EndProcedure

Procedure New_Rect2(x1.l, x2.l, y1.l, y2.l)
  *Rect = AllocateMemory(SizeOf(Rectangle2))
  Init_Mthds_Rect2(*Rect)
  Init_Mbers_Rect2(*Rect, x1, x2, y1, y2)
  ProcedureReturn *Rect
EndProcedure

Carrying out an inheritance doesn’t consist only in extending the object’s Interface and Class Structure, but also in adapting the initialization of its methods and attributes. The Init_Mthds_Rect2() and Init_Mbers_Rect2() procedures call, respectively, the initialization of Class Rectangle1’s methods (Init_Mthds_Rect1()) and attributes (Init_Mbers_Rect1()), rather than the New_Rect1() Constructor. This because the Child Class object (Rectangle2) doesn’t need to instantiate its Parent Class object (Rectangle1), but just to inherit its methods and attributes.

On the other hand, we must verify that any changes made to the Parent Class (adding a method or a variable) should be immediately reflected in its Child Class.

So, is the current state of inheritance correct? No, because it doesn’t allow the object of the Child Class (Rectangle2) to use the new Erase() method. The reason being that the function pointer *Method2 doesn’t immediately follow *Method1 in order of succession.

If we look at the explicit Structure of the Rectangle2 Class, we find:

Structure Rectangle2
  *Method1
  x1.l
  x2.l
  y1.l
  y2.l
  *Method2
EndStructure

instead of the Structure below, which permits a correct initialization of the interface:

Structure Rectangle2
  *Method1
  *Method2
  x1.l
  x2.l
  y1.l
  y2.l
EndStructure

Remember that a correct interface initialization requires that this successsion of functions addresses appears in the same order within its Interface (see previous note).

To solve this problem, we’ll just group all the methods into a specific Structure! The Class’s Structure will need just a pointer to this new Structure, as shown in the following example:

Interface
Interface Rect1
  Draw()
EndInterface
Class
Structure Rectangle1
  *Methods
  x1.l
  x2.l
  y1.l
  y2.l
EndStructure

Procedure Draw_Rectangle(*this.Rectangle1)
  ; [ ...some code... ]
EndProcedure

Structure Methds_Rect1
  *Method1
EndStructure

Procedure Init_Mthds_Rect1(*Mthds.Mthds_Rect1)
  *Mthds\Method1 = @Draw_Rectangle()
EndProcedure

Mthds_Rect1.Mthds_Rect1
Init_Mthds_Rect1(@Mthds_Rect1)
Constructor
Procedure Init_Mbers_Rect1(*Rect.Rectangle1, x1.l, x2.l, y1.l, y2.l)
  *Rect\x1 = x1
  *Rect\x2 = x2
  *Rect\y1 = y1
  *Rect\y2 = y2
EndProcedure

Procedure New_Rect1(x1.l, x2.l, y1.l, y2.l)
  Shared Mthds_Rect1
  *Rect.Rectangle1 = AllocateMemory(SizeOf(Rectangle1))
  *Rect\Methods = @Mthds_Rect1
  Init_Mbers_Rect1(*Rect, x1, x2, y1, y3)
  ProcedureReturn *Rect
EndProcedure

The Methds_Rect1 structure describes all the functions pointers of the Class’ methods. Then follows the Methds_Rect1 variable declaration (of the Methds_Rect1 type) and its initialization thanks to Init_Mthds_Rect1().

The Methds_Rect1 variable is the Class’ method table, because it contains the set of all the addresses of its methods. This set constitutes the complete description of the methods of the Class.

The Structure of Rectangle1 now contains the *Methods pointer, which is initialized by passing the Methds_Rect1 variable address to the Constructor.

The following expression:

Mthds_Rect1.Mthds_Rect1
Init_Mthds_Rect1(@Mthds_Rect1)

can be condensed into:

Init_Mthds_Rect1(@Mthds_Rect1.Mthds_Rect1)

Inheritance can be now performed correctly, because by extending Methd_Rect1’s Structure into the new Methd_Rect2, the functions’ addresses are going to be consecutive:

Interface
Interface Rect2 Extends Rect1
  Erase()
EndInterface
Class
Structure Rectangle2 Extends Rectangle1
EndStructure

Procedure Erase_Rectangle(*this.Rectangle2)
  ; [ ...some code... ]
EndProcedure

Structure Methds_Rect2 Extends Methds_Rect1
  *Method2
EndStructure

Procedure Init_Mthds_Rect2(*Mthds.Mthds_Rect2)
  Init_Mthds_Rect1(*Mthds)
  *Mthds\Method2 = @Erase_Rectangle()
EndProcedure

Mthds_Rect2.Mthds_Rect2
Init_Mthds_Rect2(@Mthds_Rect2)
Constructor
Procedure Init_Mbers_Rect2(*Rect.Rectangle2 , x1.l, x2.l, y1.l, y2.l)
  Init_Mbers_Rect1(*Rect, x1, x2, y1, y2)
EndProcedure

Procedure New_Rect2(x1.l, x2.l, y1.l, y2.l)
  Shared Mthds_Rect2
  *Rect.Rectangle2 = AllocateMemory(SizeOf(Rectangle2))
  *Rect\Methods = @Mthds_Rect2
  Init_Mbers_Rect2(*Rect, x1, x2, y1, y2)
  ProcedureReturn *Rect
EndProcedure

In this example, Rectangle2’s Structure is empty, and it isn’t a problem. Here are two reasons why:

  • First, the *Methods pointer only needs to exist once, and this is in the Parent Class.

  • Second, no supplementary attributes have been added to it.

There are three advantages in having the methods’ initialization function external to the Constructor, and the method table in a single variable:

  • The Class’ method table needs to be initialized only once, and not at each object instanciation,

  • Object instances will hold a single pointer toward their methods’ pointers: it is a substantial gain in memory,

  • Since all objects referr to the same method table, this guarantees identical behavior for all objects of the same Class.

Get() and Set() Object Methods

Through an Interface, it’s only possible to access an object’s methods. The interface encapsulates completely the object’s attributes. In order to allow access the object’s attributes — to examine, or to modify them — we must provide specific methods to the user. The methods allowing to examine objects’ attributes are called Get() methods. The methods allowing to modify objects’ attributes are called Set() methods.

In our example of the Rectangle1 Class, if I want to examine the value of the var2 attribute, I should create the following Get() method:

Procedure Get_var2(*this.Rectangle1)
  ProcedureReturn *this\var2
EndProcedure

Similarly, to modify the value of the var2 attribute, I should write the following Set() method:

Procedure Set_var2(*this.Rectangle1, value)
  *this\var2 = value
EndProcedure

Since Get() and Set() methods exist only to allow the user to modify all (or some) of the object’s attributes, they necessarily belong to the Interface.

See the Appendix of the tutorial for possible optimizations of Get()’s and Set()’s performance during execution.

Synthesis and notation

Before presenting the final Class implementation, I’m going to spend some time summarizing in a formal notation the work made so far. Implementation of an object involved the following elements:

  • An Interface,

  • A Class (concrete/abstract) including methods definitions,

  • A Constructor provided with a routine for initializating attributes,

  • A Destructor.

The following table summarizes what is our object in PureBasic.

  • The word Class refers to the name of the Class (e.g.: Methd_Class)

  • The word Parent refers to the name of the Parent Class during inheritance (e.g.: Methd_ ParentClass)

  • Expressions between braces {…} are to be used during inheritance

Interface
Interface <Interface> {Extends <ParentInterface>}
  Method1()
  [Method2()]
  [Method3()]
  ...
EndInterface
Class
Structure <Class> {Extends <ParentClass>}
  *Methods
  [Attribute1]
  [Attribute2]
  ...
EndStructure

Procedure Method1(*this.Class, [arg1]...)
  ...
EndProcedure

Procedure Method2(*this.Class, [arg1]...)
  ...
EndProcedure
...

Structure <Mthds_Class> {Extends <Mthds_ParentClass>}
  *Method1
  *Method2
  ...
EndStructure

Procedure Init_Mthds_Class(*Mthds.Mthds_Class)
  {Init_Mthds_ParentClass(*Mthds)}
  *Mthds\Method1 = @Method1()
  *Mthds\Method2 = @Method2()
  ...
EndProcedure

Mthds_Class.Mthds_Class
Init_Mthds_Class(@Mthds_Class)
Constructor
Procedure Init_Mbers_Class(*this.Class, [var1]...)
  {Init_Mbers_ParentClass(*this)}
  [*this\Attibute1 = var1]
  ...
EndProcedure

Procedure New_Class([var1]...)
  Shared Mthds_Class
  *this.Class = AllocateMemory(SizeOf(Class))
  *this\Methods = @Mthds_Class
  Init_Mbers_Class(*this, [var1]...)
  ProcedureReturn *this
EndProcedure
Destructor
Procedure Free_Class(*this)
  FreeMemory(*this)
EndProcedure

First Code-Example

Here is an example file of inheritance in action:

PB Class

Now that we’ve explored OOP concepts and their possible implementations in PureBasic, it’s time to establish an implementation.

Here I shall present an implementation which I deem, to the best of my current knowledge, as the one best fitting OOP programming in PureBasic.

It is based on all the previously exposed work, as well as on my personal practical experience with the subject matter at hand. Another goal here is to simplify the use of object concepts, through clear commands and by automating operations as much as possible. During this step, macros are going to play a decisive role. Greatly facilitated by the Interface and Macro commands, the proposed implementation remains naturally limited by the language itself.

At first, I’ll present the instructions for a Class in PureBasic. Then I’ll analyze what hides behind by firing parallels with the previous pages. This chapter ends with a discussion about the choices made.

PureBasic Class

; Object class
Class(<ClassName>)
  [Method1()]
  [Method2()]
  [Method3()]
  ...
  Methods(<ClassName>)
    [<*Method1>]
    [<*Method2>]
    [<*Method3>]
    ...
  Members(<ClassName>)
    [<Attribute1>]
    [<Attribute2>]
    ...
EndClass(<ClassName>)

; Object methods (implementation)
Method(<ClassName>, Method1) [,<variable1 [= DefaultValue]>,...])
  ...
  [ProcedureReturn value]
EndMethod(<ClassName>, Method1)

; ...(ditto For each method)...

; Object constructor
New(<ClassName>)
  ...
EndNew

; Object destructor
Free(<ClassName>)
  ...
EndFree

As shown, a PureBasic Class revolves around four main themes:

  • Definition of the Class via the Class : EndClass block,

  • Implementation of the Class’ methods via the Method : EndMethod block,

  • Construction of the object via the New : EndNew block,

  • Destruction of the object via the Free : EndFree block.

Second Code-Example

Here is a header file cotaining the definition of this set of commands, along with a usage-example file (based on the previous inheritance example, so that you might compare them):

If you have already looked at the source code of the OOP.pbi file, you might have noticed that the code of the final OOP implementation is slightly more complex than its presentation in this tutorial. This is because some rearrangements were made in the code in order simplify its maintainance.

Let me guide you through the PureBasic Class declaration…

Class : EndClass

The Class : EndClass block allows declaring three types of constituent:

  • The object’s Interface, i.e: the only part of the object that the user can handle.

  • The object’s Methods — implementation excluded — which are reduced to pointers to the methods.

  • The object’s Members — methods excluded. Henceforth, the terms «member» and (more correctly) «attribute» will mainly refer to just these elements of an object (not including its methods, which are also members of the object, strictly speaking).

; Object class
Class(<ClassName>)
  [Method1()]
  [Method2()]
  [Method3()]
  ...
  Methods(<ClassName>)
    [<*Method1>]
    [<*Method2>]
    [<*Method3>]
    ...
  Members(<ClassName>)
    [<Attribute1>]
    [<Attribute2>]
    ...
EndClass(<ClassName>)

Each constituent is clearly identified with keywords: Class/Methods/Members. Their order must be preserved and all keywords must always be present, even when a method or a member will not be declared. Also, the name of a class is always a parameter of the keyword, and must be enclosed in parentheses.

The explanation for this is to be found in the definition of each keyword. Here is the code:

Class keyword

Macro Class(ClassName)
  ; Declare the class interface
  Interface ClassName#_
EndMacro

The keyword Class defines just the header of the Interface statement. The name of the interface is derived from the Class’ name followed by “_”. So, whatever follows Class will become the definition of the object’s interface.

Methods keyword

Macro Methods(ClassName)
  EndInterface
  ; Declare the method-table structure
  Structure Mthds_#ClassName
EndMacro

The keyword Methods starts by closing the interface’s definition with EndInterface. Then it opens the definition of the Structure which defines the pointers to the methods.

Members keyword

Macro Members(ClassName)
  EndStructure
  ; Create the method-table
  Mthds_#ClassName.Mthds_#ClassName
  ; Declare the members
  ; No parent class: implement pointers for the Methods and the instance
  Structure Mbrs_#ClassName
    *Methods
    *Instance.ClassName
EndMacro

The keyword Members is more complicated than the two previous ones.

It begins by closing the Structure definition previously opened by Methods. Then it declares the method table, using the freshly-built structure (as its type). For the moment this table is empty; it will be filled up a the end of Method : EndMethod statement. I’ll be discussing this further on (I can’t wait!).

Finally Members ends by opening the Structure declaration which defines the object’s members. In first position — as expected — we find the pointer to the method table (i.e.: to the variable just mentioned above). Its assignment will be done later, by the Constructor. Then follows another pointer, which will contain the address of the object itself. I shall explain later the reasons for this new member (no, now!).

After the Members keyword, the user has only to declare the other members of the object.

EndClass keyword

Macro EndClass(ClassName)
  EndStructure

  Structure ClassName
    StructureUnion
      *Md.ClassName#_     ; its methods
      *Mb.Mbrs_#ClassName ; its memebers
    EndStructureUnion
  EndStructure
EndMacro

The EndClass keyword code is at the origin of the implementation chosen for our object. So I’m now going to describe it correctly.

As with Methods and Members, it begins by closing what was opened by the previous keyword, in this case: the Structure describing the object’s members.

Then, follows a Structure named as the Class’ name, which will be use to instanciate the object.

This Structure is in fact the union of two elements:

  1. The first is a pointer typed by the interface which allows to call the object’s methods.

  2. The second is a pointer typed by the structure defining members. It helps accessing the object’s members.

This design puts into practice the optimizations for Get() and Set() methods presented in the Appendix. The benefit of this choice is twofold:

  • It provides a seamless approach for reaching an object’s methods and members.

    To reach a method, write:

    *Rect\Md\Draw()

    To reach an attribute, write:

    *Rect\Mb\var1
  • It prevents having to systematically declare an object’s Get() and Set() methods, when these are trivial. This saves time and it’s practical. At the same time, it reduces the number of objects’ methods (small optimization).

The price of this choice is that all members of an object are visible to the user.

This structure could be slighly retouched. Since terms like “Md» and “Mb» are visually very similar, a better distinction could be arranged. Although this choice was not retained, here is an interesting possibility:

Structure ClassName
  StructureUnion
    *Md.ClassName#_       ; methods
    *Get.Mbrs_#ClassName  ; used to read a member
    *Set.Mbrs_#ClassName  ; used to modify a member
  EndStructureUnion
EndStructure

In this code, the *Mb pointer was replaced by two pointers: *Get and *Set. They have the same functionality but they can lead to more legible code, by clarifying if an attribute is being read or modified.

Method : EndMethod

The Method : EndMethod block allows to achieve implementation of the various methods of an object.

; Object methods (implementation)
Method(<ClassName>, Method1) [,<variable1 [= DefaultValue]>,...])
  ...
  [ProcedureReturn value]
EndMethod(<ClassName>, Method1)

Each keyword occurence has the Class name and the method name as parameters.

Usewise, Method : EndMethod works like Procedure : EndProcedure — in fact it’s a wrapper of this block, as we shall see next.

Note the very special syntax of the method which requires two closing parentheses. This specificity ensues from the use of a macro combined with a different number of possible arguments for each method.

Method keyword

Macro Method(ClassName, Mthd)
  Procedure Mthd#_#ClassName(*this.Mbrs_#ClassName
EndMacro

The Method keyword is nothing more than a Procedure instruction for pre-declaring the variable *this, which is required as first argument.

The code omits the closing parenthesis so that the user might complete it by adding the parameters specific to its method. It’s the user’s responsibility to close this parenthesis, as shown in the syntax — and if he forgets, the compiler won’t fail to notice it!

EndMethod keyword

Macro EndMethod(ClassName, Mthd)
  EndProcedure
  ; Save the method’s address into the method-table
  Mthds_#ClassName\Mthd=@Mthd#_#ClassName()
EndMacro

The EndMethod keyword begins by closing the Procedure opened by the Method keyword. Once defined, the method can be referenced in the method table (declared by the Members keyword of the Class). Actually, by declaring a method, this method is automatically referenced.

Object Constructor

The New : EndNew block allows to instanciate a new object of the Class by declaring and initializing it.

; Object constructor
New(<ClassName>)
  ...
EndNew

The New keyword takes the class name as parameter.

New keyword

Macro New(ClassName)
  Declare Init_Mbers_#ClassName(*this, *input.Mbrs_#ClassName=0)

  Procedure.l New_#ClassName(*input.Mbrs_#ClassName =0)
    Shared Mthds_#ClassName
    ; Allocate the memory required for the object members
    *this.Mbrs_#ClassName = AllocateMemory(SizeOf(Mbrs_#ClassName))
    ; Attach the method-table to the object
    *this\Methods=@Mthds_#ClassName
    ; The object is created then initialized
    ; Create the object
    *this\Instance= AllocateMemory(SizeOf(ClassName))
    *this\Instance\Md = *this
    ; Now init members
    Init_Mbers_#ClassName(*this, *input)
    ProcedureReturn *this\Instance
  EndProcedure

  Init_Mbers(ClassName)
EndMacro

The New keyword is dense, but hasn’t really changed compared to the previous design.

The goal of this keyword is to create a new object and initialize it. These tasks are performed in the New_ClassName procedure, which is the main part of the New macro.

This procedure accepts a single argument, the one required by Init_Mbers for attributes initialization.

It begins by allocating the memory space required for the object’s members.

Then it attaches to the object the method table of the Class.

Next it instanciates the object by assigning an address to it and initializing the interface.

Then follows initialization of the object’s attributes via the Init_Mbers method.

Finally, New returns the object’s address.

The trick in the New macro is that it ends with the Init_Mbers keyword. This way, what the user has to add inside the New : EndNew block is simply the attributes initialization. More on that in a moment though (Show me now!).

This arrangement is made possible by declaring the Init_Mbers method first in the macro.

Notice how the New_ClassName procedure is common to all kind of Classes. It is because its variable part (and therefore object-specific) was externalized into the Init_Mbers method.

EndNew keyword

Macro EndNew
  EndInit_Mbers
EndMacro

The EndNew keyword is limited to calling the EndInit_Mbers keyword, which completes the attributes’ initialization declaration started at the end of the New macro.

Conclusion: the goal is reached. Through the New : EndNew block, we have created from the Class a new object with initialized methods and attributes.

In practical use, the New : EndNew block allows to initialize attibutes like this:

New(Rect1)
  *this\var1 = *input\var1
  *this\var2 = *input\var2
  ; [ ...some code... ]
EndNew

to instanciate such an object, write:

input.Mbrs_Rect1
input\var1 = 10
input\var2 = 20

; *Rect is a new object from Rect1 class
*Rect.Rect1 = New_Rect1(input)

Note that the constructor name is New followed by the Class’ name separated by “_”.

In relation to what was studied up to now, the object will always be a pointer. It isn’t an issue, rather it’s the consequence of our choice of grouping together access to methods and members (What?! I don’t remember!).

The choice of StructureUnion requires two different memory allocations: one for the members, and one to regroup methods and members (4 bytes here). This bivalence — which didn’t exist in the previous implementation — leads us to store information into the object itself. So, within the object’s methods you can access its members address through *this, and its instance’s address (method and members) through *this\Instance.

An important feature ensues: the us of *this\Instance to call the object’s methods within its methods (No, I’m not drunk!). This is the best way to do it, because it hides the name of the procedure behind the method, which is an essential part of the inheritance process.

For this purpose, a Mtd macro is present in the OOP.pbi file.

Init_Mbers : EndInit_Mbers private block

The Init_Mbers: EndInit_Mbers is a private block of the OOP implementation, used by the New : EndNew block to initialize an object’s attributes. Explaining this internal block is important for understanding how initialization of an object will be carried out.

; Attributes initialization
Init_Mbers(<ClassName>)
  ...
EndInit_Mbers

Between the two keywords are a series of member’s initialization. Note that only the Init_Mbers keyword requires the Class’ name as parameter.

Init_Mbers keyword
Macro Init_Mbers(ClassName)
  Method(ClassName, Init_Mbers), *input.Mbrs_#ClassName =0)
EndMacro

The Init_Mbers instruction is defined as a method of the object accepting a single argument.

In order to initialize the object according to the user’s wishes, and because the number of its members can’t be known in advance, it was chosen to pass information by reference.

This choice is reinforced by the bias that it’s the Constructor’s responsibility to initialize the object (by calling this particular method). Last but not least, this arrangement allows to automate the process of inheritance.

In practical use, members’ initialization will mostly look like this:

Init_Mbers(Rect1)
  *this\var1 = *input\var1
  *this\var2 = *input\var2
  ; [ ...some code... ]
EndInit_Mbers
EndInit_Mbers keyword
Macro EndInit_Mbers
  EndProcedure
EndMacro

The EndInit_Mbers keyword is nothing more than the EndProcedure keyword, which ends the definition of the object’s initialization method.

If you are the impatient sort, and have already peeked at the source code, you might have noticed that the final OOP implementation file includes extra optional parameters, named arg1 to arg5. This is because in some situations it is useful to complete the standard *input pointer by additional information.

Object destructor

The Free: EndFree block allows to destroy an object of the Class and to restore its memory.

; Object destructor
Free(<ClassName>)
 ...
EndFree

The Free keyword takes the Class’ name as parameter.

Free : EndFree block
Macro Free(ClassName)
  Procedure Free_#ClassName(*Instance.ClassName)
    If *Instance
EndMacro

Macro EndFree
      FreeMemory(*Instance\Md)
      FreeMemory(*Instance)
    EndIf
  EndProcedure
EndMacro

The Free : EndFree block is rather simple.

  • Free opens a Procedure with the object’s address as argument. We then check that the passed argument is not a Null address — nevertheless, it doesn’t guarantee that it’s a valid address for FreeMemory()!

  • EndFree releases the memory allocated to the object’s members, then that of the object itself — in that specific order.

In practical use, to free an object’s intance write:

Free_Rect1(*Rect)

As for the constructor, note that the destructor’s name is “Free” followed by the class’ name separated by “_”.

If your object consists of other objects — i.e.: that some objects are members of the current object, and they exist by (and for) this object (hic!) — it’s then important to free them too, by calling their destructors in-between the Free and EndFree keywords.

Even if PureBasic does automatically free the allocated memory areas, it will occur only when the programs ends. During programs execution, it is up to the user to take care of any garbage memory, especially its bloat.

Inheritance

In the set of commands just exposed, nothing makes reference to the process of inheritance. It is normal, because the current commands do not support it! (Damn! What an anguish!)! We need to introduce an additional set of commands to deal with the concept (Arghhh! Mega-anguish!).

Fortunately, it is not rocket science, and our design is ready for this (Phew! I’m feeling better now).

Here is what the Class looks like in this case:

; Object class
ClassEx(<ClassName>,<ParentClass>)
  [Method1()]
  [Method2()]
  [Method3()]
  ...
  MethodsEx(<ClassName>,<ParentClass>)
    [<*Method1>]
    [<*Method2>]
    [<*Method3>]
    ...
  MembersEx(<ClassName>,<ParentClass>)
    [<Attribute1>]
    [<Attribute2>]
    ...
EndClass(<ClassName>)

; Object methods (implementation)
Method(<ClassName>, Method1) [,<variable1 [= DefaultValue]>,...])
  ...
  [ProcedureReturn value]
EndMethod(<ClassName>, Method1)

; ...(ditto For each method)...

; Object constructor
NewEx(<ClassName>,<ParentClass>)
  ...
EndNew

; Object destructor
Free(<ClassName>)
  ...
EndFree

Four extra keywords are supplied as replacements for Class, Methods, Members and New keywords: ClassEx, MethodsEx, MembersEx and NewEx (respectively).

For each new keyword, in addition to the current class’ name, its parent class’ name is now a parameter being passed.

The operation is simple enough for the end user, making the process of inheritance easily accessible.

In order to save space, I won’t review here the code of the new keywords; but it might be a good idea to check out OOP.pbi in your IDE to get a feel of it.

Discussion

Phew! The presentation of a PureBasic Class is finished.

What else? Well, macros allow to define a real set of commands that:

  • Clarify the object’s structure,

  • Facilitate or automate some processes, like methods’ initialization or inheritance.

I list here the various design choices which drive the object’s conception. Let me remind you that it’s possible to partly re-adapt the design to customize objects, according to your own style, without fundamentally altering it:

  1. Use StructureUnion to define the object. It confers to the object the peculiarity to act on the members without requiring any mutator (setter/getter) method.

  2. The method table is class-specific and not object-specific:

    • It is initialized only once, at the beginning of program execution, rather than at objects’ instanciation time,

    • Objects’ instances store only a pointer to their method table: a substantial save of memory space,

    • All the objects point to the same method table, which guarantees identical behavior for all objects of the same class.

  3. A constructor which initializes the object by taking a single pointer as its input parameter, which store the initialization data of the object. The process of inheritance is largely facilitated. We can envisage to split the process in two steps: step one, the user create an object; step two, the user calls the initialization routine himself. In this case, the Init_Mbers method is no longer called by the New method, and therefore it might contain any number of arguments. Two disadvantages:

    • The risk of an incorrect initialization of the object: one can forget to do it, but — more important — it’s no longer possible to automate the inheritance process: it’s up to the user to manage it!

    • Strong class-interdependence of input parameters: as soon as the initialization method’s parameters of a parent class change, the user has to carry out this changes across all its children classes.

    In extreme — but it’s not advisable — we can imagine the user initializing all members, one after the other, by using mutators (setters). But members’ initialization doesn’t always boil down to mere assignment operations: it may involve more complex internal operations to reach its goal. If this is going to be repeated with each new object, it is strongly recommended to keep a dedicated method.

  4. A destructor consistent with the constructor. It is not part of the interface, although it possibly could be. In this case, to free an object write Objet\Md\Free() instead of writing Free_ClassName(object). This arrangement is easy to operate, and doesn’t alter the design of the object.

  5. I have not managed to automate the generation of the method table. It is important to remember why it was implemented with a Structure. Structures allow to create abstract classes — i.e.: classes whose methods are not implemented. It is a major notion of OOP’s concepts. Structures facilitate preserving the addresses’ order within the method table — whatever the implemented methods of the Class might be —, which in turn preserves the inheritance process! Using an array, a linked list, or a hash map as replacement for a Structure shall not provide this flexibility (at least I didn’t find such a solution).

Reminder of Types

Here is a list of the types used by a Class:

Type Applied to Origin

Mbrs_<ClassName>

Members structure

EndClass

<ClassName>

Object instance

EndClass

<ClassName>_

Interface

Class

Mthds_<ClassName>

Method Table

Methods

Mbrs_<ClassName>_

Members structure

Members

The Mbrs_<ClassName>_ type wasn’t presented in this paper. It is an intermediate step used to build the Mbrs_<ClassName> structure of the members definition. This arrangement is required to achieve the *this\Instance feature explained here.

Conclusion

You should have understood that, while it is indeed possible to implement Object-Oriented Programming in PureBasic, it require some rigor in coding. But once this task is dealt with, manipulation of objects becomes extremely simple.

However, while OO-Programming introduces greater flexibility in coding (through the application of object concepts), its structural organization uses lots of methods, which leads to bigger executables and some performance losses.

Nevertheless, I hope that this tutorial has at least allowed you to grasp the underlying mechanisms of the OOP paradigm, and to understand its concepts.

Appendix A: Optimisations

This section offers some considerations on how to improve runtime performance of our Object-oriented approach.

Optimisation: Get() and Set() Methods

Making frequent calls to Get() and Set() methods means lots of function calls, and therefore a loss in performance.

For those seeking performance, there are two possible ways to speedup the process: both consist in coupling a pointer to the object, but the second solution adds a layer to the first one.

First Solution:

The pointer is specified by the Class’ Structure.

So, for an object Rect of the Rectangle1 Class, write:

Rect.Rect = New_Rect()
*Rect.Rectangle1 = Rect

To act on the var2 attribute write:

*Rect\var2

It is then possible to both examine and modify it.

This is the simpler solution to implement.

Second solution:

The first solution requires that we work with two differently typed elements: Rect and *Rect. This second solution suggests grouping these two elements in a StructureUnion block.

Structure Rect_
  StructureUnion
    Mthd.Rect
    *Mbers.Rectangle1
  EndStructureUnion
EndStructure

Creating an object of Rectangle1 Class means declaring the object through this new Structure, by adapting its constructor like this:

New_Rect(@Rect.Rect_)

with,

Procedure New_Rect(*Instance.Rect_)
  *Rect = AllocateMemory(SizeOf(Rectangle2))
  Init_Rect1(*Rect)
  Init_Rect2(*Rect)
  *Instance\Mthd = *Rect
EndProcedure

To access to the Draw() method, write:

Rect\Mthd\Draw()

To access to the var2 attribute, write:

Rect\Mbers\var2

The advantage of this second solution is that there is just a single element that can be dealt with, like an object whose attributes are all accessible from outside of the class. It also preserves object-oriented notation, although it introduces an extra level in its fields.

The inconvenience lies in the fact that it introduces the necessity of maintaining a new structure within the Class.