vid, 2008-02-06 | Revision: 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, 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:
dd TYPE_MEDIKIT ;type of object dd 10 ;x coordinate = 10 dd 15 ;y coordinate = 15 dd 50 ;hit points = 50
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:
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
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.
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.
;esi = adress of object cmp dword [esi], TYPE_MEDIKIT je touched_medikit cmp dword [esi], TYPE_MINE je touched_mine
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:
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:
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"):
;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
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; }
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.
; 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
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);
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:
; 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]
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).
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
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.
; 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
// 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)
That's all for now. Depending on feedback, i may decide to write more about lowlevel approach to modern OOP programming.
Continue to discussion board.
You can contact the author using e-mail vid@x86asm.net.
Visit author's home page.
2008-02-06 | 1.0 | First public version | vid |
(dates format correspond to ISO 8601)