[Home]
[Edit this page]
[Recent Changes]
[Special Pages]
[Help]
AsmOOP
Note: if the object doesn't have any virtual functions then there is no need for a virtual table.
Note: There is only one thing that separates a C-struct from a C++ class. A class starts with a default protection level of private while a struct starts with a default protection level of public. I believe Eric Tetz may have stated that another restriction is you can't derive from a struct, but Borland C++ 5.5 with extensions disabled seems to have no problem with it. Maybe I'm missing part of what he said...
Here's the assembly structures that would be part of implementing that class.
Here's how you instantiate an object...
This would be followed by calling the appropriate constructor. I'll get to that in a second.
You can simplify this by having the default values of the STRUC's be the necessary parameters, but you may have to assemble in two passes to resolve forward references. Also a note on the above code. You'll only have ONE vtbl per CLASS (class as opposed to instance of a class), ie there will only be one variable of type vtblWrapX in this program no matter how many instances of someClass we make, they all just point to one vtable (sensible enough).
Note that I did not define m_X in the definition of Instance_of_WX, that should be left up to the constructor, of course rules were made to be broken.
That was C-style passing (minus cleaning up the stack). Basically I added the printX function (note: method is the proper term) simply so you could see how the parameters were passed. Basically the this pointer is a pointer to the object you are manipulating and is the first (and hidden in C++) parameter of a method. Static methods don't have a this pointer passed to them because they don't work on a specific instance of a class. I'm going to assume you understand what the static storage specifier means and am not going to explain further.
Treat the this pointer as you would any other parameter inside the function.
Next let's do a virtual function call.
Here you can see the difference between the different types of methods and you can see some of the costs involved with virtual methods. All virtual method calls require removing a level of indirection and an indirect function call.
Note: if the vtblptr that eax points to has a different function located at offset getX in it, then a different function will be called. This is the basis for virtual functions and method overriding. An optimization some compilers can do is they profile the program and replace virtual function calls with non-virtualfunction calls (to save two memory references). Basically, if the compiler can prove that only one type of object will be used for a method invokation then there is no need to determine the proper function at run-time, then inlining can be applied as well.
A quick restatement an extension of an earlier idea about the body of the methods. Basically, the first parameter (this) is a pointer to a structure. Just access the member variables through the this pointer as you would any pointer to a structure.
Note: that this class overrides setX, but not getX or printX. Here's the two structures that define this class.
From this you can see that how in a virtual call the non-overridden method getX will still call the parents getX, but the overridden setX will call the child's instead of the parents. Note: As you should know from C++ virtual function calls are only necessary for POINTERS to BASE classes. You can use a non-virtual call if you know the type guaranteedly. In which case you'd call wrapX_getX directly and pass a clampX object. Since the offset of m_X is at the same location, the method won't know the difference. If wrapX_getX happened to call setX then it would call the appropriate setX method via a virtual function call.
Note: since printX isn't a virtual function anywhere, you would just treat like a normal function that takes an extra parameter specifying the object. You can pass any wrapX object or any of its children to it and it will work correctly.
Here are the relevant ASM structures.
Ok, now some explanation. The member variables can be located anywhere as far as clampXY is concerned since it knows where its variables are. However, wrapX and wrapY expect their variables to be at certain locations. However, they both expect to have their variables at the same offset as the other. That's a problem. We can't have wrapX corrupting wrapY. So, we put them in two different spots. So as far as wrapX is concerned a clampXY object is a wrapX object too. But what about wrapY, Its member variable is not in the right spot? And what's the extra vtable for?
Well if you notice, the object is exactly a wrapY object at vtblptrWrapYDUP and beyond (as far as wrapY is concerned). So, the trick is to use different values for as the base of the clampXY object in different situations. Here's some examples of what I mean...
So you see what happens is we SHIFT the base of the instance to match what the parent class expects. Note there is a problem I have not handled yet. It has to do with the fact that the method may be for wrapY or may be for clampXY. The problem is: how does clampXY access its member variables after its base has been shifted (for example if setY used m_X)? Well, as I said earlier it doesn't matter where the member variables are in a clampXY function. clampXY knows where its member variables are so they can be located anywhere, including at negative offsets.
Basically what this means is that when you override a method whose member variables aren't aligned with the derived class', you will need to access the member variables of other classes using negative offsets or you'll need to reshift the this pointer back to proper alignment inside the function. Basically you need to write overridden functions to take exactly the same thing as the parent function will get. Calling wrapY_getY virtually through clampXY will give it...
so any method overriding getY will need to expect the data to come like that.
That's about it. Here are some quick (hopefully) things about construction and deconstruction.
There are no virtual constructors. There is little reason for there to be and it would be dangerous. There is little reason because all constructors of a class (ie all it's parents' constructors) are called. Also they are called from greatest ancestor down to the type be instantiated. That is because the child object may need certain things setup before it can setup itself. Obviously the parent object doesn't need the child object to setup anything for it to work correctly.
Deconstruction goes in the opposite order as would make sense. You can have virtual deconstructors. If you have a virtual deconstructor then if the child overrides it it is responsible for handling resources allocated by the parent.
Here's a note about pure virtual functions. Pure virtual functions are pretty much the same as an abstract class in Java. Basically they specify a template for a derived class (and enforce it). The same thing can be done in ASM simply by having a STRUCT that you never instantiate (and technically can't) that you derive classes off of like normal with the pure virtual functions overridden (as would be necessary or else you'll have an undefined function pointer which would be a VERY bad thing). This can't be enforced at assemble time (maybe with some interesting macros it could), but it will probably be pretty evident when the program runs and crashes and burns at a virtual function call to an undefined function.
You will need to call the constructors and deconstructors manually. So, I would recommend taking AsmGuru62's advice and developing macros and tools to handle that for you.
Anyways, this is a bit more complete.
"We can't do nothing and think someone else will make it right." -Kyoto Now, Bad Religion
[Edit this page] [Page history] [What links here] [Discuss this topic] [Printer Friendly]
AsmOOP
(Assembler) Object-Oriented programming in Assembler
This is a modified version of post made by Darius in "Re: How is C++ OOP done in ASM?" on on April 03, 2002 at 4:54:28 AMBasic concepts
- What is OOP ?
- Inheritance
- ?Polymorphism (virtual functions)
- C++ Links
- HLA has OOP support HLA
Implementation
First off, just to say it, public, protected, and private are compiler fantasies, as they are just to enforce data encapsulation and are not required, this isn't really a problem.Virtual Table
This is an array of pointers attached to an object. It holds pointers to functions that can be overrided by child classes.Note: if the object doesn't have any virtual functions then there is no need for a virtual table.
Note: There is only one thing that separates a C-struct from a C++ class. A class starts with a default protection level of private while a struct starts with a default protection level of public. I believe Eric Tetz may have stated that another restriction is you can't derive from a struct, but Borland C++ 5.5 with extensions disabled seems to have no problem with it. Maybe I'm missing part of what he said...
Implementing C++ class in ASM
Here's a C++ class that I will implement in ASM using TASM-syntax btw. This is so you can see how each part of it comes together.
class wrapX{
public: ;force of habit and propriety
;in other words the public/private aren't relevant
wrapX():m_X(0){}
wrapX(int x):m_X(x){}
virtual int getX(){return m_X;}
virtual void setX(int x){m_X=x;}
void printX(bool alignment, const char *prompt){ ... }
~wrapX(){}
private:
int m_X;
};
Here's the assembly structures that would be part of implementing that class.
wrapX STRUC
vtblptrWrapX dd ? ;or dw in real-mode
m_X dd ?
wrapX ENDS
vtblWrapX STRUC
getX dd ?
setX dd ?
vtblWrapX ENDS
Here's how you instantiate an object...
vtblWX vtblWrapX <OFFSET wrapX_getX, OFFSET wrapX_setX> Instance_of_WX wrapX <OFFSET vtblWX,?>
This would be followed by calling the appropriate constructor. I'll get to that in a second.
You can simplify this by having the default values of the STRUC's be the necessary parameters, but you may have to assemble in two passes to resolve forward references. Also a note on the above code. You'll only have ONE vtbl per CLASS (class as opposed to instance of a class), ie there will only be one variable of type vtblWrapX in this program no matter how many instances of someClass we make, they all just point to one vtable (sensible enough).
Note that I did not define m_X in the definition of Instance_of_WX, that should be left up to the constructor, of course rules were made to be broken.
Calling methods
Now here's a call to a non-virtual function of wrapX.
mov eax, OFFSET Instance_of_WX ;eax=ptr to Instance_of_WX
push OFFSET somePrompt
push FALSE
push eax ;pushing this parameter
call printX ;you'd probably want a naming convention for the functions
;ie wrapX_printX
That was C-style passing (minus cleaning up the stack). Basically I added the printX function (note: method is the proper term) simply so you could see how the parameters were passed. Basically the this pointer is a pointer to the object you are manipulating and is the first (and hidden in C++) parameter of a method. Static methods don't have a this pointer passed to them because they don't work on a specific instance of a class. I'm going to assume you understand what the static storage specifier means and am not going to explain further.
Treat the this pointer as you would any other parameter inside the function.
Next let's do a virtual function call.
mov eax, OFFSET Instance_of_WX push eax ;this mov eax, [eax] ;effectively mov eax, [eax.vtblptrWrapX] call [eax.vtblWrapX.getX]
Here you can see the difference between the different types of methods and you can see some of the costs involved with virtual methods. All virtual method calls require removing a level of indirection and an indirect function call.
Note: if the vtblptr that eax points to has a different function located at offset getX in it, then a different function will be called. This is the basis for virtual functions and method overriding. An optimization some compilers can do is they profile the program and replace virtual function calls with non-virtualfunction calls (to save two memory references). Basically, if the compiler can prove that only one type of object will be used for a method invokation then there is no need to determine the proper function at run-time, then inlining can be applied as well.
A quick restatement an extension of an earlier idea about the body of the methods. Basically, the first parameter (this) is a pointer to a structure. Just access the member variables through the this pointer as you would any pointer to a structure.
Inheritance
Now let's get to the point where any of this matters: inheritance.
class clampX:public wrapX{
public:
clampX():m_Lower(0),m_Upper(0xFFFFFFFF){}
clampX(int l, int u){ /*you can figure it out*/}
virtual void setX(int x){
if(x>m_Upper) m_X=m_Upper;
else if(x<m_Lower) m_X=m_Lower;
else m_X=x;
}
~clampX(){}
private:
int m_Lower,m_Upper;
};
Note: that this class overrides setX, but not getX or printX. Here's the two structures that define this class.
clampX STRUC
vtblptrClampX dd ?
m_X dd ? ;NOTE DUPLICATING wrapX member variable
m_Lower dd ? ;also note that it is at the same offset for both
m_Upper dd ?
clampX ENDS
vtblClampX STRUC
getX dd ? ;duplicated b/c it's virtual in wrapX
setX dd ?
vtblClampX ENDS
;and the definition of the clampX virtual table
vtblCX vtblClampX <OFFSET wrapX_getX, OFFSET clampX_setX>
;note the difference
From this you can see that how in a virtual call the non-overridden method getX will still call the parents getX, but the overridden setX will call the child's instead of the parents. Note: As you should know from C++ virtual function calls are only necessary for POINTERS to BASE classes. You can use a non-virtual call if you know the type guaranteedly. In which case you'd call wrapX_getX directly and pass a clampX object. Since the offset of m_X is at the same location, the method won't know the difference. If wrapX_getX happened to call setX then it would call the appropriate setX method via a virtual function call.
Note: since printX isn't a virtual function anywhere, you would just treat like a normal function that takes an extra parameter specifying the object. You can pass any wrapX object or any of its children to it and it will work correctly.
multiple inheritance
I'll define another base class and a new [[CppDerivedClass | derived class.
class wrapY{
public:
wrapY():m_Y(0.0){}
wrapY(double y):m_Y(y){}
virtual double getY(){return m_Y;}
virtual void setY(double y){m_Y=y;}
~wrapY(){}
private:
double m_Y;
};
class clampXY:public wrapX,public wrapY{
public:
clampXY()/* similar to above*/{}
// etc.
virtual setX(){/*similar to above*/}
virtual setY(){/*similar to above*/}
void swapXY(){
int tmp=m_X;
setX(m_Y);
setY(tmp);
}
private:
double m_dLower,m_dUpper;
int m_iLower,m_iUpper;
};
Here are the relevant ASM structures.
wrapY STRUC
vtblptrWrapY dd ?
m_Y dq ?
wrapY ENDS
vtblWrapY STRUC
getY dd OFFSET wrapY_getY
setY dd OFFSET wrapY_setY
vtblWrapY ENDS
;now the interesting part
clampXY STRUC
vtblptrClampXY dd ?
m_X dd ?
m_dLower dq ?
m_dUpper dq ?
m_iLower dd ?
m_iUpper dd ?
vtblptrWrapYDUP dd ?
m_Y dq ?
clampXY ENDS
vtblClampXY STRUC
getX dd OFFSET wrapX_getX
setX dd OFFSET clampXY_setX
getY dd OFFSET wrapY_getY
setY dd OFFSET clampXY_setY
vtblClampXY ENDS
;vtblptrWrapYDUP points at offset getY into vtblClampXY
;(or you could make a separate table)
Ok, now some explanation. The member variables can be located anywhere as far as clampXY is concerned since it knows where its variables are. However, wrapX and wrapY expect their variables to be at certain locations. However, they both expect to have their variables at the same offset as the other. That's a problem. We can't have wrapX corrupting wrapY. So, we put them in two different spots. So as far as wrapX is concerned a clampXY object is a wrapX object too. But what about wrapY, Its member variable is not in the right spot? And what's the extra vtable for?
Well if you notice, the object is exactly a wrapY object at vtblptrWrapYDUP and beyond (as far as wrapY is concerned). So, the trick is to use different values for as the base of the clampXY object in different situations. Here's some examples of what I mean...
;calling a clampXY method mov eax, OFFSET Instance_of_CXY push eax call clampXY_swapXY ;a virtual function call would be the same as earlier demonstrated ;no difference here ;calling a method that may or may not be wrapX's (it is in this case) mov eax, OFFSET Instance_of_CXY push eax mov eax, [eax] call [eax.vtblWrapX.getX] ;calling a method that may or may not be wrapY's (it is in this case) mov eax, OFFSET Instance_of_CXY.vtblptrWrapYDUP ;DIFFERENT push eax mov eax, [eax] call [eax.vtblWrapY.getY]
So you see what happens is we SHIFT the base of the instance to match what the parent class expects. Note there is a problem I have not handled yet. It has to do with the fact that the method may be for wrapY or may be for clampXY. The problem is: how does clampXY access its member variables after its base has been shifted (for example if setY used m_X)? Well, as I said earlier it doesn't matter where the member variables are in a clampXY function. clampXY knows where its member variables are so they can be located anywhere, including at negative offsets.
Basically what this means is that when you override a method whose member variables aren't aligned with the derived class', you will need to access the member variables of other classes using negative offsets or you'll need to reshift the this pointer back to proper alignment inside the function. Basically you need to write overridden functions to take exactly the same thing as the parent function will get. Calling wrapY_getY virtually through clampXY will give it...
vtblptrClampXY
m_X
m_dLower
m_dUpper
m_iLower
m_iUpper
0-->vtblptrWrapYDUP
m_Y
so any method overriding getY will need to expect the data to come like that.
That's about it. Here are some quick (hopefully) things about construction and deconstruction.
There are no virtual constructors. There is little reason for there to be and it would be dangerous. There is little reason because all constructors of a class (ie all it's parents' constructors) are called. Also they are called from greatest ancestor down to the type be instantiated. That is because the child object may need certain things setup before it can setup itself. Obviously the parent object doesn't need the child object to setup anything for it to work correctly.
Deconstruction goes in the opposite order as would make sense. You can have virtual deconstructors. If you have a virtual deconstructor then if the child overrides it it is responsible for handling resources allocated by the parent.
Here's a note about pure virtual functions. Pure virtual functions are pretty much the same as an abstract class in Java. Basically they specify a template for a derived class (and enforce it). The same thing can be done in ASM simply by having a STRUCT that you never instantiate (and technically can't) that you derive classes off of like normal with the pure virtual functions overridden (as would be necessary or else you'll have an undefined function pointer which would be a VERY bad thing). This can't be enforced at assemble time (maybe with some interesting macros it could), but it will probably be pretty evident when the program runs and crashes and burns at a virtual function call to an undefined function.
You will need to call the constructors and deconstructors manually. So, I would recommend taking AsmGuru62's advice and developing macros and tools to handle that for you.
Anyways, this is a bit more complete.
"We can't do nothing and think someone else will make it right." -Kyoto Now, Bad Religion
C++ links
[Edit this page] [Page history] [What links here] [Discuss this topic] [Printer Friendly]
