1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
|
This program attempts to show and describe the memory foot print of a COM object. One of the fundamental ideas of COM
is the seperation of interface from implementation. So when a COM object is instantiated there will be memory allo-
cations in multiple places. Specifically, there will be a memory allocation for the object itself, and in that memory
block will be found instance data variables that will maintain 'state' for an instantiation of an object. Also in that
memory block will be pointers to structures known as virtual function tables or interfaces. An object can support any
number of interfaces, and for each interface supported there will be what is known as a virtual function table pointer,
i.e., Vtbl*.
So, for an object that supports two interfaces (as in the example below) there will be a block of memory allocated for
the object itself, and then two seperate additional allocations for each of the two VTables or interfaces. If each
interface contains five functions/methods, in a 32 bit operating system, each interface will then require a 5 X 4 = 20
byte allocation to hold function pointers to the implementations of the interface functions. From this then you can
see that a virtual function table or interface can also be likened to an array of function pointers, with each member
of the array holding a pointer to a function. In actual practice though arrays are seldom used to construct virtual
function tables, because with present day compilers and IDEs type and parameter checking doesn't work as well as when
structs are used.
In a ceetain sense one could really say there are three categories of memory needing to be allocated for a COM object,
i.e., 1) the base allocation for the object itself; 2) the VTables or interfaces; and 3) the interface functions
themselves the addresses of which are stored in the VTables. However, the writer/creator of a COM object only needes
to concern him/herself with the first two; the compiler takes care of the third (that's why most of us program in a
high/mid level language as opposed to assembler or machine code).
So lets take a look at the code below. Its a very simplified version of a COM object whose only purpose is to elucidate
the memory foot print of a real COM object. To begin with, note the interface #define. An interface is just a specific
kind of struct. This program doesn't include ObjBase.h, so to use the interface keyword I had to include the define...
#define interface struct
which actually is in ObjBase.h. Secondly, all COM interfaces inherit a COM System defined interface known as IUnknown.
This is tremendously significant. Whenever you have a pointer to a COM object - no matter what it is, you have an
IUnknown*, and can query that object for other interfaces. You'll note that the IUnknown VTable contains three
functions, which in C++ syntax are described as pure virtual functions the real declarations of which are in Unknown.h.
And they actually resolve to function pointers themselves. If you've gotten this far it might have dawned on you that
COM memory layout isn't introductory material. You'll need some understanding of pointers, memory allocations, and
function pointers to grasp it. But if you are interested in this material and are having a hard time, please read on.
I am going to do my best to explain at least some part of it, and it is my hope that if you see the actual memory
'dumped' as I'm going to do in this example, you will begin to grasp what is going on, and see the actual elegance of
it!
There is another issue I might mention too. C++ in some ways obscures an understanding of what is really going on in
memory with a COM object. To really fully understand it and see its brutally naked structure you almost have to do
it in C, and I might just do that for you! But for now let's continue with C++, pure virtual functions, and the
works!
You see we have the IUnknown interface/struct which in a 32 bit operating system is going to create a memory block of
12 bytes, i.e., four bytes for each function pointer. Then we have an IX interface/struct and an IY interface/struct
each of which inherits IUnknown. What that means is that when a COM object is instantiated which supports interfaces
IX and IY, the virtual function tables for each of these interfaces is going to have to supply room for five function
pointers each - three for the IUnknown and two more for the supported functions Fx1(), Fx2(), or Fy1() and Fy2(). So
the VTable for each will look like this...
struct IX //Offset from base allocation of VTable allocation (double these numbers for x64)
{
QueryInterface(); // 0
AddRef(); // 4
Release(); // 8
Fx1(); // 12
Fx2(); // 16
}; // =======
// 20 bytes
or
struct IY //Offset from base allocation of VTable allocation (double these numbers for x64)
{
QueryInterface(); // 0
AddRef(); // 4
Release(); // 8
Fx1(); // 12
Fx2(); // 16
}; // =======
// 20 bytes
Finally, we have a class 'Class A', i.e., 'CA', that publically and multiply inherits interfaces IX and IY. Since we
hope to create an instance of Class A in this program we're going to have to implement all the pure virtual functions of
the interfaces, and we do that by simply providing cstdio puts or printf statements that they were called. Take a look
at class CA. Having done that we can now instantiate class CA in main and locate the class on the heap so as to obtain
a pointer to it, e.g., pCA...
CA* pCA=new CA;
Of course, we could call the IX and IY interface functions like this if we wanted to...
pCA->Fx1();
pCA->Fx2();
pCA->Fy1();
pCA->Fy2();
And if we did we'd get this output...
Called Fx1()
Called Fx2()
Called Fy1()
Called Fy2()
|