Introduction
Back in the late '80s, when I first started programming on Commodores famous Amiga PC, there was no alternative than using Assembler for optimizing your code to squeeze out any resources your hardware had. Although things have changed and compiler vendors have done a great job on code optimization, there are still some cases where you can do a better job than compilers do (presuming you know a lot about Assembler programming and your processor's architecture). By the way, if I talk about code optimizing I only mean optimizing the machine code for a given algorithm, not the algorithm itself. In most cases, it is more accurate to do optimizing on the algorithm. If you compare the computational complexity between the bubblesort (n2) and heapsort (n * log n) algorithms, you will see that code optimization of the bubblesort algorithm will not prevent the heapsort algorithm being faster for a definite n1 > n.
Because this article is not intended to be an introduction on code optimization, let us just assume that you have a piece of Assembler code in your C++ project (whether or not this is due to optimization purposes) and you want to invoke a member function of a given object within this Assembler code fragment. Due to the different concepts between the Assembler (procedural paradigm) and the C++ (object-oriented paradigm) programming language, I will first give you a brief overview of how C++ concepts like virtual function calls are implemented in Assembler. Afterwards, we will see how C++ member function pointers can be used to invoke member functions from Assembler code sections.
Calling non-virtual and virtual functions
Although the syntax between non-virtual and virtual function calls does not differ in C++, the Assembler code generated by the compiler differs a lot. The reason is that virtual function calls are dynamic calls. This means that the actual callee is determined during runtime. That's why this is also called late binding. Virtual functions are essential for the realization of polymorphism which is one of the key paradigms of object-oriented languages. Let's take a look at the following class hierarchy:
class ServiceA
{
public:
void sub(int a, int b) {
printf("ServiceA: %d - %d = %d\n", a, b, a-b);
}
virtual void add(int a, int b) {
printf("ServiceA: %d + %d = %d\n", a, b, a+b);
}
virtual void mul(int a, int b) {
printf("ServiceA: %d * %d = %d\n", a, b, a*b);
}
};
class ServiceB : public ServiceA
{
public:
void sub(int a, int b) {
printf("ServiceB: %d - %d = %d\n", a, b, a-b);
}
virtual void add(int a, int b) {
printf("ServiceB: %d + %d = %d\n", a, b, a+b);
}
virtual void mul(int a, int b) {
printf("ServiceB: %d * %d = %d\n", a, b, a*b);
}
};
The class ServiceA
declares two virtual functions add()
and mul()
which are overwritten by the subclass ServiceB
. When you call one of these operations with a pointer of type ServiceA
on an instance of type ServiceB
then the correct operation of class ServiceB
will be invoked. This behavior is exactly what we know as object-oriented polymorphism and differs from calls to non-virtual functions.
1 ServiceA serviceA; ServiceB serviceB;
2 ServiceA *pSA = &serviceB;
3
4 pSA->sub(20, 5);
5 pSA->add(10, 50);
6 pSA->mul(2, 2);
By scrutinizing the Assembler code in line 4 and 5, you can compare the differences between virtual and non-virtual function calls. Non-virtual function calls like the one in line 4 are handled during compile time. When the compiler gets line 4 as an input, it knows the type of the pointer and the type of the function. It determines the address of the non-virtual function sub()
and generates the Assembler statement for calling this function. The Assembler code for line 4 looks more or less like this:
push 5
push 20
mov ecx, pSA
call 0x40000
ServiceA::sub is located.
As we already know: virtual function calls are dynamic calls. This means that the address of the function is calculated during runtime. But what's the magic behind this? To be able to call the correct function depending on the object type, the compiler generates a specific function lookup table for virtual functions which is also called a vtable. Every object has a pointer to its vtable where the compiler stores function pointers to the correct functions. It is important that the offsets of the different virtual functions are the same. Only the function pointers differ from object type to object type. The tables below describe the structure and content of the vtable for different object types:
vtable of instances of type ServiceA Offset | C++ | function pointer |
---|
0x00 | add() | 0x40010 | 0x04 | mul() | 0x40070 | |
vtable of instances of type ServiceB Offset | C++ | function pointer |
---|
0x00 | add() | 0x40230 | 0x04 | mul() | 0x402C0 | |
By the way, you can significantly reduce the memory footprint of your application if you avoid using virtual functions on classes with a few bytes of memory usage from which lots of instances are created. Let's consider an example: an instance of a class might use 4 bytes of memory for its attributes. If the class has virtual functions the compiler will generate a vtable for that class (compiler optimization could prevent that in some circumstances, but that's not the deal). Because every instance of that class would have a pointer to this vtable, you would double the memory usage for each instance (I think some compilers only use 2 bytes as an offset to the vtable which would result in an increase of "just" 50%). Therefore consider to steer clear of virtual functions where possible.
Calling a virtual function instructs the compiler to generate code for looking up the function pointer in the vtable and to call this function. The Assembler code for line 5 looks more or less like this:
push 50
push 10
mov ecx, pSA
mov edx, [ecx]
call [edx]
The Assembler code for line 6 looks accordingly:
push 2
push 2
mov ecx, pSA
mov edx, [ecx]
call [edx + 4]
The operation mul()
is stored in the second position of the vtable. That's why the call takes an offset of 4 bytes (call [edx + 4]
).
thiscall calling convention
The default calling convention for calling member functions in C++ is called thiscall
. The characteristics about this calling convention is similar to the standard calling convention. This means, that arguments are passed from right to left on the stack. The implicit this
pointer is placed in ECX
. Finally, the stack is cleaned up by the called function which does return values in EAX
if needed. Thus, we first have to push 5, then 20 on the stack and load the pointer in ECX
; for C++, a statement like pSA->sub(20, 5)
(a good introduction on calling conventions is Calling Conventions Demystified on Code Project).
Using C++ member function pointers
The problem with the examples above is that you can't use them in your inline Assembler code fragments. In case of non-virtual function calls, you need to determine the address of the member functions. But you will fail to write Assembler code like the following example:
push 5
push 20
mov ecx, pSA
call &ServiceA::sub
In case of virtual function calls we saw that the compiler creates code for accessing the vtable of the instance. In our examples, the pointer to the vtable was located in the first 4 bytes. But this memory layout is not defined by the ANSI C++ specification. Therefore, you can't rely on that. Another big problem is, that you can not determine the position of a specific virtual function within the vtable. So it is better to use C++ member function pointers to invoke member functions from C++ classes within Assembler code fragments. If you are not familiar with member function pointers, the article "Member Function Pointers and the Fastest Possible C++ Delegates" by Don Clugston is a very good starting point for the topic. Even if you know what member function pointers are, there are some confusing aspects about them, so I recommend to read this article.
As Don Clugston pointed out, there is a diversity of implementation between different compilers when it comes to member function pointers. Therefore, talking about details on the level of Assembler statements will definitely lead us to wrong results. But to understand how member functions work and how we can use them in our inline Assembler code sections, we can make our assumptions about them.
Member function pointers do not just point to some member function. As we already know this won't work for virtual functions. They rather point to another function which is implicitly created by the compiler.
1 typedef void (ServiceA::*TypeAPtr)(int, int);
2
3 int main(int argc, char* argv[])
4 {
5 ServiceA serviceA; ServiceB serviceB;
6 ServiceA *pSA = &serviceB;
7 TypeAPtr _add = &ServiceA::add;
8 (pSA->*_add)(10,20);
9 }
I think the most curious part of this code fragment is line 7. According to the C++ syntax, this statement looks like: assign the address of the virtual member function called add()
of class ServiceA
to the member function pointer _add
. But as this function is a virtual one, you can not determine the exact member function without having an instance of ServiceA
or any subclass of it. It gets even more curious because some compilers even let you omit the &
operator. But omitting the &
operator is non-standard and you should avoid it.
The Assembler code for the assignment in line 7 is straightforward. It just copies the address of the implicit function which calls the virtual member function add()
on types of ServiceA
into the member function pointer. Together with the member function call from line 8, the Assembler code looks like this:
mov [_add], address of implicit function
mov ecx, pSA
call [_add]
This is exactly what we can use in our inline Assembler sections.
int main(int argc, char* argv[])
{
ServiceA serviceA; ServiceB serviceB;
ServiceA *pSA = &serviceB;
TypeAPtr _add = &ServiceA::add;
pSA->add(10,50);
pSA->mul(102,50);
pSA->sub(20,5);
(pSA->*_add)(10,20);
_asm
{
lea ecx, serviceB;
push 60;
push 40;
call _add;
}
return 0;
}
Conclusion
This article describes a way to call member functions from C++ classes within inline Assembler code sections. As far as I know this is the only way you can do this, despite the fact if it is useful or not. Hope, you can still use this.