ACTIVE - X
Ch 1: Rocking Through The Basics
Free your mind,
And the rest will follow
- En Vogue
From now on, whatever we shall be doing, form the basics of a very complicated technology known as Active-X. Before we enter into the realm of Active-X, we may feel the need to strengthen our basics. At any point in time, it is always a good idea to gain more knowledge about programming languages that we had learnt earlier, like C and C++. But in the case of Active-X, especially, if we fail to enhance our level of knowledge of these two languages, we shall definitely stumble and fall, at some stage or the other. In a number of cases, we shall have to give up old ideas, ideas which we may have picked up while learning C / C++, as well as how we go about programming, in general. We have one suggestion to make. Until you get the hang of one stage, do not go on to the next stage. In this entire tutorial, we ourselves, shall move only one step at a time. At any point in time, if we feel that it's necessary to take a break from the topic we are doing, to look back at small things which we may have overlooked while studying C / C++, we shall not hesitate to do so. Remember that finally, it's the basics that count. We shall start from the absolute basics and go on to touch the sky. So, put aside all your tensions, free your mind, and ride along with us. We won't waste much time and get down to tackling our first problem right away.
Let us assume that we want to call a function which is not present in our program. For example, we assume that we want to call function MessageBox( ). When our program goes through the compilation process, that is, when we compile and build our program, the compiler is not going to look at any function that it encounters in our program. The compiler will simply create an OBJ file and leave the job of handling functions to the linker. The linker will look into a list of default LIB files, to see if it can find the code of the required function. The default LIB files for the Microsoft Linker are libc.lib and oldnames.lib. A LIB file is collection of OBJ files. If the code of the function is not in any of these default LIB files, we have to explicitly tell the linker to look into a particular LIB file.
The LIB file, itself, does not contain the code of the function. If we look at a LIB file, we notice that it's size is quite small. This is because all that a LIB file contains, is a single line that tells us the name of the DLL in which the code of our function can be found. Under Windows, all code is stored in a DLL, that can normally be found in the directory WINDOWS\SYSTEM. The code of function MessageBox( ) is in user32.dll. This information is given to us by the LIB file user32.lib. If we compare the sizes of user32.lib and user32.dll, we shall see that the size of user32.dll is far greater than that of user32.lib.
Under Windows, the linker creates a file known as a PE file. This means that our program, after compiling and linking, will become a PE file. PE stands for
' Portable Executable '. All PE files follow a particular file format, with a header comprising of a number of sections, followed by the actual code of the file. In one of the sections of the PE file, the line in the LIB file telling us which DLL has the code of the required function, is placed. Our executable file will be loaded into memory by someone called the Windows Loader. It is the job of the loader to fill in the address of the function MessageBox( ) in the PE file.
The code of MessageBox is thus given to our program only at run-time. This means that if the file user32.dll was not present in windows\system for some reason or the other, we would get a run-time error. Only when the PE file is being executed, does Windows check to see whether the DLL is actually present, whether the required function is exportable from that DLL or not, and so on. This type of linking is known as static linking. In static linking, the PE file already knows the name of the DLL from which the code of the function will be picked up. The combination of the compiler and the linker, together, have decided this in advance. Another way of saying this, is that the name of the DLL, from where the code of a function is going to be obtained, is hardcoded into the executable file. Static linking is one way by which code of a function can be obtained from a DLL.
The alternative to static linking is called dynamic linking. But before we talk about dynamic linking, we should say something about what we mean by the word ' dynamic '. Dynamic essentially refers to something happening at run time, that is, when the file is being executed. Take the example of an array that is created globally. We are allocating memory locations, and the size of the EXE file increases. This is because this memory is added to the size of the EXE file. However, if a similar array is placed within a function, it gets created on the stack when the function is being executed. The size of the EXE file, as a result, does not increase.
Start me up
If you start me up
I'll never stop
- The Rolling Stones
Time to get to our first program. We shall work in a directory called ' get ' and we type the programs in the DOS editor. Here, the first new function that we have is called LoadLibrary( ). This function is given a string as it's single parameter. This string contains the name of a DLL that we are interested in. Here, h is a handle or a void *. By calling it a handle, we are saying that it's value doesn't make sense to us, but it's value gives Windows some information. The function LoadLibrary( ) returns a handle to the DLL whose name we have passed as a string to the function. For the rest of the program, wherever we see h, assume that it stands for the DLL in the function LoadLibrary( ), in this case, z1.dll. LoadLibrary( ) loads the DLL into memory.
The next function that we have to tackle is called GetProcAddress( ). That's a rather long name, but if we break this name up into three parts, it begins to make quite a lot of sense. The word ' proc ' or ' procedure ', refers to a function. If we combine the first and the last parts of the name, it says ' GetAddress '. Thus, we can infer that the name GetProcAddress refers to that fact that this function is used to obtain the address of a procedure or a function. But where do we search for this procedure ? Under Windows, the code of all procedures or functions are in DLLs, so we basically have to search for the address of a procedure in a DLL. In our program, we are searching for the address of a function called abc( ) in the DLL, z1.dll. The first parameter of GetProcAddress( ) is a handle to the DLL which contains the code of the required function. As the required function is in z1.dll, we have given h, which is a handle to this DLL, as the first parameter to GetProcAddress( ). The name of the function whose address we require, abc( ), is given as it's second parameter. The return value of the function GetProcAddress( ) is what is known as a FARPROC. A FARPROC is a pointer to a function that takes no parameters and returns an int. This return value is accepted by a variable f, which we have defined as a pointer to a function that has two ints as parameters. Therefore, in this program, f will always refer to the function abc( ) whose code is present in z1.dll.
Remember that a pointer to a function is of type ' ( *f)( ) '. In the prototype of f, we have defined it to be a pointer to a function with two ints as parameters. We may feel that one reason for creating f as a pointer to a function, is to enable it to accept the return value of the function GetProcAddress( ), which is also pointer to a function. But the real reason why we have a pointer to a function is that, if we initialise the pointer to the address of a function, we can call that function by saying ' f( ); '. We shall explain this concept in detail as we go along. We compile this program using the command ' cl -c get1.c '. Since this is Microsoft's compiler, we have to have Microsoft Visual C++ 5.0 installed on our HardDisk.
get1.c:
#include<windows.h> HANDLE h; void (*f)(int, int); int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h = LoadLibrary("z1.dll"); f = GetProcAddress(h,"abc"); }
When we run this program, we get the following error message.
get1.c(7) : error C2152: '=' : pointers to functions with different attributes
The return value of the function GetProcAddress( ), that is FARPROC, is actually a pointer to a function that follows the Standard Calling Convention. In our program, however, the variable f that accepts this return value, is a pointer to a function that follows the C Calling Convention. We therefore have to redefine f as a pointer to a function that follows the Standard Calling Convention as shown in the program below.
get1.c:
#include<windows.h> HANDLE h; void (_stdcall *f)(int, int); int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h = LoadLibrary("z1.dll"); f = GetProcAddress(h,"abc"); }
This program compiles without any problems and we get an OBJ file called get1.obj. To obtain an EXE file, we have to build this OBJ file using the ' link ' command as shown below. The three LIB files that we have specified give the linker the code of almost all the functions that we normally use.
link get1.obj user32.lib kernel32.lib gdi32.lib
This gives us no errors, but on execution, we get no output either because we really haven't done anything. We shall add a single line to this program. We had earlier stated that the combined effect of the function LoadLibrary( ) and GetProcAddress( ) make f into a pointer to the function abc( ). When we say f( 10, 20 ), it should be understood that we are calling function abc( ) with two parameters.
get1.c:
#include<windows.h> HANDLE h; void (_stdcall *f)(int, int); int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h = LoadLibrary("z1.dll"); f = GetProcAddress(h,"abc"); f( 10, 20 ); }
Once again, we get no compiler or linker errors, but when we try to execute this program, we get a General Protection error, telling us that this program has performed an illegal operation.
When we have the combination of the functions LoadLibrary( ) and GetProcAddress( ) in our program, we are performing dynamic linking of functions. In other words, there is no line of code in the EXE file that has information as to where the code of the function abc( ) is located as in the case of static linking. All that is present are the locations of functions LoadLibrary( ) and GetProcAddress( ). We can, at any time change the name of the function to be called or the DLL which it is called from, in the EXE file. There is no direct reference to function abc( ) in the EXE file. When this program is being executed, the Windows loader comes into the picture. The loader has been told that the code of function abc( ) is located in z1.dll. The loader has the job of obtaining the code of function abc( ) from this DLL. But, we don't have a DLL called z1.dll that contains the code of function abc( ). This is why we get an error.
We now write a program called z1.c, shown below, in the same directory. This program has a function called abc( ), but no main( ).
z1.c:
#include<windows.h> char aa[1000]; void abc( int i, int j) { sprintf( aa, "In abc i=%d...j=%d", i, j ); MessageBox( 0, aa, aa, 0); }
Compiling this program gives us no errors. When we build the resulting OBJ file, we wish to create a DLL. Furthermore, the function abc( ) has to be exportable, so that others can access it. To make function abc( ) exportable, we must have a DEF file as shown below. We write LIBRARY followed by the name of the dll that we want to create. After this, we write the term EXPORTS, and in the next line, press the ' Tab ' key and write the name of the function that we want others to use, namely abc.
z1.def:
LIBRARY z1 EXPORTS abc
Save this DEF file and come to the DOS prompt. We haven't, as yet, built the file z1.obj. By default, a linker will create an EXE file. Since we wish to create a DLL, we have to give the option ' /dll ' after the link command. Also, function abc( ) has to be exportable, for which we also have to give the name of the DEF file, as shown below.
link /dll z1.obj -def : z1.def user32.lib kernel32.lib gdi32.lib
At the end of the linking process, we see that we have three new files, a DLL, a LIB file and an EXP file.
We may now go back to the file we started out with, namely get1.c. We had earlier already compiled and linked this file. When we execute this file, we see a message box that says ' in abc i=10...j=20 '. However, when we click on the OK sign, we get a General Protection error. Click on Cancel. Well, if it's some consolation, at least we have got our program working to some extent. The function abc( ) has been called. Now, we have to find what went wrong.
The function GetProcAddress( ) returns a pointer to a function that follows the Standard Calling Convention. This is why we had to define the variable f as a pointer to a function that follows the Standard Calling Convention, as well. When we execute the line ' f( 10, 20 ); ', we are calling function abc( ). The code of this function is in a DLL which was created by compiling and linking a file called z1.c. If we take a look at file z1.c, however, we find that the function abc( ) follows the C Calling Convention. This is the reason why we got a General Protection error.
So we go back to file z1.c and make sure function abc( ) follows the Standard Calling Convention.
z1.c:
#include<windows.h> char aa[1000]; void _stdcall abc( int i, int j) { sprintf( aa, "In abc i=%d...j=%d", i, j ); MessageBox( 0, aa, aa, 0); }
Once again, we go through the entire process of compiling and linking this program to create a new z1.dll. Having done that, get1 executes without giving us any General Protection errors.
We now turn our attention to C++ files. We copy three files get1.c, z1.c and z1.def as get2.cpp, z2.c and z2.def respectively. We also make changes wherever appropriate. For example, in the DEF file, we change the first line to ' LIBRARY z2 ' and in the file get2.cpp, we say ' h = LoadLibrary("z2.dll"); '. We reproduce only get2.cpp here.
get2.cpp:
#include<windows.h> HANDLE h; void (_stdcall *f)(int, int); int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h = LoadLibrary("z2.dll"); f = GetProcAddress(h,"abc"); f( 10, 20 ); }
Take these broken wings,
And learn to fly again,
Like you did, so free
- Mr. Mister
All too often, in life as well as in this tutorial, we shall stumble and fall. It doesn't matter at all, as long as we don't get bogged down by small failures that come our way. No matter how small or large our failures are, nothing is insurmountable. Take the case when we compile the file get2.cpp. To our dismay, we realise that we get errors as shown below.
get2.cpp(7) : error C2440: '=' : cannot convert from 'int (__stdcall *)(void)' to 'void (__stdcall *)(int,int)'
This conversion requires a reinterpret_cast, a C-style cast or function-style cast
A brief revision of our basics is always useful if we wish find out why we get an error. Take the case of any function aaa( ). When we call the function aaa( ) by saying
' aaa(1, 2); ', aaa is the name of the function. But this name has great significance. The name of the function tells us where the function starts in memory. What we mean by this is the C and C++ compilers replaces the name of a function by a number that says where the function starts in memory. Suppose we had the following series of statements at various stages of our program, with the first being the prototype of a function, the second being an initialisation statement, while the third is a call to the function.
void interrupt ( *p )( ); p = 0xffff0000; p( );
When we say p( ), we are executing code that starts at ffff0000. Both C and C++ replace the name of the function by the address. The C++ compiler is, however, less forgiving than the C compiler. The return value of the function has to be the same as what is given in the function prototype. Also, a function cannot be called with parameters that are different from those given in the prototype of the function.
But let's get back to our program and the function GetProcAddress( ). The return value of the function GetProcAddress( ), as we said earlier, is a pointer to a function following the Standard Calling Convention that takes no parameters and returns an int. In our program, however, this return value is being accepted by f which is a pointer to a function that follows the Standard Calling Convention, accepts no parameters and returns a void. Because of the differences in return values, we shall get an error at line
' f =GetProcAddress(h,"abc"); '.
To solve this problem, we could define a pointer p that is exactly what the function GetProcAddress( ) returns, that is, a pointer to a function following the Standard Calling Convention that takes no parameters and returns an int. This is shown in the next program.
get2.cpp:
#include<windows.h> HANDLE h; int (_stdcall *p)( ); int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h = LoadLibrary("z2.dll"); p = GetProcAddress(h,"abc"); p(10,20); } get2.cpp(8) : error C2197: 'int (__stdcall *)(void)' : too many actual parameters get2.cpp(8) : fatal error C1903: unable to recover from previous error(s); stopping compilation
The line ' p = GetProcAddress(h,"abc"); ' gives no errors, and p now represents function abc( ). However, when we say ' p(10, 20); ', that is, when we try to call the function with two parameters, we get an error. This is to be expected, because in the prototype, we have said that p is a pointer to a function that takes no parameters, while we are passing it two parameters here. We bring our original pointer f back into the program. This pointer f is defined as a pointer to a function that is exactly like function abc( ). So all that we have to do is give the is initialise f to p and then execute ' f(10,20); '. The only problem is that we cannot say ' f = p; '. There is, after all, a rule in C++ that forbids us from attempting to equate two pointer variables of different types. However, C++ also provide us a means by which we may break this rule. The rule is bypassed using what is known as casting. In casting, we simply fool C++ into believing that both sides are equal. In our program, we shall cast to make the datatype of p equal to that of f for this one line only. What's on the right of the equal-to sign should be equal to what's it's left. Here, f is a pointer to a function that takes two ints as parameters. So, as a first step, we say that ' f = ( (*)(int, int))p; '. However, f is also a pointer to a function that follows the Standard Calling Convention and returns a void. So we have to modify the cast in the line as ' f = (void( _stdcall *)(int,int) )p; '. Now, we shall have no problems when we say ' f(10,20); '. Actually, if we look at the cast and at the prototype where we defined f, namely ' void (_stdcall *f)(int,int); ', we notice that if we remove the f and the semicolon, after which we place a pair of round brackets around the whole line, we get the required cast. The program shown below will display the required message box.
get2.cpp:
#include<windows.h> HANDLE h; int (_stdcall *p)(); void (_stdcall *f)(int,int); int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h=LoadLibrary("z2.dll"); p=GetProcAddress(h,"abc"); f=(void(_stdcall *)(int,int))p; f(10,20); }
Remember the words we wrote at the beginning of this tutorial? Yes, the words' Free your mind '. Before we understand the next program, we shall have a lot of that to do. Let's begin by enhancing our knowledge on Calling Conventions. If a function abc( ) follows the C Calling Convention, irrespective of the number of parameters that the function has, it's name in the OBJ file will always be _abc. However, if function abc( ) follows the Standard Calling Convention and is passed two parameters, it's name in the OBJ file will appear as _abc@8, that is, the name as well as information regarding the parameters. Under Windows, since variables of all datatypes occupy four memory locations, to find the number of parameters that a function is called with, we have to divide this number at the end of the function name by four. But what is the actual reason for having this number along with the function name, or not having it at all ?
Suppose we have a function abc( ) as shown below in a program called c.c written in the DOS editor and compiled by the MS Developer Studio compiler using the command ' cl -c c.c '.
main( ) { abc(1, 2); } abc(int i,int j) { printf("%d...%d",i,j); }
When we call function abc( ), internally, the following operation takes place. First, the second parameter, 2, will go on the stack, followed by the first parameter, 1. This program is compiled using the Windows C compiler, therefore each int occupies four memory locations on the stack. Thus the stack has moved down by eight memory locations. After this, the function abc( ) is executed. After the execution of the function abc( ), control is returned to main( ). It is the responsibility of main( ) to make sure that the position of the stack is restored to exactly where it originally was, that is, the position before function abc( ) was called. This is easily done by adding 8 to the value present in the stack pointer, thus moving it up by eight positions.
Now consider a similar program that follows the Standard Calling Convention.
_stdcall abc(int,int); main( ) { abc(1, 2); } _stdcall abc(int i, int j) { printf("%d...%d",i,j); }
As usual, the two parameters are first put on stack, after which the function is executed. Once again, the stack has moved down by eight. Since this function follows the Standard Calling Convention, the name in the OBJ file is _abc@8. Unlike the earlier case of the C Calling Convention, it is now the responsibility of function abc( ) itself to restore the stack. The last line of the function restores the stack to the proper position. Thus function abc( ) has to make sure that the stack pointer has moved up by eight locations. For functions following the Standard Calling Convention, main( ) is not going to restore the stack.
We cannot have the function defined in a prototype to follow the C Calling Convention when the function is being called from within main( ), while the function actually followed the Standard Calling Convention. If such a case was permissible, at the end of the execution of the function abc( ), the function abc( ) would move the stack up by eight, and when control transferred to main( ), main( ) would further move the stack up by eight. Thus the stack pointer would not point to the correct position and the stack would go completely haywire. Of course, since the code of function abc( ) is present in the program itself, we shall get a compilation error if the Calling Conventions differ.
We now have to take the case when the code of a function is not present in the same program, but has to be called from some DLL. A LIB file will inform the linker that it contains information as to which DLL contains the code of the function abc( ). The function name in the LIB file will be _abc or _abc@8, as the case may be.
Let us assume that we are linking our program dynamically. Now, a case might arise where a DLL has been created from a .C file in which function abc( ) followed the C Calling Convention. In our program, let's assume that we did not know this, and we made a call to function abc( ) that followed the Standard Calling Convention. This is precisely what we happened the first time we created z1.dll and called function abc( ) dynamically. In this case, the function abc( ) got executed, calling a message box, but when we clicked on the OK sign of the message box, we got a General Protection error. At this point, we have to ask ourselves one thing. How did the function abc( ) even get called in the first place? After all, the function name is _abc in one place and _abc@8 in another place.
We should be very clear with something. Once the function gets into a DLL, it goes in simply as the name of the function. Inside the DLL, we shall not find the name of the function as _abc or _abc@8. The concept of Calling Conventions is something that belongs to programming languages, while DLL's are absolutely programming language neutral. The fact is that in a DLL, there is no C or Standard Calling Convention, neither is there any name mangling. We shall find only ' abc ' in z1.dll. This is the reason why the string in the function GetProcAddress( ) is simply " abc " and not _abc@8 or something similar. Function abc( ) gets executed inspite of the fact that it follows the C Calling Convention, while the call to the function from our program get1.c is made using the Standard Calling Convention. These programs are reproduced here, for our convenience.
get1.c:
#include<windows.h> HANDLE h; void (_stdcall *f)(int, int); int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h = LoadLibrary("z1.dll"); f = GetProcAddress(h,"abc"); f( 10, 20 ); }
z1.c:
#include<windows.h> char aa[1000]; void abc( int i, int j) { sprintf( aa, "In abc i=%d...j=%d", i, j ); MessageBox( 0, aa, aa, 0); }
Let's look at why we got a General Protection error. If you had noticed, the code of the function abc( ) executed as we expected it to, displaying a message box. When the function abc( ) was called, the stack was moved down by eight locations. At the end of the execution of the function abc( ), WinMain( ) expects that the function abc( ) will restore the stack itself, because a call was made to the function abc( ) that followed the Standard Calling Convention. But since the function abc( ) actually follows the C Calling Convention, it is not going to restore the stack. The moment we click on the OK button of the message box, Windows realises that the stack pointer isn't pointing to where it should be pointing, or worse, it is pointing to a memory location that it is not allowed to access. This is why we got a General Protection error.
Let's sum up what we have learnt in the last two pages. The most important thing is that if the code of a function is written to follow a particular Calling Convention, when we call the function, we should call it with the same Calling Convention. This is because the stack has to be returned to where it originally was, irrespective of the Calling Convention used. Another thing that we have learnt is that when the name of a function is placed in a DLL, factors like the Calling Convention or any name mangling are not taken into consideration.
After that long diversion, we shall get back to our C++ program, get2.cpp. Let us assume that we know that when function abc( ) was written in a file called z2.c, it followed the C Calling Convention. All that we have is a DLL, z2.dll, from which we can call the code of this function. In such a case, we have to make sure that we call the function using the C Calling Convention only. As usual, we define p to be a pointer to a function that follows the Standard Calling Convention, takes no parameters and returns an int. The return value of the function GetProcAddress( ) is given to p. We then define f to be a pointer to a function that looks exactly like function abc( ). That is, f is a pointer to a function that follows the C Calling Convention, takes two ints as parameters and returns a void. We may then cast to equate f to p. Almost anything can be casted. Even the Calling Conventions can be temporarily changed for that one statement where the value of p is given to f. Now we can say f(10,20) and the function abc( ) will get called.
z2.c:
#include<windows.h> char aa[1000]; void abc( int i, int j) { sprintf( aa, "In abc i=%d...j=%d", i, j ); MessageBox( 0, aa, aa, 0); }
get2.cpp:
#include<windows.h> HANDLE h; int (_stdcall *p)(); void (*f)(int,int); int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h=LoadLibrary("z2.dll"); p=GetProcAddress(h,"abc"); f=(void(*)(int,int))p; f(10,20); }
In the file z2.c from which we created the DLL, the function abc( ) follows the C Calling Convention, while the call to function abc( ) from get2.cpp also follows the same Calling Convention. Therefore this program get2.cpp gives us no problems.
When we all learnt the C programming language for the first time, we were told that C understands structures. When we compile a C/C++ program that contains a structure, an OBJ file will be created that contains machine language code for everything in the file that is not a function. If we scrutinise this machine language code, we do not find a trace of any term that is related to a structure. This means only one thing. C does not understand structures. In fact, C does not even understand the concept of an array. Is this all too much to digest? As we mentioned before starting out, you have to free your mind, accept new ideas, get rid of the old, and the rest will follow.
As far as C is concerned, the name of an array merely represents an address in memory. Starting from this address, we shall have read byte by byte if we have an array
of chars, or two bytes at a time if it an array of shorts. Thus, an array is nothing but an area of memory starting from the address specified by the name of the array. Suppose we have to access the member numbered as five of an array bb of shorts. This, of course, is the sixth member of the array, as counting starts from 0. C will multiply five with the number of bytes occupied by an short, that is two, to get 10, and then add this number to the address specified by bb to get a new address location in memory. The required short occupies two memory locations starting from this address. While an array consists of variables of one type only starting from that memory location, a structure is more advanced. By saying that a similar area of memory is a structure, we are giving this area of memory more meaning. We may say that starting from a particular address, the first four bytes of memory have to be read together as it is a long, after which the next four bits have to be read in groups of two bytes each, as there are two shorts here, and so on. The reason why we use structures is that programming becomes simpler. But if we get down to Assembly level programming, there are absolutely no structures. All that we are trying to say, is that every member of either an array or a structure is just located at an offset from a certain memory location.
In the next program, we have a structure a that looks like zzz. The structure tag is made up of a long, two shorts and an array of eight chars. The reason why we have used 'short' instead of 'int' is that, under Windows, an int occupies four memory locations, while we want a datatype that occupies only two memory locations. Let us look at the number of locations that this structure occupies in memory. Four bytes are occupied by the long, another four by the two shorts, while eight bytes are occupied by the array of chars. Thus the size of the structure is sixteen. We shall be coming across similar 16 byte structures all through the time that we learn Active-X.
As in the earlier programs, we get the address of a function abc( ) whose code is in z3.dll using a combination of the functions, LoadLibrary( ) and GetProcAddress( ). These two functions are executed and f is given the value of p after adding the required casting, therefore f now stands for the function abc( ). This function is called with a single parameter that is a pointer to a structure that looks like zzz. It always preferable to pass the addresses of structures instead of passing the entire structure. If the structure occupies a hundred bytes, the entire hundred bytes go across if passed as a parameter. Unnecessary copies of the structure are created in memory. It is preferable to, instead, pass a pointer to the structure. We pass only four bytes across instead of a hundred. Also, since the same area of memory is accessed by both, the function and the main program, changes made by either one can be seen in the other. We have initialised a single member of the structure a, which is displayed by the function abc( ).
get3.cpp:
#include<windows.h> struct zzz { long a1; short b1; short c1; unsigned char d1[8]; }; HANDLE h; int (_stdcall *p)(); void (_stdcall *f)(struct zzz *); struct zzz a; int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { a.a1=100; h=LoadLibrary("z3.dll"); p=GetProcAddress(h,"abc"); f=(void(_stdcall *)(struct zzz *))p; f(&a); }
z3.c:
#include<windows.h> #include<stdio.h> char aa[1000]; struct zzz { long a1; short b1; short c1; unsigned char d1[8]; }; void _stdcall abc(struct zzz *a) { sprintf(aa,"In abc a->a1 = %d ",a->a1); MessageBox( 0, aa, aa, 0 ); }
z3.def:
LIBRARY z3 EXPORTS abc
To compile, link and execute these programs, we have to follow the following series of steps:
cl -c z3.c link /dll z3.obj -def : z3.def user32.lib gdi32.lib kernel32.lib cl -c get3.cpp link get3.obj user32.lib gdi32.lib kernel32.lib get3
We see a message box displaying the value of the member a1 of structure a, that we had initialised, on executing the program.
We now make a small modification to the prototype of f in the program get3.cpp. This is written as 'void (_stdcall *f)(struct zzz &); '. When we call function abc( ) passing the structure a as a parameter by saying f( a ), the address of the structure will automatically be passed, and not the entire structure itself. We do not have to write '&a' as the parameter. So, when we see ' & ' in a prototype, we have to pretend that we haven't seen it at all when it comes to passing parameters to the function. The function will receive only an address of the structure. This is called passing parameters by reference.
Keeping the DLL the same, that is, the DLL created by compiling and linking z3.c, the execution of the program shown below, get3.cpp, is the same as that of the previous program.
get3.cpp:
#include<windows.h> struct zzz { long a1; short b1; short c1; unsigned char d1[8]; }; HANDLE h; int (_stdcall *p)( ); void (_stdcall *f)(struct zzz &); struct zzz a; int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { a.a1=100; h=LoadLibrary("z3.dll"); p=GetProcAddress( h,"abc" ); f=( void(_stdcall *)(struct zzz &) )p; f(a); }
Now consider the case where the DLL is created from a C++ file, z3.cpp. We have to use an ' & ' in the parameter, as the function abc should have the same format as the prototype of f in the program, get3.cpp. Once again, the structure is passed by reference, that is, only a pointer goes across. While the function accepts a pointer to an structure, the code within the function is written as if we are passing the entire structure, and not the pointer to the structure. Therefore, to access the member a1 of structure a, we do not write a->a1, but instead, we write a.a1.
z3.cpp:
#include<windows.h> #include<stdio.h> char aa[1000]; struct zzz { long a1; short b1; short c1; unsigned char d1[8]; }; void _stdcall abc(struct zzz &a) { sprintf(aa,"In abc a.a1 = %d ",a.a1); MessageBox( 0, aa, aa, 0 ); }
Once again we see our message box, when we execute get3.cpp, without any problems.
A whole new world
With new horizons to pursue
I'll chase them everywhere
There's time to spare
Let me share this whole new world with you
- Peabo Bryson and Regina Belle
Soundtrack of the movie 'Alladin'
A few years ago, around 1994-95, the Internet suddenly became the ' in thing '. No longer, was it seen as purely a place where geeks, like yours truly, spent their entire nights and days. Any firm who unveiled products for the Internet saw their stock prices shoot sky high. Beneficiaries of this phenomenon were pretty diverse firms such as upstarts like Netscape with their browser called the Navigator, and an established Engineering Workstation manufacturer like Sun Microsystems, with their Java programming language, touted as the language of the Internet. But where was Microsoft?
Microsoft suddenly realised that it didn't have any products for the Internet. It was some time before they managed to release anything at all. Then one day, Microsoft released Active-X, and announced that all it products would be developed using this technology. The fight back had begun. The Internet Explorer, versions 3 and 4, written using this technology, were unveiled. The year is 1998, and Netscape has just announced that it has posted a loss for the last quarter. On the other hand, Microsoft, inspite of it's late start, has grabbed a forty percent market share of the browser market! So what do we do? Learn Active-X!
In Active-X, all the programs that we may write are either a client or a server. One problem with Active-X is that there are too many complicated names floating around. Let's start with a server. A server is also known as an Object Application, Ole Custom Control, OCX, Active-X Object, COM Object, or simple as an Object. The server is responsible for doing something for us based on a request that we make to it. The client is the one that gets the output of the request and displays it for us. Clients are also known as Containers. Remember that Active-X is not a technology solely for the Internet. In fact to learn Active-X, we shall build a client and a server on our own machine.
Before we go ahead, we should know something about the origin of the terms Client and Server. In the early days of computing, a single program would be used to perform all tasks. For example, in a database application, the part that searched for and delivered the data which we requested, as well as the part that displayed this data for us, were all in the same program. Someone thought that it made sense to decouple these two functions, or separate them into two programs. Let the people who specialised in writing databases stick to what they were good at, while the job of displaying the data using proper user-interfaces, in other words, a front-end, would be handled by other specialists. Since there were two programs involved which closely worked together, both of them would have to be fully compatible with each other. By this, we mean that both these applications would follow a certain set of rules.
To a large extent, Active-X follows the same basic concept of having two separate programs, a client and a server. In fact, Active-X is not really a programming language, but is, to a large extent, simply a set of rules that will allow a client to talk to a server. If we look at documentation provided by Microsoft, nowhere is anything mentioned about the language in which these rules of Active-X should be implemented. However, for reasons best explained later on, we shall use only C++ to create our Active-X server and client.
Microsoft has given us a calendar along with Windows95. This calendar is on a file on our hard disk called mscal.ocx. An ocx is, in fact, nothing but a DLL with a different file extension. The file extension should always tell you more about the file. If all files were DLLs, we would have problems distinguishing a dll from an Active-X object, so we give it the extension .ocx. This file, mscal.ocx, which is in the windows/system directory, is going to be the server for the first Active-X application that we are going to write, which is a Client. Thus our client will have to import and display a calendar for us.
According to the rules of Active-X, if anyone claims to be an Active-X server, he must have an exportable function called DllGetClassObject( ). This function, DllGetClassObject( ), will be the first function to be called by a client. This already gives us an idea as to what the first step of our client will have to be. We have to get a handle to the required DLL using the function LoadLibrary( ). We then give this handle as a parameter to the function GetProcAddress( ) to get a pointer to the function DllGetClassObject( ).
After we have performed the usual set of steps, f will represent a pointer to the function DllGetClassObject( ). Therefore, if we say ' f( ); ', function DllGetClassObject( ) will get called. However, function DllGetClassObject( ) has to be called with three parameters. The prototype of f is
'long (_stdcall *f)(struct zzz *, struct zzz *, IClassFactory **);
The first parameter of DllGetClassObject( ) has to be a number that has been assigned to the server that we want to contact. We have already said that the server we wish to contact is called mscal.ocx. Then why should this server also be known by a number? We should realise that if Microsoft named it's calendar as mscal.ocx, anyone can also name their server as mscal.ocx in another subdirectory. They may not know that on their machine, a file called mscal.ocx already exists. If Windows has to search for anything, it looks into a hidden file called the registry. To prevent it from getting confused by names, it looks mainly at numbers that are assigned to different servers. We can draw comparisons with the case of how connections are made to different sites on the Internet. In our browser, when we type ' www.microsoft.com ', this name is internally replaced by a long number. Similarly, every ocx or server is also assigned a number by which it is identified. We know that we have to give the number that has been assigned to mscal.ocx by Microsoft, as the first parameter of DllGetClassObject( ), or in the case of our program, as the first parameter of f( ). Now, a small problem arises. How do we find this number that has been assigned to mscal.ocx. Since Windows searches for everything in the registry, this is the place where we shall do the same.
Click on ' Start ' at the bottom the screen and select ' Run...'. In the edit box, type in the term 'regedit ' and click on OK. A new screen titled Registry Editor, which is divided into two parts, unfolds before our eyes. We click on ' Edit ' on the toolbar, and select ' Find '. Here, we type in ' mscal.ocx ' and click on ' Find Next '. We see a message box with the title ' Find ' saying ' searching the registry '.
And I can't explain,
But there's something about the way you look tonight
Takes my breath away,
It's that feeling I get about you deep inside
- Elton John in ‘Something About The Way...’
All of a sudden the left half of our screen fills up with numbers, numbers, and more numbers. One thing about these numbers strikes us. They are so big! We don't know about you, but in general, we tend to forget seven-digit phone numbers. And these numbers are a lot larger than seven digits. Is it possible to actually remember any of them? We have no intention of even trying. Before each of these numbers, we see a plus sign. That is, before all number except the last number on our screen, which has a minus sign in front of it. Also, there are a few terms written below this last number like ' Control ', ' InprocServer32 ' and so on. If we look at the first line of the right half of the screen, it says ' c:\windows\system\mscal.ocx '. This is precisely what we were looking for. Also, the lowermost of the series of the numbers, the one with the minus sign in front of it, is the number that has been assigned to mscal.ocx by Microsoft. If we click on this number, we see ' Calendar Control 8.0 ' on the right of the screen. This number is shown below.
{ 8E27C92B - 1264 - 101C - 8A2F - 040224009C02 }
If we count the digits of this number, we find that it is16 bytes or 128 bits in length. Sixteen bytes? Isn't that a very familiar number ? If we look at our last program, sixteen bytes is precisely the length of the structure tag zzz we had in that program. We shall reproduce this structure tag here.
struct zzz { long a1; short b1; short c1; unsigned char d1[8]; };
Microsoft faced a problem. It had to give every Active-X application a number that would uniquely identify that application. The largest datatype that was available in the C programming language, was a long which was four bytes in length. Using a long, we could have upto four billion combinations of numbers. Microsoft had been writing software for years, and realised the pitfalls of some of it's earlier decisions, the hard way. For example, when DOS was first designed, it could access 640 K of memory. At that time, half the applications available on the entire planet could fit into 100 K. So they decided that this much memory was more than adequate. In less than a year's time, they realised just how inadequate 640 K of memory was. Many other such mistakes followed, but Microsoft slowly wisened up to the way software was written. The people at Microsoft made a policy decision. All software should be written to have a life-span of at least a hundred and twenty-five years. Since only four billion combinations were possible with a long number, Microsoft decided against using a long to uniquely identify an Active-X application. After all, there were already over five billion people on this planet, and if each person created just a single application in Active-X, all the different combinations available to a long number would be exhausted in no time at all. Some research towards solving this problem had been done by a firm called Digital Corporation. The engineers at Digital decided that the only way that a number could be absolutely unique, is when it is a 16 byte or 128 bit number. Microsoft, a company famous for buying up ideas from other companies, bought this idea along with some other technology, from Digital. It was decided that every Active-X application would also be assigned a sixteen byte number. This number would be generated by another program called GUIDGEN that would assure it's uniqueness over space and time. In other words, if two machines tried to generate a sixteen byte number using this program, the number generated was guaranteed to be different. There was, however, no way that such a large number would fit into any data type in C. It was therefore decided that the number would be stored in a structure precisely like the one shown above.
Now that was a long diversion from the fact that we still have to give
DllGetClassObject( ) a sixteen byte number that has been assigned to mscal. Actually, we have to initialise a structure that looks like zzz to this number. This is done as shown below.
struct zzz a={0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00, 0x9C, 0x02 } };
Look carefully at the statement shown above. The first number of structure a that we have initialised, is a long, followed by the two shorts. After this, within an additional pair of brackets, we have initialised the array of eight chars. We then pass the address of this structure as the first parameter to DllGetClassObject( ).
Having taken care of the first parameter, we now turn our attention to the next two parameters of DllGetClassObject( ). Once again we have to look at what the rules state, and follow them blindly. According to these rules, we have to ask the server for a pointer to a particular class. This class is not just any class, but a special kind of class known as an interface. All the functions that an interface contains, are virtual functions equated to zero. More on that later on. All these interfaces in Active-X have a name that starts with the letter ' I '. The interface that we want from the server is called IClassFactory. Once again, the problem arises that anyone can name his class as IClassFactory. So Microsoft provided all interfaces in Active-X with a sixteen byte number to uniquely identify them, as well. How do we find out the number that was given to IClassFactory. Once again, we search in the registry as explained earlier. We find that this number is 00000001-0000-0000-C000-000000000046. We initialise a structure b that looks like zzz to this number. This structure b is then passed by reference as the second parameter to function DllGetClassFactory( ). In the prototype of the function DllGetClassFactory( ), the third parameter is a pointer to a pointer to the interface, IClassFactory. Suppose we write ' IClassFactory *s; ' as a parameter, we are saying that s is a pointer to a class called IClassFactory. But suppose we write the parameter of the function as ' IClassFactory **s; ', we are saying that we have created a pointer to a class and stored the address of this pointer in s.
We have defined c to be a pointer to the interface IClassFactory. Since it has been defined as a global variable, the value of c will be zero before we call function DllGetClassObject( ). Since we have written the third parameter as &c, it means that we have put the address of c on the stack so that it’s value can be changed. After this function has been executed, this variable will have the address of an IClassFactory pointer. If you haven't realised it, all that we have done in the last couple of pages, is explain the parameters of a single function, DllGetClassObject( ). Having explained the parameters of the function DllGetClassObject( ), we see that there actually is some sense in the name of the function. The words in the middle of the name, ie. 'GetClass', refer to the fact that our client wants a pointer to a particular class from the server or 'Object' which happens to be a 'Dll'.
Before we actually execute the program, once again, we take a look at what we expect the program to do for us. After the execution of the functions LoadLibrary( ) and GetProcAddress( ), p is a pointer to the function DllGetClassObject( ) in mscal.ocx. Since we equate p to f with proper casting, f is now a pointer to DllGetClassObject( ). We may call this function by saying ' f(&a, &b, &c); ', where a, b, c are the three parameters that we are passing to DllGetClassObject( ). The first parameter represents the server that our client is calling. The second parameter is the tells the server the name of the class that we want from it. It the execution of the function is successful, the server will fill up the third parameter with a pointer to this particular class. Also, if the execution of the function is successful, the function should return 0. This return value is accepted by h1 and then stored into array aa, which is displayed in a message box.
get4.cpp:
#include<windows.h> #include<stdio.h> struct zzz { long a1; short b1; short c1; unsigned char d1[8]; }; int (_stdcall *p)( ); long (_stdcall *f)(struct zzz *, struct zzz *, IClassFactory **); HANDLE h; long h1; struct zzz a={0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00, 0x9C, 0x02 } }; struct zzz b={0x00000001, 0x0000, 0x0000, { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } }; IClassFactory *c; char aa[1000]; int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h = LoadLibrary("mscal.ocx"); p = GetProcAddress( h,"DllGetClassObject" ); f = (long(_stdcall *)(struct zzz *,struct zzz *,IClassFactory **) )p; sprintf(aa, "Before calling DllGetClassObject... c = %p",c); MessageBox(0,aa,aa,0); h1 = f( &a, &b, &c ); sprintf(aa, "After calling DllGetClassObject...h1 = %p... c = %p",h1,c); MessageBox(0,aa,aa,0); }
We compile this program with the command ' cl -c get4.cpp ' and to build it, we say ' link get4.obj user32.lib kernel32.lib gdi32.lib '.
When we execute the above program, we get two message boxes, telling us the value of the third parameter c, before and after the function DllGetClassObject was called. If the execution of the function was successful, h1 will be zero and c should be filled with some value.
In the next program, we have made a minor modification to the prototype of f.
Here, we have made changes to the datatype of the second parameter of the function which f is defined as a pointer to. In this parameter, we are passing a constant structure that looks like IID, to DllGetClassObject( ), by reference. We remind ourselves that when we see an ' & ' in a prototype, only the address goes across. By adding the term ' const ', we are saying that no changes have to be made to the values that the members of the structure have been initialised to. When we are actually calling function DllGetClassObject( ) by saying ' f( ); ', we pass IID_IClassFactory as the second parameter. When we see the term IID, we have to assume that it is a structure similar to the structure tag zzz. IID is also known as GUID, CLSID and so on. All these terms are #defined in a header file to each other. Just remember that they are all the same 16 byte structures. When we say IID_IClassFactory, we are referring to the 16 byte number that has been given to IClassFactory. This number is stored in a structure. Since there is a '& ' in the prototype of f , only the address of this structure goes across, and will be picked up from the stack by DllGetClassObject( ) of mscal.ocx. If we go into windows.h or some header file that is called by windows.h, we shall see an extern variable called IID_IClassFactory. This program should not give us any problems when we compile it using ' cl -c get5.cpp '.
get5.cpp:
#include<windows.h> #include<stdio.h> struct zzz { long a1; short b1; short c1; unsigned char d1[8]; }; int (_stdcall *p)( ); long (_stdcall *f)(struct zzz *, const IID &, IClassFactory **); HANDLE h; long h1; struct zzz a={0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00, 0x9C, 0x02 } }; IClassFactory *c; char aa[1000]; int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { h = LoadLibrary("mscal.ocx"); p = GetProcAddress( h,"DllGetClassObject" ); f = ( long(_stdcall *)(struct zzz *, const IID &, IClassFactory **) )p; sprintf(aa, "Before calling DllGetClassObject... c = %p",c); MessageBox(0,aa,aa,0); h1 = f(&a, IID_IClassFactory, &c); sprintf(aa, "After calling DllGetClassObject...h1 = %p... c = %p",h1,c); MessageBox(0,aa,aa,0); }
When we give the normal linker command 'link get5.obj user32.lib kernel32.lib gdi32.lib ', we get a linker error shown below.
get5.obj : error LNK2001: unresolved external symbol _IID_IClassFactory get5.exe : fatal error LNK1120: 1 unresolved externals
Stop the world, stop the world
I wanna get off
- Extreme in ‘Stop The World’
Never let it be said that when we were working on a technology as advanced as Active-X, we were ashamed to divert our minds to run lowly C programs. It is only when we stop for a while, and reflect on the mistakes we make, that we shall truly be able to learn something.
Let us explain this error with the help of a simple example. We have a tiny program called p1.c shown below, where ii is defined as extern. When we compile this program by saying ' cl -c p1.c ', we get no errors.
p1.c:
extern int ii; main() { printf("%d",ii); }
However, when we say ' link p1.obj ' we get the error saying ' unresolved external symbol _ii '. Since ii is an extern, it is defined elsewhere and it is the job of the linker to obtain the code of ii.
This code has to be obtained from an OBJ file or a LIB file, which in turn, is nothing but a collection of OBJ files. We create another file called p2.c in which we have a single line of code in which we define as well as initialise ii.
p2.c:
int ii = 10;
We compile this program but do not build it. Instead, we go back to our earlier program, and build p1.obj by saying ' link p1.obj p2.obj '. Thus we have told the linker to also look at file p2.obj in addition to the files that it normally looks at. We now get no linker errors, and p1.c executes to display the value of ii as 10.
We have mentioned that IID_IClassFactory is an extern variable. However, we haven't given the linker the name of the OBJ or LIB file that contains the code of this variable. The name of this file is uuid .lib. This LIB file, uuid.lib, does not contain any functions. All it contains are structures, initialised to some value. We have to tell the linker to look at this LIB file also while linking get5.obj. Now, when we say ' link get5.obj user32.lib kernel32.lib gdi32.lib uuid.lib ', we get no errors. This is because in the LIB file uuid.lib, the structure IID_IClassFactory is created as well as initialised. This creation and initialisation is not done in header files like windows.h, as this would unnecessarily increase the size of these files. The program get5.cpp executes as we expect it to.
All this is a rather long procedure just to call the function DllGetClassObject( ). Microsoft realised this, and therefore gave us a single function called CoGetClassObject( )
to do all this work for us. Unfortunately, function CoGetClassObject( ) will not work until we have an additional function before it, called OleInitialize( ). In fact, the rest of the code will not work at all if function OleInitialise( ) is not present. We always pass 0 as a parameter to this function. OleInitialize( ) probably performs some initialisation routines that allow Active-X functions to execute without any problems. For example, it may create some structures, initialise them or allocate memory for various purposes. We don't really know what this function does exactly, but we can always make a good guess. Microsoft has a number of operating systems, all beginning with the word Windows, like Windows95, WindowsNT, WindowsCE and so on. Internally, all these operating systems may be quite different from each other. Active-X has to work flawlessly on all these operating systems. It may also have to work on the Apple Macintosh, now that Microsoft owns a stake in Apple, as well as on some versions of UNIX. Microsoft probably uses this function to take care of the pecularities of all these different operating systems, without us having to worry whether the Active-X code that we write on Windows95 will be work on WindowsNT, or not.
But let's get back to the function CoGetClassObject( ). This single function will do most of the work that we have done in the last program in a single line. Compare the size of program get6.cpp with that of get5.cpp. Internally, this function CoGetClassObject( ) gets a pointer to DllGetClassObject( ) from mscal.ocx using a combination of functions LoadLibrary( ) and GetProcAddress( ). CoGetClassObject( ) then calls DllGetClassObject( ) from the server.
Take a look at the parameters of the function CoGetClassObject( ). The first parameter a refers to the server that we want to call. This is a structure that has been initialised to the sixteen byte number that has been assigned to mscal.ocx. Notice that we haven't written the structure tag zzz as we had been doing, so far. Instead, we have defined a to be like CLSID. CLSID is another structure tag, exactly like zzz in the previous programs, which is defined in windows.h. The second parameter, 1, means that the server is a DLL. We might have to use another number here if we want to call a function from an EXE file. The next parameter is 0. We have made it a policy to never explain any parameter that is 0. Since it is 0, it probably means that it is not going to be used anywhere. A client always asks a server if it has a particular interface called IClassFactory. If the server supports this interface, it has to return a pointer to this interface to the client. Once our client has a pointer to the required interface within the server, it can call any function present in that interface. Our client gives the server the number of the interface that it is looking for, as the fourth parameter of CoGetClassObject( ). IID_IClassFactory represents the 16 byte number assigned to IClassFactory. If the server supports this interface, it will fill up the last parameter c with a pointer to this interface. What appears very strange to us is the cast of ( void ** ) in front of the last parameter. In fact, a pointer to a void makes no sense at all. Suppose we had a pointer to a void, for example, ' void *q '. We cannot increment or decrement q, nor can we say 'q->'. If we try to do so, we shall get errors. There is a simple explanation for this. Consider the case if we had a pointer to a short, which we incremented. A pointer to a short increments, each time, by the number of bytes occupied by a short, that is, by two memory locations. But a void is supposed to mean nothing, so how can we ever increment q by the number of bytes occupied by a void? If a pointer to a void is meaningless and absolutely useless, a pointer to a pointer to a void is even more useless. Then what is the advantage of having a cast of a pointer to a pointer to a void out here? Basically, we should be happy that we have such a cast here. This cast enables the last parameter c to be filled up with a pointer to any type of structure or class. It also might happen that we do not know what datatype will be used to fill up this variable. The cast of void ** is Microsoft’s way of telling us that we may use multiple datatypes here.
While linking the OBJ file, we have to give an additional LIB file, ole32.lib, wehich is needed for these new functions. The link command is now
link get6.obj user32.lib kernel32.lib gdi32.lib uuid.lib ole32.lib
get6.cpp:
#include<windows.h> #include<stdio.h> long h1; IClassFactory *c; char aa[1000]; CLSID a = {0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00, 0x9C, 0x02 } }; int _stdcall WinMain( HINSTANCE i, HINSTANCE j, char *k, int l ) { OleInitialize( 0 ); h1 = CoGetClassObject( a, 1, 0, IID_IClassFactory, (void **)&c ); sprintf( aa, "CoGetClassObject...h1 = %p... c = %p", h1, c ); MessageBox( 0, aa, aa, 0 ); }
We should understand one thing about the above program. While Microsoft has given us the function CoGetClassObject( ), it hasn't told us anything about what the function does internally. We have actually shown you the source code of this function. Throughout this tutorial, we shall be going to depths, previously unheard of, in Active-X, and we shall be doing it one step at a time so that nothing is too complicated.
We shall take the same program shown above, and make minor modifications that will enable it to display a window. When we click in the window, a message box is displayed. The main code has been placed in our callback function zzz, and will get called the moment we click on the window. In case you have trouble understanding how the window appeared, please consult our tutorial on the C Windows programming language.
Every time we make use of a new function in our program, we shall check to see if it's return value is 0, indicating that the function performed it's task successfully. Any value other than 0 indicates failure.
get7.cpp:
#include <windows.h> #include <stdio.h> WNDCLASS aa; HWND bb; MSG cc; CLSID a = {0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00,0x9C, 0x02 } }; long h1; IClassFactory *c; char aaa[1000]; long _stdcall zzz(void *,unsigned int, unsigned int, long); int _stdcall WinMain(void *i, void *j, char *k, int l) { aa.lpszClassName = "Active-X"; aa.hInstance = i; aa.hbrBackground = GetStockObject(WHITE_BRUSH); aa.lpfnWndProc = zzz; RegisterClass( &aa ); bb=CreateWindow("Active-X", "Client", WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, i, 0); ShowWindow(bb,3); while(GetMessage(&cc,0,0,0)) DispatchMessage(&cc); } long _stdcall zzz(void *w, unsigned int x, unsigned int y, long z) { if(x==WM_LBUTTONDOWN) { MessageBox(0,"Hello","HI",0); OleInitialize(0); MessageBox(0, aaa, aaa, 0 ); h1=CoGetClassObject(a, 1, 0, IID_IClassFactory, (void **)&c ); sprintf(aaa,"from CoGetClassObject h1 = %ld...c = %p\n", h1, c); MessageBox(0, aaa, aaa, 0 ); } if(x==WM_DESTROY) PostQuitMessage(0); return DefWindowProc(w,x,y,z); }
We had said that the last variable c of the function CoGetClassObject( ) is filled up with a pointer to the interface IClassFactory. Once we have a pointer to IClassFactory, we can call any function that is present in this class. A certain sequence of steps to be followed while a client is talking to a server has been laid down by Microsoft. In the next program, we have added the line ' c->CreateInstance( ) '. We do not get any compilation errors, which means that CreateInstance( ) is a function in IClassFactory. It has been specified by Microsoft that when a client has a pointer to the server's IClassFactory, the first function that the client will call from this class or interface is CreateInstance( ). CreateInstance( ) has three parameters. The first parameter is zero, and therefore, is unimportant. The last two parameters have greater significance. The second parameter is IID_IUnknown. IID_IUnknown is the 16 byte number by which the interface IUnknown is known internally to the computer. As before, this 16 byte number is stored in a structure known by different names such as IID, CLSID, GUID, and so on. By placing the number of this interface in the second parameter, our container is telling the server that it wants an interface called IUnknown from the server. We have defined d as a pointer to IUnknown. We have put the address of this pointer that looks like IUnknown on the stack. The function CreateInstance( ) of IClassFactory of the server will fill up this parameter with a pointer to it's IUnknown.
get8.cpp:
#include <windows.h> #include <stdio.h> WNDCLASS aa; HWND bb; MSG cc; CLSID a = {0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00,0x9C, 0x02 } }; long h1; IClassFactory *c; IUnknown *d; char aaa[1000]; long _stdcall zzz(void *,unsigned int, unsigned int, long); int _stdcall WinMain(void *i, void *j, char *k, int l) { aa.lpszClassName = "Active-X"; aa.hInstance = i; aa.hbrBackground = GetStockObject(WHITE_BRUSH); aa.lpfnWndProc = zzz; RegisterClass( &aa ); bb=CreateWindow("Active-X", "Client", WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, i, 0); ShowWindow(bb,3); while(GetMessage(&cc,0,0,0)) DispatchMessage(&cc); } long _stdcall zzz(void *w, unsigned int x, unsigned int y, long z) { if(x==WM_LBUTTONDOWN) { MessageBox(0,"Hello","HI",0); OleInitialize(0); MessageBox(0, aaa, aaa, 0 ); h1=CoGetClassObject(a, 1, 0, IID_IClassFactory, (void **)&c ); sprintf(aaa,"CoGetClassObject h1 = %ld...c = %p\n", h1, c ); MessageBox(0, aaa, aaa, 0 ); h1=c->CreateInstance( 0, IID_IUnknown, (void **)&d ); sprintf(aaa,"CreateInstance h1 = %ld...d = %p", h1, d); MessageBox(0, aaa, aaa, 0 ); } if(x==WM_DESTROY) PostQuitMessage(0); return DefWindowProc(w,x,y,z); }
Once we have a pointer to IUnknown in the server, we can call any function in this interface. IUnknown is actually a very special interface in Active-X. All other interfaces in Active-X are derived from this interface. That means that all the functions present in IUnknown will be present in all other Active-X interfaces. One of these functions is absolutely crucial. This function is called QueryInterface( ). The name of the function gives us some indication of what it is used for. A container has no idea as to which interfaces are supported by the server. When we talk about an interface, we are referring to a number of functions that are grouped together because they contribute towards performing a common task. For example, all functions that assist in printing out what the object looks like on screen, may be put into another interface. Of these functions, one function may handle everything related to different colours that appear on the screen, another function may take care of different fonts, and so on. There are a few thousand interfaces that a server could support, of which, a few are important and have to be included. If a certain task has to be performed, the server has to support a certain interface that contains the functions that can perform this task. Our container program can find out whether the server supports the required interface by using this function called QueryInterface( ) present in the server's IUnknown interface. Function QueryInterface( ) is passed two parameters. The first parameter is a number stored in a sixteen byte structure that identifies a particular interface. The client program is querying or asking the server as to whether it supports the interface mentioned in the first parameter. In the program shown below, our program is asking mscal if an interface called IPersistPropertyBag is supported. For the moment, let's assume that a little birdie whispered into our ears that mscal.ocx supports this interface. In other words, we already know that this interface is supported, and we are confirming this fact with
QueryInterface( ). At a much later stage, we shall show you how obtain a list of interfaces that are supported by any Active-X application. In our program, we have also defined e as a pointer to interface IPersistPropertyBag, and placed it as the last parameter of QueryInterface( ). As in the earlier functions, we have casted the last parameter to(void **). Using function QueryInterface( ), we might want to know whether a server supports a number of interfaces. When we say that a server supports an interface, all that we mean is that the server program contains the particular interface with all it's functions. If the server supports this interface, it will give the client a pointer to this interface. A pointer to an interface is given to the client by filling up the last parameter of QueryInterface( ). Every time, a pointer to a different class or interface will be returned. Now we can really appreciate the cast of void ** out here. Since no one can tell what datatype will be placed as this parameter, it is casted to void **, so that we may have multiple datatypes here. Notice that by using function QueryInterface( ), we can dynamically find out whether a server supports an interface or not.
Before we compile, note that we have included an additional header file called ocidl.h in our program. If this header file is not included, the compiler will not recognise IPersistPropertyBag.
get9.cpp:
#include <windows.h> #include <stdio.h> #include<ocidl.h> WNDCLASS aa; HWND bb; MSG cc; CLSID a = {0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00,0x9C, 0x02 } }; long h1; IClassFactory *c; IUnknown *d; IPersistPropertyBag *e; char aaa[1000]; long _stdcall zzz(void *,unsigned int, unsigned int, long); int _stdcall WinMain(void *i, void *j, char *k, int l) { aa.lpszClassName = "Active-X"; aa.hInstance = i; aa.hbrBackground = GetStockObject(WHITE_BRUSH); aa.lpfnWndProc = zzz; RegisterClass( &aa ); bb=CreateWindow("Active-X", "Client", WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, i, 0); ShowWindow(bb,3); while(GetMessage(&cc,0,0,0)) DispatchMessage(&cc); } long _stdcall zzz(void *w, unsigned int x, unsigned int y, long z) { if(x==WM_LBUTTONDOWN) { MessageBox(0,"Hello","HI",0); OleInitialize(0); MessageBox(0, aaa, aaa, 0 ); h1=CoGetClassObject(a, 1, 0, IID_IClassFactory, (void **)&c ); sprintf(aaa,"CoGetClassObject h1 = %ld...c = %p\n", h1, c); MessageBox(0, aaa, aaa, 0 ); h1=c->CreateInstance(0,IID_IUnknown,(void **)&d); sprintf(aaa,"CreateInstance h1 = %ld...d = %p", h1, d); MessageBox(0, aaa, aaa, 0 ); h1=d->QueryInterface( IID_IPersistPropertyBag, (void **)&e ); sprintf(aaa,"QueryInterface IID_IPersistPropertyBag h1 = %ld...e = %p", h1, e); MessageBox(0, aaa, aaa, 0 ); } if(x==WM_DESTROY) PostQuitMessage(0); return DefWindowProc(w,x,y,z); }
Since h1 is 0, it means that the server supports IPersistPropertyBag. We also notice that e has been filled with some value. Before the execution of QueryInterface( ), the value of e had to be 0 as it is a global variable. This means that we now have a pointer to interface IPersistPropertyBag. Microsoft has laid down a rule saying that once a client has a pointer to the interface IPersistPropertyBag in the server, the function InitNew( ) from this interface should be called. We hate to argue with rules. So we shall go by the rule book and call this function.
get10.cpp:
#include <windows.h> #include <stdio.h> #include<ocidl.h> WNDCLASS aa; HWND bb; MSG cc; CLSID a = {0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00,0x9C, 0x02 } }; long h1; IClassFactory *c; IUnknown *d; IPersistPropertyBag *e; char aaa[1000]; long _stdcall zzz(void *,unsigned int, unsigned int, long); int _stdcall WinMain(void *i, void *j, char *k, int l) { aa.lpszClassName = "Active-X"; aa.hInstance = i; aa.hbrBackground = GetStockObject(WHITE_BRUSH); aa.lpfnWndProc = zzz; RegisterClass( &aa ); bb=CreateWindow("Active-X", "Client", WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, i, 0); ShowWindow(bb,3); while(GetMessage(&cc,0,0,0)) DispatchMessage(&cc); } long _stdcall zzz(void *w, unsigned int x, unsigned int y, long z) { if(x==WM_LBUTTONDOWN) { MessageBox(0,"Hello","HI",0); OleInitialize(0); h1=CoGetClassObject(a, 1, 0, IID_IClassFactory, (void **)&c ); sprintf(aaa,"CoGetClassObject h1 = %ld...c = %p\n", h1, c); MessageBox(0, aaa, aaa, 0); h1=c->CreateInstance(0,IID_IUnknown,(void **)&d); sprintf(aaa,"CreateInstance h1 = %ld...d = %p", h1, d); MessageBox(0, aaa, aaa, 0); h1=d->QueryInterface(IID_IPersistPropertyBag,(void **)&e); sprintf(aaa,"QueryInterface IID_IPersistPropertyBag h1 = %ld...e = %p", h1, e); MessageBox(0, aaa, aaa, 0); h1=e->InitNew(); sprintf(aaa,"e->InitNew() h1 = %ld ", h1); MessageBox(0, aaa, aaa, 0); } if(x==WM_DESTROY) PostQuitMessage(0); return DefWindowProc(w,x,y,z); }
Life's a journey,
Not a destination,
And you just can't tell,
Just what tomorrow brings,
You have to learn to crawl,
Before you learn to walk
-Aerosmith in 'Amazing'
Active-X is a technology that surely tests our strengths, our weaknesses, and our ability to be persist in the face of that disease that afflicts all young minds when it comes to doing anything resembling work. Yes, we are talking about is a combination of boredom and frustration! There is, I suppose no cure for this disease, except to love the work that you do. Maybe, things do get tedious sometimes, but a number of very successful people have also learnt things the hard way. It often pays to have an intermediate goal, one that is achievable. Once we define this goal, we really should put all our efforts into achieving it.
We have to keep in mind that our initial goal is to be able to display a server called mscal.ocx within our client's window. The functions that are used to display something on screen are present in an interface called IViewObject. Our next step will be to find out whether the server mscal.ocx supports this interface. We have also defined a pointer f to IViewObject, which should be filled up by the server if this interface is supported.
There is one thing that we notice about functions in Active-X, in general. In many of these functions, we ask for a particular class from a server by giving the 16 byte structure that identifies the required interface. We also have a pointer to the same interface in our client program. This should be filled up with the address of the interface in the server, if the server supports that interface.
get11.cpp:
#include <windows.h> #include <stdio.h> #include<ocidl.h> WNDCLASS aa; HWND bb; MSG cc; CLSID a = {0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00,0x9C, 0x02 } }; long h1; IClassFactory *c; IUnknown *d; IPersistPropertyBag *e; IViewObject *f; char aaa[1000]; long _stdcall zzz(void *,unsigned int, unsigned int, long); int _stdcall WinMain(void *i, void *j, char *k, int l) { aa.lpszClassName = "Active-X"; aa.hInstance = i; aa.hbrBackground = GetStockObject(WHITE_BRUSH); aa.lpfnWndProc = zzz; RegisterClass( &aa ); bb=CreateWindow("Active-X", "Client", WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, i, 0); ShowWindow(bb,3); while(GetMessage(&cc,0,0,0)) DispatchMessage(&cc); } long _stdcall zzz(void *w, unsigned int x, unsigned int y, long z) { if(x==WM_LBUTTONDOWN) { MessageBox(0,"Hello","HI",0); OleInitialize(0); h1=CoGetClassObject(a, 1, 0, IID_IClassFactory, (void **)&c); sprintf(aaa,"CoGetClassObject h1 = %ld...c = %p\n", h1, c); MessageBox(0, aaa, aaa, 0); h1=c->CreateInstance(0,IID_IUnknown,(void **)&d); sprintf(aaa,"CreateInstance h1 = %ld...d = %p", h1, d); MessageBox(0, aaa, aaa, 0); h1=d->QueryInterface(IID_IPersistPropertyBag,(void **)&e); sprintf(aaa,"QueryInterface IID_IPersistPropertyBag h1 = %ld...e = %p", h1, e); MessageBox(0, aaa, aaa, 0); h1=e->InitNew(); sprintf(aaa,"e->InitNew() h1 = %ld ", h1); MessageBox(0, aaa, aaa, 0); h1=d->QueryInterface(IID_IViewObject,(void **)&f); sprintf(aaa,"QueryInterface IID_IViewObject h1 = %ld...f = %p", h1, f); MessageBox(0, aaa, aaa, 0); } if(x==WM_DESTROY) PostQuitMessage(0); return DefWindowProc(w,x,y,z); }
And it's your face I'm looking for,
On every street
-Dire Straits in ‘On Every Street’
Since the QueryInterface( ) was successful, we now have a pointer to the interface IViewObject. This interface has a particular function called Draw( ). Do we need to tell you that this function is actually responsible for displaying the server, mscal.ocx, on our screen? We don't think so. This function has a number of parameters, most of which have the value 0, and are unimportant. The first parameter of this function has been set to 1. The sixth and the seventh parameters are very important, though. The function Draw( ) has to know where the server is to be displayed. Of course, we all know that it has to be displayed within our window, naturally. The only problem is that, so far, we haven't told the server anything about this. We shall do so, right now, by giving it a DeviceContext to our window which will enable it to write to our window. The seventh parameter is the address of a structure that looks like RECTL. This structure will define a rectangle within our window, in which the calendar, mscal.ocx, will be displayed. Before this function, we therefore define pp to be a const structure of type RECTL. We also initialise it by giving it four parameters representing the left, top, right and bottom values of the rectangle.
get12.cpp:
#include <windows.h> #include <stdio.h> #include<ocidl.h> WNDCLASS aa; HWND bb; MSG cc; CLSID a = {0x8E27C92B, 0x1264, 0x101C, { 0x8A, 0x2F, 0x04, 0x02, 0x24, 0x00,0x9C, 0x02 } }; long h1; IClassFactory *c; IUnknown *d; IPersistPropertyBag *e; IViewObject *f; char aaa[1000]; long _stdcall zzz(void *,unsigned int, unsigned int, long); int _stdcall WinMain(void *i, void *j, char *k, int l) { aa.lpszClassName = "Active-X"; aa.hInstance = i; aa.hbrBackground = GetStockObject(WHITE_BRUSH); aa.lpfnWndProc = zzz; RegisterClass( &aa ); bb=CreateWindow("Active-X", "Client", WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, i, 0); ShowWindow(bb,3); while(GetMessage(&cc, 0, 0, 0 )) DispatchMessage(&cc); } long _stdcall zzz(void *w, unsigned int x, unsigned int y, long z) { if(x==WM_LBUTTONDOWN) { MessageBox(0,"Hello","HI",0); OleInitialize( 0 ); h1=CoGetClassObject(a, 1, 0, IID_IClassFactory, (void **)&c ); sprintf(aaa,"CoGetClassObject h1 = %ld...c = %p\n", h1, c); MessageBox(0, aaa, aaa, 0); h1=c->CreateInstance(0, IID_IUnknown, (void **)&d); sprintf(aaa,"CreateInstance h1 = %ld...d = %p", h1, d); MessageBox(0, aaa, aaa, 0); h1=d->QueryInterface(IID_IPersistPropertyBag,(void **)&e); sprintf(aaa,"QueryInterface IID_IPersistPropertyBag h1 = %ld...e = %p", h1, e); MessageBox(0, aaa, aaa, 0); h1=e->InitNew( ); sprintf(aaa,"e->InitNew( ) h1 = %ld ", h1); MessageBox(0, aaa, aaa, 0); h1=d->QueryInterface(IID_IViewObject, ( void ** )&f ); sprintf(aaa,"QueryInterface IID_IViewObject h1 = %ld...f = %p", h1, f); MessageBox(0, aaa, aaa, 0); const RECTL pp={12, 17, 300, 250}; h1=f->Draw( 1, 0, 0, 0, 0, GetDC(w), &pp, 0, 0, 0 ); sprintf(aaa,"f->Draw( ) h1 = %ld ", h1); MessageBox(0, aaa, aaa, 0); MessageBox(0, "End", "Over", 0); } if(x==WM_DESTROY) PostQuitMessage(0); return DefWindowProc(w,x,y,z); }
It's amazing,
With the blink of an eye,
You finally see the light,
Oh! It's amazing,
When the moment arrives,
And you know you'll be all right
- Aerosmith in 'Amazing'
We've actually met our goal ! Atleast, our intermediate goal. Because, on our screen, after we see the normal message boxes, we also see a calendar. This calendar fits into a rectangle whose limits we had set while initialising pp. Not even Aerosmith could write lyrics to describe just how happy we are right now. Did anyone of us expect to see a calendar within our container so soon? None of us did. But there, you see it! In other words our program actually worked ! Are you surprised ? You can count on us to get you to your goal.
By no means are we saying that the client that we have written is a full fledged client, or that it has managed to fully import a server. All that we have done, is call some functions from a number of interfaces that display the server. When we click on any button or date on the calendar, we see that it does not get activated. This means that we haven’t written a complete client. We still have to obtain more pointers to different interfaces, that contain functions which will enable us to activate a date on the calendar. But the basic principle, as well as the procedure to be followed, remains the same.
Having understood how we have to go about writing at least the initial part of a client, let's turn our attention to writing the smallest server. A number of the concepts that arise while a server is being written, have already been partially explained while we were writing a client. This should make things a lot easier for us. We shall write our server program in another subdirectory called server.
While writing the client, we made use of functions like LoadLibrary( ) and GetProcAddress( ) to obtain a pointer to a function called DllGetClassObject( ) that was present in the server. We did all this so that our client could call this function. This function DllGetClassObject( ) has to be present in all Active-X servers that are DLLs. To allow someone else to call DllGetClassObject( ), this function has to be made exportable.
Thus the first function that our server should have, is DllGetClassObject( ). This function, as we have mentioned earlier, has three parameters. If we see an '&' in front of any of the parameters, we assume that a pointer is being passed.
z.cpp:
#include<windows.h> long DllGetClassObject(CLSID &a, CLSID &b, void **c) { return 0; }
We only compile this program for the time being, by saying ' cl -c z.cpp '. We see that the compiler gives us no errors. The first two parameters are pointers to structures that look like CLSID. CLSID, as we have discussed earlier, is a 16 byte number that is assigned to a server or an Active-X interface. Once a 16 byte number has been assigned to a particular server by Microsoft, for example, the number of mscal.ocx, this number will be constant for mscal on all machines. Thus, we know that the CLSID of a server is a const, and we can't change the value of this 16 byte number. We shall therefore place the word ' const ' in front of the first two parameters of DllGetClassObject in the next program.
z.cpp:
#include<windows.h> long DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { return 0; } z.cpp(3) : error C2373: 'DllGetClassObject' : redefinition; different type modifiers
Take my hand, we're off to never never land
- Metallica
The previous program worked perfectly, but this program gave us an error after we added the term ' const ' before CLSID. This is one error that puzzles us to no end. But let us look closely at one word in the error message, namely ' redefinition '. This word gives us a hint as to why the error took place.
We should remember that, in C++, the actual name of a function is the name plus the parameters of the function. In the header file windows.h or another header file that is called by windows.h, there is a prototype of the function DllGetClassObject with parameters as (const CLSID &a, const CLSID &b, void **c). The moment the C++ compiler sees this function in our program, it knows that the prototype of this function is already present in windows.h. The only problem is that, in the prototype, the function follows the Standard Calling Convention. Since the calling convention of the function in our program does not match the calling convention in the function prototype, we get an error that tells us that the function has been remodified.
In the earlier program we had a function with the name DllGetClassObject but with the parameters (CLSID &a, CLSID &b, void **c). This function DllGetClassObject(CLSID &a, CLSID &b, void **c) is not the same function as DllGetClassObject(const CLSID &a, const CLSID &b, void **c) even though both functions have the same name. When the C++ compiler looks at DllGetClassObject with the parameters (CLSID &a, CLSID &b, void **c), as far as it is concerned, there is no prototype of this function in windows.h.
Thus, if the name and parameters of a function match those of a function prototype, then the calling convention of the function has to be the same as that given in the prototype. If only the name of the function is the same as that of a prototype while the parameters are different, then there is absolutely no problem if the calling conventions differ, as they both refer to different functions.
The error in our program will disappear if we ensure that the function DllGetClassObject(CLSID &a, CLSID &b, void **c) follows the Standard Calling Convention. There are some inconsistencies in prototypes given in header files. We have to make sure that some terms which appear in a function prototype in a header file are present in the function name in our program. There are other terms that do not have to be written in our program, and will be automatically added to our function name. Consider the case of the function DllGetClassObject( ). We are forced to mention that this function follows the Standard Calling Convention, because this is what the prototype specifies. However, the prototype also specifies that there is no name mangling in this function because the term ' extern "C" ' is also present in front of the name of the function. We do not have to write this term as the C/C++ compiler looks into the header file and automatically adds it. For a change, can't designers of programming languages write rules that do not conflict with one another? Until someone comes up with something better than C/C++, we are, alas, forced to put up with these rules.
They tell us to specify the Calling Convention, then this is what we are going to do.
z.cpp:
#include<windows.h> long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { return 0; }
When we compile this program, we do not get any errors. We want our server to be a DLL, so we shall use the /dll option while linking. Remember, any client that calls our server will have to call the function DllGetClassObject, which is why this function has to be an exportable function. Since function DllGetClassObject has to be an exportable function, we have to have a DEF file as shown below.
z.def:
LIBRARY z EXPORTS DllGetClassObject
The linker command is as follows:
link /dll z.obj -def:z.def user32.lib kernel32.lib gdi32.lib.
Note that we may interchange a ' / ' with a ' - ' in front of either the term dll or the term def, in the link command. We now see an additional library file, an exportable file as well as a DLL. So what's the difference between a DLL and an OCX? Merely the extention. That's something we can change easily with the command ' copy z.dll z.ocx'. We are forced to resort to this option of renaming a DLL to an OCX because linkers are limited to creating only one of three types of files, an EXE file, a DLL or a VxD. To overcome this limitation, we merely change the extension of the file that the linker creates, from .dll to .ocx.
Until now, whenever we wanted to check values obtained at various stages while a program was running, we used a message box. A message box is, however, not the most appropriate way of collecting information, because we are unable to compare results of various stages easily. Also, a message box often gets in the way of a program executing in one shot, without any breaks in between. From now onwards, whenever we wish to see the result of any particular stage, we shall write the result of that stage into a file called z.txt in our root directory, by calling a function called abc( ). This function abc( ) is passed a string as a parameter. Within the function, file z.txt will be opened in the append mode, the string will be written into the file, after which, the file will be closed. This is shown in the next program.
z.cpp:
#include<windows.h> #include<stdio.h> void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"Server %s\n",p); fclose(fp); } long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc(" DllGetClassObject "); return 0; }
To reduce the drudgery of having to type commands to compile, link, and so on, again and again, we shall create a batch file called a.bat. To execute this file, just type the letter a at the DOS prompt.
a.bat:
cl -c z.cpp link /dll z.obj -def:z.def user32.lib kernel32.lib gdi32.lib copy z.dll z.ocx del c:\z.txt
If we recall, the function DllGetClassObject( ) in the server is called by the function CoGetClassObject( ) that is present in the client. We shall now rewrite the initial part of our client program, and see if we can the function CoGetClassObject( ) of the client can be executed successfully. By this, we mean that it should return 0.
y.cpp:
#include<windows.h> #include<stdio.h> void abc(char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"Client %s\n",p); fclose(fp); } char aaa[100]; CLSID a={0x51515150, 0x5151, 0x5151,{ 0x51, 0x51, 0x51, 0x51, 0x51, 0x51, 0x51, 0x51}}; WNDCLASS aa; HWND bb; MSG cc; long _stdcall zzz(void *,unsigned int, unsigned int, long); IClassFactory *c; long h1; int _stdcall WinMain(void *i, void *j, char *k, int l) { aa.lpszClassName = "Active-X"; aa.hInstance = i; aa.hbrBackground = GetStockObject(WHITE_BRUSH); aa.lpfnWndProc = zzz; RegisterClass( &aa ); bb=CreateWindow("Active-X", "Client", WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, i, 0); ShowWindow(bb,3); while(GetMessage(&cc,0,0,0)) DispatchMessage(&cc); } long _stdcall zzz(void *w, unsigned int x, unsigned int y, long z) { if(x==WM_LBUTTONDOWN) { OleInitialize(0); h1=CoGetClassObject(a, 1, 0, IID_IClassFactory, (void **)&c ); sprintf(aaa,"CoGetClassObject h1 = %ld...c = %p\n", h1, c); abc(aaa); } if (x==WM_DESTROY) PostQuitMessage(0); return DefWindowProc(w,x,y,z); }
b.bat:
cl -c y.cpp link y.obj user32.lib kernel32.lib gdi32.lib uuid.lib ole32.lib
Let's scan through our client program. Have we, at any point mentioned that we wish to call a server named z.ocx? We definitely haven't. How does a client call a server, anyway? It looks into the registry for a 16 byte number or CLSID that is given as the first parameter of the function CoGetClassObject( ). We now encounter a major problem. There is no entry of a server called z.ocx in the Windows registry whose CLSID is {0x51515150, 0x5151, 0x5151,{ 0x51, 0x51, 0x51, 0x51, 0x51, 0x51, 0x51, 0x51}}. To get our server registered with Windows, we have to first write a REG file as shown below.
ss.reg:
REGEDIT HKEY_CLASSES_ROOT\zzzz\CLSID = {51515150-5151-5151-5151-515151515151} HKEY_CLASSES_ROOT\CLSID\{51515150-5151-5151-5151-515151515151} \InprocServer32 = c:\server\z.ocx
A word of caution while writing this program. There has to be one and only one space before and after each and every equal to sign in this program. We cannot have more spaces or even less number of spaces. The last line of the program should be written on one line only.
e have already told you a little about the registry. At first sight, the registry breaks up into a lengthy tree, and each term after the ' + ' sign is called a key or a subkey, depending on it's position. To get a better understanding of the concept of keys and subkeys, we could probably find an analogy with directories and subdirectories. One of the first keys that we see in the registry is called HKEY_CLASSES_ROOT. We see this same term in our REG file. Look at the left hand side of the second line of the REG file. This part reads as 'HKEY_CLASSES_ROOT\zzzz\CLSID '. By this line, within the key called HKEY_CLASSES_ROOT, a subkey called zzzz will be created. Within this subkey, another subkey called CLSID will be created. This subkey is assigned the value on the right hand side of the equal to sign. Similarly, the left hand side of the next line of the REG file reads as 'HKEY_CLASSES_ROOT\CLSID\{51515150-5151-5151-5151-515151515151}\InprocServer32 '. There is already a subkey called CLSID in HKEY_CLASSES_ROOT. Within CLSID, a subkey called {51515150-5151-5151-5151-515151515151} will be added. Within this subkey, a subkey known as InprocServer32 is created. This subkey is given the address of our server, z.ocx. Of course, the difference between a key and a subkey is quite blurred, as is the difference between a directory and a subdirectory. Also, in the REG file, the first line has to be the word ' REGEDIT '. If this line is not present, when we try to import this file into the registry, we shall get an error telling us that the specified file is not a registry script.
Having prepared our REG file, we now have to enter it into the Windows Registry. At the bottom of our screen, we click on ' Start ' and select ' Run '. Here, we type ' regedit ' in the text box and click on OK. This takes us into the Windows Registry. We now click on ' Registry ' and select ' Import Registry File '. A dialog box appears and we type in the path of our REG file, in the edit box following ' file ', as c:\server\ss.reg. After this, we press Enter on the keyboard, and receive a message ' Information in c:\server\ss.reg has been successfully entered into the registry '. This means that our server has now been registered with Windows. It also means that any client is able to call the server z.ocx.
We now look at the client program. The function that we have to really understand is CoGetClassObject( ). When this function wakes up, it goes into the registry. Here, it looks at the key called HKEY_CLASSES_ROOT. It goes into one of the subkeys called CLSID. The first parameter of the function CoGetClassObject( ) is a 16 byte number. The function checks whether it can find a match for this very number which should be a subkey of CLSID in the registry. As soon as it finds a match for the key {51515150-5151-5151-5151-515151515151}, it looks for a subkey called InprocServer32. It is this subkey that will tell function CoGetClassObject( ) in the client about the name of the ocx that has to be called. The above procedure, as well as the fact that CoGetClassObject( ) will look only at the subkey called InprocServer32, has been standardised by Microsoft.
Once the function CoGetClassObject( ) knows the name of the server, it has to call function DllGetClassObject( ) from the server. Internally, this is done exactly the same way which we had initially used when we wanted to call a function from a DLL. That is,
CoGetClassObject( ) internally obtains a handle to z.ocx using function LoadLibrary( ). It then uses this handle to obtain a pointer to the exportable function DllGetClassObject( ) in the server. CoGetClassObject( ) then calls function DllGetClassObject( ) using the pointer to this function.
Yesterday, all my troubles seemed so far away
Now it looks as though they are here to stay
- The Beatles in 'Yesterday'
That was an awfully large amount of theory to handle. We are now ready to execute the client program y.cpp, and see things for ourselves. We have to click in the window that appears when we execute the client. This time, when we click in the window, no message boxes appear. To see the results of the execution, we look into file z.txt in the root directory.
z.txt:
Server DllGetClassObject Client CoGetClassObject h1 = -2147221164...c = 00000000
Here, we can see that the function DllGetClassObject has indeed been called by the server. However the return value of the function CoGetClassObject( ), that is, h, has a negative value and c has not been filled up. This indicates that the execution of the function CoGetClassObject( ) has not entirely been successful. The last parameter of CoGetClassObject( ) ie. c has to be filled with some value when the server is called. But before we come to that stage, we remind ourselves that the second parameter of DllGetClassObject( ) is IID_IClassFactory, the 16 byte number that stands for the interface IClassFactory. We shall confirm that b is equal to IID_IClassFactory in the next program.
z.cpp:
#include<windows.h> #include<stdio.h> void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp, "server %s\n ",p); fclose(fp); } long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); if(b = = IID_IClassFactory) abc("Hello"); return 0; }
We first make some modifications to the batch file a.bat. We add the linker option uuid.lib, so that the file now looks as shown below. We have explained earlier that IID_IClassFactory gives us a linker error as it is defined as an ' extern', and this error is eliminated with the addition of uuid.lib in the linker options.
a.bat:
cl -c z.cpp link /dll z.obj -def:z.def user32.lib kernel32.lib gdi32.lib uuid.lib copy z.dll z.ocx del c:\z.txt
We now run the batch file a.bat and then execute the client program y.exe. We click in the window, and look at the output in the file z.txt. We now confirm that b is indeed equal to IID_IClassFactory.
z.txt:
server DllGetClassObject server Hello Client CoGetClassObject h1 = -2147221164...c = 00000000
We are supposed to fill the last parameter c of DllGetClassObject with a pointer to an interface IClassFactory. This interface contains a number of functions. Instead of giving c a pointer to an IClassFactory, we shall create our own class ccc with four functions and create an object d of this class. We shall now initialise *c to this object d. We shall also display *c of the function DllGetClassFactory( ) in the server, and compare this value with the value of the last parameter of function CoGetClassFactory( ) in the client program.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"server %s\n",p); fclose(fp); } class ccc { public: void a1( ){abc("in a1");} void a2( ){abc("in a2");} void a3( ){abc("in a3");} void a4( ){abc("in a4");} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; sprintf(aa,"*c = %p",*c); abc(aa); return 0; }
Run a.bat, execute the client program, and click in the window. The output in z.txt is as below.
z.txt:
server DllGetClassObject server *c = 00C70520 Client CoGetClassObject h1 = 0...c = 00C70520
We now see that the last parameter c of CoGetClassObject( ) in the client program is filled with some value. We also see that this value is the same as that of the last parameter of function DllGetClassObject( ) in the server and that the function CoGetClassObject( ) returns a value 0. Thus the execution of this function was successful. This means that the server has returned a pointer of some class to the function CoGetClassObject( ) in the client. The client always assumes that the pointer which it just obtained, is a pointer to IClassFactory. Since the client has a pointer to this class, it also assumes that any function in this class can be called. If we refer back to one of the client programs that we had done earlier, namely get8.cpp, we see that the first function that we call is always CreateInstance( ). The function CreateInstance( ) is called with three parameters. The first parameter is 0. The second parameter is the GUID or the 16 byte number by which the interface IUnknown is known. We also create a pointer d to IUnknown. The address of this pointer is given as the last parameter of CreateInstance( ) after casting to void**. We modify our current client program y.cpp to call CreateInstance( ).
y.cpp:
#include<windows.h> #include<stdio.h> void abc(char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"Client %s\n",p); fclose(fp); } char aaa[100]; CLSID a={0x51515150,0x5151,0x5151,{ 0x51, 0x51, 0x51, 0x51, 0x51, 0x51, 0x51, 0x51}}; WNDCLASS aa; HWND bb; MSG cc; long _stdcall zzz(void *,unsigned int, unsigned int, long); IClassFactory *c; IUnknown *d; long h1; int _stdcall WinMain(void *i, void *j, char *k, int l) { aa.lpszClassName = "Active-X"; aa.hInstance = i; aa.hbrBackground = GetStockObject(WHITE_BRUSH); aa.lpfnWndProc = zzz; RegisterClass( &aa ); bb=CreateWindow("Active-X", "Client", WS_OVERLAPPEDWINDOW, 0, 0, 0, 0, 0, 0, i, 0); ShowWindow(bb,3); while(GetMessage(&cc,0,0,0)) DispatchMessage(&cc); } long _stdcall zzz(void *w, unsigned int x, unsigned int y, long z) { if(x==WM_LBUTTONDOWN) { OleInitialize(0); h1=CoGetClassObject(a, 1, 0, IID_IClassFactory, (void **)&c ); sprintf(aaa,"CoGetClassObject h1 = %ld...c = %p", h1, c); abc(aaa); h1=c->CreateInstance(0,IID_IUnknown,(void **)&d); sprintf(aaa,"CreateInstance h1 = %ld...d = %p",h1,d); abc(aaa); } if (x==WM_DESTROY) PostQuitMessage(0); return DefWindowProc(w,x,y,z); }
Gonna break these chains around me
Gonna learn to fly again
Maybe hard, maybe hard, But I'll do it
-Micheal Boulton
in ' When I'm Back On My Feet Again'
We now run the batch file b.bat that will compile and link the client program y.cpp.
We execute this program and click in the window. We now get a message telling us that this program has performed an illegal operation and will be shut down. In other words, the program gives us a run time error. Also a look at z.txt shows us that none of the functions in class ccc got called.
z.txt:
server DllGetClassObject server *c = 00C70570 Client CoGetClassObject h1 = 0...c = 00C70570
One thing strikes us. We have put our own class in the place of the interface IClassFactory in the server program z.cpp. However in any interface, all the functions are virtual functions. Keeping this in mind, we shall place the term ' virtual ' in front of all the functions of class ccc in z.cpp.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"server %s\n",p); fclose(fp); } class ccc { public: virtual void a1( ){ abc("in a1"); } virtual void a2( ){ abc("in a2"); } virtual void a3( ){ abc("in a3"); } virtual void a4( ){ abc("in a4"); } }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; sprintf(aa,"*c = %p",*c); abc(aa); return 0; }
We still get the same run time error message, but when we look into the file z.txt, we are in for a surprise.
z.txt:
server DllGetClassObject server *c = 00C70520 Client CoGetClassObject h1 = 0...c = 00C70520 server in a4 Client CreateInstance h1 = 0...d = 00000000
We see that one of the functions in class ccc has, in fact, been called. The function that has been called is function a4( ). We can infer something from this information. First of all, because the functions of class ccc are virtual functions, a Virtual Table has been created. The client assumes that the server is returning a pointer to an interface IClassFactory in which the fourth function is CreateInstance( ). This is why, when we call CreateInstance( ), the fourth function of class ccc gets called. We shall perform a small experiment. We shall interchange the positions of functions a4( ) and a3( ) in class ccc, and see which one gets called.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"server %s\n",p); fclose(fp); } class ccc { public: virtual void a1( ){abc("in a1");} virtual void a2( ){abc("in a2");} virtual void a4( ){abc("in a4");} virtual void a3( ){abc("in a3");} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; sprintf(aa,"*c = %p",*c); abc(aa); return 0; }
As usual, we run a.bat, execute the client y.exe, and click in the window. We didn't really make any major changes to our program, so we still get the General Protection error. When we look at z.txt, we see that function a4( ) has not been called. Instead, function a3( ) was called. This is as shown below.
z.txt:
server DllGetClassObject server *c = 00C70520 Client CoGetClassObject h1 = 0...c = 00C70520 server in a3 Client CreateInstance h1 = 0...d = 00000000
This means that the function that is placed fourth in order in class ccc gets called. Remember that the function is getting called because the client assumes that it is calling function CreateInstance( ) from the class IClassFactory. Thus, function CreateInstance( ) has to be the fourth function in IClassFactory.
Let's rearrange the functions back to their original order, that is a1,a2, a3 and a4. Our goal for the time being should still be to avoid the General Protection error when we make a call to the function CreateInstance( ). If we look at the client, we are calling CreateInstance( ) with three parameters. This results in the fourth function of class ccc in the server being called. It makes sense to give the fourth function a4( ) three parameters, as well. We shall give three longs as parameters to function a4( ). We can probably get away with giving the wrong parameters because, under Windows, all variables occupy four bytes and four locations will be allocated on the stack for any datatype.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"server %s\n",p); fclose(fp); } class ccc { public: virtual void a1( ){abc("in a1");} virtual void a2( ){abc("in a2");} virtual void a3( ){abc("in a3");} virtual void a4(long a, long b, long c){abc("in a4");} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; sprintf(aa,"*c = %p",*c); abc(aa); return 0; }
We once again get the usual General Protection error. Even the contents of z.txt do not change much.
z.txt:
server DllGetClassObject server *c = 00C70520 Client CoGetClassObject h1 = 0...c = 00C70520 server in a4 Client CreateInstance h1 = 0...d = 00000000
How many roads must a man walk down
Before you call him a man?
Yes, 'n how many seas must a white dove sail
Before she sleeps in the sand?
Yes, 'n how many times must the cannonball fly
Before they are forever banned?
The answer, my friend, is blowing in the wind
The answer is blowing in the wind
- Bob Dylan
Whenever we get general protection errors, we are probably trying to access memory that we shouldn't be accessing or the stack is not restored to where it should be.
There is a possibility that all the function CreateInstance( ) follows the Standard Calling Convention, while in class ccc, we have assumed that function a4( ) follows the C Calling Convention. Let us modify all the functions in class ccc so that they follow the Standard Calling Convention.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"server %s\n",p); fclose(fp); } class ccc { public: virtual void _stdcall a1( ){abc("in a1");} virtual void _stdcall a2( ){abc("in a2");} virtual void _stdcall a3( ){abc("in a3");} virtual void _stdcall a4(long a, long b, long c){abc("in a4");} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; sprintf(aa,"*c = %p",*c); abc(aa); return 0; }
Now when we run a.bat, execute the server and click in the window, we no longer get a General Protection error. So we have learn at least one thing from this program. The function CreateInstance( ) in IClassFactory follows the Standard Calling Convention. In fact, we shall find that all the functions in any Active-X interface follow the Standard Calling Convention. We now look into the file z.txt.
z.txt:
server DllGetClassObject server *c = 00C70520 Client CoGetClassObject h1 = 0...c = 00C70520 server in a4 Client CreateInstance h1 = 0...d = 00000000
The way we have been going about things is not the right way. The server is expected to return a pointer to an IClassFactory, but we are returning a pointer to our own class. The reason why we have been doing this, is to get a better understanding of how function CreateInstance( ) is being called.
All this time, we have been trying to fool the client into believing that it is calling function CreateInstance( ) from the server. It is time to change our approach. We shall now derive class ccc from IClassFactory, so that all the functions of IClassFactory get inserted into class ccc automatically. We can then call function CreateInstance( ) from this class. We shall also remove functions a1( ) to a4( ) from class ccc.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"server %s\n",p); fclose(fp); } class ccc:public IClassFactory { public: }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; sprintf(aa,"*c = %p",*c); abc(aa); return 0; }
Instead of running the batch file a.bat as we have been doing, let us proceed with the compilation and linking, step by step. We shall run the command ' cl -c z.cpp '. To our horror, the entire screen fills up with errors. It's a good thing that we hadn't run the batch file. An OBJ file obtained from the previous compilation still exists, and the linker would have used this file to create a DLL which would then be copied with a different extension to give us an OCX. Actually, even now, we cannot see all the errors properly. Let's save all the errors into a file called e.txt by recompiling the server using the command ' cl -c z.cpp > e.txt '. We now look into the errors in e.txt
e.txt:
z.cpp z.cpp(20) : error C2259: 'ccc' : cannot instantiate abstract class due to following members: z.cpp(20) : warning C4259: 'long __stdcall IUnknown::QueryInterface(const struct _GUID &,void ** )' : pure virtual function was not defined z.cpp(20) : warning C4259: 'unsigned long __stdcall IUnknown::AddRef(void)' : pure virtual function was not defined z.cpp(20) : warning C4259: 'unsigned long __stdcall IUnknown::Release(void)' : pure virtual function was not defined z.cpp(20) : warning C4259: 'long __stdcall IClassFactory::CreateInstance(struct IUnknown *,const struct _GUID &,void ** )' : pure virtual function was not defined z.cpp(20) : warning C4259: 'long __stdcall IClassFactory::LockServer(int)' : pure virtual function was not defined
If we look carefully at the errors in e.txt, we shall find that there are only two errors. In fact, there is only one error that is repeated twice. All the warnings are also repeated twice. We have reproduced only the first half of this error file. Delete the other half of the file.
Before we tell you what the error means, let us look at two programs shown below. In the first program, class zzz has a virtual function abc( ) that has been equated to zero. Class yyy is derived from class zzz. We are trying to create an object of class yyy.
zv1.cpp:
class zzz { public: virtual void abc( )=0; }; class yyy:public zzz { public: } main( ) { yyy *a; a = new yyy; }
We however realise that when we compile this program, we get errors. We store the errors in a file called ee.txt by recompiling, using the command 'cl -c zv1.cpp>ee.txt'.
ee.txt:
zv1.cpp zv1.cpp(11) : error C2259: 'yyy' : cannot instantiate abstract class due to following members: zv1.cpp(11) : warning C4259: 'void zzz::abc(void)' : pure virtual function was not defined
When we look into the file ee.txt, we see an error that is similar to what we had obtained in file e.txt when we compiled z.cpp. We also have a single warning. As in e.txt, in this file also, the error and warning is repeated. Whenever a base class contains a function that is a virtual function equated to zero, then this function has to be present in the derived class with proper return values, parameters as well as Calling Conventions. We shall take the function abc( ) that was virtual void =0 in the base class zzz, and place it in the derived class yyy, in the next program.
zv2.cpp:
class zzz { public: virtual void abc( )=0; }; class yyy:public zzz { public: void abc( ){ } } main( ) { yyy *a; a = new yyy; }
We now get no compilation errors. IClassFactory is also a class that contains virtual void functions equated to 0. It strikes us that the errors in z.cpp can also be removed if we insert all the virtual functions, whose names, parameters as well as return values can be obtained from the warning messages in e.txt, into class ccc. There are five warning messages, so there must be five virtual functions equated to 0 in IClassFactory, all of which have to be inserted into class ccc.
Look once again into file e.txt in the DOS editor. Remove the first two lines of the file. We are left with five warnings. These functions contain the names of the virtual functions as well as their parameters. Delete all words that have nothing to do with the
function names or their parameters like ‘ z.cpp(20) : warning C4259: ' or ‘ : pure virtual function was not defined‘. We also remove the words ‘IUnknown::’ and ‘IClassFactory::’. Finally, at the end of each line, we place a pair a braces within which we pass a string to function abc( ) indicating which function is being called and which interface it is called from, as well as a return statement. For example, the first function now looks like
long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc(" IClassFactory QueryInterface"); return 0; }
We now insert these five functions into class ccc in the server. The server z.cpp appears as shown below.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\z.txt","a+"); fprintf(fp,"server %s\n",p); fclose(fp); } class ccc:public IClassFactory { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc("IClassFactory QueryInterface"); return 0;} unsigned long __stdcall AddRef(void) { abc("IClassFactory AddRef"); return 0;} unsigned long __stdcall Release(void) { abc("IClassFactory Release"); return 0;} long __stdcall CreateInstance(struct IUnknown *, const struct _GUID &,void ** ) { abc("IClassFactory CreateInstance"); return 0;} long __stdcall LockServer(int) { abc("IClassFactory LockServer"); return 0;} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; sprintf(aa,"*c = %p",*c); abc(aa); return 0; }
When we run a.bat, the server compiles and links without any errors. Execute the client y.exe and click in the window. We do not get any general protection error. If we look into z.txt, we find that CreateInstance has been called.
z.txt:
server DllGetClassObject server *c = 00C70520 Client CoGetClassObject h1 = 0...c = 00C70520 server IClassFactory CreateInstance Client CreateInstance h1 = 0...d = 00000000
Even if we change the order of the functions in class ccc, or place our original functions a1( ) to a4( ) before these functions, function CreateInstance will still get called. This is because it's order in the virtual table of IClassFactory is fourth, and the base class always decides in which order the virtual functions are called.
The Test Container
I look in the mirror, and all I see,
Is a young grown man, with only a dream
- Patrick Swayze in 'She's Like The Wind'
Soundtrack of the movie 'Dirty Dancing'
Until now, we have written the first few commands of a client program, and then tried to write a server which could be imported into our client. But then, this doesn't give us any real satisfaction. After all, we have designed the client to call certain functions that are present in some interfaces. At the same time, we are just making sure that these interfaces are present in the server and giving pointers to the interfaces to the client, so that the client can call functions from them. While the fact remains that doing this is no real challenge, what upsets us a bit is that we do not really know whether we are moving along the right path while writing a server. It is one thing to write a server that our own client can import. But can we write a server that someone else's client would be able to import? Probably the best way to find out whether the server that we are writing is a proper Active-X server or not, is to test it with a client that has been written by Microsoft. But there must be quite a few clients or containers that Microsoft has written. So which one do we test our server with? We click on 'Start' at the bottom of our screen, go to 'Programs' and then to 'Microsoft Visual C++ 5.0'. Here, we see an option 'ActiveX Control Test Container'. When we click on this option, a container gets displayed. We select this container, as it is already present in VC 5.0. We shall use the 'ActiveX Control Test Container' as our reference client when we write a server. We could have used other clients like Visual Basic, Internet Explorer and so on, but we weren't sure whether everyone would have them or not.
Remember the time when we wrote our own client, within which, we displayed a server given to us by Microsoft, namely a calendar called mscal.ocx? This time, our goal will be to write a server that will display something in a client given to us by Microsoft, namely the ActiveX Control Test Container. Let us take a closer look at the features that are present in this client. If we click on 'Edit' and select the option 'Insert OLE Control', we obtain a list of servers that this client can obtain. In our earlier REG file, we had created a subkey called InprocServer32 that contained the path where the server was present. We had also created another subkey of HKEY_CLASSES_ROOT called zzzz. Thus if we searched in the registry for zzzz, we would find it easily. However, when we search in the Test Container, we do not find any trace of our object in this list. This means that the REG file which we had earlier written, is no longer good enough. We have to make some changes to this REG file so that the Test Container can recognise it. Instead of our old name of zzzz, we shall create a new key in HKEY_CLASSES_ROOT called avzzz. This key is called the ProgID. We shall give this key the name 'avzzz Control'. Of course, if you wish to, you may give it any name. This is the name that should be seen in the list of servers that can be called by the Test Container. We shall keep the 16 byte number same as before, but we shall create our server in another directory called tc. In the key called CLSID, we already have a key {51515150-5151-5151-5151-515151515151}. We create a subkey within this called Control. Since it is not necessary to assign any value to this key, we haven't initialised it to anything. The new REG file is shown below. As usual, we make sure that there is a single space before and after every equal-to sign.
ss.reg:
REGEDIT HKEY_CLASSES_ROOT\avzzz = avzzz Control HKEY_CLASSES_ROOT\avzzz\CLSID = {51515150-5151-5151-5151-515151515151} HKEY_CLASSES_ROOT\CLSID\{51515150-5151-5151-5151-515151515151}\InprocServer32 = c:\tc\z.ocx HKEY_CLASSES_ROOT\CLSID\{51515150-5151-5151-5151-515151515151} \Control =
We go about the usual procedure of importing this REG file into the registry. When we go back to our Test Container, click on Edit and select Insert OLE Control, we shall see avzzz Control in the list of servers that this client can import. The server program that we start off with is similar to the last server program that we wrote. This is shown below. The server follows the same rules as before. The second parameter b, of function DllGetClassObject( ), should be equal to the CLSID or 16 byte number by which IClassFactory is known. We shall also create a class called ccc, which is derived from IClassFactory, and insert all the functions present in IClassFactory, that are virtual void equated to zero, into class ccc. An object d of this class is created. We then assign d to *c, that is, the last parameter of DllGetClassObject( ). We do all this because the rules of Active-X state that all servers have to give a client a pointer to an IClassfactory interface. This will allow the client to call functions from this interface.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\tc\\z.txt","a+"); fprintf(fp," %s\n",p); fclose(fp); } class ccc:public IClassFactory { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc("IClassFactory QueryInterface"); return 0;} unsigned long __stdcall AddRef(void) { abc("IClassFactory AddRef"); return 0;} unsigned long __stdcall Release(void) { abc("IClassFactory Release"); return 0;} long __stdcall CreateInstance(struct IUnknown *a, const struct _GUID &b,void **c ) { abc("IClassFactory CreateInstance"); return 0; } long __stdcall LockServer(int) { abc("IClassFactory LockServer"); return 0;} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); if(b==IID_IClassFactory) { abc("IClassFactory"); d=new ccc; *c=d; } return 0; }
We once again run a batch file that will compile and link this program to create a DLL that will be copied to give it a .ocx extension. We also delete any previous file called z.txt in the current directory, in which we shall be storing our output each time. The batch file is reproduced for our convenience.
a.bat:
cl -c z.cpp link /dll z.obj -def:z.def user32.lib kernel32.lib gdi32.lib uuid.lib copy z.dll z.ocx del z.txt
We now go to the Test Container, click on Edit and select Insert OLE Control. In the list of servers that appear, we select our server to be imported, which is called 'avzzz Control'. Unfortunately, as soon as we click on this name, we get a General Protection error. This error may appear more than once. We click on Cancel, each time it appears. Since an illegal operation has been performed, the Test Container automatically shuts down.
I feel that no one ever told the truth to me
About growing up and what a struggle it would be
In my tangled state of mind, I've been looking back to find
Where I went wrong
- Queen in ' Too Much Love..'
Well, we never claimed that importing our server into Microsoft's Test Container was going to be easy. Never-the-less, when we look into z.txt, we find that running this program hasn't been a waste, after all. The contents of this file are shown below.
z.txt:
DllGetClassObject IClassFactory IClassFactory CreateInstance IClassFactory Release
We see that the functions called from our server by the Microsoft's Test Container are similar to the functions that our own client called. Armed with this knowledge, we may assume that all we have to do, is make sure that a few more interfaces exist in our server, so that the Test Container can call certain functions from these interfaces.
We know that CreateInstance( ) is called from IClassFactory by looking into z.txt. When we insert virtual functions equated to zero of IClassFactory into class ccc from the warnings, we realise that these are just function prototypes in which the parameters have not been given names. We name the parameters of CreateInstance( ) as a, b and c. We check to find whether the second parameter of CreateInstance( ) is the GUID of an interface called IOleObject.
Very often, we shall see a function named Release( ) being called from different interfaces. It’s going to be a very long time before we shall explain this function.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\tc\\z.txt","a+"); fprintf(fp," %s\n",p); fclose(fp); } class ccc:public IClassFactory { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc("IClassFactory QueryInterface"); return 0;} unsigned long __stdcall AddRef(void) { abc("IClassFactory AddRef"); return 0;} unsigned long __stdcall Release(void) { abc("IClassFactory Release"); return 0;} long __stdcall CreateInstance(struct IUnknown *a, const struct _GUID &b,void **c ) { abc("IClassFactory CreateInstance"); if(b==IID_IOleObject) abc("IOleObject"); return 0; } long __stdcall LockServer(int){ abc("IClassFactory LockServer"); return 0;} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); if(b==IID_IClassFactory) { abc("IClassFactory"); d=new ccc; *c=d; } return 0; }
For the time being, the run-time errors should not bother us. Look into z.txt.
z.txt:
DllGetClassObject IClassFactory IClassFactory CreateInstance IOleObject IClassFactory Release
Having confirmed from the output of z.txt that the second parameter b of CreateInstance( ) is indeed the 16 byte number of IOleObject, we have to inform the Test Container that our server supports this interface. This is done by giving the Test Container a pointer to this interface so that the Test Container can call functions from this interface. Our server first has to have a class ioo which is derived from IOleObject. An object i of this class is created. The third parameter c of CreateInstance( ) is then initialised to i.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\tc\\z.txt","a+"); fprintf(fp," %s\n",p); fclose(fp); } class ioo:public IOleObject { public: }; ioo *i; class ccc:public IClassFactory { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc("IClassFactory QueryInterface"); return 0;} unsigned long __stdcall AddRef(void) { abc("IClassFactory AddRef"); return 0;} unsigned long __stdcall Release(void) { abc("IClassFactory Release"); return 0;} long __stdcall CreateInstance(struct IUnknown *a, const struct _GUID &b,void **c ) { abc("IClassFactory CreateInstance"); if(b==IID_IOleObject) abc("IOleObject"); i=new ioo; *c=i; return 0; } long __stdcall LockServer(int){ abc("IClassFactory LockServer"); return 0;} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); if(b==IID_IClassFactory) { abc("IClassFactory"); d=new ccc; *c=d; } return 0; }
When we compile this program, we get an error and a number of warnings, which may be repeated twice. The error is shown below.
z.cpp(31) : error C2259: 'ioo' : cannot instantiate abstract class due to following members:
We get this error because all the functions within IOleObject are virtual functions that are equated to zero. All these functions therefore have to be included in class ioo. If we count, there are twenty four functions, all of which have to be placed in the class ioo. This is done by taking the warning messages, stripping them of everything except the full function names along with Calling Conventions, return value types and parameters. We add an abc( ) function within a pair of braces for each of these twenty four functions so that we know which of these functions is being called. We also return 0 from each of these functions. The entire procedure has been explained before. Our server will now appear as shown below.
z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\tc\\z.txt","a+"); fprintf(fp," %s\n",p); fclose(fp); } class ioo:public IOleObject { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) {abc("IOleObject QueryInterface");return 0;} unsigned long __stdcall AddRef(void) {abc("IOleObject AddRef");return 0;} unsigned long __stdcall Release(void) {abc("IOleObject Release");return 0;} long __stdcall SetClientSite(struct IOleClientSite *) {abc("IOleObject SetClientSite");return 0;} long __stdcall GetClientSite(struct IOleClientSite ** ) {abc("IOleObject GetClientSite");return 0;} long __stdcall SetHostNames(const unsigned short *, const unsigned short *) {abc("IOleObject SetHostNames");return 0;} long __stdcall Close(unsigned long) {abc("IOleObject Close");return 0;} long __stdcall SetMoniker(unsigned long,struct IMoniker *) {abc("IOleObject SetMoniker");return 0;} long __stdcall GetMoniker(unsigned long,unsigned long, struct IMoniker ** ) {abc("IOleObject GetMoniker");return 0;} long __stdcall InitFromData(struct IDataObject *,int,unsigned long) {abc("IOleObject InitFromData");return 0;} long __stdcall GetClipboardData(unsigned long,struct IDataObject ** ) {abc("IOleObject GetClipboardData");return 0;} long __stdcall DoVerb(long,struct tagMSG *,struct IOleClientSite *,long,void *,const struct tagRECT *) {abc("IOleObject DoVerb");return 0;} long __stdcall EnumVerbs(struct IEnumOLEVERB ** ) {abc("IOleObject EnumVerbs");return 0;} long __stdcall Update(void) {abc("IOleObject Update");return 0;} long __stdcall IsUpToDate(void) {abc("IOleObject IsUpToDate");return 0;} long __stdcall GetUserClassID(struct _GUID *) {abc("IOleObject GetUserClassID");return 0;} long __stdcall GetUserType(unsigned long,unsigned short ** ) {abc("IOleObject GetUserType");return 0;} long __stdcall SetExtent(unsigned long,struct tagSIZE *) {abc("IOleObject SetExtent");return 0;} long __stdcall GetExtent(unsigned long,struct tagSIZE *) {abc("IOleObject GetExtent");return 0;} long __stdcall Advise(struct IAdviseSink *,unsigned long *) {abc("IOleObject Advise");return 0;} long __stdcall Unadvise(unsigned long) {abc("IOleObject Unadvise");return 0;} long __stdcall EnumAdvise(struct IEnumSTATDATA ** ) {abc("IOleObject EnumAdvise");return 0;} long __stdcall GetMiscStatus(unsigned long,unsigned long *) {abc("IOleObject GetMiscStatus");return 0;} long __stdcall SetColorScheme(struct tagLOGPALETTE *) {abc("IOleObject SetColorScheme");return 0;} }; ioo *i; class ccc:public IClassFactory { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc("IClassFactory QueryInterface"); return 0;} unsigned long __stdcall AddRef(void) { abc("IClassFactory AddRef"); return 0;} unsigned long __stdcall Release(void) { abc("IClassFactory Release"); return 0;} long __stdcall CreateInstance(struct IUnknown *a, const struct _GUID &b,void **c ) { abc("IClassFactory CreateInstance"); if(b==IID_IOleObject) abc("IOleObject"); i=new ioo; *c=i; return 0; } long __stdcall LockServer(int) { abc("IClassFactory LockServer"); return 0;} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; return 0; }
All we need, Is just a little patience...
...You and I got what it takes to make it
- Gun n' Roses. in ' Patience'
One thing that we should all learn, is never to be intimidated by the size of an Active-X program. Our server program is made up of just a few interfaces that contain a number of functions. Only a few of these functions will be called by a client. We may now run our batch file a.bat so that z.ocx is created. Once again, we run the Test Container, click on Edit, select InsertOLEControl and then choose avzzz Control. Once again, we get run time errors. That doesn't matter, so long as we are making some progress. When the Test Container has a pointer to a class derived from IOleObject in the server, he will call a function named QueryInterface( ) from this class. As we can see in file z.txt, function QueryInterface has been called.
z.txt:
DllGetClassObject IClassFactory CreateInstance IOleObject IClassFactory Release IOleObject QueryInterface
Sometimes, we really get irritated when we keep proceeding in the right direction, yet never seem to succeed. The client, that is, the ActiveX Control Test Container seems to call functions from interfaces that are present in our server. And yet, inspite of having the correct interfaces in our server, from which Microsoft's Test Container calls the expected functions, we still get run time errors telling us that an illegal operation has been performed. So, are we going wrong somewhere?
Let us look at the last function that was called. This function is QueryInterface( ) of IOleObject. In this function, we are returning a 0. We had mentioned earlier that a server may implement a large number of interfaces, but need not implement all the interfaces. When we say that the server implements or supports an interface, it merely means that the interface is present in the server and the client is given a pointer to this interface which the client can use to call any function from this interface. If a particular set of interfaces that the client requires from the server are not present, the server cannot be imported into the client. There has to be a means by which the client can find out about the interfaces that are supported by the server. In Active-X, the client gets this information by calling a function called QueryInterface( ). This is done by placing the address of a 16 byte number that stands for the required interface, on the stack, while calling this function. This number is passed as the first parameter of the function QueryInterface( ). If the server supports the interface which is identified by this 16 byte number or GUID, it gives the client a pointer to this interface, and returns 0. The next time the client needs an interface, it calls function QueryInterface( ) with the GUID of that interface.
The problem with our program is that whenever the client asks for an interface, we are returning 0 in QueryInterface( ), thus giving the client the impression that the interface that was requested for, is supported by the server. In fact, whenever the client asks for any interface, he is always told that this interface is supported, due to the presence of a 'return 0;' statement. When the client now attempts to use this pointer to call a function, a run-time error will occur. Instead of our server telling the client that all interfaces are supported, let us make a small change to our server program so that QueryInterface( ) tells the client that no interfaces are supported. We do this by returning a macro called
E_NOINTERFACE. Only a part of the program where changes have been made, has been reproduced here.
class ioo:public IOleObject { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc("IOleObject QueryInterface"); return E_NOINTERFACE; }
And it's just another piece of the puzzle
Just another part of the plan
How one life touches the other
It’s so hard to understand
So we walk this world together
Try and go as far as we can
‘Cause we’ve been waiting for each other in time
Ever since the world began
- Tommy Shaw in 'Ever Since The World Began'
I guess many of us had been bracing ourselves for the usual errors when we call our server from the Test Container. This time, however, we do not get an error telling us that an illegal operation has been performed, with the Test Container shutting down by itself. Instead, we simply get a message box informing us that the Test Container is unable to insert the specified control. But no errors! Click on OK and close the Test Container. We should close the Test Container everytime, otherwise while the batch file a.bat is executing, we may get a 'sharing violation' error. If we look closely at the functions in IOleObject that are being called by the client, we find that one additional function named Release( ) is now called. We may thus infer that the function QueryInterface( ) has been successfully completed what it set out to do. Only after the execution of QueryInterface( ) could the next function could be called, which did not happen in the previous program.
z.txt:
DllGetClassObject IClassFactory CreateInstance IOleObject IClassFactory Release IOleObject QueryInterface IOleObject Release
The client will ask the server if it supports certain interfaces. Since our server is telling the client that no interfaces are supported, the client cannot go ahead with the process of importing our server. This is why the client displays a message box informing us that our server cannot be imported into it.
In the next program, we shall see whether the client asks for the interface IOleObject. Actually, this is the interface from which function QueryInterface( ) is being called, but the client may wish to confirm this fact for some crazy reason. Since our server supports this interface, QueryInterface( ) should return 0 as well as give a pointer to this interface to the client. If we go back to the basics of C++, we had learnt about the 'this' pointer which is a pointer to the current object. If we have use 'this' in the function QueryInterface( ), it refers to the class ioo as well as IOleObject. Thus if we initialise the second parameter of QueryInterface( ) to 'this', it means that we are returning an IOleObject pointer to the client.
Instead of 'this', we may initialise *b to i, that is, the pointer to class ioo. If we resort to this route, we have to have a class prototype of ioo , that is, the statement 'class ioo;', followed by a line defining a pointer to class ioo, that is, 'ioo *i;'. All this has to be done before the position of the actual class ioo. Earlier, we could define a pointer to class ioo only after writing the entire class.
The function QueryInterface( ) as well as two other functions AddRef( ) and Release( ) are actually functions that are defined in an interface called IUnknown and all other Active-X interfaces are derived from this interface. This is why these three functions are present in all interfaces. We shall check to find out whether this interface is also being called by the test container. If we initialise the second parameter of QueryInterface( ) to 'this' here as well, we shall still be giving a the client a pointer to IUnknown, because IOleObject itself is derived from IUnknown.
z.cpp:
class ioo:public IOleObject { public: long __stdcall QueryInterface(const struct _GUID &a,void **b) { abc("IOleObject QueryInterface"); if (a == IID_IOleObject) { abc("IOleObject QueryInterface IOleObj"); *b = this; return 0; } if( a == IID_IUnknown) { abc("IOleObject QueryInterface IUnkn"); *b = this; return 0; } return E_NOINTERFACE; }
The rest of the program remains the same. The output in z.txt is shown below.
z.txt:
DllGetClassObject IClassFactory CreateInstance IOleObject IClassFactory Release IOleObject QueryInterface IOleObject QueryInterface IOleObj IOleObject GetMiscStatus IOleObject SetClientSite IOleObject QueryInterface IOleObject Release IOleObject Release
From the file z.txt, we find that the Test Container has indeed asked the server for IOleObject in the same interface's QueryInterface( ). IUnknown has not been called, so we may remove it.
It is probably impossible to know what each and every function in an Active-X interface does. We aren't expected to know what functions like GetMiscStatus( ) do when they are called by a client. The problems arise when the client expects us to do something more than just return 0 in a function that he has called. In the case of this particular function GetMiscStatus( ), the client doesn't expect us to do anything, and we can get away with returning 0. The program still works perfectly.
Another function that has been called by the Test Container, is SetClientSite( ). When the Test Container calls this function, it also passes a pointer to an IOleClientSite class. This class is created by the Client. When a function from any interface is called by a client, and the function has pointers as parameters, it's useful to remember a few points. If the pointer has a single *, it means that the client has given the server something, and the server may do whatever he wishes with this pointer. However, if the pointer in the function that has been called is a **, it normally means that the client expects this pointer to be filled up. But let's get back to function SetClientSite( ). All this time, the client has been accepting pointers from the server, for example, a pointer to IClassFactory, IOleObject, and so on, to enable the client to call functions from these interfaces in the server. Reminds us of the affairs that most of us guys have. Strictly one-sided relationships! Shouldn't both sides reciprocate feelings for each other? Well, the Test Container, at least, is a pretty decent personality. After doing all that accepting of pointers to various interfaces which would allow the client to call any function in these interfaces in our server, the client now gives our server an opportunity to call the client's functions. The client does this by giving us a pointer to an IOleClientSite class. Right now, we have just started learning Active-X. Therefore, for the time being, we shall not be calling any functions in the client. But, it is still pretty nice of the client to tell us that our server can call any of it's functions.
In the next program, we shall once again check, within QueryInterface( ) of IOleObject to see whether the Client calls an interface called IPersistStorage.
class ioo:public IOleObject { public: long __stdcall QueryInterface(const struct _GUID &a,void **b) { abc("IOleObject QueryInterface"); if (a == IID_IOleObject) { abc("IOleObject QueryInterface IOleObj"); *b = i; return 0; } if( a == IID_IUnknown) { abc("IOleObject QueryInterface IUnkn"); *b = this; return 0; } if(a==IID_IPersistStorage) abc("IPersistStorage"); return E_NOINTERFACE; }
z.txt:
DllGetClassObject IClassFactory CreateInstance IOleObject IClassFactory Release IOleObject QueryInterface IOleObject QueryInterface IOleObj IOleObject GetMiscStatus IOleObject SetClientSite IOleObject QueryInterface IPersistStorage IOleObject Release IOleObject Release
We can see that the Test Container has called an interface called IPersistStorage from the output in z.txt. We have come across interfaces that belong to the IPersist family of interfaces, in previous programs. Let us tell you a little more about this family of interfaces. Any server may have certain properties to which changes could be made from time to time. For example, when the server was created, an option may have been given to change the colour of the server, as it appears on the screen, according to what we want it to be. The next time we look at the server through our client, the server should appear with the colour that we had set it to. Thus, when we change the colour, we expect this change to persist. What we are doing, is storing this colour, or writing it to our Hard Disk. When we say that our server supports any of the IPersist family of interfaces, we mean that our server has the ability to write to Hard Disk. Under practical circumstances, every server should have the ability to write to the Hard Disk, otherwise many Active-X clients do not consider them to be Active-X servers. In other words, every Active-X server should preferrably support at least one member of the IPersist family. There is, of course, no rule saying that a server has to support one of this family of interfaces. We are designing a server that could work with the ActiveX Control Test Container that is in VC++ 5.0. This client calls the interface IPersistStorage. There are other members of the IPersist family like IPersistStream, IPersistPropertyBag, IPersistStreamInit, and so on. In due course of time, we shall be telling you all about these interfaces.
Just finding out that the Test Container calls IPersistStorage is not enough. Our server has to tell the Test Container that this interface is supported. This is done in the usual way. We create a class called ips, which is derived from IPersistStorage. We then create an object of class ips and initialise the second parameter, b, of QueryInterface( ), to this object. Thus, our server gives the client a pointer to an IPersistStorage class. We compile the program, and store all the errors and warning messages into a text file. We take that functions from the warning messages and place them in class ips. We may now run our batch file a.bat which will create z.ocx for us. z.cpp:
#include<windows.h> #include<stdio.h> char aa[100]; void abc( char *p) { FILE *fp; fp=fopen("c:\\tx\\z.txt","a+"); fprintf(fp," %s\n",p); fclose(fp); } class ips:public IPersistStorage { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc("IPersistStorage QueryInterface"); return 0; } unsigned long __stdcall AddRef(void) { abc("IPersistStorage AddRef"); return 0; } unsigned long __stdcall Release(void) { abc("IPersistStorage Release"); return 0; } long __stdcall GetClassID(struct _GUID *) { abc("IPersistStorage GetClassID"); return 0; } long __stdcall IsDirty(void) { abc("IPersistStorage IsDirty"); return 0; } long __stdcall InitNew(struct IStorage *) { abc("IPersistStorage InitNew"); return 0; } long __stdcall Load(struct IStorage *) { abc("IPersistStorage Load"); return 0; } long __stdcall Save(struct IStorage *,int) { abc("IPersistStorage Save"); return 0; } long __stdcall SaveCompleted(struct IStorage *) { abc("IPersistStorage SaveCompleted"); return 0; } long __stdcall HandsOffStorage(void) { abc("IPersistStorage HandsOffStorage"); return 0; } }; ips *ip; class ioo:public IOleObject { public: long __stdcall QueryInterface(const struct _GUID &a,void **b ) { abc("IOleObject QueryInterface"); if (a == IID_IOleObject) { abc("IOleObject QueryInterface IOleObj"); *b = this; return 0; } if( a == IID_IUnknown) { abc("IOleObject QueryInterface IUnkn"); *b = this; return 0; } if(a==IID_IPersistStorage) { abc("IPersistStorage"); ip = new ips; *b=ip; return 0; } return E_NOINTERFACE; } unsigned long __stdcall AddRef(void) { abc("IOleObject AddRef"); return 0; } unsigned long __stdcall Release(void) { abc("IOleObject Release"); return 0; } long __stdcall SetClientSite(struct IOleClientSite *) { abc("IOleObject SetClientSite"); return 0; } long __stdcall GetClientSite(struct IOleClientSite ** ) { abc("IOleObject GetClientSite"); return 0; } long __stdcall SetHostNames(const unsigned short *, const unsigned short *) { abc("IOleObject SetHostNames"); return 0; } long __stdcall Close(unsigned long) { abc("IOleObject Close"); return 0; } long __stdcall SetMoniker(unsigned long,struct IMoniker *) { abc("IOleObject SetMoniker"); return 0; } long __stdcall GetMoniker(unsigned long, unsigned long,struct IMoniker ** ) { abc("IOleObject GetMoniker"); return 0; } long __stdcall InitFromData(struct IDataObject *,int,unsigned long) { abc("IOleObject InitFromData"); return 0; } long __stdcall GetClipboardData(unsigned long,struct IDataObject ** ) { abc("IOleObject GetClipboardData"); return 0; } long __stdcall DoVerb(long,struct tagMSG *,struct IOleClientSite *, long,void *,const struct tagRECT *) { abc("IOleObject DoVerb"); return 0; } long __stdcall EnumVerbs(struct IEnumOLEVERB ** ) { abc("IOleObject EnumVerbs"); return 0; } long __stdcall Update(void) { abc("IOleObject Update"); return 0; } long __stdcall IsUpToDate(void) { abc("IOleObject IsUpToDate"); return 0; } long __stdcall GetUserClassID(struct _GUID *) { abc("IOleObject GetUserClassID"); return 0; } long __stdcall GetUserType(unsigned long,unsigned short ** ) { abc("IOleObject GetUserType"); return 0; } long __stdcall SetExtent(unsigned long,struct tagSIZE *) { abc("IOleObject SetExtent"); return 0; } long __stdcall GetExtent(unsigned long,struct tagSIZE *) { abc("IOleObject GetExtent"); return 0; } long __stdcall Advise(struct IAdviseSink *,unsigned long *) { abc("IOleObject Advise"); return 0; } long __stdcall Unadvise(unsigned long) { abc("IOleObject Unadvise"); return 0; } long __stdcall EnumAdvise(struct IEnumSTATDATA ** ) { abc("IOleObject EnumAdvise"); return 0; } long __stdcall GetMiscStatus(unsigned long,unsigned long *) { abc("IOleObject GetMiscStatus"); return 0; } long __stdcall SetColorScheme(struct tagLOGPALETTE *) { abc("IOleObject SetColorScheme"); return 0; } }; ioo *i; class ccc:public IClassFactory { public: long __stdcall QueryInterface(const struct _GUID &,void ** ){ abc("IClassFactory QueryInterface"); return 0;} unsigned long __stdcall AddRef(void) { abc("IClassFactory AddRef"); return 0;} unsigned long __stdcall Release(void) { abc("IClassFactory Release"); return 0;} long __stdcall CreateInstance(struct IUnknown *a, const struct _GUID &b,void **c ) { abc("IClassFactory CreateInstance"); if(b==IID_IOleObject) abc("IOleObject"); i=new ioo; *c=i; return 0; } long __stdcall LockServer(int) { abc("IClassFactory LockServer"); return 0;} }; ccc *d; long _stdcall DllGetClassObject(const CLSID &a, const CLSID &b, void **c) { abc("DllGetClassObject"); d=new ccc; *c=d; return 0; }
z.txt:
DllGetClassObject IClassFactory CreateInstance IOleObject IClassFactory Release IOleObject QueryInterface IOleObject QueryInterface IOleObj IOleObject GetMiscStatus IOleObject SetClientSite IOleObject QueryInterface IPersistStorage IPersistStorage InitNew IPersistStorage Release IOleObject Release IOleObject QueryInterface IOleObject QueryInterface IOleObj IOleObject Release IOleObject QueryInterface IOleObject GetMiscStatus IOleObject QueryInterface IOleObject Close IOleObject Release
We didn't start the fire
It was always burning
Since the world was turning
We didn't start the fire
And we didn't light it
But we tried to fight it
- Billy Joel in 'We Didn't Start The Fire'
Once we give the Test Container a pointer to IPersistStorage, the TestContainer will call a function called InitNew( ) from this interface. The people at Microsoft have a wierd way of naming their functions. The names always bear some resemblance to the tasks that these functions perform. Take the case of the function InitNew( ). Our server may wish to create and initialise certain variables and structures or linked lists or something else, for the first time. These variables may have to be created when, for instance, something to store a colour on to the Hard Disk for the first time, is needed. In such cases, the server needs to tell the client that a job needs to be performed in which some variables will be created. Once these variables are created, the same variables can be used when the same task is to be performed the next time. The server gives the client the opportunity to create and initialise these new variables by calling a function called InitNew( ) in the interface IPersistStorage. Once the function InitNew( ) completes it’s tasks, it never gets called again.
Of course, whenever we are given the opportunity to do something, it is not necessary to do that thing. For instance, whenever we attend a buffet dinner hosted by a friend that has fifty dishes, it is really not essential to taste all fifty dishes. But we may, if we wish to. In InitNew( ), we do not want to do anything, right now. Also, if we don't do anything, we do not get any errors, and in no way does our program get affected.
We also notice that the function Release( ) gets called a number of times. This function will be explained later on. Also, a function Close( ) in IOleObject get called. This function gets called when the client no longer needs this Interface.
Time to find our the next interface that is called by the client. We go back to the function QueryInterface( ) in IOleObject, and check if the first parameter a is equal to the GUID of the interface IViewObject2. If it is, then the server has to give the client a pointer to an IViewObject2 class. The changes to be made in the next program are shown below.
z.cpp:
class cview:public IViewObject2 { public: long __stdcall QueryInterface(const struct _GUID &,void ** ) { abc("IViewObject2 QueryInterface"); return 0; } unsigned long __stdcall AddRef(void) { abc("IViewObject2 AddRef"); return 0; } unsigned long __stdcall Release(void) { abc("IViewObject2 Release"); return 0; } long __stdcall Draw(unsigned long,long,void *,struct tagDVTARGETDEVICE *,void *,void *,const struct _RECTL *,const struct _RECTL *, int (__stdcall *)(unsigned long),unsigned long) { abc("IViewObject2 Draw"); return 0; } long __stdcall GetColorSet(unsigned long,long,void *,struct tagDVTARGETDEVICE *,void *,struct tagLOGPALETTE ** ) { abc("IViewObject2 GetColorSet"); return 0; } long __stdcall Freeze(unsigned long,long,void *,unsigned long *) { abc("IViewObject2 Freeze"); return 0; } long __stdcall Unfreeze(unsigned long) { abc("IViewObject2 Unfreeze"); return 0; } long __stdcall SetAdvise(unsigned long,unsigned long,struct IAdviseSink *) { abc("IViewObject2 SetAdvise"); return 0; } long __stdcall GetAdvise(unsigned long *,unsigned long *, struct IAdviseSink ** ) { abc("IViewObject2 GetAdvise"); return 0; } long __stdcall GetExtent(unsigned long,long, struct tagDVTARGETDEVICE *,struct tagSIZE *) { abc("IViewObject2 GetExtent"); return 0; } }; cview *cv; class ioo:public IOleObject { public: long __stdcall QueryInterface(const struct _GUID &a,void **b ) { abc("IOleObject QueryInterface"); if (a == IID_IOleObject) { abc("IOleObject QueryInterface11"); *b = this; return 0; } if( a == IID_IUnknown) { abc("IOleObject QueryInterface22"); *b = this; return 0; } if(a==IID_IPersistStorage) { abc("IPersistStorage"); ip = new ips; *b=ip; return 0; } if (a==IID_IViewObject2) { abc("IViewObject2"); cv = new cview; *b=cv; return 0; } return E_NOINTERFACE; }
Since the interface IViewObject2 does get called, our server has returned a pointer to an IViewObject2 to the client to tell the client that the interface is supported. We now find that the client calls a number of functions from the interface IViewObject2, by looking at the output in z.txt. The most important of these functions is the function, Draw( ). Also notice that QueryInterface( ) from IOleObject might be called a number of times. Right now, we shall not bother ourselves about how many interfaces does the Test Container ask for.
z.txt:
DllGetClassObject IClassFactory CreateInstance IOleObject IClassFactory Release IOleObject QueryInterface IOleObject QueryInterface IOleObj IOleObject GetMiscStatus IOleObject SetClientSite IOleObject QueryInterface IPersistStorage IPersistStorage InitNew IPersistStorage Release IOleObject Release IOleObject QueryInterface IOleObject QueryInterface IOleObj IOleObject Release IOleObject QueryInterface IOleObject GetMiscStatus IOleObject QueryInterface IViewObject2 IOleObject Advise IViewObject2 SetAdvise IOleObject SetHostNames IOleObject QueryInterface IOleObject QueryInterface IOleObject GetMiscStatus IOleObject IsUpToDate IOleObject GetExtent IOleObject DoVerb IOleObject GetUserType IOleObject GetUserType IOleObject GetUserType IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IViewObject2 GetAdvise IViewObject2 SetAdvise IViewObject2 Release IOleObject Close IOleObject Release
You got to take it to the limit
One more time
- Eagles
We had mentioned earlier that our aim is to display something in the Test Container. To enable our server to write something in the Test Container's window, the Test Container will have to create a child window. When the server gives the Test Container a pointer to an IViewObject2 class, it is telling the Test Container that it wishes to display something in the Test Container's window. A function called Draw( ) is present in the interface IViewObject2. Once the Test Container knows that the interface IViewObject2 is supported by the server, it calls the function Draw( ). This is done to inform the server that it can display something in the client's child window. Of course, if the server is going to display something in this window, it needs a DeviceContext to this window. Right now, if we look at the Test Container, we see a rectangle of some sort. We shall explain why this rectangle appeared in a short while. Right now, let us look at the parameters of the function Draw( ). We name the parameters of this function in the alphabetical order.
long __stdcall Draw(unsigned long a, long b, void *c,struct tagDVTARGETDEVICE *d, void *e, void *f, const struct _RECTL *g, const struct _RECTL *h, int (__stdcall *i)(unsigned long),unsigned long j)
When the Test Container called the function Draw( ) in IViewObject2 in the server, it passes a handle to the child window's device context as the sixth parameter f of this function. Using this handle, our server program can display anything in the Test Container's child window. The easiest thing to display in the Test Container, is some text like the word 'Hello'. This can be done using the function TextOut( ). The first parameter to this function is the handle to the device context of the window in which the text is to be written, followed by the co-ordinates of the position where it has to be written, the actual text and the number of characters to be displayed.
long __stdcall Draw(unsigned long a, long b, void *c,struct tagDVTARGETDEVICE *d, void *e, void *f, const struct _RECTL *g, const struct _RECTL *h, int (__stdcall *i)(unsigned long),unsigned long j) { abc("IViewObject2 Draw"); TextOut(f,1,1,"hello",5); return 0; }
The function Draw( ) now appears as shown above. Make this change to the program z.cpp, and run the file a.bat that will create z.ocx.
I never know what got me here
As if somebody led my hand
Seems I hardly had to steer
My course was planned
- Tommy Shaw From 'Ever Since The World Began'
When we import our server program into the Test Container, we naturally expect to see the word 'Hello' in the Container's window. But what do we see? Nothing! Actually, we still see the the long rectangle that we had noticed earlier in the window, but not the text that we expected to see. Maybe, our text is in another part of the Test Container's window that didn't fit on one screen. We move the scroll bar up and down.
To our utter delight, we now find that the word 'Hello' is right at the top of our screen. It appears only after that part of the screen is rewritten. We now look at z.txt. It is much larger than all our previous z.txt files, and may vary in length each time we try this program.
z.txt:
DllGetClassObject IClassFactory CreateInstance IOleObject IClassFactory Release IOleObject QueryInterface IOleObject QueryInterface11 IOleObject GetMiscStatus IOleObject SetClientSite IOleObject QueryInterface IPersistStorage IPersistStorage InitNew IPersistStorage Release IOleObject Release IOleObject QueryInterface IOleObject QueryInterface11 IOleObject Release IOleObject QueryInterface IOleObject GetMiscStatus IOleObject QueryInterface IViewObject2 IOleObject Advise IViewObject2 SetAdvise IOleObject SetHostNames IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject GetMiscStatus IOleObject IsUpToDate IOleObject GetExtent IOleObject DoVerb IOleObject GetUserType IOleObject GetUserType IOleObject GetUserType IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IViewObject2 Draw IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IOleObject QueryInterface IViewObject2 GetAdvise IViewObject2 SetAdvise IViewObject2 Release IOleObject Close IOleObject Release
Internet Explorer and Calendar Control
a.htm
<object name=aa classid="clsid:8e27c92b-1264-101c-8a2f-040224009c02"> </object> <input type=button name=bb value="Click"> <script language="vbscript"> sub bb_OnClick aa.Day = 10 end sub </script>
Let us look at the programs that we have done upto this point. We have written a client that could import a server given to us by Microsoft, namely, the calendar MSCAL.ocx. We have also written a server that one of Microsoft's client's, namely, the Active-X Control Test Container could import.
Distributed Component Object Model (DCOM)
Server
sserver.cpp
#define INC_OLE2 #define STRICT #include <stdio.h> #include <windows.h> #include <initguid.h> DEFINE_GUID(CLSID_SimpleObject, 0x5e9ddec7, 0x5767, 0x11cf, 0xbe, 0xab, 0x0, 0xaa, 0x0, 0x6c, 0x36, 0x6); HRESULT hr;DWORD dwRegister; class CIUnknown:public IUnknown { public: STDMETHODIMP QueryInterface (REFIID riid, void** ppv) { if(riid == IID_IUnknown) { *ppv = this; return 0; } return E_NOINTERFACE; } STDMETHODIMP_(ULONG) AddRef(void) { return 1; }; STDMETHODIMP_(ULONG) Release(void) { return 1; } }; CIUnknown *m_pIUnknown; class CClassFactory : public IClassFactory { public: STDMETHODIMP QueryInterface (REFIID riid, void** ppv) { MessageBox(0,L"byec",L"byec",0); if (riid == IID_IClassFactory || riid == IID_IUnknown) { *ppv = (IClassFactory *) this; return S_OK; } return E_NOINTERFACE; } STDMETHODIMP_(ULONG) AddRef(void) { return 1; }; STDMETHODIMP_(ULONG) Release(void) { return 1; } STDMETHODIMP CreateInstance (LPUNKNOWN punkOuter, REFIID iid,void **ppv) { MessageBox(0,L"CClassFactory::CreateInstance",L"CClassFactory::CreateInstance",0); m_pIUnknown = new CIUnknown; *ppv = m_pIUnknown; return 0; } STDMETHODIMP LockServer (BOOL fLock) { return 0; }; }; CClassFactory g_ClassFactory; void __cdecl main() { hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); hr = CoRegisterClassObject(CLSID_SimpleObject, &g_ClassFactory, CLSCTX_SERVER, REGCLS_SINGLEUSE, &dwRegister); MessageBox(0,L"bye",L"bye",0); }
Client
sclient.cpp
#define INC_OLE2 #include <stdio.h> #include <windows.h> #include <initguid.h> #include <tchar.h> #include <conio.h> DEFINE_GUID(CLSID_SimpleObject, 0x5e9ddec7, 0x5767, 0x11cf, 0xbe, 0xab, 0x0, 0xaa, 0x0, 0x6c, 0x36, 0x6); unsigned short wsz [100]; HRESULT hr; COSERVERINFO csi; IClassFactory *a; IUnknown *b; void __cdecl main(int argc, CHAR **argv) { MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, "70.0.0.5", -1, wsz, 100); csi.pwszName = wsz; hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); hr=CoGetClassObject(CLSID_SimpleObject,CLSCTX_REMOTE_SERVER,&csi,IID_IClassFactory,(void **)&a); printf("CGetClassObject %ld\n",hr); hr =a->CreateInstance(0,IID_IUnknown,(void **)&b); printf("a->CreateInstance %ld\n",hr); }