PureBasic and the Object-Oriented Programming

PB Class

Now we saw OOP concepts and their possible implementations in PureBasic, it's time to establish an implementation.

Here I present an implementation which seems to me, according to my current knowledge, the most adapted to the OOP by the way of PureBasic.

It is based on the whole work exposed previously but also of my own experience of the practice of the subject.
The other goal is to tend to simplify the use of object concepts, by clear commands and by automating operations as much as possible.
In this step, macros are going to play a decisive role.
Greatly facilitated by Interface and Macro commands, the proposed implementation remains naturally limited by the language itself.

At first, I'll present the instructions of a Class in PureBasic. Then I'll analyze what hides behind by firing parallels with the previous pages. This chapter ended on a discussion of 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, the PureBasic Class is articulated with four main subjects:

  • The definition of the class with Class : EndClass block,
  • The implementation of the methods of the class with Method: EndMethod block,
  • The construction of the object with New : EndNew block,
  • The destructor of the object with Free : EndFree block.

You will find here the file containing the declaration of this set of commands as well as an example file of use based on the inheritance example, what will allow you to compare with previous implementation:

OOP.pbi

OOP_Inheritance.pb

If you have already looked at the source code from the OOP.pbi file, you are going to notice that the final OOP implementation in the source is a little more complicated than the basic explanation from this acticle. This is because some arrangements are made to service update of the source.

Let me run you through the Purebasic Class declaration...

Class : EndClass
Class: EndClass block allows declaring three types of constituents:

  • The interface of the object, only part that the user can handle.
  • The methods of the object -except implementation- which are reduced to the pointers of the methods.
  • Members (except methods) of the object. Afterward, words 'member' and more correctly 'attribute' will often make reference to these only elements (and not to the methods which are also members of the object in the strict meaning).

;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. This order must be preserved and keywords always have to appear even though any method or member will not be declared. Also, at each time the name of the class is a parameter of the keyword.

The explanation takes place in the definition of every keyword. Here is the code:

Class keyword


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

The keyword Class declares just the header of the interface statement. The name of the interface comes from the class name merged with "_". So what follows Class will be the definition of the interface of the object.

Methods keyword

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

The keyword Methods begins by closing the definition of the interface with EndInterface. Then it opens the statement of the structure which defined the pointers of the methods.

Members keyword

Macro Members(ClassName)
EndStructure
; Create the methods table
Mthds_#ClassName.Mthds_#ClassName
; Declare the members
; No mother 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 definition of the structure first opened by Methods. Then it declares the table of the methods based on the structure freshly built. For the moment this table is empty and will be fill a the end of Method : EndMethod statement. I'll be discussing this farther (I can't wait).

Finally Members ends by opening the structure declaration which defined the members of the object. At first position -as expected- is the pointer to the table of the methods, i.e. to the variable just above. The assignment is done later by the constructor. Follows another pointer which will contain the address of the object itself. I shall explain later the reason of this new member (not! Now).

It remains for the user simply to declare the other members of the object next Members keyword.

EndClass keyword

Macro EndClass(ClassName)
EndStructure

Structure ClassName
StructureUnion
*Md.ClassName#_ ; les méthodes
*Mb.Mbrs_#ClassName ; les membres
EndStructureUnion
EndStructure
EndMacro

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

As for Methods and Members, it begins by closing what was opened by the previous keyword, here the structure describing the members of the object.

Then, follows the structure called with the name of the class and 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 methods of the object.
  2. The second is a pointer typed by the structure defining members. It helps in acting on the members of the object.

This design puts into practice optimization on the accessors exposed in appendix. The benefit of this choice is double:

  • It provides a same process to reach methods and members of an object.

    To reach a method, write:

*Rect\Md\Draw()

To reach a attribute, write:

*Rect\Mb\var1

  • It avoids to declare systematically Get and Set methods of an object when they are ordinary. That saves time and it's practical. At the same moment, that limits the numbers of methods of an object (small optimization).

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


This structure could be adapted a little. As terms ' Md ' and ' Mb ' are visually very close, a better distinguish could be studied. Although this choice was not retain, 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 menbre
EndStructureUnion
EndStructure

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

 

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

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

Each keyword has the class name and the method name as parameters.

In use, Method : EndMethod works like Procedure: EndProcedure. In fact it is a wrap of this block as exposed hereafter.

Note the very special syntax of the method which requires two closed brackets. This specificity comes 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

Method keyword is not more than a Procedure keyword where are pre-declared the variable *this required as first argument.

Code doesn't end by a bracket to allow the user to complete it by the specific parameters of its method. Up to him to close this bracket as shown in the syntax, otherwise the compiler will not miss to notify this!

EndMethod keyword

Macro EndMethod(ClassName, Mthd)
EndProcedure
; Save the method adress into the methods table
Mthds_#ClassName\Mthd=@Mthd#_#ClassName()
EndMacro

EndMethod keyword begins by closing the procedure opened by the Method keyword.
Once the method defined, it can be referenced into the table of methods (declared by the Members keyword of the class). Actually, by declaring a method, this method is automatically referenced.


Object Constructor
New : EndNew block allows to instanciate a new object of the class by declaring and initialising.

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

The New keyword has 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 methods table to the object
*this\Methods=@Mthds_#ClassName
;The object is created then initialised
;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

New keyword is dense but doesn't change really compare to previous design.

The goal of this keyword is to create a new object and to initialise it. These tasks are performed into 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 attributs initialisation.

It begins by allocated the memory space required for the members of the object.

Then it attaches to it the table of the methods of the class.

Next it performs the object instanciation by assigning an address to the object then by initializing the interface.

The initialization of the attributes of the object follows through Init_Mbers method.

Finally, New returns the address of the object.

The trick is that the New macro ends by the keyword Init_Mbers. Like this, what the user has to add inside the New : EndNew block is simply the attributs initialisation. More on that in a moment though (Give me now!).

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

Notice that the New_ClassName procedure is common to all kind of Class. It is because the variant part was externalized into Init_Mbers method.

EndNew keyword

Macro EndNew
EndInit_Mbers
EndMacro

EndNew keyword is limited to call the EndInit_Mbers keyword that end the statement of the attribut initialisation start at the end of the New macro.

 

Conclusion: the goal is reached. Through the New : EndNew block, we have a new object of the Class initialised (methods and attributs)

In use, the New : EndNew block allows the attibuts initialisation like this:

New(Rect1)
*this\var1 = *input\var1
*this\var2 = *input\var2
...
EndNew

to instanciate an such 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 name of the class separated by "_".

With regard to what was studied until now, the object will always be a pointer. It doesn’t matter but it's the consequence of the choice made in regrouping the access of the methods and the access of the members (What!? I don't remember!).

The choice of the StructureUnion requires two different memory allocations: the one for the members and the one to regroup methods and members (4 bytes here).
This bivalence -which didn't exist for the previous implementation- leads us to keep information into the object. So in the methods of the object, you could access to the address of members with *this and to the address of the instance (method and members) by *this\Instance.

It ensues an important feature by using *this\Instance to call the methods of the object within its methods (No I did not drink!). This feature is the best way to do this task because the name of the procedure behind the method isn't really known, what is needed in the inheritance process.

For that purpose, a macro Mtd is directly proposed in source file OOP.pbi.


 

Init_Mbers : EndInit_Mbers private block
Init_Mbers: EndInit_Mbers block is a private block of the OOP implementation, used by the New : EndNew block to initilalise attributs of an object. But it is important to present this internal block to understand how an object initialisation will by specified.

;Attributs initialisation
Init_Mbers(<ClassName>)
...
EndInit_Mbers

Between the two keywords are a series of member’s initialization.
Note that only 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

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

To initialize the object according to the wishes of the user and because the number of members couldn't be known in advance, the choice was made to pass information by referent.

This choice is reinforced by the prejudice that the constructor has the responsibility to initialize the object (by calling this particular method).
Finally, but not the least, this disposal allows to automate the process of inheritance.

In use, the initialization of members will look like mostly what follows:

Init_Mbers(Rect1)
*this\var1 = *input\var1
*this\var2 = *input\var2
...
EndInit_Mbers


EndInit_Mbers keyword

Macro EndInit_Mbers
EndProcedure
EndMacro

EndInit_Mbers keyword is not more and not less that the EndProcedure keyword which ends the statement of the method of the object initialization.

If you are the impatient sort and have already looked at the source code, you may have noticed that the final update OOP implementation source file include extra optional parameters called arg1 to arg5. This is because in some situation, it is useful to complet the standard *input pointer by additional information.

Object destructor
Free: EndFree block allows to destroy object of the class and to restore memory.

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

Free keyword has 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

Free:EndFree block is rather simple.

  • Free opens a procedure with the object address as argument. In passing we check that it is a none null address (nevertheless it not guaranteed a valid address for FreeMemory() ).
  • EndFree releases sequentially the memory assigned to members then that of the object.

In use, to free an object intance write:

Free_Rect1(*Rect)

As for the constructor, note that the destructor name is 'Free' followed by the name of the class separated by "_".

If your object consists of other objects, i.e. that objects are members of the current object and exist by this object (hic!) it is important to free them too by calling their destructors between keywords Free and EndFree.

Even if PureBasic 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 memory waste especially its growth.

 

Inheritance
In the set of commands just exposed, nothing makes reference to the inheritance process. It is normal because current commands do not support this! (Damn! Fear!)! We need to decline an additional set of commands to deal with the concept (Arghhh! <death pangs>).

Fortunately, it is not rocket science as our conception is ready for this (Phew! I feeling better now).

Here what looks like the class in that case:

;Object class
ClassEx(<ClassName>,<MotherClass>)
[Method1()]
[Method2()]
[Method3()]
...
MethodsEx(<ClassName>,<MotherClass>)
[<*Method1>]
[<*Method2>]
[<*Method3>]

...
MembersEx(<ClassName>,<MotherClass>)
[<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>,<MotherClass>)
...
EndNew

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

Four extra keywords are supplied: ClassEx, MethodsEx, MembersEx and NewEx as a replacement of Class, Methods, Members and New.

For each new keyword, in addition of the name of the current class, the name of the mother class is given as a new argument.

The operation is finally rather simple for the user with a very easy process for inheritance.

I won't list the declaration of the new keywords here to save space, but it might be a good idea to check out OOP.pbi in your IDE to get a feel for it.

Discussion
Phew! The presentation of a PureBasic Class is finished.

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

  • Clarified the object structure,
  • Facilitate or automate some processes, as the methods initialization or the inheritance.

I list here the various choices which drive the conception to the object. Let me you feel that it is possible to adapt some parts to suit one’s style to one’s object without fundamentally modifying it:

  1. Use of StructureUnion to define the object. It confers to the object the peculiarity to act on the members without using any accessor method.
  2. The table of methods is class-specific and not object-specific.
    • It is initialized once at the beginning of the program and not at the intanciate level of an object,
    • Objects instance store only a pointer towards the table of methods: substantial save of memory space,
    • All the objects point towards the same table of methods, which guarantees an identical behavior of the objects from the same class.
  3. A constructor which initializes the object, driving to use a single pointer as input parameter which store the initialisation 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 routine of initialization himself. In this case, method Init_Mbers is not called by New method and then it can support 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 not possible any more to automate the process of inheritance: it’s up to the user to manage it!
    • A strong class-interdependence for enter parameters: as soon as the parameters of the initialization method change from the mother class, the user has to proceed this modification in all the child classes.

    In extreme, but it’s not advisable, we can imagine that the user initializes member after member by using accessors. But a member initialization is not always a single assignation. It can require more complex internal operations to reach the assignation. Repeat this at each new object, it is advised to keep this into a dedicated method.

  4. A destructor consistent with the constructor. It is not a part of the interface although it is possible. In this case, to free an object write ' Objet\Md\Free () ' instead of writing ' Free_ClassName ( object) '. This disposal is easy to operate and doesn't modify the conception of the object.
  5. I didn't achieve the automation of the table of methods. It is important to remind why it is articulated with a structure. Structure allows to create abstract classes, that are classes where all the methods are not implemented. It is a major notion of the object concepts. Structure facilitates to preserve the addresses order into the table of methods what ever the implemented methods are while preserve the inheritance process! An array, a linkedlist or a hash map as a replacement of a structure shall not have this flexibility (at least I didn't find a such solution).

Reminder of types
Find here the list of the types which are used by a class:

Type Applied to Origin
<ClassName> Object instance EndClass
<ClassName>_ Interface Class
Mthds_<ClassName> Table of methods Methods
Mbrs_<ClassName>_ Members structure Members
Mbrs_<ClassName> Members structure EndClass

 

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

 

Contents

[1-2-3-4-5-6-7-8-9]
Top of the page