OOP From Low-level Perspective

vid, 2008-02-06Revision: 1.0

For many lowlevel programmers, object oriented programming (OOP) tends to be confusing and filled with abstractions. In this article I build up and explain some aspects of OOP from C or Assembly programmer's point of view.

Note that example codes are not always syntactically 100% okay - readability of examples is preffered.

What is OOP good for? Generally, it allows to access multiple similar types of objects with same interface. This would be best explained by practice, in rest of article.

First object

First, we need to understand what object is. Nice example where using OOP makes sense is game programming. Imagine we are developing computer game. In our game, there will be enemies, weapons, bonus items, flying missiles, etc. All these will be represented as "objects", eg. data structures in memory.

There can (of course) be more types of object. Type of object can for example be medikit (object that replenishes health). Of course, there can be more objects of same type in game. All objects of same type beheave in same way, but they can have different attributes (like coordinates on map, or amount of health restored). In OOP terminology, type of object is called "class". Objects of this type are called "instances" (of class). Attributes of instance (those which can be different in every instance) are calleed "data members" of object/instance.

In following examples, all our objects will start by number, which specifies type of object. Following the type are data members of object. For medikit, data members would be X and Y coordinates, and number of hit points that medikit restores. Declaration of medikit object at coordinates [10,15], that restores 50 hit points, would then be:

Asm:
dd TYPE_MEDIKIT		;type of object
dd 10			;x coordinate = 10
dd 15			;y coordinate = 15
dd 50			;hit points = 50
C:
struct MEDIKIT {
	int type;
	int x, y;
	int hp;
};
MEDIKIT mk = {TYPE_MEDIKIT, 10, 15, 50};

Now we will define another object: mine. Mine is opposite of medikit: after player touches mine, it explodes and decreases his health. Mine will too have coordinates, number of hit point damage it causes, but also another attribute (data member): time to explode since player touches it. Our object can look like this:

Asm:
dd TYPE_MINE		;type of object = mine
dd 10			;x coordinate = 10
dd 15			;y coordinate = 15
dd 5			;time to explode = 5 seconds
dd 50			;damage = 50
C:
struct MINE {
	int type;
	int x, y;
	int timeout;
	int hp;
}; 

When accessing object in our code, we will first read it's type from first dword. Depending on type, we know number and meaning of following data members.

Methods

Let's see how does program working with such objects look like. Specifically for our example objects, let's focus on part of code that handles contact of player with object (mine or medikit).

First, we have to determine type of object (eg. determine which class is the object instance of). If object is medikit, we restore player's energy, and if object is mine, we start countdown to explosion.

Asm:
;esi = adress of object
cmp	dword [esi], TYPE_MEDIKIT
je	touched_medikit
cmp	dword [esi], TYPE_MINE
je	touched_mine
C:
switch(obj->type) {
case TYPE_MEDIKIT:
	MEDIKIT *mk = (MEDIKIT*)obj;
	...
case TYPE_MINE:
	MINE *mn = (MINE*)obj;
	...

However, this approach has a fallback: Parts of code that work with particular class are scattered among entire program, grouped by event they handle (eg. player touching object). This way, if we want to change some aspect of object's behavior, we have to look up and fix all places in source code where we access object. We can easily miss some, and cause ugly bug.

Because of that, better approach is to group all code that works with particular class. For every class, we place code working with object into separate function. We will keep these functions in one place, grouped by object they work with. In our example, we will have function that handles medikit being touched by player, and another that handles mine being touched by player:

Asm:
cmp	dword [esi], TYPE_MEDIKIT
jne	not_medikit
push	esi
call	medikit_touched
jmp	done
not_medikit:

cmp	dword [esi], TYPE_MINE
jne	not_mine
push	esi
call	mine_touched
jmp	done
not_mine:
C:
switch(obj->type) {
case TYPE_MEDIKIT:
	medikit_touched((MEDIKIT*)obj);
	break;
case TYPE_MINE:
	mine_touched((MINE*)obj);
	break;

Such functions, which work with particular class, we call "methods".

Example of such method follows. Every method's first argument is pointer to object it works with (commonly called "this pointer"):

Asm:
;offsets of members within MEDIKIT structure
MEDIKIT.X	= 8
MEDIKIT.Y	= 12
MEDIKIT.HP	= 16
	
;when called
medikit_touched:
	mov	esi, [esp+4]		;get argument from stack
	mov	eax, [esi + MEDIKIT.HP]
	add	[player.hp], eax	;restore player's health
	mov	[esi + MEDIKIT.HP], 0	;medikit empty
	mov	[esi + MEDIKIT.X], -1	;move medikit out of map
	mov	[esi + MEDIKIT.Y], -1
	retn 4
C:
void medikit_touched((MEDIKIT*)this) {
	player.hp += this->hp;		// restore player's health
	this->hp = 0;			// medikit empty
	this->x = -1;			// move medikit out of map
	this->y = -1;
}

Virtual methods

Still this way isn't ideal one. When adding new class, we still have to add to many places code that checks type of object and calls appropriate method. There is much more elegant.

We can place pointer to method as data memeber of all objects that have this method. Then, when calling method, we don't need to check type of object, we'll just call function (method) pointed by that pointer.

Asm:
; medikit object 
dd TYPE_MEDIKIT		;object type = medikit
dd medikit_touched	;pointer to "touched" method
dd 10			;x coordinate = 10
dd 15			;y coordinate = 15
dd 50			;hit points = 50

; mine object
dd TYPE_MINE		;type of object = mine
dd mine_touched		;pointer to "touched" method
dd 10			;x coordinate = 10
dd 15			;y coordinate = 15
dd 5			;time to explode = 5 seconds
dd 50			;damage = 50

; call "touched" method of object pointed by ESI
push	esi		;address of object for which method is called
call	dword [esi+4]	;call pointed method
C:
struct MEDIKIT {
	int type;
	void (*touched)(OBJECT* this);	// pointer to "touched" method
	int x, y;
	int hp;
}; 
struct MINE {
	int type;
	void (*touched)(OBJECT* this);	// pointer to "touched" method
	int x, y;
	int timeout;
	int hp;
}; 
struct OBJECT {		//common part of all objects
	int type;
	void (*touched)(OBJECT* this);	// pointer to "touched" method
} *obj;

// call "touched" method of "obj" object
obj->touched(obj);

Virtual Table

Let's create another method. It will be method calle "shot", which will be called when player shots at the object. So far, our objects will be declared this way:

Asm:
; medikit object
dd TYPE_MEDIKIT
dd medikit_touched
dd medikit_shot		;we added pointer to "shot" method
dd 10
dd 15
dd 50

; mine object
dd TYPE_MINE
dd mine_touched
dd medikit_shot 	;we added pointer to "shot" method
dd 10
dd 15
dd 5
dd 50

; calling "touched" method of object pointed by ESI
push	esi	
call	dword [esi+4]

; calling "shot" method of object pointed by ESI
push	esi
call	dword [esi+8]
C:
struct MEDIKIT {
	int type;
	void (*touched)(OBJECT* this);
	void (*shot)(OBJECT* this);	// we added pointer to "shot" method
	int x, y;
	int hp;
}; 
struct MINE {
	int type;
	void (*touched)(OBJECT* this);	
	void (*shot)(OBJECT* this);	// we added pointer to "shot" method
	int x, y;
	int timeout;
	int hp;
}; 
struct OBJECT {		// common part
	int type;
	void (*touched)(OBJECT* this);		
	void (*shot)(OBJECT* this);	// pointer to "shot" method
} *obj;

// calling "touched" method of object "obj"
obj->touched(obj);

// calling "shot" method of object "obj"
obj->shot(obj);

For sake of completness, we can demonstrate examples of these methods. "medikit_shot" will reduce medikit hit points to half, and "mine_shot" will cause mine to immediately explode (by setting it's "timeout" to zero).

Asm:
medikit_shot:
	mov	esi, [esp+4]
	mov	eax, [esi + MEDIKIT.HP]
	shr	eax, 1
	mov	[esi + MEDIKIT.HP], eax
	retn	4
mine_shot:
	mov	esi, [esp+4]
	mov	dword [esi + MINE.TIMEOUT], 0
	retn	4
C:
void medikit_shot(MEDIKIT *this)
{
	this->hp = this->hp / 2;
}
void mine_shot(MINE *this)
{
	this->timeout = 0;
}

Here we can notice problem with this approach. Every object contains pointers to methods of its class. But for all instances of same class, these pointers are same. That means we waste memory with every object.

Solution is to define static table of pointer once for every class, and place pointer to it to every object. This table is called "virtual table". This way, objects need to hold only the pointer (besides it's regular data members). Not that this pointer is also unique for objects of every class, and so it can be used to determine type of object. That means "type" member is no longer needed too.

Asm:
; virtual table for MEDIKIT class
medikit_vtab:
	dd medikit_touched
	dd medikit_shot

	; instance of MEDIKIT class
mk:
	dd medikit_vtab		;pointer to virtual table
	dd 10			;x
	dd 15			;y
	dd 50			;hp

; virtual table for MINE class
mine_vtab:
	dd mine_touched
	dd mine_shot

;instance of MINE class
mn:
	dd mine_vtab		;pointer to virtual table
	dd 20			;x
	dd 20			;y
	dd 30			;timeout
	dd 50			;hp

;calling "touched" method for object pointed by ESI
push	esi			;argument for method
mov	eax, dword [esi]	;EAX = address of virtual tablle
call	dword [eax]		;EAX+0 = address of "touched" method within the virtual table

;calling "shot" method for object pointed by ESI
push	esi			;argument for method
mov	eax, dword [esi]	;EAX = address of virtual tablle
call	dword [eax+4]		;EAX+4 = address of "shot" method within the virtual table
C:
// declaration of common virtual table interface
struct VTAB {
	void (*touched)(OBJECT* this);
	void (*shot)(OBJECT* this);
};

// medikit
struct MEDIKIT {
	VTAB *vtab;
	int x,y;
	int hp;
};
VTAB medikit_vtab = {&medikit_touched, &medikit_shot};	// virtual table of MEDIKIT class
MEDIKIT mk1 = {&medikit_vtab, 10, 15, 50};
MEDIKIT mk2 = {&medikit_vtab, 20, 20, 10};

// mine
struct MINE {
	VTAB *vtab;	
	int x,y;
	int timeout;
	int hp;
};
VTAB mine_vtab = {&mine_touched, &mine_shot};	// virtual table of MINE class
MINE mn1 = {&mine_vtab, 10, 15, 30, 30};

// common part of all objects
struct OBJECT {		
	VTAB *vtab;	// uz iba vtab
};
OBJECT *obj;

// calling "touched" method for object pointed by "obj"
obj->vtab->touched(obj);

// calling "shot" method for object pointed by "obj"
obj->vtab->shot(obj);

Note: Usually most methods doesn't need to be virtual, because compiler can (if it is possible) call appropriate method directly. Virtual methods are useful along with inheritance. We haven't defined "inheritance" yet (even though we did use it, without knowing about it)

To be continued?

That's all for now. Depending on feedback, i may decide to write more about lowlevel approach to modern OOP programming.


Comments

Continue to discussion board.

You can contact the author using e-mail vid@x86asm.net.

Visit author's home page.


Revisions

2008-02-061.0First public versionvid

(dates format correspond to ISO 8601)